You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2740 lines
81 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <!-- @format -->
  2. <template>
  3. <!-- 容器 -->
  4. <view class="container">
  5. <!-- 标题 -->
  6. <view class="title-container">
  7. <view class="title">
  8. <image class="back-homepage-btn" src="/static/marketSituation-image/back.png" mode="返回按钮" @click="backToHomepage()"> </image>
  9. <view class="mid-title">
  10. <view class="arrow-left" @click="toLeftPage()"> </view>
  11. <view class="stock-id">
  12. <view class="stock-name"> {{ stockInformation.stockName }} </view>
  13. <view class="stock-code"> {{ stockInformation.stockCode }} </view>
  14. </view>
  15. <view class="arrow-right" @click="toRightPage()"> </view>
  16. </view>
  17. <image class="search" src="/static/marketSituation-image/search.png" mode="搜索" @click="search()"> </image>
  18. <view class="more" @click="getMore()">···</view>
  19. </view>
  20. </view>
  21. <view class="body">
  22. <!-- 股票信息栏 -->
  23. <view class="stock-information" @click="openStockDetail()">
  24. <view v-if="isStockDetail" class="stock-detail-container" @click.stop @click="closeStockDetail()">
  25. <view class="stock-detail" @click.stop>
  26. <view class="first-column">
  27. <view class="first-column-data" v-for="item in firstColumData" :key="item">
  28. <view class="stock-detail-title">
  29. {{ item.title }}
  30. </view>
  31. <view class="stock-detail-value">
  32. {{ item.value }}
  33. </view>
  34. </view>
  35. </view>
  36. <view class="second-column">
  37. <view class="second-column-data" v-for="item in secondColumnData" :key="item">
  38. <view class="stock-detail-title">
  39. {{ item.title }}
  40. </view>
  41. <view class="stock-detail-value">
  42. {{ item.value }}
  43. </view>
  44. </view>
  45. </view>
  46. </view>
  47. </view>
  48. <view class="stock-current-data">
  49. <view class="stock-current-price" :class="confirmStockColor(stockInformation.currentPrice, stockInformation.lastDayStockClosePrice)">
  50. {{ Number(stockInformation.currentPrice).toFixed(2) }}
  51. </view>
  52. <view class="stock-current-other">
  53. <view class="stock-current-value" :class="confirmStockColor(stockInformation.currentValue)">
  54. {{ Number(stockInformation.currentValue).toFixed(2) }}
  55. </view>
  56. <view class="stock-current-ratio" :class="confirmStockColor(stockInformation.currentRatio)"> {{ Number(stockInformation.currentRatio).toFixed(2) }}% </view>
  57. </view>
  58. </view>
  59. <view class="stock-other-data">
  60. <view class="first-line">
  61. <view class="high-price">
  62. <view class="value" :class="confirmStockColor(stockInformation.highPrice, stockInformation.lastDayStockClosePrice)">
  63. {{ Number(stockInformation.highPrice).toFixed(2) }}
  64. </view>
  65. </view>
  66. <view class="volume">
  67. <view class="value">
  68. {{ utils.formatStockNumber(stockInformation.volume, 2) }}
  69. </view>
  70. </view>
  71. <view class="volume-ratio">
  72. 量比
  73. <view class="value">
  74. {{ Number(stockInformation.volumeRatio).toFixed(2) }}
  75. </view>
  76. </view>
  77. </view>
  78. <view class="second-line">
  79. <view class="low-price">
  80. <view class="value" :class="confirmStockColor(stockInformation.lowPrice, stockInformation.lastDayStockClosePrice)">
  81. {{ Number(stockInformation.lowPrice).toFixed(2) }}
  82. </view>
  83. </view>
  84. <view class="amount">
  85. <view class="value">
  86. {{ utils.formatStockNumber(stockInformation.amount, 2) }}
  87. </view>
  88. </view>
  89. <view class="market-earn">
  90. 市盈
  91. <view class="value">
  92. {{ Number(stockInformation.marketEarn).toFixed(2) }}
  93. </view>
  94. </view>
  95. </view>
  96. <view class="third-line">
  97. <view class="open-price">
  98. <view class="value" :class="confirmStockColor(stockInformation.openPrice, stockInformation.lastDayStockClosePrice)">
  99. {{ Number(stockInformation.openPrice).toFixed(2) }}
  100. </view>
  101. </view>
  102. <view class="turnover-ratio">
  103. <view class="value"> {{ Number(stockInformation.turnoverRatio).toFixed(2) }}% </view>
  104. </view>
  105. <view class="market-value">
  106. 市值
  107. <view class="value">
  108. {{ utils.formatStockNumber(stockInformation.marketValue, 2) }}
  109. </view>
  110. </view>
  111. </view>
  112. </view>
  113. </view>
  114. <!-- 股票图表 -->
  115. <view class="stock-chart">
  116. <view class="stock-kline-tab">
  117. <!-- 1:分时 2:日K 3:周K 4:月K -->
  118. <view class="tab-time" :class="{ 'tab-selected': klineTab == 1 }" @click="selectKlineTab(1)"> 分时 </view>
  119. <view class="tab-day" :class="{ 'tab-selected': klineTab == 2 }" @click="selectKlineTab(2)"> 日K </view>
  120. <view class="tab-week" :class="{ 'tab-selected': klineTab == 3 }" @click="selectKlineTab(3)"> 周K </view>
  121. <view class="tab-month" :class="{ 'tab-selected': klineTab == 4 }" @click="selectKlineTab(4)"> 月K </view>
  122. <view class="tab-more" :class="{ 'tab-selected': klineTab != 1 && klineTab != 2 && klineTab != 3 && klineTab != 4 }" @click="isMoreTabs ? closeMoreTabs() : openMoreTabs()">
  123. <view class="more-no-choose" v-if="klineTab == 1 || klineTab == 2 || klineTab == 3 || klineTab == 4"> 更多 </view>
  124. <view class="more-choose" v-else>
  125. {{ chooseTabName(klineTab) }}
  126. </view>
  127. <view :class="[isMoreTabs ? 'arrow-down' : 'arrow-up']"> </view>
  128. </view>
  129. <view class="tab-setting">
  130. <image class="tab-setting-img" src="/static/marketSituation-image/marketCondition-image/setting2.png" mode="设置"></image>
  131. </view>
  132. </view>
  133. <view v-if="isMoreTabs" class="moreTabsContainer">
  134. <view v-for="item in moreTabsData" :key="item" class="moreTabItem" :class="{ 'tab-selected': klineTab == item.value }" @click="selectMoreTab(item.value)">
  135. {{ item.title }}
  136. </view>
  137. </view>
  138. <view class="stock-kline">
  139. <view v-if="klineTab === 1 || klineTab === 2 || klineTab === 3 || klineTab === 4 || klineTab === 5 || klineTab === 6 || klineTab === 7 || klineTab === 8 || klineTab === 9" class="time-chart-container" style="position: relative">
  140. <!-- 主图Canvas -->
  141. <canvas
  142. canvas-id="stockChart"
  143. class="stock-chart"
  144. :width="canvasWidth"
  145. :height="canvasHeight"
  146. :style="{
  147. width: canvasWidth + 'px',
  148. height: canvasHeight + 'px',
  149. pointerEvents: 'none',
  150. }"
  151. will-read-frequently="true"
  152. ></canvas>
  153. <!-- 动态数据Canvas层 -->
  154. <canvas
  155. canvas-id="dynamicCanvas"
  156. id="dynamicCanvas"
  157. :width="canvasWidth"
  158. :height="canvasHeight"
  159. :style="{
  160. width: canvasWidth + 'px',
  161. height: canvasHeight + 'px',
  162. pointerEvents: 'none',
  163. position: 'absolute',
  164. top: '0',
  165. left: '0',
  166. }"
  167. will-read-frequently="true"
  168. ></canvas>
  169. <!-- 十字准心Canvas层 -->
  170. <canvas
  171. canvas-id="crosshairCanvas"
  172. id="crosshairCanvas"
  173. :width="canvasWidth"
  174. :height="canvasHeight"
  175. :style="{
  176. width: canvasWidth + 'px',
  177. height: canvasHeight + 'px',
  178. position: 'absolute',
  179. top: '0',
  180. left: '0',
  181. }"
  182. will-read-frequently="true"
  183. @touchstart="touchStart"
  184. @touchmove="touchMove"
  185. @touchend="touchEnd"
  186. ></canvas>
  187. </view>
  188. <!-- K线图区域 -->
  189. <view class="test" v-else-if="klineTab === 10">
  190. <button @click="startTcp()">接收消息</button>
  191. <button @click="sendStopTimeData()">停止消息</button>
  192. <button @click="sendTcpMessage('real_time')">实时行情推送</button>
  193. <button @click="sendTcpMessage('init_real_time')">初始化获取行情历史数据</button>
  194. <button @click="sendTcpMessage('stop_real_time')">停止实时推送</button>
  195. </view>
  196. <view v-else class="kline-chart-container">
  197. <text>K线图开发中...</text>
  198. </view>
  199. </view>
  200. </view>
  201. </view>
  202. <view class="bottomTool">
  203. <view class="index" @click="goToIndexRepository">
  204. <image class="icon" src="/static/marketSituation-image/marketCondition-image/index.png" mode="指标仓库图标"> </image>
  205. 指标仓库
  206. </view>
  207. <view class="function" @click="goToFunction">
  208. <image class="icon" src="/static/marketSituation-image/marketCondition-image/function.png" mode="功能图标"> </image>
  209. 功能
  210. </view>
  211. <view class="favorites" @click="goToFavorites">
  212. <image class="icon" src="/static/marketSituation-image/marketCondition-image/favorites.png" mode="加自选图标"></image>
  213. 加自选
  214. </view>
  215. </view>
  216. </view>
  217. </template>
  218. <script setup>
  219. import { ref, reactive, computed, onMounted, watch, nextTick, onUnmounted, getCurrentInstance } from "vue";
  220. import { onLoad } from "@dcloudio/uni-app";
  221. const instance = getCurrentInstance();
  222. import { prevClosePrice, timeData as testTimeData, klineData as testKlineData } from "@/common/stockTimeInformation.js";
  223. import { throttle } from "@/common/util.js";
  224. import { HCharts } from "@/common/canvasMethod.js";
  225. import tcpConnection, { TCPConnection, TCP_CONFIG } from "../../api/tcpConnection";
  226. import { useMarketSituationStore } from "../../stores/modules/marketSituation.js";
  227. const marketSituationStore = useMarketSituationStore();
  228. // TCP相关响应式变量
  229. const tcpConnected = ref(false);
  230. const connectionListener = ref(null);
  231. const messageListener = ref(null);
  232. // 股票来源
  233. const currentStockFrom = ref();
  234. // 当前股票位置
  235. const currentStockIndex = ref(-1);
  236. const currentStockParentIndex = ref(-1);
  237. // 股票信息栏变量
  238. const stockInformation = ref({
  239. stockName: "----", //股票名称
  240. stockCode: "------", //股票代码
  241. lastDayStockClosePrice: 0.0, //前一日收盘价
  242. currentPrice: 0.0, //当前股价
  243. currentValue: 0.0, //涨跌额度
  244. currentRatio: 0.0, //涨跌幅度
  245. highPrice: 0.0, //最高价
  246. lowPrice: 0.0, //最低价
  247. openPrice: 0.0, //开盘价
  248. closePrice: 0.0, //收盘价
  249. volume: 0.0, //成交量
  250. volumeRatio: 0.0, //成交量比
  251. amount: 0.0, //成交额
  252. marketEarn: 0.0, //市盈
  253. turnoverRatio: 0.0, //换手率
  254. marketValue: 0.0, //市值
  255. });
  256. // 是否展开股票信息细节栏的判断变量
  257. const isStockDetail = ref(false);
  258. // 股票信息细节内容变量
  259. const firstColumData = [
  260. {
  261. title: "振幅",
  262. value: 0,
  263. },
  264. {
  265. title: "现手",
  266. value: 0,
  267. },
  268. {
  269. title: "内盘",
  270. value: 0,
  271. },
  272. {
  273. title: "外盘",
  274. value: 0,
  275. },
  276. {
  277. title: "流通股",
  278. value: 0,
  279. },
  280. {
  281. title: "每股净资产",
  282. value: 0,
  283. },
  284. {
  285. title: "净资产收益率",
  286. value: 0,
  287. },
  288. {
  289. title: "总股本",
  290. value: 0,
  291. },
  292. {
  293. title: "总资本",
  294. value: 0,
  295. },
  296. {
  297. title: "总市值",
  298. value: 0,
  299. },
  300. {
  301. title: "一个月最高",
  302. value: 0,
  303. },
  304. {
  305. title: "一个月最低",
  306. value: 0,
  307. },
  308. ];
  309. const secondColumnData = [
  310. {
  311. title: "均价",
  312. value: 0,
  313. },
  314. {
  315. title: "昨收",
  316. value: 0,
  317. },
  318. {
  319. title: "委比",
  320. value: 0,
  321. },
  322. {
  323. title: "委买",
  324. value: 0,
  325. },
  326. {
  327. title: "委卖",
  328. value: 0,
  329. },
  330. {
  331. title: "市盈利(静)",
  332. value: 0,
  333. },
  334. {
  335. title: "市盈利(动)",
  336. value: 0,
  337. },
  338. {
  339. title: "市净率",
  340. value: 0,
  341. },
  342. {
  343. title: "涨停价",
  344. value: 0,
  345. },
  346. {
  347. title: "跌停价",
  348. value: 0,
  349. },
  350. {
  351. title: "一年最高",
  352. value: 0,
  353. },
  354. {
  355. title: "一年最低",
  356. value: 0,
  357. },
  358. ];
  359. // 是否展开更多Tab的判断变量
  360. const isMoreTabs = ref(false);
  361. const moreTabsData = ref([
  362. {
  363. title: "1分",
  364. value: 5,
  365. },
  366. {
  367. title: "5分",
  368. value: 6,
  369. },
  370. {
  371. title: "15分",
  372. value: 7,
  373. },
  374. {
  375. title: "30分",
  376. value: 8,
  377. },
  378. {
  379. title: "60分",
  380. value: 9,
  381. },
  382. {
  383. title: "季K",
  384. value: 10,
  385. },
  386. {
  387. title: "年K",
  388. value: 11,
  389. },
  390. ]);
  391. // 股票当前选中的K线类型Tab
  392. // 1:分时 2:日K 3:周K 4:月K
  393. const klineTab = ref(1);
  394. const startTcp = () => {
  395. try {
  396. removeTcpListeners();
  397. disconnectTcp();
  398. initTcpListeners();
  399. connectTcp();
  400. } catch (error) {
  401. console.error("建立连接并设置监听出错:", error);
  402. }
  403. };
  404. const sendStopTimeData = () => {
  405. disconnectTcp();
  406. removeTcpListeners();
  407. };
  408. // 确定股票数据的颜色方法
  409. const confirmStockColor = (price, lastDayStockClosePrice) => {
  410. if (typeof lastDayStockClosePrice === "undefined") {
  411. if (price == 0) {
  412. return "price-none";
  413. } else if (price > 0) {
  414. return "price-up";
  415. } else {
  416. return "price-down";
  417. }
  418. } else {
  419. if (price == lastDayStockClosePrice) {
  420. return "price-none";
  421. } else if (price > lastDayStockClosePrice) {
  422. return "price-up";
  423. } else {
  424. return "price-down";
  425. }
  426. }
  427. };
  428. // 股票K线类型方法
  429. const selectKlineTab = (tabId) => {
  430. klineTab.value = tabId;
  431. if (klineTab.value) {
  432. sendTcpMessage("stop_real_time");
  433. }
  434. switch (klineTab.value) {
  435. case 1:
  436. sendTcpMessage("init_real_time");
  437. break;
  438. case 2:
  439. sendTcpMessage("daily_data");
  440. break;
  441. case 3:
  442. sendTcpMessage("weekly_data");
  443. break;
  444. case 4:
  445. sendTcpMessage("monthly_data");
  446. break;
  447. case 5:
  448. sendTcpMessage("daily_one_minutes_data");
  449. break;
  450. case 6:
  451. sendTcpMessage("daily_five_minutes_data");
  452. break;
  453. case 7:
  454. sendTcpMessage("daily_fifteen_minutes_data");
  455. break;
  456. case 8:
  457. sendTcpMessage("daily_thirty_minutes_data");
  458. break;
  459. case 9:
  460. sendTcpMessage("daily_sixty_minutes_data");
  461. break;
  462. case 10:
  463. uni.showToast({
  464. title: "暂无季K数据",
  465. icon: "none",
  466. duration: 2000,
  467. });
  468. break;
  469. case 11:
  470. uni.showToast({
  471. title: "暂无年K数据",
  472. icon: "none",
  473. duration: 2000,
  474. });
  475. break;
  476. default:
  477. break;
  478. }
  479. initCanvas();
  480. // startAddDataTimer();
  481. };
  482. // 返回按钮
  483. const backToHomepage = () => {
  484. const pages = getCurrentPages();
  485. if (pages.length > 1) {
  486. uni.navigateBack();
  487. } else {
  488. // 如果没有上一页,跳转到首页
  489. uni.reLaunch({
  490. url: "/pages/home/home",
  491. });
  492. }
  493. };
  494. const toLeftPage = () => {
  495. if (currentStockIndex.value == 0) {
  496. uni.showToast({
  497. title: "没有更多股票了",
  498. icon: "none",
  499. duration: 1000,
  500. });
  501. return;
  502. } else {
  503. currentStockIndex.value--;
  504. let nextStockInformation;
  505. if (currentStockFrom.value == "marketOverview") {
  506. nextStockInformation = marketSituationStore.cardData[currentStockIndex.value];
  507. } else if (currentStockFrom.value == "marketDetail") {
  508. nextStockInformation = marketSituationStore.marketDetailCardData[currentStockIndex.value];
  509. } else if (currentStockFrom.value == "globalIndex") {
  510. nextStockInformation = marketSituationStore.gloablCardData[currentStockParentIndex.value].list[currentStockIndex.value];
  511. } else {
  512. uni.showToast({
  513. title: "没有更多股票了",
  514. icon: "none",
  515. duration: 1000,
  516. });
  517. return;
  518. }
  519. updateStockInformation(nextStockInformation);
  520. }
  521. };
  522. const toRightPage = () => {
  523. let nextStockInformation;
  524. if (currentStockFrom.value == "marketOverview") {
  525. if (currentStockIndex.value == marketSituationStore.cardData.length) {
  526. uni.showToast({
  527. title: "没有更多股票了",
  528. icon: "none",
  529. duration: 1000,
  530. });
  531. return;
  532. } else {
  533. currentStockIndex.value++;
  534. nextStockInformation = marketSituationStore.cardData[currentStockIndex.value];
  535. updateStockInformation(nextStockInformation);
  536. }
  537. } else if (currentStockFrom.value == "marketDetail") {
  538. if (currentStockIndex.value == marketSituationStore.marketDetailCardData.length) {
  539. uni.showToast({
  540. title: "没有更多股票了",
  541. icon: "none",
  542. duration: 1000,
  543. });
  544. return;
  545. } else {
  546. currentStockIndex.value++;
  547. nextStockInformation = marketSituationStore.marketDetailCardData[currentStockIndex.value];
  548. updateStockInformation(nextStockInformation);
  549. }
  550. } else if (currentStockFrom.value == "globalIndex") {
  551. if (currentStockIndex.value == marketSituationStore.gloablCardData[currentStockParentIndex.value].list.length) {
  552. uni.showToast({
  553. title: "没有更多股票了",
  554. icon: "none",
  555. duration: 1000,
  556. });
  557. return;
  558. } else {
  559. currentStockIndex.value++;
  560. nextStockInformation = marketSituationStore.gloablCardData[currentStockParentIndex.value].list[currentStockIndex.value];
  561. updateStockInformation(nextStockInformation);
  562. }
  563. } else {
  564. uni.showToast({
  565. title: "没有更多股票了",
  566. icon: "none",
  567. duration: 1000,
  568. });
  569. }
  570. };
  571. const updateStockInformation = (stock) => {
  572. klineTab.value = 1;
  573. stockInformation.value.stockName = stock.stockName||stock.name;
  574. stockInformation.value.stockCode = stock.stockCode||stock.code;
  575. sendTcpMessage("stop_real_time");
  576. sendTcpMessage("init_real_time");
  577. };
  578. const goToIndexRepository = () => {
  579. uni.showToast({
  580. title: "指标仓库",
  581. icon: "none",
  582. duration: 1000,
  583. });
  584. };
  585. const goToFunction = () => {
  586. uni.showToast({
  587. title: "功能",
  588. icon: "none",
  589. duration: 1000,
  590. });
  591. };
  592. const goToFavorites = () => {
  593. uni.showToast({
  594. title: "加自选",
  595. icon: "none",
  596. duration: 1000,
  597. });
  598. };
  599. const openStockDetail = () => {
  600. isStockDetail.value = true;
  601. };
  602. const closeStockDetail = () => {
  603. isStockDetail.value = false;
  604. };
  605. const openMoreTabs = () => {
  606. isMoreTabs.value = true;
  607. };
  608. const closeMoreTabs = () => {
  609. isMoreTabs.value = false;
  610. };
  611. const selectMoreTab = (value) => {
  612. selectKlineTab(value);
  613. };
  614. const chooseTabName = () => {
  615. if (klineTab.value === 5) {
  616. return "1分";
  617. } else if (klineTab.value === 6) {
  618. return "5分";
  619. } else if (klineTab.value === 7) {
  620. return "15分";
  621. } else if (klineTab.value === 8) {
  622. return "30分";
  623. } else if (klineTab.value === 9) {
  624. return "60分";
  625. } else if (klineTab.value === 10) {
  626. return "季K";
  627. } else if (klineTab.value === 11) {
  628. return "年K";
  629. }
  630. };
  631. // 画布对象
  632. const canvasWidth = ref(100);
  633. const canvasHeight = ref(100);
  634. const CANVAS_BACKGROUND_COLOR = "#fff";
  635. const TEXT_COLOR1 = "#000";
  636. const TEXT_COLOR2 = "red";
  637. const LINE_COLOR = "#e5e5e5";
  638. const timeChartObject = ref({
  639. min: 0,
  640. max: 0,
  641. });
  642. const klineChartObject = ref({
  643. min: 0,
  644. max: 0,
  645. });
  646. const chartRange = ref();
  647. // 开盘时间
  648. const openTime = "09:30";
  649. // 收盘时间
  650. const closeTime = "15:00";
  651. const ctx = ref(null);
  652. const dynamicCtx = ref(null); // 动态Canvas上下文
  653. const crosshairCtx = ref(null); // 十字准心Canvas上下文
  654. const pixelRatio = ref(1);
  655. // 触屏对象
  656. const touchState = reactive({
  657. startX: 0,
  658. startY: 0,
  659. isMoving: false,
  660. moveDistance: 0,
  661. moveThreshold: 10, //移动阈值(用于判断是否在点击)
  662. scale: 1, // 当前缩放比例
  663. minScale: 0.3, // 最小缩放比例
  664. maxScale: 5, // 最大缩放比例
  665. baseVisibleCount: 40, // 基准可见K线数量
  666. offset: 0, // 数据偏移量
  667. isZooming: false, // 是否正在缩放
  668. initialDistance: 0, // 初始两指距离
  669. initialScale: 1, // 初始缩放比例
  670. });
  671. // 计算当前可见的K线数量
  672. const visibleCount = computed(() => {
  673. return Math.floor(touchState.baseVisibleCount / touchState.scale);
  674. });
  675. // 计算当前显示的数据范围
  676. const visibleDataRange = computed(() => {
  677. const start = Math.max(0, klineData.value.length - visibleCount.value - touchState.offset);
  678. const end = Math.min(klineData.value.length, start + visibleCount.value);
  679. return {
  680. start,
  681. end,
  682. };
  683. });
  684. // 获取当前可见的数据
  685. const visibleKlineData = computed(() => {
  686. const { start, end } = visibleDataRange.value;
  687. return klineData.value.slice(start, end);
  688. });
  689. // 计算两点之间的距离
  690. const getDistance = (touch1, touch2) => {
  691. const dx = touch1.x - touch2.x;
  692. const dy = touch1.y - touch2.y;
  693. return Math.sqrt(dx * dx + dy * dy);
  694. };
  695. // 十字准线相关状态
  696. const crosshair = reactive({
  697. show: false,
  698. x: 0,
  699. y: 0,
  700. currentData: null,
  701. snapToData: true,
  702. });
  703. // 绘制网格和坐标轴
  704. const grid = [
  705. {
  706. top: 20,
  707. bottom: canvasHeight.value * 0.5,
  708. left: 5,
  709. right: 5,
  710. lineColor: LINE_COLOR,
  711. lineWidth: 1,
  712. horizontalLineNum: 5,
  713. verticalLineNum: 5,
  714. },
  715. {
  716. top: canvasHeight.value * 0.5 + 20,
  717. bottom: 20,
  718. left: 5,
  719. right: 5,
  720. lineColor: LINE_COLOR,
  721. lineWidth: 1,
  722. horizontalLineNum: 3,
  723. verticalLineNum: 5,
  724. },
  725. ];
  726. // 绘制网格和坐标轴
  727. const dayGrid = [
  728. {
  729. top: 20,
  730. bottom: canvasHeight.value * 0.5,
  731. left: 5,
  732. right: 5,
  733. lineColor: LINE_COLOR,
  734. lineWidth: 1,
  735. horizontalLineNum: 5,
  736. verticalLineNum: 2,
  737. },
  738. {
  739. top: canvasHeight.value * 0.5 + 20,
  740. bottom: 20,
  741. left: 5,
  742. right: 5,
  743. lineColor: LINE_COLOR,
  744. lineWidth: 1,
  745. horizontalLineNum: 3,
  746. verticalLineNum: 2,
  747. },
  748. {
  749. top: canvasHeight.value * 0.5 + 20,
  750. bottom: 20,
  751. left: 5,
  752. right: 5,
  753. lineColor: LINE_COLOR,
  754. lineWidth: 1,
  755. horizontalLineNum: 3,
  756. verticalLineNum: 2,
  757. },
  758. ];
  759. // 工具函数
  760. const utils = {
  761. // 格式化价格
  762. formatPrice(price) {
  763. return price.toFixed(2);
  764. },
  765. // 计算数据范围
  766. calculateDataRange(data, key) {
  767. if (!data || data.length === 0) {
  768. return {
  769. min: 0,
  770. max: 0,
  771. };
  772. }
  773. const values = data.map((item) => item[key]);
  774. return {
  775. min: Math.min(...values),
  776. max: Math.max(...values),
  777. };
  778. },
  779. // 计算标签
  780. calculateLabel(data, type = 2, preClosePrice = 0, key, num) {
  781. let label = [];
  782. if (key === "price") {
  783. // 分时价格区间
  784. if (type == 1) {
  785. const priceRange = utils.calculateDataRange(data, "price");
  786. const theMost = Math.max(priceRange.max - preClosePrice, preClosePrice - priceRange.min);
  787. const mid = (num - 1) / 2;
  788. // 计算分时价格标签
  789. label[mid] = {
  790. value: utils.formatPrice(preClosePrice),
  791. ratio: utils.formatPrice(0) + "%",
  792. };
  793. for (let i = 0; i < mid; i++) {
  794. label[i] = {
  795. value: utils.formatPrice(preClosePrice + (theMost * (mid - i)) / mid),
  796. ratio: utils.formatPrice((100 * (theMost * (mid - i))) / mid / preClosePrice) + "%",
  797. };
  798. label[num - 1 - i] = {
  799. value: utils.formatPrice(preClosePrice - (theMost * (mid - i)) / mid),
  800. ratio: utils.formatPrice((-1 * 100 * (theMost * (mid - i))) / mid / preClosePrice) + "%",
  801. };
  802. }
  803. chartRange.value.push({
  804. max: preClosePrice + theMost,
  805. min: preClosePrice - theMost,
  806. });
  807. // timeChartObject.value.max = preClosePrice + theMost;
  808. // timeChartObject.value.min = preClosePrice - theMost;
  809. return label;
  810. } else {
  811. const highPriceRange = utils.calculateDataRange(data, "high");
  812. const lowPriceRange = utils.calculateDataRange(data, "low");
  813. const priceRange = {
  814. max: highPriceRange.max * 1.01,
  815. min: lowPriceRange.min * 0.99,
  816. };
  817. const priceDiff = priceRange.max - priceRange.min;
  818. for (let i = 0; i < num; ++i) {
  819. label[i] = {
  820. value: utils.formatPrice(priceRange.max - (i * priceDiff) / (num - 1)),
  821. };
  822. }
  823. chartRange.value.push(priceRange);
  824. // klineChartObject.value.max = highPriceRange.max * 1.01;
  825. // klineChartObject.value.min = lowPriceRange.min * 0.99;
  826. return label;
  827. }
  828. } else if (key === "volume") {
  829. const volumeRange = utils.calculateDataRange(data, "volume");
  830. chartRange.value.push({
  831. max: volumeRange.max,
  832. min: 0,
  833. });
  834. label[0] = {
  835. value: utils.formatPrice(volumeRange.max),
  836. };
  837. label[1] = {
  838. value: utils.formatPrice(0),
  839. };
  840. return label;
  841. }
  842. return null;
  843. },
  844. // 线性插值
  845. lerp(start, end, factor) {
  846. return start + (end - start) * factor;
  847. },
  848. // 根据X坐标找到最近的数据点
  849. findNearestDataPoint(x, pointLen, data, grid, offset) {
  850. if (!data.length) return null;
  851. const width = canvasWidth.value;
  852. const height = canvasHeight.value;
  853. // 计算每个数据点的X坐标间隔
  854. // 倒推 const x=5+(index*(width-10)/pointLen) 已知x求index
  855. const xStep = width - grid[0].left - grid[0].right;
  856. // 计算触摸点对应的数据索引
  857. const touchX = (x - grid[0].left - offset) * pointLen;
  858. let nearestIndex = Math.round(touchX / xStep);
  859. let dataX;
  860. // 确保索引在有效范围内
  861. if (nearestIndex >= 0 && nearestIndex <= data.length - 1) {
  862. dataX = offset + grid[0].left + (nearestIndex * (width - grid[0].left - grid[0].right)) / pointLen;
  863. } else {
  864. dataX = x;
  865. }
  866. nearestIndex = Math.max(0, Math.min(nearestIndex, data.length - 1));
  867. return {
  868. ...data[nearestIndex],
  869. index: nearestIndex,
  870. x: dataX,
  871. };
  872. },
  873. // 根据Y坐标计算价格
  874. calculatePriceFromY(y, data, grid) {
  875. if (!data.length) return 0;
  876. const width = canvasWidth.value;
  877. const height = canvasHeight.value;
  878. // 上下边距1
  879. const topPadding1 = 20;
  880. const bottomPadding1 = height * 0.4;
  881. // 上下边距2
  882. const topPadding2 = height - bottomPadding1 + 40;
  883. const bottomPadding2 = 5;
  884. // 左右边距
  885. const verticalPadding = 5;
  886. let chartY;
  887. let price;
  888. for (let i = 0; i < grid.length; i++) {
  889. if (y >= grid[i].top && y <= height - grid[i].bottom) {
  890. const chartDiff = chartRange.value[i].max - chartRange.value[i].min;
  891. chartY = y - grid[i].top;
  892. price = chartRange.value[i].max - (chartY / (height - grid[i].bottom - grid[i].top)) * chartDiff;
  893. break;
  894. }
  895. }
  896. // if (y >= topPadding1 && y <= height - bottomPadding1) {
  897. // const priceDiff = priceRange.max - priceRange.min;
  898. // chartY = y - topPadding1;
  899. // price = priceRange.max - (chartY / (height - topPadding1 - bottomPadding1)) * priceDiff;
  900. // } else if (y >= topPadding2 && y <= height - bottomPadding2) {
  901. // const volumeRange = utils.calculateDataRange(data, "volume");
  902. // const volumeDiff = volumeRange.max - 0;
  903. // chartY = y - topPadding2;
  904. // price = volumeRange.max - (chartY / (height - topPadding2 - bottomPadding2)) * volumeDiff;
  905. // }
  906. return price;
  907. },
  908. // 股市数值格式化方法
  909. formatStockNumber(value, decimalPlaces = 2) {
  910. const num = Number(value);
  911. if (isNaN(num)) return "0";
  912. const absNum = Math.abs(num);
  913. const sign = num < 0 ? "-" : "";
  914. if (absNum >= 1000000000000) {
  915. // 万亿级别
  916. return sign + (absNum / 1000000000000).toFixed(decimalPlaces) + "万亿";
  917. } else if (absNum >= 100000000) {
  918. // 亿级别
  919. return sign + (absNum / 100000000).toFixed(decimalPlaces) + "亿";
  920. } else if (absNum >= 10000) {
  921. // 万级别
  922. return sign + (absNum / 10000).toFixed(decimalPlaces) + "万";
  923. } else {
  924. // 小于万的直接显示
  925. return sign + absNum.toFixed(decimalPlaces);
  926. }
  927. },
  928. };
  929. let text = [
  930. [
  931. {
  932. name: "领先",
  933. value: "暂无数据",
  934. color: "red",
  935. },
  936. {
  937. name: "价",
  938. value: utils.formatPrice(stockInformation.value.currentPrice),
  939. color: "black",
  940. },
  941. ],
  942. [
  943. {
  944. name: "量",
  945. value: utils.formatStockNumber(stockInformation.value.volume),
  946. color: "green",
  947. },
  948. {
  949. name: "额",
  950. value: "暂无数据",
  951. color: "black",
  952. },
  953. ],
  954. ];
  955. // 示例数据
  956. const timeData = ref([]);
  957. const klineData = ref([]);
  958. const initCanvas = async () => {
  959. try {
  960. crosshair.show = false;
  961. grid[0].bottom = canvasHeight.value * 0.4;
  962. grid[1].top = canvasHeight.value * 0.6 + 30;
  963. dayGrid[0].top = 20;
  964. dayGrid[0].bottom = canvasHeight.value * 0.6;
  965. dayGrid[1].top = canvasHeight.value * 0.4 + 30;
  966. dayGrid[1].bottom = canvasHeight.value * 0.3;
  967. dayGrid[2].top = canvasHeight.value * 0.7 + 20;
  968. dayGrid[2].bottom = 20;
  969. // 等待DOM更新
  970. await nextTick();
  971. ctx.value = uni.createCanvasContext("stockChart", instance.proxy);
  972. // 初始化动态数据Canvas上下文
  973. dynamicCtx.value = uni.createCanvasContext("dynamicCanvas", instance.proxy);
  974. // 初始化十字准心Canvas上下文
  975. crosshairCtx.value = uni.createCanvasContext("crosshairCanvas", instance.proxy);
  976. if (ctx.value) {
  977. // 设置Canvas的内部绘图区域尺寸
  978. ctx.value.canvas = {
  979. width: canvasWidth.value,
  980. height: canvasHeight.value,
  981. };
  982. } else {
  983. console.warn("Canvas上下文未初始化,跳过绘制");
  984. }
  985. // 确保Canvas上下文知道正确的尺寸
  986. if (dynamicCtx.value) {
  987. // 设置Canvas的内部绘图区域尺寸
  988. dynamicCtx.value.canvas = {
  989. width: canvasWidth.value,
  990. height: canvasHeight.value,
  991. };
  992. } else {
  993. console.warn("动态Canvas上下文未初始化,跳过绘制");
  994. }
  995. if (crosshairCtx.value) {
  996. // 设置Canvas的内部绘图区域尺寸
  997. crosshairCtx.value.canvas = {
  998. width: canvasWidth.value,
  999. height: canvasHeight.value,
  1000. };
  1001. } else {
  1002. console.warn("十字准心Canvas上下文未初始化,跳过绘制");
  1003. }
  1004. // 等待DOM更新
  1005. await nextTick();
  1006. console.log("ctx", ctx.value);
  1007. drawChart();
  1008. } catch (error) {
  1009. console.error("初始化Canvas失败:", error);
  1010. }
  1011. };
  1012. // 绘制图表主函数-设置基础数据
  1013. const drawChart = () => {
  1014. if (!ctx.value || !dynamicCtx.value || !crosshairCtx.value) {
  1015. console.warn("Canvas上下文未初始化,跳过绘制");
  1016. return;
  1017. }
  1018. const data = klineTab.value == 1 ? timeData.value : klineData.value;
  1019. console.log("data", data);
  1020. chartRange.value = [];
  1021. // 清除画布
  1022. // HCharts.setCanvasColor(ctx.value, width, height, CANVAS_BACKGROUND_COLOR);
  1023. // HCharts.setCanvasColor(dynamicCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
  1024. // HCharts.setCanvasColor(crosshairCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
  1025. // 根据当前标签绘制对应图表
  1026. if (klineTab.value == 1) {
  1027. console.log("stockInfomaton.lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1028. // 设置标签样式
  1029. const label = [
  1030. {
  1031. text: utils.calculateLabel(data, 1, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum),
  1032. color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2],
  1033. fontSize: 12,
  1034. lineStyle: ["solid", "solid", "solid", "solid", "solid"],
  1035. onlyTwo: false,
  1036. },
  1037. {
  1038. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  1039. color: [TEXT_COLOR1, TEXT_COLOR1],
  1040. lineStyle: ["solid", "solid", "solid"],
  1041. fontSize: 12,
  1042. onlyTwo: true,
  1043. },
  1044. ];
  1045. // 把label加进grid中
  1046. grid[0].label = label[0];
  1047. grid[1].label = label[1];
  1048. drawTimeChart();
  1049. } else {
  1050. // 设置标签样式
  1051. const label = [
  1052. {
  1053. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum),
  1054. color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2],
  1055. lineStyle: ["solid", "dash", "dash", "dash", "solid"],
  1056. fontSize: 12,
  1057. onlyTwo: false,
  1058. },
  1059. {
  1060. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  1061. color: [TEXT_COLOR1, TEXT_COLOR1],
  1062. lineStyle: ["solid", "dash", "solid"],
  1063. fontSize: 12,
  1064. onlyTwo: true,
  1065. },
  1066. {
  1067. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  1068. color: [TEXT_COLOR1, TEXT_COLOR1],
  1069. lineStyle: ["solid", "dash", "solid"],
  1070. fontSize: 12,
  1071. onlyTwo: true,
  1072. },
  1073. ];
  1074. // 把label加进grid中
  1075. dayGrid[0].label = label[0];
  1076. dayGrid[1].label = label[1];
  1077. dayGrid[2].label = label[2];
  1078. // HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, dayGrid, data[0].date, data[data.length - 1].date);
  1079. drawKLineChart();
  1080. }
  1081. // crosshairCtx.value.draw();
  1082. };
  1083. const throttledDrawChart = throttle(drawChart, 50);
  1084. // // 绘制分时图
  1085. const drawTimeChart = () => {
  1086. drawCtxChart(grid);
  1087. drawDynamicCtxChart();
  1088. };
  1089. // // 绘制K线图
  1090. const drawKLineChart = () => {
  1091. drawCtxChart(dayGrid);
  1092. drawDynamicCtxChart();
  1093. };
  1094. const drawCtxChart = (paramGrid) => {
  1095. // 检查Canvas上下文是否已初始化
  1096. if (!ctx.value) {
  1097. console.warn("Canvas上下文未初始化,跳过绘制");
  1098. return;
  1099. }
  1100. // 绘制网格
  1101. HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, paramGrid, openTime, closeTime);
  1102. //执行绘制
  1103. ctx.value.draw();
  1104. };
  1105. const drawDynamicCtxChart = () => {
  1106. if (klineTab.value == 1) {
  1107. //绘制价格曲线
  1108. HCharts.drawPriceLine(dynamicCtx.value, canvasWidth.value, canvasHeight.value, timeData.value, grid, chartRange.value[0]);
  1109. //绘制成交量
  1110. HCharts.drawVolume(
  1111. dynamicCtx.value,
  1112. canvasWidth.value,
  1113. canvasHeight.value,
  1114. timeData.value,
  1115. 2,
  1116. 240,
  1117. grid,
  1118. {
  1119. max: utils.calculateDataRange(timeData.value, "volume").max,
  1120. min: 0,
  1121. },
  1122. 0
  1123. );
  1124. HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, grid);
  1125. HCharts.drawTopPriceDisplay(crosshairCtx.value, grid, text);
  1126. } else {
  1127. drawKLine(dynamicCtx.value);
  1128. //绘制成交量
  1129. HCharts.drawVolume(
  1130. dynamicCtx.value,
  1131. canvasWidth.value,
  1132. canvasHeight.value,
  1133. klineData.value,
  1134. 2,
  1135. klineData.value.length,
  1136. dayGrid,
  1137. {
  1138. max: utils.calculateDataRange(klineData.value, "volume").max,
  1139. min: 0,
  1140. },
  1141. touchState.offset
  1142. );
  1143. //绘制成交量
  1144. HCharts.drawVolume(
  1145. dynamicCtx.value,
  1146. canvasWidth.value,
  1147. canvasHeight.value,
  1148. klineData.value,
  1149. 3,
  1150. klineData.value.length,
  1151. dayGrid,
  1152. {
  1153. max: utils.calculateDataRange(klineData.value, "volume").max,
  1154. min: 0,
  1155. },
  1156. touchState.offset
  1157. );
  1158. HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, dayGrid);
  1159. // drawMovingAverage();
  1160. HCharts.drawTopPriceDisplay(crosshairCtx.value, dayGrid, []);
  1161. }
  1162. //执行绘制
  1163. dynamicCtx.value.draw(false);
  1164. crosshairCtx.value.draw();
  1165. };
  1166. const throttledDrawDynamicCtxChart = throttle(drawDynamicCtxChart, 50);
  1167. // const throttledDrawKLineChart = throttle(drawKLineChart, 10);
  1168. // 绘制K线
  1169. const drawKLine = (ctx) => {
  1170. const data = klineData.value;
  1171. if (!data.length) return;
  1172. const width = canvasWidth.value;
  1173. const height = canvasHeight.value;
  1174. // 计算价格范围
  1175. const priceRange = chartRange.value[0];
  1176. const priceDiff = priceRange.max - priceRange.min;
  1177. const areaWidth = (width - dayGrid[0].left - dayGrid[0].right) / data.length;
  1178. const candleWidth = areaWidth * 0.6;
  1179. data.forEach((item, index) => {
  1180. const x = touchState.offset + dayGrid[0].left + (index * (width - dayGrid[0].left - dayGrid[0].right)) / data.length;
  1181. // 计算坐标
  1182. const highY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.high - priceRange.min) / priceDiff);
  1183. const lowY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.low - priceRange.min) / priceDiff);
  1184. const openY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.open - priceRange.min) / priceDiff);
  1185. const closeY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.close - priceRange.min) / priceDiff);
  1186. // 判断涨跌
  1187. const isRise = item.close >= item.open;
  1188. const color = isRise ? "green" : "red";
  1189. // 绘制上下影线
  1190. ctx.setStrokeStyle(color);
  1191. ctx.setLineWidth(1);
  1192. ctx.beginPath();
  1193. ctx.moveTo(x, highY);
  1194. ctx.lineTo(x, lowY);
  1195. ctx.stroke();
  1196. // 绘制K线实体
  1197. const entityTop = isRise ? closeY : openY;
  1198. const entityBottom = isRise ? openY : closeY;
  1199. const entityHeight = Math.max(Math.abs(entityBottom - entityTop), 1);
  1200. ctx.setFillStyle(color);
  1201. ctx.fillRect(x - candleWidth / 2, entityTop, candleWidth, entityHeight);
  1202. });
  1203. };
  1204. // 绘制移动平均线
  1205. const drawMovingAverage = () => {
  1206. const data = klineData.value;
  1207. if (!data.length) return;
  1208. const ma5 = calculateMA(data, 5);
  1209. const ma10 = calculateMA(data, 10);
  1210. drawMALine(ma5, "#fadb14", "MA5"); // 黄色
  1211. drawMALine(ma10, "#eb2f96", "MA10"); // 粉色
  1212. };
  1213. // 计算移动平均线
  1214. const calculateMA = (data, period) => {
  1215. const result = [];
  1216. for (let i = period - 1; i < data.length; i++) {
  1217. let sum = 0;
  1218. for (let j = 0; j < period; j++) {
  1219. sum += data[i - j].close;
  1220. }
  1221. result.push({
  1222. index: i,
  1223. value: sum / period,
  1224. });
  1225. }
  1226. return result;
  1227. };
  1228. // 绘制均线
  1229. const drawMALine = (maData, color, label) => {
  1230. if (!maData.length) return;
  1231. const width = canvasWidth.value;
  1232. const height = canvasHeight.value;
  1233. const data = klineData.value;
  1234. // 计算价格范围
  1235. const highs = data.map((item) => item.high);
  1236. const lows = data.map((item) => item.low);
  1237. const priceRange = chartRange.value[0];
  1238. const priceDiff = priceRange.max - priceRange.min;
  1239. ctx.value.setStrokeStyle(color);
  1240. ctx.value.setLineWidth(1.5);
  1241. ctx.value.beginPath();
  1242. maData.forEach((point, idx) => {
  1243. const x = 40 + (point.index * (width - 60)) / (data.length - 1);
  1244. const y = 20 + (height - 60) * (1 - (point.value - priceRange.min) / priceDiff);
  1245. if (idx === 0) {
  1246. ctx.value.moveTo(x, y);
  1247. } else {
  1248. ctx.value.lineTo(x, y);
  1249. }
  1250. });
  1251. ctx.value.stroke();
  1252. // 绘制图例
  1253. if (maData.length > 0) {
  1254. const lastPoint = maData[maData.length - 1];
  1255. const x = 40 + (lastPoint.index * (width - 60)) / (data.length - 1);
  1256. const y = 20 + (height - 60) * (1 - (lastPoint.value - priceRange.min) / priceDiff);
  1257. HCharts.drawText(ctx.value`${label}: ${utils.formatPrice(lastPoint.value)}`, x + 10, y - 5, 12, color);
  1258. }
  1259. };
  1260. // 触摸事件处理
  1261. const touchStart = (e) => {
  1262. e.preventDefault(); // 阻止页面滚动等默认行为
  1263. if (typeof e.touches[1] === "undefined") {
  1264. touchState.startX = e.touches[0].x;
  1265. touchState.startY = e.touches[0].y;
  1266. touchState.isMoving = false;
  1267. } else if (typeof e.touches[1] !== "undefined" && !crosshair.show) {
  1268. // touchState.startX1 = e.touches[0].x;
  1269. // touchState.startY1 = e.touches[0].y;
  1270. // touchState.startX2 = e.touches[1].x;
  1271. // touchState.startY2 = e.touches[1].y;
  1272. // touchState.isMoving = false;
  1273. }
  1274. };
  1275. const touchMove = (e) => {
  1276. touchState.isMoving = true;
  1277. // 计算移动距离
  1278. const currentX = e.touches[0].x;
  1279. const currentY = e.touches[0].y;
  1280. const deltaX = currentX - touchState.startX;
  1281. const deltaY = currentY - touchState.startY;
  1282. touchState.moveDistance = Math.max(touchState.moveDistance, Math.sqrt(deltaX * deltaX + deltaY * deltaY));
  1283. if (crosshair.show) {
  1284. if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) {
  1285. throttledUpdateCrosshair(currentX, currentY);
  1286. } else {
  1287. // 如果移出图表区域,隐藏十字准线
  1288. // crosshair.show = false;
  1289. if (klineTab.value === 1) {
  1290. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1291. } else {
  1292. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1293. }
  1294. }
  1295. } else {
  1296. return;
  1297. if (klineTab.value === 2) {
  1298. // if(currentY)
  1299. if (currentX < touchState.startX) {
  1300. touchState.offset += currentX - touchState.startX;
  1301. throttledDrawDynamicCtxChart();
  1302. touchState.startX = currentX;
  1303. } else {
  1304. touchState.offset += currentX - touchState.startX;
  1305. throttledDrawDynamicCtxChart();
  1306. touchState.startX = currentX;
  1307. }
  1308. if (canvasWidth.value + touchState.offset < 30) {
  1309. touchState.offset = 0;
  1310. }
  1311. }
  1312. }
  1313. };
  1314. const touchEnd = (e) => {
  1315. // 触摸结束,隐藏十字准线
  1316. if (touchState.moveDistance < touchState.moveThreshold) {
  1317. // 移动距离小于阈值,认为是点击事件
  1318. crosshair.show = !crosshair.show;
  1319. }
  1320. if (crosshair.show) {
  1321. const currentX = e.changedTouches[0].x;
  1322. const currentY = e.changedTouches[0].y;
  1323. if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) {
  1324. throttledUpdateCrosshair(currentX, currentY);
  1325. }
  1326. }
  1327. if (klineTab.value === 1) {
  1328. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1329. } else {
  1330. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1331. }
  1332. // 重置移动距离
  1333. touchState.moveDistance = 0;
  1334. };
  1335. // 检查坐标是否在图表区域内
  1336. const isInChartArea = (x, y, grid) => {
  1337. const width = canvasWidth.value;
  1338. const height = canvasHeight.value;
  1339. if (x < grid[0].left || x > width - grid[0].right) return false;
  1340. for (let i = 0; i < grid.length; i++) {
  1341. if (y >= grid[i].top && y <= height - grid[i].bottom) return true;
  1342. }
  1343. return false;
  1344. };
  1345. // 更新十字准线位置和数据
  1346. const updateCrosshair = (x, y) => {
  1347. if (!crosshair.show) return;
  1348. // 更新Y坐标以匹配实际价格
  1349. const data = klineTab.value === 1 ? timeData.value : klineData.value;
  1350. const width = canvasWidth.value;
  1351. const height = canvasHeight.value;
  1352. crosshair.x = x;
  1353. crosshair.y = y;
  1354. const nearestData = utils.findNearestDataPoint(x, klineTab.value === 1 ? 240 : data.length, data, klineTab.value === 1 ? grid : dayGrid, klineTab.value === 1 ? 0 : touchState.offset);
  1355. crosshair.x = nearestData.x;
  1356. stockInformation.value.currentPrice = nearestData.price || nearestData.close;
  1357. stockInformation.value.volume = nearestData.volume;
  1358. // 涨跌额度
  1359. if (klineTab.value == 1) {
  1360. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1361. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1362. } else {
  1363. stockInformation.value.openPrice = nearestData.open;
  1364. stockInformation.value.highPrice = nearestData.high;
  1365. stockInformation.value.lowPrice = nearestData.low;
  1366. if (nearestData.index != 0) {
  1367. stockInformation.value.lastDayStockClosePrice = data[nearestData.index - 1].close;
  1368. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1369. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1370. }
  1371. }
  1372. if (klineTab.value === 1 && crosshair.snapToData) {
  1373. // 吸附到最近的数据点
  1374. if (nearestData) {
  1375. crosshair.currentData = nearestData;
  1376. const priceDiff = chartRange.value[0].max - chartRange.value[0].min;
  1377. crosshair.y = grid[0].top + (height - grid[0].bottom - grid[0].top) * (1 - (nearestData.price - chartRange.value[0].min) / priceDiff);
  1378. }
  1379. } else {
  1380. // 自由移动模式
  1381. const currentPrice = utils.calculatePriceFromY(y, data, klineTab.value === 1 ? grid : dayGrid);
  1382. crosshair.currentData = {
  1383. ...nearestData,
  1384. price: currentPrice,
  1385. };
  1386. }
  1387. if (crosshair?.currentData?.volume) {
  1388. text[0][1].value = crosshair.currentData.price;
  1389. text[1][0].value = crosshair.currentData.volume;
  1390. }
  1391. // 只重绘十字准心,不重绘整个图表
  1392. if (klineTab.value === 1) {
  1393. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1394. } else {
  1395. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1396. }
  1397. };
  1398. const throttledUpdateCrosshair = throttle(updateCrosshair, 10);
  1399. // 缩放控制函数
  1400. const zoomIn = () => {
  1401. touchState.scale = Math.min(touchState.maxScale, touchState.scale * 1.2);
  1402. drawChart();
  1403. };
  1404. const zoomOut = () => {
  1405. touchState.scale = Math.max(touchState.minScale, touchState.scale / 1.2);
  1406. drawChart();
  1407. };
  1408. // 监听数据变化
  1409. // watch([timeData, klineData], () => {
  1410. // console.log("数据变化");
  1411. // drawChart();
  1412. // });
  1413. watch(klineTab, () => {
  1414. console.log("标签页变化");
  1415. drawChart();
  1416. });
  1417. // 初始化TCP监听器
  1418. const initTcpListeners = () => {
  1419. // 创建连接状态监听器并保存引用
  1420. connectionListener.value = (status, result) => {
  1421. tcpConnected.value = status === "connected";
  1422. console.log("TCP连接状态变化:", status, tcpConnected.value);
  1423. // 显示连接状态提示
  1424. if (status === "connected") {
  1425. if (klineTab.value == 1) {
  1426. sendTcpMessage("init_real_time");
  1427. }
  1428. }
  1429. };
  1430. // 创建消息监听器并保存引用
  1431. messageListener.value = (type, message, parsedArray) => {
  1432. const messageObj = {
  1433. type: type,
  1434. content: message,
  1435. parsedArray: parsedArray,
  1436. timestamp: new Date().toLocaleTimeString(),
  1437. direction: "received",
  1438. };
  1439. // 解析股票数据
  1440. parseStockData(message);
  1441. };
  1442. // 注册监听器
  1443. tcpConnection.onConnectionChange(connectionListener.value);
  1444. tcpConnection.onMessage(messageListener.value);
  1445. };
  1446. // 连接TCP服务器
  1447. const connectTcp = () => {
  1448. console.log("开始连接TCP服务器...");
  1449. tcpConnection.connect();
  1450. };
  1451. // 断开TCP连接
  1452. const disconnectTcp = () => {
  1453. console.log("断开TCP连接...");
  1454. tcpConnection.disconnect();
  1455. tcpConnected.value = false;
  1456. };
  1457. // 发送TCP消息
  1458. const sendTcpMessage = (command) => {
  1459. let messageData;
  1460. switch (command) {
  1461. // 实时行情推送
  1462. case "real_time":
  1463. messageData = {
  1464. command: "real_time",
  1465. stock_code: "SH.000001",
  1466. };
  1467. break;
  1468. // 初始化获取行情历史数据
  1469. case "init_real_time":
  1470. messageData = {
  1471. command: "init_real_time",
  1472. stock_code: "SH.000001",
  1473. };
  1474. break;
  1475. case "stop_real_time":
  1476. messageData = {
  1477. command: "stop_real_time",
  1478. };
  1479. break;
  1480. // 股票列表
  1481. case "stock_list":
  1482. messageData = {
  1483. command: "stock_list",
  1484. };
  1485. break;
  1486. // 日线数据
  1487. case "daily_data":
  1488. messageData = {
  1489. command: "daily_data",
  1490. stock_code: "GBPAUD.FXCM",
  1491. start_date: "20250801",
  1492. end_date: "20251029",
  1493. };
  1494. break;
  1495. // 周线数据
  1496. case "weekly_data":
  1497. messageData = {
  1498. command: "weekly_data",
  1499. stock_code: "000001.SZ",
  1500. start_date: "2024912",
  1501. end_date: "20251029",
  1502. };
  1503. break;
  1504. // 周线数据
  1505. case "monthly_data":
  1506. messageData = {
  1507. command: "monthly_data",
  1508. stock_code: "000001.SZ",
  1509. start_date: "20201130",
  1510. end_date: "20251029",
  1511. };
  1512. break;
  1513. // 1分钟线数据
  1514. case "daily_one_minutes_data":
  1515. messageData = {
  1516. command: "daily_one_minutes_data",
  1517. stock_code: "000001.SZ",
  1518. };
  1519. break;
  1520. // 5分钟线数据
  1521. case "daily_five_minutes_data":
  1522. messageData = {
  1523. command: "daily_five_minutes_data",
  1524. stock_code: "000001.SZ",
  1525. };
  1526. break;
  1527. // 15分钟线数据
  1528. case "daily_fifteen_minutes_data":
  1529. messageData = {
  1530. command: "daily_fifteen_minutes_data",
  1531. stock_code: "000001.SZ",
  1532. };
  1533. break;
  1534. // 30分钟线数据
  1535. case "daily_thirty_minutes_data":
  1536. messageData = {
  1537. command: "daily_thirty_minutes_data",
  1538. stock_code: "000001.SZ",
  1539. };
  1540. break;
  1541. // 60分钟线数据
  1542. case "daily_sixty_minutes_data":
  1543. messageData = {
  1544. command: "daily_sixty_minutes_data",
  1545. stock_code: "000001.SZ",
  1546. };
  1547. break;
  1548. case "batch_real_time":
  1549. messageData = {
  1550. command: "batch_real_time",
  1551. stock_codes: ["SH.000001", "SH.000002", "SH.000003", "SH.000004", "SH.000005"],
  1552. };
  1553. break;
  1554. case "help":
  1555. messageData = {
  1556. command: "help",
  1557. };
  1558. break;
  1559. }
  1560. if (!messageData) {
  1561. return;
  1562. } else {
  1563. try {
  1564. // 发送消息
  1565. const success = tcpConnection.send(messageData);
  1566. if (success) {
  1567. console.log("home发送TCP消息:", messageData);
  1568. }
  1569. } catch (error) {
  1570. console.error("发送TCP消息时出错:", error);
  1571. }
  1572. }
  1573. };
  1574. // 获取TCP连接状态
  1575. const getTcpStatus = () => {
  1576. const status = tcpConnection.getConnectionStatus();
  1577. uni.showModal({
  1578. title: "TCP连接状态",
  1579. content: `当前状态: ${status ? "已连接" : "未连接"}`,
  1580. showCancel: false,
  1581. });
  1582. };
  1583. let isMorePacket = {
  1584. init_real_time: false,
  1585. daily_data: false,
  1586. weekly_data: false,
  1587. monthly_data: false,
  1588. daily_one_minutes_data: false,
  1589. daily_five_minutes_data: false,
  1590. daily_fifteen_minutes_data: false,
  1591. daily_thirty_minutes_data: false,
  1592. daily_sixty_minutes_data: false,
  1593. };
  1594. let receivedMessage;
  1595. const findJsonPacket = (message, command) => {
  1596. let jsonStartIndex = 0;
  1597. let jsonEndIndex = message.indexOf(command);
  1598. let jsonStartCount = 0;
  1599. let jsonEndCount = 0;
  1600. for (let i = 0; i < message.length - 1; ++i) {
  1601. if (message[i] == "{") {
  1602. jsonStartCount++;
  1603. if (jsonStartCount == 2) {
  1604. jsonStartIndex = i;
  1605. break;
  1606. }
  1607. }
  1608. }
  1609. for (let i = message.indexOf(command); i >= 0; --i) {
  1610. if (message[i] == "}" || i == jsonStartIndex) {
  1611. jsonEndCount++;
  1612. if (jsonEndCount == 1) {
  1613. jsonEndIndex = i;
  1614. break;
  1615. }
  1616. }
  1617. }
  1618. // 检查JSON字符串是否有效
  1619. if (jsonStartIndex >= jsonEndIndex) {
  1620. return { error: true };
  1621. }
  1622. return { json: JSON.parse(message.substring(jsonStartIndex, jsonEndIndex + 1)) };
  1623. };
  1624. // 根据timeData中最后一个时间生成下一个时间
  1625. const generateNextTime = () => {
  1626. if (timeData.value.length === 0) {
  1627. return "09:30"; // 如果没有数据,返回开盘时间
  1628. }
  1629. const lastTime = timeData.value[timeData.value.length - 1].time;
  1630. if (!lastTime) {
  1631. return "09:30";
  1632. }
  1633. // 解析时间字符串,格式为 "HH:MM"
  1634. const [hours, minutes] = lastTime.split(":").map(Number);
  1635. // 计算下一分钟
  1636. let nextMinutes = minutes + 1;
  1637. let nextHours = hours;
  1638. // 处理分钟进位
  1639. if (nextMinutes >= 60) {
  1640. nextMinutes = 0;
  1641. nextHours += 1;
  1642. }
  1643. // 处理小时进位(24小时制)
  1644. if (nextHours >= 24) {
  1645. nextHours = 0;
  1646. }
  1647. // 格式化为 "HH:MM" 格式
  1648. const formattedHours = nextHours.toString().padStart(2, "0");
  1649. const formattedMinutes = nextMinutes.toString().padStart(2, "0");
  1650. return `${formattedHours}:${formattedMinutes}`;
  1651. };
  1652. // 解析TCP股票数据
  1653. const parseStockData = (message) => {
  1654. try {
  1655. console.log("进入parseStockData, message类型:", typeof message);
  1656. let parsedMessage;
  1657. // 如果isMorePacket是true,说明正在接受分包数据,无条件接收
  1658. // 如果message是字符串且以{开头,说明是JSON字符串,需要解析
  1659. // 如果不属于以上两种情况,说明是普通字符串,不预解析
  1660. if (message.includes("欢迎连接到股票数据服务器")) {
  1661. console.log("服务器命令列表,不予处理");
  1662. return;
  1663. }
  1664. if (message.includes("real_time")) {
  1665. let startIndex = 0;
  1666. let endIndex = message.length;
  1667. for (let i = 0; i < message.length - 1; ++i) {
  1668. if (message[i] == "{") {
  1669. startIndex = i;
  1670. break;
  1671. }
  1672. }
  1673. for (let i = message.length - 1; i >= 0; --i) {
  1674. if (message[i] == "}") {
  1675. endIndex = i;
  1676. break;
  1677. }
  1678. }
  1679. parsedMessage = JSON.parse(message.substring(startIndex, endIndex + 1));
  1680. console.log("实时数据解析", parsedMessage);
  1681. // 处理实时数据
  1682. timeData.value.push({
  1683. time: generateNextTime(),
  1684. price: parsedMessage.current_price,
  1685. volume: parsedMessage.volume,
  1686. amount: parsedMessage.amount,
  1687. });
  1688. // 实时更新股票信息
  1689. stockInformation.value.currentPrice = parsedMessage.current_price;
  1690. stockInformation.value.openPrice = parsedMessage.open_price;
  1691. stockInformation.value.closePrice = parsedMessage.close_price;
  1692. stockInformation.value.highPrice = parsedMessage.high_price;
  1693. stockInformation.value.lowPrice = parsedMessage.low_price;
  1694. stockInformation.value.volume = parsedMessage.volume;
  1695. stockInformation.value.amount = parsedMessage.amount;
  1696. stockInformation.value.turnoverRatio = parsedMessage.turnover_ratio;
  1697. stockInformation.value.marketValue = parsedMessage.total_market_value;
  1698. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1699. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1700. console.log("重绘画面");
  1701. drawChart();
  1702. if (timeData.value.length >= 240) {
  1703. sendTcpMessage("stop_real_time");
  1704. }
  1705. return;
  1706. } else if ((typeof message === "string" && message.includes("init_real_data_start")) || isMorePacket.init_real_time) {
  1707. if (typeof message === "string" && message.includes("init_real_data_start")) {
  1708. console.log("开始接受分包数据");
  1709. receivedMessage = "";
  1710. } else {
  1711. console.log("接收分包数据过程中");
  1712. }
  1713. isMorePacket.init_real_time = true;
  1714. receivedMessage += message;
  1715. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1716. if (receivedMessage.includes("init_real_data_complete")) {
  1717. console.log("接受分包数据结束");
  1718. isMorePacket.init_real_time = false;
  1719. console.log("展示数据", receivedMessage);
  1720. const result = findJsonPacket(receivedMessage, "init_real_data_complete");
  1721. if (result.error) {
  1722. throw new Error("解析JSON字符串失败");
  1723. } else {
  1724. parsedMessage = result.json;
  1725. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1726. if (parsedMessage.type === "daily_data") {
  1727. timeData.value = parsedMessage.data;
  1728. stockInformation.value.lastDayStockClosePrice = parsedMessage.pre_close;
  1729. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1730. drawChart();
  1731. sendTcpMessage("stop_real_time");
  1732. sendTcpMessage("real_time");
  1733. }
  1734. }
  1735. }
  1736. } else if ((typeof message === "string" && message.includes("daily_data_start")) || isMorePacket.daily_data) {
  1737. if (typeof message === "string" && message.includes("daily_data_start")) {
  1738. console.log("开始接受分包数据");
  1739. receivedMessage = "";
  1740. } else {
  1741. console.log("接收分包数据过程中");
  1742. }
  1743. isMorePacket.daily_data = true;
  1744. receivedMessage += message;
  1745. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1746. if (receivedMessage.includes("daily_data_complete")) {
  1747. console.log("接受分包数据结束");
  1748. isMorePacket.daily_data = false;
  1749. console.log("展示数据", receivedMessage);
  1750. const result = findJsonPacket(receivedMessage, "daily_data_complete");
  1751. if (result.error) {
  1752. throw new Error("解析JSON字符串失败");
  1753. } else {
  1754. parsedMessage = result.json;
  1755. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1756. if (parsedMessage.type === "daily_data") {
  1757. klineData.value = parsedMessage.data.map((item) => ({
  1758. open: item.ask_open,
  1759. close: item.ask_close,
  1760. high: item.ask_high,
  1761. low: item.ask_low,
  1762. volume: item.tick_qty,
  1763. date: item.trade_date ? `${item.trade_date.slice(0, 4)}-${item.trade_date.slice(4, 6)}-${item.trade_date.slice(6, 8)}` : item.trade_date,
  1764. }));
  1765. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1766. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1767. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1768. drawChart();
  1769. }
  1770. }
  1771. }
  1772. } else if ((typeof message === "string" && message.includes("weekly_data_start")) || isMorePacket.weekly_data) {
  1773. if (typeof message === "string" && message.includes("weekly_data_start")) {
  1774. console.log("开始接受分包数据");
  1775. receivedMessage = "";
  1776. } else {
  1777. console.log("接收分包数据过程中");
  1778. }
  1779. isMorePacket.weekly_data = true;
  1780. receivedMessage += message;
  1781. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1782. if (receivedMessage.includes("weekly_data_complete")) {
  1783. console.log("接受分包数据结束");
  1784. isMorePacket.weekly_data = false;
  1785. console.log("展示数据", receivedMessage);
  1786. const result = findJsonPacket(receivedMessage, "weekly_data_complete");
  1787. if (result.error) {
  1788. throw new Error("解析JSON字符串失败");
  1789. } else {
  1790. parsedMessage = result.json;
  1791. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1792. if (parsedMessage.type === "weekly_data") {
  1793. klineData.value = parsedMessage.data.map((item) => ({
  1794. open: item.bid_open,
  1795. close: item.bid_close,
  1796. high: item.bid_high,
  1797. low: item.bid_low,
  1798. volume: item.vol,
  1799. amount: item.amount,
  1800. date: item.trade_date ? `${item.trade_date.slice(0, 4)}-${item.trade_date.slice(4, 6)}-${item.trade_date.slice(6, 8)}` : item.trade_date,
  1801. }));
  1802. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1803. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1804. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1805. drawChart();
  1806. }
  1807. }
  1808. }
  1809. } else if ((typeof message === "string" && message.includes("monthly_data_start")) || isMorePacket.monthly_data) {
  1810. if (typeof message === "string" && message.includes("monthly_data_start")) {
  1811. console.log("开始接受分包数据");
  1812. receivedMessage = "";
  1813. } else {
  1814. console.log("接收分包数据过程中");
  1815. }
  1816. isMorePacket.monthly_data = true;
  1817. receivedMessage += message;
  1818. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1819. if (receivedMessage.includes("monthly_data_complete")) {
  1820. console.log("接受分包数据结束");
  1821. isMorePacket.monthly_data = false;
  1822. console.log("展示数据", receivedMessage);
  1823. const result = findJsonPacket(receivedMessage, "monthly_data_complete");
  1824. if (result.error) {
  1825. throw new Error("解析JSON字符串失败");
  1826. } else {
  1827. parsedMessage = result.json;
  1828. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1829. if (parsedMessage.type === "monthly_data") {
  1830. klineData.value = parsedMessage.data.map((item) => ({
  1831. open: item.bid_open,
  1832. close: item.bid_close,
  1833. high: item.bid_high,
  1834. low: item.bid_low,
  1835. volume: item.vol,
  1836. amount: item.amount,
  1837. date: item.trade_date ? `${item.trade_date.slice(0, 4)}-${item.trade_date.slice(4, 6)}-${item.trade_date.slice(6, 8)}` : item.trade_date,
  1838. }));
  1839. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1840. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1841. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1842. drawChart();
  1843. }
  1844. }
  1845. }
  1846. } else if ((typeof message === "string" && message.includes("daily_one_minutes_data_start")) || isMorePacket.daily_one_minutes_data) {
  1847. if (typeof message === "string" && message.includes("daily_one_minutes_data_start")) {
  1848. console.log("开始接受分包数据");
  1849. receivedMessage = "";
  1850. } else {
  1851. console.log("接收分包数据过程中");
  1852. }
  1853. isMorePacket.daily_one_minutes_data = true;
  1854. receivedMessage += message;
  1855. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1856. if (receivedMessage.includes("daily_one_minutes_data_complete")) {
  1857. console.log("接受分包数据结束");
  1858. isMorePacket.daily_one_minutes_data = false;
  1859. console.log("展示数据", receivedMessage);
  1860. const result = findJsonPacket(receivedMessage, "daily_one_minutes_data_complete");
  1861. if (result.error) {
  1862. throw new Error("解析JSON字符串失败");
  1863. } else {
  1864. parsedMessage = result.json;
  1865. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1866. if (parsedMessage.type === "daily_one_minutes_data") {
  1867. klineData.value = parsedMessage.data.map((item) => ({
  1868. open: item.open,
  1869. close: item.close,
  1870. high: Math.max(item.low, item.high),
  1871. low: Math.min(item.low, item.high),
  1872. volume: item.volume,
  1873. amount: item.amount,
  1874. date: item.timestamp,
  1875. }));
  1876. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1877. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1878. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1879. drawChart();
  1880. }
  1881. }
  1882. }
  1883. } else if ((typeof message === "string" && message.includes("daily_five_minutes_data_start")) || isMorePacket.daily_five_minutes_data) {
  1884. if (typeof message === "string" && message.includes("daily_five_minutes_data_start")) {
  1885. console.log("开始接受分包数据");
  1886. receivedMessage = "";
  1887. } else {
  1888. console.log("接收分包数据过程中");
  1889. }
  1890. isMorePacket.daily_five_minutes_data = true;
  1891. receivedMessage += message;
  1892. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1893. if (receivedMessage.includes("daily_five_minutes_data_complete")) {
  1894. console.log("接受分包数据结束");
  1895. isMorePacket.daily_five_minutes_data = false;
  1896. console.log("展示数据", receivedMessage);
  1897. const result = findJsonPacket(receivedMessage, "daily_five_minutes_data_complete");
  1898. if (result.error) {
  1899. throw new Error("解析JSON字符串失败");
  1900. } else {
  1901. parsedMessage = result.json;
  1902. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1903. if (parsedMessage.type === "daily_five_minutes_data") {
  1904. klineData.value = parsedMessage.data.map((item) => ({
  1905. open: item.open,
  1906. close: item.close,
  1907. high: Math.max(item.low, item.high),
  1908. low: Math.min(item.low, item.high),
  1909. volume: item.volume,
  1910. amount: item.amount,
  1911. date: item.timestamp,
  1912. }));
  1913. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1914. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1915. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1916. drawChart();
  1917. }
  1918. }
  1919. }
  1920. } else if ((typeof message === "string" && message.includes("daily_fifteen_minutes_data_start")) || isMorePacket.daily_fifteen_minutes_data) {
  1921. if (typeof message === "string" && message.includes("daily_fifteen_minutes_data_start")) {
  1922. console.log("开始接受分包数据");
  1923. receivedMessage = "";
  1924. } else {
  1925. console.log("接收分包数据过程中");
  1926. }
  1927. isMorePacket.daily_fifteen_minutes_data = true;
  1928. receivedMessage += message;
  1929. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1930. if (receivedMessage.includes("daily_fifteen_minutes_data_complete")) {
  1931. console.log("接受分包数据结束");
  1932. isMorePacket.daily_fifteen_minutes_data = false;
  1933. console.log("展示数据", receivedMessage);
  1934. const result = findJsonPacket(receivedMessage, "daily_fifteen_minutes_data_complete");
  1935. if (result.error) {
  1936. throw new Error("解析JSON字符串失败");
  1937. } else {
  1938. parsedMessage = result.json;
  1939. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1940. if (parsedMessage.type === "daily_fifteen_minutes_data") {
  1941. klineData.value = parsedMessage.data.map((item) => ({
  1942. open: item.open,
  1943. close: item.close,
  1944. high: Math.max(item.low, item.high),
  1945. low: Math.min(item.low, item.high),
  1946. volume: item.volume,
  1947. amount: item.amount,
  1948. date: item.timestamp,
  1949. }));
  1950. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1951. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1952. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1953. drawChart();
  1954. }
  1955. }
  1956. }
  1957. } else if ((typeof message === "string" && message.includes("daily_thirty_minutes_data_start")) || isMorePacket.daily_thirty_minutes_data) {
  1958. if (typeof message === "string" && message.includes("daily_thirty_minutes_data_start")) {
  1959. console.log("开始接受分包数据");
  1960. receivedMessage = "";
  1961. } else {
  1962. console.log("接收分包数据过程中");
  1963. }
  1964. isMorePacket.daily_thirty_minutes_data = true;
  1965. receivedMessage += message;
  1966. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  1967. if (receivedMessage.includes("daily_thirty_minutes_data_complete")) {
  1968. console.log("接受分包数据结束");
  1969. isMorePacket.daily_thirty_minutes_data = false;
  1970. console.log("展示数据", receivedMessage);
  1971. const result = findJsonPacket(receivedMessage, "daily_thirty_minutes_data_complete");
  1972. if (result.error) {
  1973. throw new Error("解析JSON字符串失败");
  1974. } else {
  1975. parsedMessage = result.json;
  1976. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  1977. if (parsedMessage.type === "daily_thirty_minutes_data") {
  1978. klineData.value = parsedMessage.data.map((item) => ({
  1979. open: item.open,
  1980. close: item.close,
  1981. high: Math.max(item.low, item.high),
  1982. low: Math.min(item.low, item.high),
  1983. volume: item.volume,
  1984. amount: item.amount,
  1985. date: item.timestamp,
  1986. }));
  1987. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  1988. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  1989. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  1990. drawChart();
  1991. }
  1992. }
  1993. }
  1994. } else if ((typeof message === "string" && message.includes("daily_sixty_minutes_data_start")) || isMorePacket.daily_sixty_minutes_data) {
  1995. if (typeof message === "string" && message.includes("daily_sixty_minutes_data_start")) {
  1996. console.log("开始接受分包数据");
  1997. receivedMessage = "";
  1998. } else {
  1999. console.log("接收分包数据过程中");
  2000. }
  2001. isMorePacket.daily_sixty_minutes_data = true;
  2002. receivedMessage += message;
  2003. // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
  2004. if (receivedMessage.includes("daily_sixty_minutes_data_complete")) {
  2005. console.log("接受分包数据结束");
  2006. isMorePacket.daily_sixty_minutes_data = false;
  2007. console.log("展示数据", receivedMessage);
  2008. const result = findJsonPacket(receivedMessage, "daily_sixty_minutes_data_complete");
  2009. if (result.error) {
  2010. throw new Error("解析JSON字符串失败");
  2011. } else {
  2012. parsedMessage = result.json;
  2013. console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage);
  2014. if (parsedMessage.type === "daily_sixty_minutes_data") {
  2015. klineData.value = parsedMessage.data.map((item) => ({
  2016. open: item.open,
  2017. close: item.close,
  2018. high: Math.max(item.low, item.high),
  2019. low: Math.min(item.low, item.high),
  2020. volume: item.volume,
  2021. amount: item.amount,
  2022. date: item.timestamp,
  2023. }));
  2024. stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close;
  2025. touchState.offset = canvasWidth.value / klineData.value.length / 2;
  2026. console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice);
  2027. drawChart();
  2028. }
  2029. }
  2030. }
  2031. } else {
  2032. // 没有通过JSON解析判断,说明不是需要的数据
  2033. console.log("不是需要的数据,不做处理");
  2034. }
  2035. } catch (error) {
  2036. console.error("解析TCP股票数据失败:", error.message);
  2037. console.error("错误详情:", error);
  2038. }
  2039. };
  2040. // 移除TCP监听器
  2041. const removeTcpListeners = () => {
  2042. if (connectionListener.value) {
  2043. tcpConnection.removeConnectionListener(connectionListener.value);
  2044. connectionListener.value = null;
  2045. console.log("已移除TCP连接状态监听器");
  2046. }
  2047. if (messageListener.value) {
  2048. tcpConnection.removeMessageListener(messageListener.value);
  2049. messageListener.value = null;
  2050. console.log("已移除TCP消息监听器");
  2051. }
  2052. };
  2053. // 定时器标识(用于清除定时器)
  2054. let timer = null;
  2055. let index = 0;
  2056. // 定时添加数据的函数
  2057. const startAddDataTimer = () => {
  2058. if (timer) {
  2059. console.log("存在旧定时器,卸载旧定时器");
  2060. clearInterval(timer);
  2061. }
  2062. console.log("开始定时任务");
  2063. // 每隔5秒执行一次
  2064. timer = setInterval(() => {
  2065. if (index < testTimeData.length) {
  2066. timeData.value.push(testTimeData[index]);
  2067. console.log("新增数据:", testTimeData[index]);
  2068. // 触发图表重新绘制
  2069. drawChart();
  2070. index++;
  2071. } else {
  2072. clearInterval(timer);
  2073. }
  2074. }, 2000); // 5000毫秒 = 5秒
  2075. };
  2076. onLoad((options) => {
  2077. console.log("页面接收到的参数:", options);
  2078. // 处理通过stockInformation参数传递的复杂对象
  2079. if (options.stockInformation) {
  2080. try {
  2081. const stockData = JSON.parse(decodeURIComponent(options.stockInformation));
  2082. console.log("解析的股票数据:", stockData);
  2083. // 更新stockInformation
  2084. if (stockData) {
  2085. stockInformation.value.stockName = stockData.stockName||stockData.name;
  2086. stockInformation.value.stockCode = stockData.stockCode||stockData.code;
  2087. }
  2088. } catch (error) {
  2089. console.error("解析股票数据失败:", error);
  2090. }
  2091. }
  2092. // 处理index参数(股票在列表中的位置)
  2093. if (options.index !== undefined) {
  2094. const stockIndex = parseInt(options.index);
  2095. console.log("股票在列表中的索引:", stockIndex);
  2096. // 将index保存到响应式变量中,用于后续的左右切换功能
  2097. currentStockIndex.value = stockIndex;
  2098. }
  2099. // 处理index参数(股票在列表中的位置)
  2100. if (options.parentIndex) {
  2101. const stockParentIndex = parseInt(options.parentIndex);
  2102. console.log("股票在列表中的父索引:", stockParentIndex);
  2103. // 将index保存到响应式变量中,用于后续的左右切换功能
  2104. currentStockParentIndex.value = stockParentIndex;
  2105. }
  2106. // 处理stockFrom参数(股票来源)
  2107. if (options.from) {
  2108. currentStockFrom.value = options.from;
  2109. }
  2110. });
  2111. // 保存定时器,用于页面卸载时清理
  2112. onUnmounted(() => {
  2113. removeTcpListeners();
  2114. disconnectTcp();
  2115. if (timer) {
  2116. console.log("卸载定时器");
  2117. clearInterval(timer);
  2118. }
  2119. });
  2120. onMounted(async () => {
  2121. try {
  2122. console.log("步骤1: 初始化系统信息");
  2123. const systemInfo = uni.getSystemInfoSync();
  2124. pixelRatio.value = systemInfo.pixelRatio;
  2125. // 设置Canvas实际像素(考虑pixelRatio以获得高清效果)
  2126. // 1rpx = 设备屏幕宽度 / 750
  2127. const rpxToPx = systemInfo.windowWidth / 750;
  2128. const offsetHeight = (150 + 200 + 80 + 150 + 30) * rpxToPx; // 350rpx转换为px
  2129. const calculatedHeight = systemInfo.windowHeight - offsetHeight;
  2130. canvasWidth.value = systemInfo.windowWidth;
  2131. canvasHeight.value = Math.max(calculatedHeight, canvasHeight.value);
  2132. initTcpListeners();
  2133. await nextTick();
  2134. // 开始连接
  2135. startTcp();
  2136. if (timeData.value && timeData.value.length > 0) {
  2137. // 当前股价
  2138. stockInformation.value.currentPrice = timeData.value[timeData.value.length - 1].price;
  2139. // 涨跌额度
  2140. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  2141. // 涨跌幅度
  2142. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  2143. // 成交量
  2144. stockInformation.value.volume = timeData.value[timeData.value.length - 1].volume;
  2145. text[0][1].value = utils.formatPrice(stockInformation.value.currentPrice);
  2146. text[1][0].value = utils.formatStockNumber(stockInformation.value.volume);
  2147. } else {
  2148. console.warn("没有时间数据,跳过股票信息计算");
  2149. }
  2150. await nextTick();
  2151. setTimeout(() => {
  2152. initCanvas();
  2153. }, 100);
  2154. console.log("所有初始化步骤完成");
  2155. } catch (error) {
  2156. console.error("初始化过程中出现错误:", error);
  2157. }
  2158. });
  2159. </script>
  2160. <style>
  2161. .container {
  2162. width: 100%;
  2163. min-height: 100vh;
  2164. background-color: #f6f6f6;
  2165. }
  2166. .title-container {
  2167. width: 100%;
  2168. height: 150rpx;
  2169. background-color: white;
  2170. display: flex;
  2171. flex-direction: column;
  2172. justify-content: flex-end;
  2173. }
  2174. .title {
  2175. /* border: 1px solid #ff0000; */
  2176. position: relative;
  2177. /* 关键:作为绝对定位的父容器 */
  2178. width: 100%;
  2179. height: 80rpx;
  2180. display: flex;
  2181. justify-content: center;
  2182. align-items: center;
  2183. }
  2184. .back-homepage-btn {
  2185. margin-left: 40rpx;
  2186. margin-right: auto;
  2187. height: 30rpx;
  2188. width: 20rpx;
  2189. z-index: 1;
  2190. }
  2191. .mid-title {
  2192. position: absolute;
  2193. left: 0;
  2194. right: 0;
  2195. display: flex;
  2196. justify-content: center;
  2197. align-items: center;
  2198. }
  2199. .left-page {
  2200. height: 40rpx;
  2201. width: 40rpx;
  2202. }
  2203. .right-page {
  2204. height: 40rpx;
  2205. width: 40rpx;
  2206. }
  2207. .stock-id {
  2208. margin: 0rpx 40rpx;
  2209. display: flex;
  2210. flex-direction: column;
  2211. text-align: center;
  2212. }
  2213. .stock-name {
  2214. font-weight: bold;
  2215. }
  2216. .stock-code {
  2217. font-size: 0.8rem;
  2218. font-weight: bold;
  2219. color: #a1a1a1;
  2220. }
  2221. .search {
  2222. height: 40rpx;
  2223. width: 40rpx;
  2224. }
  2225. .more {
  2226. height: 100%;
  2227. display: flex;
  2228. justify-content: center;
  2229. align-items: center;
  2230. margin-right: 40rpx;
  2231. margin-left: 20rpx;
  2232. }
  2233. .body {
  2234. overflow: auto;
  2235. height: calc(100vh - 305rpx);
  2236. /* border: 1px solid red; */
  2237. }
  2238. .stock-information {
  2239. background-color: white;
  2240. width: 100%;
  2241. height: 200rpx;
  2242. margin: 10rpx 0rpx;
  2243. display: flex;
  2244. align-items: center;
  2245. position: relative;
  2246. /* 为伪元素定位做准备 */
  2247. }
  2248. /* 右下角黑色三角形 */
  2249. .stock-information::after {
  2250. content: "";
  2251. position: absolute;
  2252. bottom: 0;
  2253. right: 0;
  2254. width: 0;
  2255. height: 0;
  2256. border-left: 20rpx solid transparent;
  2257. border-bottom: 20rpx solid #6a6a6a;
  2258. }
  2259. .stock-detail-container {
  2260. position: absolute;
  2261. top: 100%;
  2262. /* 在父容器下方 */
  2263. left: 0;
  2264. /* 从左边开始 */
  2265. right: 0;
  2266. /* 到右边结束 */
  2267. width: 100%;
  2268. /* height: 300rpx; 使用固定高度替代calc,避免计算问题 */
  2269. height: calc(100vh - 515rpx);
  2270. display: flex;
  2271. flex-direction: column;
  2272. background-color: rgba(0, 0, 0, 0.6);
  2273. z-index: 100;
  2274. box-sizing: border-box;
  2275. pointer-events: auto;
  2276. }
  2277. .stock-detail {
  2278. border: 1px solid #cacaca;
  2279. width: 100%;
  2280. display: flex;
  2281. background-color: white;
  2282. padding: 10rpx 0;
  2283. }
  2284. .first-column,
  2285. .second-column {
  2286. width: 50%;
  2287. height: 100%;
  2288. display: flex;
  2289. flex-direction: column;
  2290. color: #6a6a6a;
  2291. font-size: 0.8rem;
  2292. }
  2293. .first-column-data,
  2294. .second-column-data {
  2295. display: flex;
  2296. justify-content: space-between;
  2297. align-items: center;
  2298. padding: 10rpx 40rpx;
  2299. }
  2300. .stock-detail-title {
  2301. color: #6a6a6a;
  2302. }
  2303. .stock-detail-value {
  2304. color: black;
  2305. }
  2306. .price-up {
  2307. color: #10b981;
  2308. }
  2309. .price-down {
  2310. color: #ef4444;
  2311. }
  2312. .price-none {
  2313. color: black;
  2314. }
  2315. .stock-current-data {
  2316. width: 30%;
  2317. height: 70%;
  2318. display: flex;
  2319. flex-direction: column;
  2320. }
  2321. .stock-current-price {
  2322. font-weight: bold;
  2323. font-size: 1.2rem;
  2324. width: 100%;
  2325. height: 50%;
  2326. display: flex;
  2327. justify-content: center;
  2328. align-items: center;
  2329. }
  2330. .stock-current-other {
  2331. width: 100%;
  2332. height: 50%;
  2333. display: flex;
  2334. font-weight: bold;
  2335. font-size: 0.6rem;
  2336. }
  2337. .stock-current-value {
  2338. width: 50%;
  2339. height: 100%;
  2340. display: flex;
  2341. justify-content: center;
  2342. align-items: center;
  2343. }
  2344. .stock-current-ratio {
  2345. width: 50%;
  2346. height: 100%;
  2347. display: flex;
  2348. justify-content: center;
  2349. align-items: center;
  2350. }
  2351. .stock-other-data {
  2352. height: 100%;
  2353. width: 70%;
  2354. display: flex;
  2355. flex-direction: column;
  2356. }
  2357. .first-line,
  2358. .second-line,
  2359. .third-line {
  2360. display: flex;
  2361. align-items: center;
  2362. width: 100%;
  2363. height: 33%;
  2364. /* font-weight: bold; */
  2365. font-size: 0.8rem;
  2366. }
  2367. .value {
  2368. margin-left: auto;
  2369. margin-right: 20rpx;
  2370. }
  2371. .high-price,
  2372. .volume,
  2373. .volume-ratio,
  2374. .low-price,
  2375. .amount,
  2376. .market-earn,
  2377. .open-price,
  2378. .turnover-ratio,
  2379. .market-value {
  2380. display: flex;
  2381. flex: 1;
  2382. }
  2383. .stock-chart {
  2384. width: 100%;
  2385. }
  2386. .stock-kline-tab {
  2387. display: flex;
  2388. width: 100%;
  2389. height: 80rpx;
  2390. /* border: 1px solid black; */
  2391. }
  2392. .tab-day,
  2393. .tab-month,
  2394. .tab-time,
  2395. .tab-week,
  2396. .tab-more,
  2397. .tab-setting {
  2398. display: flex;
  2399. justify-content: center;
  2400. align-items: center;
  2401. flex: 1;
  2402. font-size: 0.8rem;
  2403. color: #6a6a6a;
  2404. position: relative;
  2405. }
  2406. /* 向下小三角样式 */
  2407. .arrow-down {
  2408. width: 0;
  2409. height: 0;
  2410. border-left: 10rpx solid transparent;
  2411. border-right: 10rpx solid transparent;
  2412. border-bottom: 12rpx solid currentColor;
  2413. margin-left: 8rpx;
  2414. display: inline-block;
  2415. }
  2416. .arrow-up {
  2417. width: 0;
  2418. height: 0;
  2419. border-left: 10rpx solid transparent;
  2420. border-right: 10rpx solid transparent;
  2421. border-top: 12rpx solid currentColor;
  2422. margin-left: 8rpx;
  2423. display: inline-block;
  2424. }
  2425. .arrow-left {
  2426. width: 0;
  2427. height: 0;
  2428. border-top: 10rpx solid transparent;
  2429. border-bottom: 10rpx solid transparent;
  2430. border-right: 12rpx solid currentColor;
  2431. margin-left: 8rpx;
  2432. display: inline-block;
  2433. }
  2434. .arrow-right {
  2435. width: 0;
  2436. height: 0;
  2437. border-top: 10rpx solid transparent;
  2438. border-bottom: 10rpx solid transparent;
  2439. border-left: 12rpx solid currentColor;
  2440. margin-left: 8rpx;
  2441. display: inline-block;
  2442. }
  2443. .moreTabsContainer {
  2444. width: 100%;
  2445. display: flex;
  2446. gap: 5rpx;
  2447. margin-bottom: 5rpx;
  2448. }
  2449. .moreTabItem {
  2450. flex: 1;
  2451. padding: 10rpx 20rpx;
  2452. border-radius: 10rpx;
  2453. background-color: white;
  2454. font-size: 0.85rem;
  2455. color: #6a6a6a;
  2456. text-align: center;
  2457. }
  2458. .tab-selected {
  2459. color: #db1f1d;
  2460. }
  2461. .tab-selected::after {
  2462. content: "";
  2463. position: absolute;
  2464. bottom: 10rpx;
  2465. left: 50%;
  2466. transform: translateX(-50%);
  2467. width: 40rpx;
  2468. height: 5rpx;
  2469. background-color: #db1f1d;
  2470. }
  2471. .tab-setting-img {
  2472. width: 30rpx;
  2473. height: 30rpx;
  2474. }
  2475. .time-chart-container {
  2476. width: 100%;
  2477. min-height: 400rpx;
  2478. background-color: #ffffff;
  2479. box-sizing: border-box;
  2480. }
  2481. .stock-chart {
  2482. display: flex;
  2483. flex-direction: column;
  2484. align-items: center;
  2485. }
  2486. .kline-chart-container {
  2487. width: 100%;
  2488. height: 400rpx;
  2489. background-color: #ffffff;
  2490. padding: 20rpx;
  2491. box-sizing: border-box;
  2492. display: flex;
  2493. justify-content: center;
  2494. align-items: center;
  2495. }
  2496. .bottomTool {
  2497. width: 100%;
  2498. height: 150rpx;
  2499. position: fixed;
  2500. bottom: 0;
  2501. background-color: white;
  2502. display: flex;
  2503. box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
  2504. }
  2505. .index,
  2506. .function,
  2507. .favorites {
  2508. flex: 1;
  2509. display: flex;
  2510. justify-content: center;
  2511. align-items: center;
  2512. flex-direction: column;
  2513. font-size: 12px;
  2514. transition: all 0.2s ease;
  2515. }
  2516. .index:active,
  2517. .function:active,
  2518. .favorites:active {
  2519. background-color: rgba(99, 99, 99, 0.5);
  2520. transform: scale(0.95);
  2521. transition: all 0.1s ease;
  2522. }
  2523. .icon {
  2524. width: 40rpx;
  2525. height: 40rpx;
  2526. }
  2527. </style>