App.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import React from 'react';
  2. import StockHeatmap from '@rongmz/react-stock-heatmap';
  3. import '@rongmz/react-stock-heatmap/example/src/index.css';
  4. import toast, { Toaster } from 'react-hot-toast';
  5. import { v4 as uuidv4 } from 'uuid';
  6. import axios from 'axios';
  7. import { Select, Button, Input, Space, Checkbox } from "tdesign-react";
  8. import { DeleteIcon } from 'tdesign-icons-react';
  9. import 'tdesign-react/es/style/index.css';
  10. import PQueue from 'p-queue';
  11. const queue = new PQueue({ concurrency: 5 });
  12. const {Option} = Select;
  13. function formatTimestamp(timestamp) {
  14. // 创建一个新的 Date 对象
  15. const date = new Date(timestamp);
  16. // 获取小时、分钟和秒
  17. const hours = date.getHours();
  18. const minutes = date.getMinutes();
  19. const seconds = date.getSeconds();
  20. // 获取毫秒
  21. const milliseconds = date.getMilliseconds();
  22. // 将小时、分钟、秒、毫秒格式化为两位数字(除了毫秒可能是三位)
  23. const formattedHours = hours.toString().padStart(2, '0');
  24. const formattedMinutes = minutes.toString().padStart(2, '0');
  25. const formattedSeconds = seconds.toString().padStart(2, '0');
  26. const formattedMilliseconds = milliseconds.toString().padStart(3, '0');
  27. // 返回格式化的时间字符串
  28. return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
  29. }
  30. function parseStockData(data) {
  31. // Extracting values from the input data
  32. const { asks, bids, last_price, last_qty, total_qty, time, side } = data;
  33. // Convert asks and bids to the required format
  34. const processOrders = (orders) => orders.map(([rate, qty]) => ({
  35. rate: rate,
  36. orders: 1, // Assuming each price level has one order
  37. qty
  38. }));
  39. // Calculate additional values
  40. const high = Math.max(...asks.map(a => a[0]), last_price);
  41. const low = Math.min(...bids.map(b => b[0]), last_price);
  42. const open = bids[0][0]; // Assuming the first bid rate as open
  43. const close = last_price;
  44. const volume = total_qty; // Total traded volume
  45. // Calculate average price (simplified as an average of high and low)
  46. const avgPrice = (high + low) / 2;
  47. // Construct the final object
  48. return {
  49. marketDepth: {
  50. lastBuyPrice: side === 'buy' ? last_price : 0,
  51. lastBuyQty: side === 'buy' ? last_qty : 0,
  52. lastSellPrice: side === 'sell' ? last_price : 0,
  53. lastSellQty: side === 'sell' ? last_qty : 0,
  54. priceChangeAmt: last_price - open, // Simplified price change amount
  55. priceChangePct: ((last_price - open) / open * 100).toFixed(2),
  56. lastTradedTS: Date.now(),
  57. open,
  58. high,
  59. low,
  60. close,
  61. volume,
  62. avgPrice,
  63. buyOrderVolume: bids.reduce((sum, [, qty]) => sum + qty, 0),
  64. buys: processOrders(bids),
  65. sellOrderVolume: asks.reduce((sum, [, qty]) => sum + qty, 0),
  66. sells: processOrders(asks),
  67. side: side
  68. },
  69. ts: formatTimestamp(time),
  70. time: time,
  71. tradingsymbol: "XYZ123",
  72. pendingOrders: []
  73. };
  74. }
  75. const flushMemoryDbData = async (data, symbol) => {
  76. // console.log('Invoking flush-memory-db-data', data.length, symbol);
  77. try {
  78. await window.electronAPI.flushMemoryDbData(data, symbol);
  79. // const result = await window.electronAPI.flushMemoryDbData(data, symbol);
  80. // console.log('Flush successful', result);
  81. } catch (error) {
  82. // console.error('Error flushing memory DB data', error);
  83. }
  84. };
  85. export default () => {
  86. const [isActivation, setIsActivation] = React.useState(false);
  87. const [activationCode, setActivationCode] = React.useState();
  88. const [loginSymbol,setLoginSymbol]= React.useState();
  89. const [isPersistent,setIsPersistent]= React.useState(false);
  90. const [loading, setLoading] = React.useState(true);
  91. const progressRef = React.useRef(null);
  92. /** @type {React.MutableRefObject<StockHeatmap>} */
  93. const heatmapRef = React.useRef(null);
  94. const [windowDim, setWindowDim] = React.useState([0, 0]);
  95. const [autoScroll, setAutoScroll] = React.useState(true);
  96. const toggleAutoScroll = (value) => {
  97. setAutoScroll(value);
  98. };
  99. // 下拉框部分
  100. const [symbolOptions, setSymbolOptions] = React.useState([]);
  101. const [editOrCreate, toggleEditOrCreate] = React.useState('edit');
  102. const [inputSymbol, changeInputSymbol] = React.useState({ });
  103. const handleClickConfirm = async () => {
  104. toast.remove()
  105. if (!inputSymbol.symbol) return toast.error("品种不能为空!")
  106. if (!inputSymbol.port) return toast.error("端口号不能为空!")
  107. const options = {...inputSymbol, id: uuidv4()};
  108. const newOptions = [...symbolOptions, options];
  109. setSymbolOptions(newOptions);
  110. changeInputSymbol({symbol:"",port:""});
  111. toggleEditOrCreate('edit');
  112. await window.electronAPI.setSymbolData(newOptions)
  113. };
  114. const handleDeleteSymbolOption= async (option)=>{
  115. const newOptions = symbolOptions.filter(item=>item.id !== option.id)
  116. setSymbolOptions(newOptions)
  117. if(loginSymbol === option.id) setLoginSymbol(undefined)
  118. await window.electronAPI.setSymbolData(newOptions)
  119. }
  120. // ------------ Load data -------------
  121. // React.useEffect(() => {
  122. // const symbolInfo = symbolOptions.find((item)=> item.id == loginSymbol)
  123. // const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`);
  124. // let ref = heatmapRef.current
  125. //
  126. // console.log('ws创建完成')
  127. // ws.onmessage = function(event) {
  128. // const message = JSON.parse(event.data);
  129. //
  130. // let stock = parseStockData(message)
  131. //
  132. // ref.addData(stock)
  133. //
  134. // if (progressRef.current !== null) {
  135. // progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...`
  136. //
  137. // if (ref.data.length >= ref.windowLength) {
  138. // setLoading(false)
  139. // }
  140. // }
  141. // };
  142. //
  143. // ws.onerror = function(event) {
  144. // console.error("WebSocket error observed:", event);
  145. // };
  146. // }, []);
  147. const connectWebSocket = ()=>{
  148. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  149. const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`);
  150. let ref = heatmapRef.current
  151. // let prevFlushMemoryDbDataTimestamp = new Date().getTime()
  152. console.log('ws创建完成')
  153. ws.onmessage = function(event) {
  154. const message = JSON.parse(event.data);
  155. let stock = parseStockData(message)
  156. ref.addData(stock)
  157. // 每1秒刷新一次后台的数据
  158. // let now = new Date().getTime()
  159. // if (now - 1000 > prevFlushMemoryDbDataTimestamp) {
  160. // // 使用 slice 方法提取最后 5000 条数据,保存到本地
  161. // const startIndex = Math.max(ref.data.length - 5000, 0);
  162. // let saveData = ref.data.slice(startIndex);
  163. // queue.add(() => flushMemoryDbData(saveData, symbolInfo.symbol));
  164. // }
  165. // 数据推送进度条
  166. if (progressRef.current !== null) {
  167. progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...`
  168. if (ref.data.length >= ref.windowLength) {
  169. setLoading(false)
  170. }
  171. }
  172. };
  173. ws.onerror = function(event) {
  174. console.error("WebSocket error observed:", event);
  175. };
  176. }
  177. // ------------ Load data -------------
  178. const checkStatus = async ()=>{
  179. toast.remove();
  180. const loginInfo = await window.electronAPI.getLoginInfoData() || {}
  181. const params = {
  182. "code": loginInfo.code,
  183. "machine": loginInfo.machine
  184. }
  185. const response = await axios.post('http://139.159.224.218:38888/check_status',params)
  186. if(response.data.code !== 200){
  187. if(window.sessionStorage.getItem("_HEATMAP_IS_LOGIN") === "1") toast.error(response.data.msg)
  188. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0")
  189. setIsActivation(false)
  190. return
  191. }
  192. setIsActivation(true)
  193. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1")
  194. }
  195. // 保存数据库到本地
  196. const saveLocalDb = async () => {
  197. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  198. let ref = heatmapRef.current
  199. await window.electronAPI.setDbData(ref.data, symbolInfo.symbol)
  200. }
  201. // 获取本地的数据库
  202. const readLocalDb = async () => {
  203. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  204. let ref = heatmapRef.current
  205. ref.data = await window.electronAPI.getDbData(symbolInfo.symbol)
  206. }
  207. const handleActivation = async ()=>{
  208. toast.remove();
  209. const params = {
  210. "code": activationCode,
  211. "machine": uuidv4()
  212. }
  213. const response = await axios.post('http://139.159.224.218:38888/login', params)
  214. if(response.data.code === 200){
  215. await window.electronAPI.setLoginInfoData(params)
  216. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1")
  217. toast.success(response.data.msg)
  218. setInterval(()=>{
  219. checkStatus()
  220. }, 5000)
  221. setIsActivation(true)
  222. await readLocalDb()
  223. connectWebSocket()
  224. if (isPersistent){
  225. // 每30秒存放一次本地数据
  226. setInterval(() => {
  227. saveLocalDb()
  228. }, 30 * 1000)
  229. }
  230. }else{
  231. toast.error(response.data.msg);
  232. }
  233. }
  234. // ---------- window update ------------
  235. React.useEffect( async () => {
  236. // 处理品种选择
  237. let symbolInfo = await window.electronAPI.getSymbolData();
  238. if(symbolInfo.length > 0) {
  239. setSymbolOptions(symbolInfo)
  240. }
  241. // 处理是否有登录信息
  242. let loginInfo = await window.electronAPI.getLoginInfoData() || {};
  243. if(loginInfo.code) {
  244. toast.remove();
  245. try {
  246. const code = loginInfo.code || ''
  247. setActivationCode(code || '')
  248. }catch (err){
  249. await window.electronAPI.setLoginInfoData({})
  250. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0")
  251. return
  252. }
  253. }
  254. // 处理持久化选项
  255. let persistent = await window.electronAPI.getIsPersistentData() || false;
  256. if (persistent) setIsPersistent(persistent)
  257. const updateFn = () => {
  258. setWindowDim([
  259. window.innerWidth,
  260. window.innerHeight
  261. ]);
  262. }
  263. updateFn();
  264. window.addEventListener('resize', updateFn);
  265. return () => window.removeEventListener('resize', updateFn);
  266. }, []);
  267. // ---------- window update ------------
  268. return (
  269. <div className="wapper">
  270. <div className={isActivation ? "visibilityVisible" : "visibilityHidden"}>
  271. <React.Fragment>
  272. {loading &&
  273. <div className="loadingIndicator">
  274. <div className="loadingSpinner">
  275. <div className="loader">等待数据推送...</div>
  276. </div>
  277. <div ref={progressRef}> 等待数据推送 0%</div>
  278. </div>
  279. }
  280. <StockHeatmap ref={heatmapRef} width={windowDim[0]} height={windowDim[1]} autoScroll={autoScroll}
  281. toggleAutoScroll={toggleAutoScroll} />
  282. <div className="btnContainer">
  283. <button onClick={() => {
  284. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60)
  285. }}>1分钟视域
  286. </button>
  287. <button onClick={() => {
  288. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 5)
  289. }}>5分钟视域
  290. </button>
  291. <button onClick={() => {
  292. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 10)
  293. }}>10分钟视域
  294. </button>
  295. <button onClick={() => {
  296. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 15)
  297. }}>15分钟视域
  298. </button>
  299. <button onClick={() => {
  300. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 30)
  301. }}>30分钟视域
  302. </button>
  303. <button onClick={() => {
  304. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 60)
  305. }}>60分钟视域
  306. </button>
  307. {!autoScroll &&
  308. <div className="playBtn" onClick={() => {
  309. setAutoScroll(true)
  310. }}> 继续
  311. </div>
  312. }
  313. {/* <button onClick={() => { */}
  314. {/* const HHmmss = window.prompt('Enter HH:mm:ss', '00:00:00'); */}
  315. {/* let split = HHmmss.split(':'); */}
  316. {/* let position = (+split[0]-9)*3600 + (+split[1]*60) + (+split[2]); */}
  317. {/* if (heatmapRef.current !== null) heatmapRef.current.moveDataWindow(position); */}
  318. {/* }}>Set Position</button> */}
  319. </div>
  320. </React.Fragment>
  321. </div>
  322. {!isActivation &&
  323. <div className="layoutContainer">
  324. <div className="activationBox">
  325. <div className="title">
  326. 软件登录
  327. </div>
  328. <div className="iptWp customIpt">
  329. <Input className="password ipt" placeholder="请输入登录码" type="password" value={activationCode || ''} onChange={(value)=>{setActivationCode(value)}} />
  330. <Select
  331. placeholder="请选择品种"
  332. className="symbol ipt"
  333. clearable
  334. value={loginSymbol}
  335. onChange={(value)=>{setLoginSymbol(value)}}
  336. panelBottomContent={
  337. <div
  338. className="select-panel-footer"
  339. style={{
  340. position: 'sticky',
  341. bottom: 0,
  342. backgroundColor: 'var(--td-bg-color-container)',
  343. zIndex: 2,
  344. }}
  345. >
  346. {editOrCreate === 'edit' ? (
  347. <div
  348. style={{
  349. padding: '8px 6px',
  350. borderTop: '1px solid var(--td-border-level-2-color)',
  351. }}
  352. >
  353. <Button theme="primary" size="small" variant="text" onClick={() => toggleEditOrCreate('create')}>
  354. 新增选项
  355. </Button>
  356. </div>
  357. ) : (
  358. <div style={{ padding: 8, borderTop: '1px solid var(--td-border-level-2-color)' }}>
  359. <Space>
  360. <Input placeholder="请输入品种" size="small" autofocus value={inputSymbol.symbol} onChange={(value) => changeInputSymbol({...inputSymbol,symbol:value})}></Input>
  361. <Input placeholder="请输入端口号" size="small" autofocus value={inputSymbol.port} onChange={(value) => changeInputSymbol({...inputSymbol,port:value})}></Input>
  362. </Space>
  363. <Button size="small" style={{ marginTop: '12px' }} onClick={handleClickConfirm}>
  364. 确认
  365. </Button>
  366. <Button
  367. theme="default"
  368. size="small"
  369. style={{ marginTop: '12px', marginLeft: '8px' }}
  370. onClick={() => toggleEditOrCreate('edit')}
  371. >
  372. 取消
  373. </Button>
  374. </div>
  375. )}
  376. </div>
  377. }
  378. >
  379. {symbolOptions.map((option) => (
  380. <Option key={option.id} value={option.id} label={`${option.symbol}:${option.port}`}>
  381. <div style={{ display: 'flex', alignItems: 'center' }}>
  382. <div style={{ flex:"1",paddingRight:"10px",boxSizing:"border-box" }}>{`${option.symbol}:${option.port}`}</div>
  383. <DeleteIcon style={{ width:"16px"}} onClick={(e)=>{
  384. e.stopPropagation();
  385. handleDeleteSymbolOption(option)
  386. }}/>
  387. </div>
  388. </Option>
  389. ))}
  390. </Select>
  391. <Checkbox checked={isPersistent} onChange={async (value)=>{
  392. setIsPersistent(value)
  393. await window.electronAPI.setIsPersistentData(value)
  394. }}>是否持久化数据</Checkbox>
  395. </div>
  396. <div className="btnWp">
  397. <div className="btn" onClick={handleActivation}>登 录</div>
  398. </div>
  399. </div>
  400. </div>
  401. }
  402. <Toaster className="toasterWp" />
  403. </div>
  404. )
  405. }