App.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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 } 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 [loading, setLoading] = React.useState(true);
  90. const progressRef = React.useRef(null);
  91. /** @type {React.MutableRefObject<StockHeatmap>} */
  92. const heatmapRef = React.useRef(null);
  93. const [windowDim, setWindowDim] = React.useState([0, 0]);
  94. const [autoScroll, setAutoScroll] = React.useState(true);
  95. const toggleAutoScroll = (value) => {
  96. setAutoScroll(value);
  97. };
  98. // 下拉框部分
  99. const [symbolOptions, setSymbolOptions] = React.useState([]);
  100. const [editOrCreate, toggleEditOrCreate] = React.useState('edit');
  101. const [inputSymbol, changeInputSymbol] = React.useState({ });
  102. const handleClickConfirm = async () => {
  103. toast.remove()
  104. if (!inputSymbol.symbol) return toast.error("品种不能为空!")
  105. if (!inputSymbol.port) return toast.error("端口号不能为空!")
  106. const options = {...inputSymbol, id: uuidv4()};
  107. const newOptions = [...symbolOptions, options];
  108. setSymbolOptions(newOptions);
  109. changeInputSymbol({symbol:"",port:""});
  110. toggleEditOrCreate('edit');
  111. await window.electronAPI.setSymbolData(newOptions)
  112. };
  113. const handleDeleteSymbolOption= async (option)=>{
  114. const newOptions = symbolOptions.filter(item=>item.id !== option.id)
  115. setSymbolOptions(newOptions)
  116. if(loginSymbol === option.id) setLoginSymbol(undefined)
  117. await window.electronAPI.setSymbolData(newOptions)
  118. }
  119. // ------------ Load data -------------
  120. // React.useEffect(() => {
  121. // const symbolInfo = symbolOptions.find((item)=> item.id == loginSymbol)
  122. // const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`);
  123. // let ref = heatmapRef.current
  124. //
  125. // console.log('ws创建完成')
  126. // ws.onmessage = function(event) {
  127. // const message = JSON.parse(event.data);
  128. //
  129. // let stock = parseStockData(message)
  130. //
  131. // ref.addData(stock)
  132. //
  133. // if (progressRef.current !== null) {
  134. // progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...`
  135. //
  136. // if (ref.data.length >= ref.windowLength) {
  137. // setLoading(false)
  138. // }
  139. // }
  140. // };
  141. //
  142. // ws.onerror = function(event) {
  143. // console.error("WebSocket error observed:", event);
  144. // };
  145. // }, []);
  146. const connectWebSocket = ()=>{
  147. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  148. const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`);
  149. let ref = heatmapRef.current
  150. // let prevFlushMemoryDbDataTimestamp = new Date().getTime()
  151. console.log('ws创建完成')
  152. ws.onmessage = function(event) {
  153. const message = JSON.parse(event.data);
  154. let stock = parseStockData(message)
  155. ref.addData(stock)
  156. // 每1秒刷新一次后台的数据
  157. // let now = new Date().getTime()
  158. // if (now - 1000 > prevFlushMemoryDbDataTimestamp) {
  159. // // 使用 slice 方法提取最后 5000 条数据,保存到本地
  160. // const startIndex = Math.max(ref.data.length - 5000, 0);
  161. // let saveData = ref.data.slice(startIndex);
  162. // queue.add(() => flushMemoryDbData(saveData, symbolInfo.symbol));
  163. // }
  164. // 数据推送进度条
  165. if (progressRef.current !== null) {
  166. progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...`
  167. if (ref.data.length >= ref.windowLength) {
  168. setLoading(false)
  169. }
  170. }
  171. };
  172. ws.onerror = function(event) {
  173. console.error("WebSocket error observed:", event);
  174. };
  175. }
  176. // ------------ Load data -------------
  177. const checkStatus = async ()=>{
  178. toast.remove();
  179. const loginInfo = await window.electronAPI.getLoginInfoData() || {}
  180. const params = {
  181. "code": loginInfo.code,
  182. "machine": loginInfo.machine
  183. }
  184. const response = await axios.post('http://139.159.224.218:38888/check_status',params)
  185. if(response.data.code !== 200){
  186. if(window.sessionStorage.getItem("_HEATMAP_IS_LOGIN") === "1") toast.error(response.data.msg)
  187. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0")
  188. setIsActivation(false)
  189. return
  190. }
  191. setIsActivation(true)
  192. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1")
  193. }
  194. // 保存数据库到本地
  195. const saveLocalDb = async () => {
  196. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  197. let ref = heatmapRef.current
  198. await window.electronAPI.setDbData(ref.data, symbolInfo.symbol)
  199. }
  200. // 获取本地的数据库
  201. const readLocalDb = async () => {
  202. const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol)
  203. let ref = heatmapRef.current
  204. ref.data = await window.electronAPI.getDbData(symbolInfo.symbol)
  205. }
  206. const handleActivation = async ()=>{
  207. toast.remove();
  208. const params = {
  209. "code": activationCode,
  210. "machine": uuidv4()
  211. }
  212. const response = await axios.post('http://139.159.224.218:38888/login', params)
  213. if(response.data.code === 200){
  214. await window.electronAPI.setLoginInfoData(params)
  215. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1")
  216. toast.success(response.data.msg)
  217. setInterval(()=>{
  218. checkStatus()
  219. },5000)
  220. setIsActivation(true)
  221. await readLocalDb()
  222. connectWebSocket()
  223. // 每300秒存放一次本地数据
  224. // setInterval(() => {
  225. // saveLocalDb()
  226. // }, 300 * 1000)
  227. }else{
  228. toast.error(response.data.msg);
  229. }
  230. }
  231. // ---------- window update ------------
  232. React.useEffect( async () => {
  233. // 处理品种选择
  234. let symbolInfo = await window.electronAPI.getSymbolData();
  235. if(symbolInfo.length > 0) {
  236. setSymbolOptions(symbolInfo)
  237. }
  238. // 处理是否有登录信息
  239. let loginInfo = await window.electronAPI.getLoginInfoData() || {};
  240. if(loginInfo.code) {
  241. toast.remove();
  242. try {
  243. const code = loginInfo.code || ''
  244. setActivationCode(code || '')
  245. }catch (err){
  246. await window.electronAPI.setLoginInfoData({})
  247. window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0")
  248. return
  249. }
  250. }
  251. const updateFn = () => {
  252. setWindowDim([
  253. window.innerWidth,
  254. window.innerHeight
  255. ]);
  256. }
  257. updateFn();
  258. window.addEventListener('resize', updateFn);
  259. return () => window.removeEventListener('resize', updateFn);
  260. }, []);
  261. // ---------- window update ------------
  262. return (
  263. <div className="wapper">
  264. <div className={isActivation ? "visibilityVisible" : "visibilityHidden"}>
  265. <React.Fragment>
  266. {loading &&
  267. <div className="loadingIndicator">
  268. <div className="loadingSpinner">
  269. <div className="loader">等待数据推送...</div>
  270. </div>
  271. <div ref={progressRef}> 等待数据推送 0%</div>
  272. </div>
  273. }
  274. <StockHeatmap ref={heatmapRef} width={windowDim[0]} height={windowDim[1]} autoScroll={autoScroll}
  275. toggleAutoScroll={toggleAutoScroll} />
  276. <div className="btnContainer">
  277. <button onClick={() => {
  278. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60)
  279. }}>1分钟视域
  280. </button>
  281. <button onClick={() => {
  282. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 5)
  283. }}>5分钟视域
  284. </button>
  285. <button onClick={() => {
  286. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 10)
  287. }}>10分钟视域
  288. </button>
  289. <button onClick={() => {
  290. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 15)
  291. }}>15分钟视域
  292. </button>
  293. <button onClick={() => {
  294. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 30)
  295. }}>30分钟视域
  296. </button>
  297. <button onClick={() => {
  298. if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 60)
  299. }}>60分钟视域
  300. </button>
  301. {!autoScroll &&
  302. <div className="playBtn" onClick={() => {
  303. setAutoScroll(true)
  304. }}> 继续
  305. </div>
  306. }
  307. {/* <button onClick={() => { */}
  308. {/* const HHmmss = window.prompt('Enter HH:mm:ss', '00:00:00'); */}
  309. {/* let split = HHmmss.split(':'); */}
  310. {/* let position = (+split[0]-9)*3600 + (+split[1]*60) + (+split[2]); */}
  311. {/* if (heatmapRef.current !== null) heatmapRef.current.moveDataWindow(position); */}
  312. {/* }}>Set Position</button> */}
  313. </div>
  314. </React.Fragment>
  315. </div>
  316. {!isActivation &&
  317. <div className="layoutContainer">
  318. <div className="activationBox">
  319. <div className="title">
  320. 软件登录
  321. </div>
  322. <div className="iptWp customIpt">
  323. <Input className="password ipt" placeholder="请输入登录码" type="password" value={activationCode || ''} onChange={(value)=>{setActivationCode(value)}} />
  324. <Select
  325. placeholder="请选择品种"
  326. className="symbol ipt"
  327. clearable
  328. value={loginSymbol}
  329. onChange={(value)=>{setLoginSymbol(value)}}
  330. panelBottomContent={
  331. <div
  332. className="select-panel-footer"
  333. style={{
  334. position: 'sticky',
  335. bottom: 0,
  336. backgroundColor: 'var(--td-bg-color-container)',
  337. zIndex: 2,
  338. }}
  339. >
  340. {editOrCreate === 'edit' ? (
  341. <div
  342. style={{
  343. padding: '8px 6px',
  344. borderTop: '1px solid var(--td-border-level-2-color)',
  345. }}
  346. >
  347. <Button theme="primary" size="small" variant="text" onClick={() => toggleEditOrCreate('create')}>
  348. 新增选项
  349. </Button>
  350. </div>
  351. ) : (
  352. <div style={{ padding: 8, borderTop: '1px solid var(--td-border-level-2-color)' }}>
  353. <Space>
  354. <Input placeholder="请输入品种" size="small" autofocus value={inputSymbol.symbol} onChange={(value) => changeInputSymbol({...inputSymbol,symbol:value})}></Input>
  355. <Input placeholder="请输入端口号" size="small" autofocus value={inputSymbol.port} onChange={(value) => changeInputSymbol({...inputSymbol,port:value})}></Input>
  356. </Space>
  357. <Button size="small" style={{ marginTop: '12px' }} onClick={handleClickConfirm}>
  358. 确认
  359. </Button>
  360. <Button
  361. theme="default"
  362. size="small"
  363. style={{ marginTop: '12px', marginLeft: '8px' }}
  364. onClick={() => toggleEditOrCreate('edit')}
  365. >
  366. 取消
  367. </Button>
  368. </div>
  369. )}
  370. </div>
  371. }
  372. >
  373. {symbolOptions.map((option) => (
  374. <Option key={option.id} value={option.id} label={`${option.symbol}:${option.port}`}>
  375. <div style={{ display: 'flex', alignItems: 'center' }}>
  376. <div style={{ flex:"1",paddingRight:"10px",boxSizing:"border-box" }}>{`${option.symbol}:${option.port}`}</div>
  377. <DeleteIcon style={{ width:"16px"}} onClick={(e)=>{
  378. e.stopPropagation();
  379. handleDeleteSymbolOption(option)
  380. }}/>
  381. </div>
  382. </Option>
  383. ))}
  384. </Select>
  385. </div>
  386. <div className="btnWp">
  387. <div className="btn" onClick={handleActivation}>登 录</div>
  388. </div>
  389. </div>
  390. </div>
  391. }
  392. <Toaster className="toasterWp" />
  393. </div>
  394. )
  395. }