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.

2046 lines
56 KiB

  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"> </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"> </view>
  16. </view>
  17. <image class="search" src="/static/marketSituation-image/search.png" mode="搜索" @click="startTcp()"> </image>
  18. <view class="more" @click="disconnect()">···</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" 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 === 3">
  190. <button @click="sendGetTimeData()">接收消息</button>
  191. <button @click="sendStopTimeData()">停止消息</button>
  192. </view>
  193. <view v-else class="kline-chart-container">
  194. <text>K线图开发中...</text>
  195. </view>
  196. </view>
  197. </view>
  198. </view>
  199. <view class="bottomTool">
  200. <view class="index">
  201. <image class="icon" src="/static/marketSituation-image/marketCondition-image/index.png" mode="指标仓库图标"> </image>
  202. 指标仓库
  203. </view>
  204. <view class="function">
  205. <image class="icon" src="/static/marketSituation-image/marketCondition-image/function.png" mode="功能图标"> </image>
  206. 功能
  207. </view>
  208. <view class="favorites">
  209. <image class="icon" src="/static/marketSituation-image/marketCondition-image/favorites.png" mode="加自选图标"></image>
  210. 加自选
  211. </view>
  212. </view>
  213. </view>
  214. </template>
  215. <script setup>
  216. import { ref, reactive, computed, onMounted, watch, nextTick, onUnmounted, getCurrentInstance } from "vue";
  217. import { onLoad } from "@dcloudio/uni-app";
  218. const instance = getCurrentInstance();
  219. import { prevClosePrice, timeData as testTimeData, klineData as testKlineData } from "@/common/stockTimeInformation.js";
  220. import { throttle } from "@/common/util.js";
  221. import { HCharts } from "@/common/canvasMethod.js";
  222. // const TCPSocket = uni.requireNativePlugin("Aimer-TCPPlugin");
  223. const tcpObject = {
  224. ip: "192.168.1.9",
  225. port: 8080,
  226. reconnectInterval: 3000,
  227. isConnected: false,
  228. };
  229. const TIME_OUT = 1000 * 5;
  230. const resultR = ref([]);
  231. // 股票信息栏变量
  232. const stockInformation = ref({
  233. stockName: "----", //股票名称
  234. stockCode: "------", //股票代码
  235. lastDayStockClosePrice: 0.0, //前一日收盘价
  236. currentPrice: 0.0, //当前股价
  237. currentValue: 0.0, //涨跌额度
  238. currentRatio: 0.0, //涨跌幅度
  239. highPrice: 0.0, //最高价
  240. lowPrice: 0.0, //最低价
  241. openPrice: 0.0, //开盘价
  242. closePrice: 0.0, //收盘价
  243. volume: 0.0, //成交量
  244. volumeRatio: 0.0, //成交量比
  245. amount: 0.0, //成交额
  246. marketEarn: 0.0, //市盈
  247. turnoverRatio: 0.0, //换手率
  248. marketValue: 0.0, //市值
  249. });
  250. // 是否展开股票信息细节栏的判断变量
  251. const isStockDetail = ref(false);
  252. // 股票信息细节内容变量
  253. const firstColumData = [
  254. {
  255. title: "振幅",
  256. value: 0,
  257. },
  258. {
  259. title: "现手",
  260. value: 0,
  261. },
  262. {
  263. title: "内盘",
  264. value: 0,
  265. },
  266. {
  267. title: "外盘",
  268. value: 0,
  269. },
  270. {
  271. title: "流通股",
  272. value: 0,
  273. },
  274. {
  275. title: "每股净资产",
  276. value: 0,
  277. },
  278. {
  279. title: "净资产收益率",
  280. value: 0,
  281. },
  282. {
  283. title: "总股本",
  284. value: 0,
  285. },
  286. {
  287. title: "总资本",
  288. value: 0,
  289. },
  290. {
  291. title: "总市值",
  292. value: 0,
  293. },
  294. {
  295. title: "一个月最高",
  296. value: 0,
  297. },
  298. {
  299. title: "一个月最低",
  300. value: 0,
  301. },
  302. ];
  303. const secondColumnData = [
  304. {
  305. title: "均价",
  306. value: 0,
  307. },
  308. {
  309. title: "昨收",
  310. value: 0,
  311. },
  312. {
  313. title: "委比",
  314. value: 0,
  315. },
  316. {
  317. title: "委买",
  318. value: 0,
  319. },
  320. {
  321. title: "委卖",
  322. value: 0,
  323. },
  324. {
  325. title: "市盈利(静)",
  326. value: 0,
  327. },
  328. {
  329. title: "市盈利(动)",
  330. value: 0,
  331. },
  332. {
  333. title: "市净率",
  334. value: 0,
  335. },
  336. {
  337. title: "涨停价",
  338. value: 0,
  339. },
  340. {
  341. title: "跌停价",
  342. value: 0,
  343. },
  344. {
  345. title: "一年最高",
  346. value: 0,
  347. },
  348. {
  349. title: "一年最低",
  350. value: 0,
  351. },
  352. ];
  353. // 是否展开更多Tab的判断变量
  354. const isMoreTabs = ref(false);
  355. const moreTabsData = ref([
  356. {
  357. title: "1分",
  358. value: 5,
  359. },
  360. {
  361. title: "5分",
  362. value: 6,
  363. },
  364. {
  365. title: "15分",
  366. value: 7,
  367. },
  368. {
  369. title: "30分",
  370. value: 8,
  371. },
  372. {
  373. title: "60分",
  374. value: 9,
  375. },
  376. {
  377. title: "季K",
  378. value: 10,
  379. },
  380. {
  381. title: "年K",
  382. value: 11,
  383. },
  384. ]);
  385. // 股票当前选中的K线类型Tab
  386. // 1:分时 2:日K 3:周K 4:月K
  387. const klineTab = ref(2);
  388. const sendGetTimeData = () => {
  389. console.log("执行发送消息的方法");
  390. try {
  391. TCPSocket.send({
  392. channel: "1",
  393. message: '{"command": "real_time", "stock_code": "SH.000001"}',
  394. });
  395. } catch (e) {
  396. console.log("error", e);
  397. }
  398. };
  399. const sendStopTimeData = () => {
  400. try {
  401. TCPSocket.send({
  402. channel: "1",
  403. message: '{"command": "stop_real_time"}',
  404. });
  405. } catch (e) {
  406. console.log("error", e);
  407. }
  408. };
  409. // 确定股票数据的颜色方法
  410. const confirmStockColor = (price, lastDayStockClosePrice) => {
  411. if (typeof lastDayStockClosePrice === "undefined") {
  412. if (price == 0) {
  413. return "price-none";
  414. } else if (price > 0) {
  415. return "price-up";
  416. } else {
  417. return "price-down";
  418. }
  419. } else {
  420. if (price == lastDayStockClosePrice) {
  421. return "price-none";
  422. } else if (price > lastDayStockClosePrice) {
  423. return "price-up";
  424. } else {
  425. return "price-down";
  426. }
  427. }
  428. };
  429. // 股票K线类型方法
  430. const selectKlineTab = (tabId) => {
  431. klineTab.value = tabId;
  432. if (tabId === 1) {
  433. stockInformation.value.lastDayStockClosePrice = prevClosePrice;
  434. }
  435. initCanvas();
  436. // startAddDataTimer();
  437. };
  438. // 返回按钮
  439. const backToHomepage = () => {
  440. const pages = getCurrentPages();
  441. if (pages.length > 1) {
  442. uni.navigateBack();
  443. } else {
  444. // 如果没有上一页,跳转到首页
  445. uni.reLaunch({
  446. url: "/pages/home/home",
  447. });
  448. }
  449. };
  450. const openStockDetail = () => {
  451. isStockDetail.value = true;
  452. };
  453. const closeStockDetail = () => {
  454. isStockDetail.value = false;
  455. };
  456. const openMoreTabs = () => {
  457. isMoreTabs.value = true;
  458. };
  459. const closeMoreTabs = () => {
  460. isMoreTabs.value = false;
  461. };
  462. const selectMoreTab = (value) => {
  463. selectKlineTab(value);
  464. };
  465. const chooseTabName = () => {
  466. if (klineTab.value === 5) {
  467. return "1分";
  468. } else if (klineTab.value === 6) {
  469. return "5分";
  470. } else if (klineTab.value === 7) {
  471. return "15分";
  472. } else if (klineTab.value === 8) {
  473. return "30分";
  474. } else if (klineTab.value === 9) {
  475. return "60分";
  476. } else if (klineTab.value === 10) {
  477. return "季K";
  478. } else if (klineTab.value === 11) {
  479. return "年K";
  480. }
  481. };
  482. // 画布对象
  483. const canvasWidth = ref(100);
  484. const canvasHeight = ref(100);
  485. const CANVAS_BACKGROUND_COLOR = "#fff";
  486. const TEXT_COLOR1 = "#000";
  487. const TEXT_COLOR2 = "red";
  488. const LINE_COLOR = "#e5e5e5";
  489. const timeChartObject = ref({
  490. min: 0,
  491. max: 0,
  492. });
  493. const klineChartObject = ref({
  494. min: 0,
  495. max: 0,
  496. });
  497. const chartRange = ref();
  498. // 开盘时间
  499. const openTime = "09:30";
  500. // 收盘时间
  501. const closeTime = "15:00";
  502. const ctx = ref(null);
  503. const dynamicCtx = ref(null); // 动态Canvas上下文
  504. const crosshairCtx = ref(null); // 十字准心Canvas上下文
  505. const pixelRatio = ref(1);
  506. // 触屏对象
  507. const touchState = reactive({
  508. startX: 0,
  509. startY: 0,
  510. isMoving: false,
  511. moveDistance: 0,
  512. moveThreshold: 10, //移动阈值(用于判断是否在点击)
  513. scale: 1, // 当前缩放比例
  514. minScale: 0.3, // 最小缩放比例
  515. maxScale: 5, // 最大缩放比例
  516. baseVisibleCount: 40, // 基准可见K线数量
  517. offset: 0, // 数据偏移量
  518. isZooming: false, // 是否正在缩放
  519. initialDistance: 0, // 初始两指距离
  520. initialScale: 1, // 初始缩放比例
  521. });
  522. // 计算当前可见的K线数量
  523. const visibleCount = computed(() => {
  524. return Math.floor(touchState.baseVisibleCount / touchState.scale);
  525. });
  526. // 计算当前显示的数据范围
  527. const visibleDataRange = computed(() => {
  528. const start = Math.max(0, klineData.value.length - visibleCount.value - touchState.offset);
  529. const end = Math.min(klineData.value.length, start + visibleCount.value);
  530. return {
  531. start,
  532. end,
  533. };
  534. });
  535. // 获取当前可见的数据
  536. const visibleKlineData = computed(() => {
  537. const { start, end } = visibleDataRange.value;
  538. return klineData.value.slice(start, end);
  539. });
  540. // 计算两点之间的距离
  541. const getDistance = (touch1, touch2) => {
  542. const dx = touch1.x - touch2.x;
  543. const dy = touch1.y - touch2.y;
  544. return Math.sqrt(dx * dx + dy * dy);
  545. };
  546. // 十字准线相关状态
  547. const crosshair = reactive({
  548. show: false,
  549. x: 0,
  550. y: 0,
  551. currentData: null,
  552. snapToData: true,
  553. });
  554. // 绘制网格和坐标轴
  555. const grid = [
  556. {
  557. top: 20,
  558. bottom: canvasHeight.value * 0.5,
  559. left: 5,
  560. right: 5,
  561. lineColor: LINE_COLOR,
  562. lineWidth: 1,
  563. horizontalLineNum: 5,
  564. verticalLineNum: 5,
  565. },
  566. {
  567. top: canvasHeight.value * 0.5 + 20,
  568. bottom: 20,
  569. left: 5,
  570. right: 5,
  571. lineColor: LINE_COLOR,
  572. lineWidth: 1,
  573. horizontalLineNum: 3,
  574. verticalLineNum: 5,
  575. },
  576. ];
  577. // 绘制网格和坐标轴
  578. const dayGrid = [
  579. {
  580. top: 20,
  581. bottom: canvasHeight.value * 0.5,
  582. left: 5,
  583. right: 5,
  584. lineColor: LINE_COLOR,
  585. lineWidth: 1,
  586. horizontalLineNum: 5,
  587. verticalLineNum: 2,
  588. },
  589. {
  590. top: canvasHeight.value * 0.5 + 20,
  591. bottom: 20,
  592. left: 5,
  593. right: 5,
  594. lineColor: LINE_COLOR,
  595. lineWidth: 1,
  596. horizontalLineNum: 3,
  597. verticalLineNum: 2,
  598. },
  599. {
  600. top: canvasHeight.value * 0.5 + 20,
  601. bottom: 20,
  602. left: 5,
  603. right: 5,
  604. lineColor: LINE_COLOR,
  605. lineWidth: 1,
  606. horizontalLineNum: 3,
  607. verticalLineNum: 2,
  608. },
  609. ];
  610. // 工具函数
  611. const utils = {
  612. // 格式化价格
  613. formatPrice(price) {
  614. return price.toFixed(2);
  615. },
  616. // 计算数据范围
  617. calculateDataRange(data, key) {
  618. if (!data || data.length === 0) {
  619. return {
  620. min: 0,
  621. max: 0,
  622. };
  623. }
  624. const values = data.map((item) => item[key]);
  625. return {
  626. min: Math.min(...values),
  627. max: Math.max(...values),
  628. };
  629. },
  630. // 计算标签
  631. calculateLabel(data, type = 2, preClosePrice = 0, key, num) {
  632. let label = [];
  633. if (key === "price") {
  634. // 分时价格区间
  635. if (type == 1) {
  636. const priceRange = utils.calculateDataRange(data, "price");
  637. const theMost = Math.max(priceRange.max - preClosePrice, preClosePrice - priceRange.min);
  638. const mid = (num - 1) / 2;
  639. // 计算分时价格标签
  640. label[mid] = {
  641. value: utils.formatPrice(preClosePrice),
  642. ratio: utils.formatPrice(0) + "%",
  643. };
  644. for (let i = 0; i < mid; i++) {
  645. label[i] = {
  646. value: utils.formatPrice(preClosePrice + (theMost * (mid - i)) / mid),
  647. ratio: utils.formatPrice((100 * (theMost * (mid - i))) / mid / preClosePrice) + "%",
  648. };
  649. label[num - 1 - i] = {
  650. value: utils.formatPrice(preClosePrice - (theMost * (mid - i)) / mid),
  651. ratio: utils.formatPrice((-1 * 100 * (theMost * (mid - i))) / mid / preClosePrice) + "%",
  652. };
  653. }
  654. chartRange.value.push({
  655. max: preClosePrice + theMost,
  656. min: preClosePrice - theMost,
  657. });
  658. // timeChartObject.value.max = preClosePrice + theMost;
  659. // timeChartObject.value.min = preClosePrice - theMost;
  660. return label;
  661. } else {
  662. const highPriceRange = utils.calculateDataRange(data, "high");
  663. const lowPriceRange = utils.calculateDataRange(data, "low");
  664. const priceRange = {
  665. max: highPriceRange.max * 1.01,
  666. min: lowPriceRange.min * 0.99,
  667. };
  668. const priceDiff = priceRange.max - priceRange.min;
  669. for (let i = 0; i < num; ++i) {
  670. label[i] = {
  671. value: utils.formatPrice(priceRange.max - (i * priceDiff) / (num - 1)),
  672. };
  673. }
  674. chartRange.value.push(priceRange);
  675. // klineChartObject.value.max = highPriceRange.max * 1.01;
  676. // klineChartObject.value.min = lowPriceRange.min * 0.99;
  677. return label;
  678. }
  679. } else if (key === "volume") {
  680. const volumeRange = utils.calculateDataRange(data, "volume");
  681. chartRange.value.push({
  682. max: volumeRange.max,
  683. min: 0,
  684. });
  685. label[0] = {
  686. value: utils.formatPrice(volumeRange.max),
  687. };
  688. label[1] = {
  689. value: utils.formatPrice(0),
  690. };
  691. return label;
  692. }
  693. return null;
  694. },
  695. // 线性插值
  696. lerp(start, end, factor) {
  697. return start + (end - start) * factor;
  698. },
  699. // 根据X坐标找到最近的数据点
  700. findNearestDataPoint(x, pointLen, data, grid, offset) {
  701. if (!data.length) return null;
  702. const width = canvasWidth.value;
  703. const height = canvasHeight.value;
  704. // 计算每个数据点的X坐标间隔
  705. // 倒推 const x=5+(index*(width-10)/pointLen) 已知x求index
  706. const xStep = width - grid[0].left - grid[0].right;
  707. // 计算触摸点对应的数据索引
  708. const touchX = (x - grid[0].left - offset) * pointLen;
  709. let nearestIndex = Math.round(touchX / xStep);
  710. let dataX;
  711. // 确保索引在有效范围内
  712. if (nearestIndex >= 0 && nearestIndex <= data.length - 1) {
  713. dataX = offset + grid[0].left + (nearestIndex * (width - grid[0].left - grid[0].right)) / pointLen;
  714. } else {
  715. dataX = x;
  716. }
  717. nearestIndex = Math.max(0, Math.min(nearestIndex, data.length - 1));
  718. return {
  719. ...data[nearestIndex],
  720. index: nearestIndex,
  721. x: dataX,
  722. };
  723. },
  724. // 根据Y坐标计算价格
  725. calculatePriceFromY(y, data, grid) {
  726. if (!data.length) return 0;
  727. const width = canvasWidth.value;
  728. const height = canvasHeight.value;
  729. // 上下边距1
  730. const topPadding1 = 20;
  731. const bottomPadding1 = height * 0.4;
  732. // 上下边距2
  733. const topPadding2 = height - bottomPadding1 + 40;
  734. const bottomPadding2 = 5;
  735. // 左右边距
  736. const verticalPadding = 5;
  737. let chartY;
  738. let price;
  739. for (let i = 0; i < grid.length; i++) {
  740. if (y >= grid[i].top && y <= height - grid[i].bottom) {
  741. const chartDiff = chartRange.value[i].max - chartRange.value[i].min;
  742. chartY = y - grid[i].top;
  743. price = chartRange.value[i].max - (chartY / (height - grid[i].bottom - grid[i].top)) * chartDiff;
  744. break;
  745. }
  746. }
  747. // if (y >= topPadding1 && y <= height - bottomPadding1) {
  748. // const priceDiff = priceRange.max - priceRange.min;
  749. // chartY = y - topPadding1;
  750. // price = priceRange.max - (chartY / (height - topPadding1 - bottomPadding1)) * priceDiff;
  751. // } else if (y >= topPadding2 && y <= height - bottomPadding2) {
  752. // const volumeRange = utils.calculateDataRange(data, "volume");
  753. // const volumeDiff = volumeRange.max - 0;
  754. // chartY = y - topPadding2;
  755. // price = volumeRange.max - (chartY / (height - topPadding2 - bottomPadding2)) * volumeDiff;
  756. // }
  757. return price;
  758. },
  759. // 股市数值格式化方法
  760. formatStockNumber(value, decimalPlaces = 2) {
  761. const num = Number(value);
  762. if (isNaN(num)) return "0";
  763. const absNum = Math.abs(num);
  764. const sign = num < 0 ? "-" : "";
  765. if (absNum >= 1000000000000) {
  766. // 万亿级别
  767. return sign + (absNum / 1000000000000).toFixed(decimalPlaces) + "万亿";
  768. } else if (absNum >= 100000000) {
  769. // 亿级别
  770. return sign + (absNum / 100000000).toFixed(decimalPlaces) + "亿";
  771. } else if (absNum >= 10000) {
  772. // 万级别
  773. return sign + (absNum / 10000).toFixed(decimalPlaces) + "万";
  774. } else {
  775. // 小于万的直接显示
  776. return sign + absNum.toFixed(decimalPlaces);
  777. }
  778. },
  779. };
  780. let text = [
  781. [
  782. {
  783. name: "领先",
  784. value: "暂无数据",
  785. color: "red",
  786. },
  787. {
  788. name: "价",
  789. value: utils.formatPrice(stockInformation.value.currentPrice),
  790. color: "black",
  791. },
  792. ],
  793. [
  794. {
  795. name: "量",
  796. value: utils.formatStockNumber(stockInformation.value.volume),
  797. color: "green",
  798. },
  799. {
  800. name: "额",
  801. value: "暂无数据",
  802. color: "black",
  803. },
  804. ],
  805. ];
  806. // 示例数据
  807. const timeData = ref([]);
  808. const klineData = ref([]);
  809. const initCanvas = async () => {
  810. try {
  811. crosshair.show = false;
  812. grid[0].bottom = canvasHeight.value * 0.4;
  813. grid[1].top = canvasHeight.value * 0.6 + 30;
  814. dayGrid[0].top = 20;
  815. dayGrid[0].bottom = canvasHeight.value * 0.6;
  816. dayGrid[1].top = canvasHeight.value * 0.4 + 30;
  817. dayGrid[1].bottom = canvasHeight.value * 0.3;
  818. dayGrid[2].top = canvasHeight.value * 0.7 + 20;
  819. dayGrid[2].bottom = 20;
  820. // 等待DOM更新
  821. await nextTick();
  822. ctx.value = uni.createCanvasContext("stockChart", instance.proxy);
  823. // 初始化动态数据Canvas上下文
  824. dynamicCtx.value = uni.createCanvasContext("dynamicCanvas", instance.proxy);
  825. // 初始化十字准心Canvas上下文
  826. crosshairCtx.value = uni.createCanvasContext("crosshairCanvas", instance.proxy);
  827. if (ctx.value) {
  828. // 设置Canvas的内部绘图区域尺寸
  829. ctx.value.canvas = {
  830. width: canvasWidth.value,
  831. height: canvasHeight.value,
  832. };
  833. } else {
  834. console.warn("Canvas上下文未初始化,跳过绘制");
  835. }
  836. // 确保Canvas上下文知道正确的尺寸
  837. if (dynamicCtx.value) {
  838. // 设置Canvas的内部绘图区域尺寸
  839. dynamicCtx.value.canvas = {
  840. width: canvasWidth.value,
  841. height: canvasHeight.value,
  842. };
  843. } else {
  844. console.warn("动态Canvas上下文未初始化,跳过绘制");
  845. }
  846. if (crosshairCtx.value) {
  847. // 设置Canvas的内部绘图区域尺寸
  848. crosshairCtx.value.canvas = {
  849. width: canvasWidth.value,
  850. height: canvasHeight.value,
  851. };
  852. } else {
  853. console.warn("十字准心Canvas上下文未初始化,跳过绘制");
  854. }
  855. // 等待DOM更新
  856. await nextTick();
  857. console.log("ctx", ctx.value);
  858. drawChart();
  859. } catch (error) {
  860. console.error("初始化Canvas失败:", error);
  861. }
  862. };
  863. // 绘制图表主函数-设置基础数据
  864. const drawChart = () => {
  865. if (!ctx.value || !dynamicCtx.value || !crosshairCtx.value) {
  866. console.warn("Canvas上下文未初始化,跳过绘制");
  867. return;
  868. }
  869. const data = klineTab.value == 1 ? timeData.value : klineData.value;
  870. chartRange.value = [];
  871. // 清除画布
  872. // HCharts.setCanvasColor(ctx.value, width, height, CANVAS_BACKGROUND_COLOR);
  873. // HCharts.setCanvasColor(dynamicCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
  874. // HCharts.setCanvasColor(crosshairCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
  875. // 根据当前标签绘制对应图表
  876. if (klineTab.value == 1) {
  877. // 设置标签样式
  878. const label = [
  879. {
  880. text: utils.calculateLabel(data, 1, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum),
  881. color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2],
  882. fontSize: 12,
  883. lineStyle: ["solid", "solid", "solid", "solid", "solid"],
  884. onlyTwo: false,
  885. },
  886. {
  887. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  888. color: [TEXT_COLOR1, TEXT_COLOR1],
  889. lineStyle: ["solid", "solid", "solid"],
  890. fontSize: 12,
  891. onlyTwo: true,
  892. },
  893. ];
  894. // 把label加进grid中
  895. grid[0].label = label[0];
  896. grid[1].label = label[1];
  897. drawTimeChart();
  898. } else {
  899. // 设置标签样式
  900. const label = [
  901. {
  902. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum),
  903. color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2],
  904. lineStyle: ["solid", "dash", "dash", "dash", "solid"],
  905. fontSize: 12,
  906. onlyTwo: false,
  907. },
  908. {
  909. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  910. color: [TEXT_COLOR1, TEXT_COLOR1],
  911. lineStyle: ["solid", "dash", "solid"],
  912. fontSize: 12,
  913. onlyTwo: true,
  914. },
  915. {
  916. text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum),
  917. color: [TEXT_COLOR1, TEXT_COLOR1],
  918. lineStyle: ["solid", "dash", "solid"],
  919. fontSize: 12,
  920. onlyTwo: true,
  921. },
  922. ];
  923. // 把label加进grid中
  924. dayGrid[0].label = label[0];
  925. dayGrid[1].label = label[1];
  926. dayGrid[2].label = label[2];
  927. // HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, dayGrid, data[0].date, data[data.length - 1].date);
  928. drawKLineChart();
  929. }
  930. // crosshairCtx.value.draw();
  931. };
  932. const throttledDrawChart = throttle(drawChart, 50);
  933. // // 绘制分时图
  934. const drawTimeChart = () => {
  935. drawCtxChart(grid);
  936. drawDynamicCtxChart();
  937. };
  938. // // 绘制K线图
  939. const drawKLineChart = () => {
  940. drawCtxChart(dayGrid);
  941. drawDynamicCtxChart();
  942. };
  943. const drawCtxChart = (paramGrid) => {
  944. // 检查Canvas上下文是否已初始化
  945. if (!ctx.value) {
  946. console.warn("Canvas上下文未初始化,跳过绘制");
  947. return;
  948. }
  949. // 绘制网格
  950. HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, paramGrid, openTime, closeTime);
  951. //执行绘制
  952. ctx.value.draw();
  953. };
  954. const drawDynamicCtxChart = () => {
  955. if (klineTab.value == 1) {
  956. //绘制价格曲线
  957. HCharts.drawPriceLine(dynamicCtx.value, canvasWidth.value, canvasHeight.value, timeData.value, grid, chartRange.value[0]);
  958. //绘制成交量
  959. HCharts.drawVolume(
  960. dynamicCtx.value,
  961. canvasWidth.value,
  962. canvasHeight.value,
  963. timeData.value,
  964. 2,
  965. 240,
  966. grid,
  967. {
  968. max: utils.calculateDataRange(timeData.value, "volume").max,
  969. min: 0,
  970. },
  971. 0
  972. );
  973. HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, grid);
  974. HCharts.drawTopPriceDisplay(crosshairCtx.value, grid, text);
  975. } else {
  976. drawKLine(dynamicCtx.value);
  977. //绘制成交量
  978. HCharts.drawVolume(
  979. dynamicCtx.value,
  980. canvasWidth.value,
  981. canvasHeight.value,
  982. klineData.value,
  983. 2,
  984. klineData.value.length,
  985. dayGrid,
  986. {
  987. max: utils.calculateDataRange(klineData.value, "volume").max,
  988. min: 0,
  989. },
  990. touchState.offset
  991. );
  992. //绘制成交量
  993. HCharts.drawVolume(
  994. dynamicCtx.value,
  995. canvasWidth.value,
  996. canvasHeight.value,
  997. klineData.value,
  998. 3,
  999. klineData.value.length,
  1000. dayGrid,
  1001. {
  1002. max: utils.calculateDataRange(klineData.value, "volume").max,
  1003. min: 0,
  1004. },
  1005. touchState.offset
  1006. );
  1007. HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, dayGrid);
  1008. // drawMovingAverage();
  1009. HCharts.drawTopPriceDisplay(crosshairCtx.value, dayGrid, []);
  1010. }
  1011. //执行绘制
  1012. dynamicCtx.value.draw(false);
  1013. crosshairCtx.value.draw();
  1014. };
  1015. const throttledDrawDynamicCtxChart = throttle(drawDynamicCtxChart, 50);
  1016. // const throttledDrawKLineChart = throttle(drawKLineChart, 10);
  1017. // 绘制K线
  1018. const drawKLine = (ctx) => {
  1019. const data = klineData.value;
  1020. if (!data.length) return;
  1021. const width = canvasWidth.value;
  1022. const height = canvasHeight.value;
  1023. // 计算价格范围
  1024. const priceRange = chartRange.value[0];
  1025. const priceDiff = priceRange.max - priceRange.min;
  1026. const areaWidth = (width - dayGrid[0].left - dayGrid[0].right) / data.length;
  1027. const candleWidth = areaWidth * 0.6;
  1028. data.forEach((item, index) => {
  1029. const x = touchState.offset + dayGrid[0].left + (index * (width - dayGrid[0].left - dayGrid[0].right)) / data.length;
  1030. // 计算坐标
  1031. const highY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.high - priceRange.min) / priceDiff);
  1032. const lowY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.low - priceRange.min) / priceDiff);
  1033. const openY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.open - priceRange.min) / priceDiff);
  1034. const closeY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.close - priceRange.min) / priceDiff);
  1035. // 判断涨跌
  1036. const isRise = item.close >= item.open;
  1037. const color = isRise ? "green" : "red";
  1038. // 绘制上下影线
  1039. ctx.setStrokeStyle(color);
  1040. ctx.setLineWidth(1);
  1041. ctx.beginPath();
  1042. ctx.moveTo(x, highY);
  1043. ctx.lineTo(x, lowY);
  1044. ctx.stroke();
  1045. // 绘制K线实体
  1046. const entityTop = isRise ? closeY : openY;
  1047. const entityBottom = isRise ? openY : closeY;
  1048. const entityHeight = Math.max(Math.abs(entityBottom - entityTop), 1);
  1049. ctx.setFillStyle(color);
  1050. ctx.fillRect(x - candleWidth / 2, entityTop, candleWidth, entityHeight);
  1051. });
  1052. };
  1053. // 绘制移动平均线
  1054. const drawMovingAverage = () => {
  1055. const data = klineData.value;
  1056. if (!data.length) return;
  1057. const ma5 = calculateMA(data, 5);
  1058. const ma10 = calculateMA(data, 10);
  1059. drawMALine(ma5, "#fadb14", "MA5"); // 黄色
  1060. drawMALine(ma10, "#eb2f96", "MA10"); // 粉色
  1061. };
  1062. // 计算移动平均线
  1063. const calculateMA = (data, period) => {
  1064. const result = [];
  1065. for (let i = period - 1; i < data.length; i++) {
  1066. let sum = 0;
  1067. for (let j = 0; j < period; j++) {
  1068. sum += data[i - j].close;
  1069. }
  1070. result.push({
  1071. index: i,
  1072. value: sum / period,
  1073. });
  1074. }
  1075. return result;
  1076. };
  1077. // 绘制均线
  1078. const drawMALine = (maData, color, label) => {
  1079. if (!maData.length) return;
  1080. const width = canvasWidth.value;
  1081. const height = canvasHeight.value;
  1082. const data = klineData.value;
  1083. // 计算价格范围
  1084. const highs = data.map((item) => item.high);
  1085. const lows = data.map((item) => item.low);
  1086. const priceRange = chartRange.value[0];
  1087. const priceDiff = priceRange.max - priceRange.min;
  1088. ctx.value.setStrokeStyle(color);
  1089. ctx.value.setLineWidth(1.5);
  1090. ctx.value.beginPath();
  1091. maData.forEach((point, idx) => {
  1092. const x = 40 + (point.index * (width - 60)) / (data.length - 1);
  1093. const y = 20 + (height - 60) * (1 - (point.value - priceRange.min) / priceDiff);
  1094. if (idx === 0) {
  1095. ctx.value.moveTo(x, y);
  1096. } else {
  1097. ctx.value.lineTo(x, y);
  1098. }
  1099. });
  1100. ctx.value.stroke();
  1101. // 绘制图例
  1102. if (maData.length > 0) {
  1103. const lastPoint = maData[maData.length - 1];
  1104. const x = 40 + (lastPoint.index * (width - 60)) / (data.length - 1);
  1105. const y = 20 + (height - 60) * (1 - (lastPoint.value - priceRange.min) / priceDiff);
  1106. HCharts.drawText(ctx.value`${label}: ${utils.formatPrice(lastPoint.value)}`, x + 10, y - 5, 12, color);
  1107. }
  1108. };
  1109. // 触摸事件处理
  1110. const touchStart = (e) => {
  1111. e.preventDefault(); // 阻止页面滚动等默认行为
  1112. if (typeof e.touches[1] === "undefined") {
  1113. touchState.startX = e.touches[0].x;
  1114. touchState.startY = e.touches[0].y;
  1115. touchState.isMoving = false;
  1116. } else if (typeof e.touches[1] !== "undefined" && !crosshair.show) {
  1117. // touchState.startX1 = e.touches[0].x;
  1118. // touchState.startY1 = e.touches[0].y;
  1119. // touchState.startX2 = e.touches[1].x;
  1120. // touchState.startY2 = e.touches[1].y;
  1121. // touchState.isMoving = false;
  1122. }
  1123. };
  1124. const touchMove = (e) => {
  1125. touchState.isMoving = true;
  1126. // 计算移动距离
  1127. const currentX = e.touches[0].x;
  1128. const currentY = e.touches[0].y;
  1129. const deltaX = currentX - touchState.startX;
  1130. const deltaY = currentY - touchState.startY;
  1131. touchState.moveDistance = Math.max(touchState.moveDistance, Math.sqrt(deltaX * deltaX + deltaY * deltaY));
  1132. if (crosshair.show) {
  1133. if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) {
  1134. throttledUpdateCrosshair(currentX, currentY);
  1135. } else {
  1136. // 如果移出图表区域,隐藏十字准线
  1137. // crosshair.show = false;
  1138. if (klineTab.value === 1) {
  1139. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1140. } else {
  1141. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1142. }
  1143. }
  1144. } else {
  1145. if (klineTab.value === 2) {
  1146. // if(currentY)
  1147. if (currentX < touchState.startX) {
  1148. touchState.offset += currentX - touchState.startX;
  1149. throttledDrawDynamicCtxChart();
  1150. touchState.startX = currentX;
  1151. } else {
  1152. touchState.offset += currentX - touchState.startX;
  1153. throttledDrawDynamicCtxChart();
  1154. touchState.startX = currentX;
  1155. }
  1156. if (canvasWidth.value + touchState.offset < 30) {
  1157. touchState.offset = 0;
  1158. }
  1159. }
  1160. }
  1161. };
  1162. const touchEnd = (e) => {
  1163. // 触摸结束,隐藏十字准线
  1164. if (touchState.moveDistance < touchState.moveThreshold) {
  1165. // 移动距离小于阈值,认为是点击事件
  1166. crosshair.show = !crosshair.show;
  1167. }
  1168. if (crosshair.show) {
  1169. const currentX = e.changedTouches[0].x;
  1170. const currentY = e.changedTouches[0].y;
  1171. if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) {
  1172. throttledUpdateCrosshair(currentX, currentY);
  1173. }
  1174. }
  1175. if (klineTab.value === 1) {
  1176. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1177. } else {
  1178. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1179. }
  1180. // 重置移动距离
  1181. touchState.moveDistance = 0;
  1182. };
  1183. // 检查坐标是否在图表区域内
  1184. const isInChartArea = (x, y, grid) => {
  1185. const width = canvasWidth.value;
  1186. const height = canvasHeight.value;
  1187. if (x < grid[0].left || x > width - grid[0].right) return false;
  1188. for (let i = 0; i < grid.length; i++) {
  1189. if (y >= grid[i].top && y <= height - grid[i].bottom) return true;
  1190. }
  1191. return false;
  1192. };
  1193. // 更新十字准线位置和数据
  1194. const updateCrosshair = (x, y) => {
  1195. if (!crosshair.show) return;
  1196. // 更新Y坐标以匹配实际价格
  1197. const data = klineTab.value === 1 ? timeData.value : klineData.value;
  1198. const width = canvasWidth.value;
  1199. const height = canvasHeight.value;
  1200. crosshair.x = x;
  1201. crosshair.y = y;
  1202. const nearestData = utils.findNearestDataPoint(x, klineTab.value === 1 ? 240 : data.length, data, klineTab.value === 1 ? grid : dayGrid, klineTab.value === 1 ? 0 : touchState.offset);
  1203. crosshair.x = nearestData.x;
  1204. stockInformation.value.currentPrice = nearestData.price || nearestData.close;
  1205. stockInformation.value.volume = nearestData.volume;
  1206. // 涨跌额度
  1207. if (klineTab.value == 1) {
  1208. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1209. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1210. } else {
  1211. stockInformation.value.openPrice = nearestData.open;
  1212. stockInformation.value.highPrice = nearestData.high;
  1213. stockInformation.value.lowPrice = nearestData.low;
  1214. if (nearestData.index != 0) {
  1215. stockInformation.value.lastDayStockClosePrice = data[nearestData.index - 1].close;
  1216. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1217. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1218. }
  1219. }
  1220. if (klineTab.value === 1 && crosshair.snapToData) {
  1221. // 吸附到最近的数据点
  1222. if (nearestData) {
  1223. crosshair.currentData = nearestData;
  1224. const priceDiff = chartRange.value[0].max - chartRange.value[0].min;
  1225. crosshair.y = grid[0].top + (height - grid[0].bottom - grid[0].top) * (1 - (nearestData.price - chartRange.value[0].min) / priceDiff);
  1226. }
  1227. } else {
  1228. // 自由移动模式
  1229. const currentPrice = utils.calculatePriceFromY(y, data, klineTab.value === 1 ? grid : dayGrid);
  1230. crosshair.currentData = {
  1231. ...nearestData,
  1232. price: currentPrice,
  1233. };
  1234. }
  1235. if (crosshair?.currentData?.volume) {
  1236. text[0][1].value = crosshair.currentData.price;
  1237. text[1][0].value = crosshair.currentData.volume;
  1238. }
  1239. // 只重绘十字准心,不重绘整个图表
  1240. if (klineTab.value === 1) {
  1241. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text);
  1242. } else {
  1243. HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []);
  1244. }
  1245. };
  1246. const throttledUpdateCrosshair = throttle(updateCrosshair, 10);
  1247. // 缩放控制函数
  1248. const zoomIn = () => {
  1249. touchState.scale = Math.min(touchState.maxScale, touchState.scale * 1.2);
  1250. drawChart();
  1251. };
  1252. const zoomOut = () => {
  1253. touchState.scale = Math.max(touchState.minScale, touchState.scale / 1.2);
  1254. drawChart();
  1255. };
  1256. // 监听数据变化
  1257. // watch([timeData, klineData], () => {
  1258. // console.log("数据变化");
  1259. // drawChart();
  1260. // });
  1261. watch(klineTab, () => {
  1262. console.log("标签页变化");
  1263. drawChart();
  1264. });
  1265. /*
  1266. const connectTcp = (ip, port) => {
  1267. console.log(`🚀 正在连接TCP服务器 ${ip}:${port}...`);
  1268. TCPSocket.connect(
  1269. {
  1270. channel: "1",
  1271. ip: ip,
  1272. port: port,
  1273. },
  1274. (result) => {
  1275. console.log(result);
  1276. if (result.status == 0) {
  1277. tcpObject.isConnected = true;
  1278. console.log("tcp连接成功");
  1279. } else if (result.status == 1) {
  1280. tcpObject.isConnected = false;
  1281. console.log("tcp断开连接");
  1282. }
  1283. if (result.receivedMsg) {
  1284. console.log("收到服务器发送的数据", result.receivedMsg);
  1285. if (result.receivedMsg.length == 0 || result.receivedMsg.includes("欢迎")) {
  1286. resultR.value = [];
  1287. } else {
  1288. const msgPacket = JSON.parse(result.receivedMsg);
  1289. resultR.value.push(msgPacket);
  1290. // 股票代码
  1291. if (msgPacket.stock_code) {
  1292. stockInformation.value.stockCode = msgPacket.stock_code;
  1293. }
  1294. // 股票名称
  1295. if (msgPacket.stock_name) {
  1296. stockInformation.value.stockName = msgPacket.stock_name;
  1297. }
  1298. // 前一日收盘价
  1299. if (msgPacket.pre_close) {
  1300. stockInformation.value.lastDayStockClosePrice = msgPacket.pre_close;
  1301. }
  1302. // 当前股价(收盘价)
  1303. if (msgPacket.current_price) {
  1304. stockInformation.value.currentPrice = msgPacket.current_price;
  1305. }
  1306. // 涨跌额度
  1307. if (msgPacket.current_price && msgPacket.pre_close) {
  1308. stockInformation.value.currentValue = msgPacket.current_price - msgPacket.pre_close;
  1309. stockInformation.value.currentRatio = ((msgPacket.current_price - msgPacket.pre_close) / msgPacket.pre_close) * 100;
  1310. }
  1311. // 最高价
  1312. if (msgPacket.high_price) {
  1313. stockInformation.value.highPrice = msgPacket.high_price;
  1314. }
  1315. // 最低价
  1316. if (msgPacket.close_price) {
  1317. stockInformation.value.lowPrice = msgPacket.close_price;
  1318. }
  1319. // 开盘价
  1320. if (msgPacket.open_price) {
  1321. stockInformation.value.openPrice = msgPacket.open_price;
  1322. }
  1323. // 开盘价
  1324. if (msgPacket.low_price) {
  1325. stockInformation.value.lowPrice = msgPacket.low_price;
  1326. }
  1327. // 收盘价(当前股价)
  1328. if (msgPacket.current_price) {
  1329. stockInformation.value.closePrice = msgPacket.current_price;
  1330. }
  1331. // 成交量
  1332. if (msgPacket.volume) {
  1333. stockInformation.value.volume = msgPacket.volume;
  1334. }
  1335. // 成交量比
  1336. if (msgPacket.volume_ratio) {
  1337. stockInformation.value.volumeRatio = msgPacket.volume_ratio;
  1338. }
  1339. // 成交额
  1340. if (msgPacket.amount) {
  1341. stockInformation.value.amount = msgPacket.amount;
  1342. }
  1343. // 市盈
  1344. if (msgPacket.market_earn) {
  1345. stockInformation.value.marketEarn = msgPacket.market_earn;
  1346. }
  1347. // 换手率
  1348. if (msgPacket.turnover_ratio) {
  1349. stockInformation.value.turnoverRatio = msgPacket.turnover_ratio;
  1350. }
  1351. // 市值
  1352. if (msgPacket.total_market_value) {
  1353. stockInformation.value.marketValue = msgPacket.total_market_value;
  1354. }
  1355. }
  1356. }
  1357. // if (result.receivedHexMsg) {
  1358. // //服务器返回16进制数据
  1359. // console.log("📥 收到16进制数据:", result.receivedHexMsg);
  1360. // let msg = result.receivedHexMsg;
  1361. // let sum = msg.length / 2;
  1362. // let arr = [];
  1363. // for (let k = 0; k < sum; k++) {
  1364. // let i = msg.substring(k * 2, k * 2 + 2);
  1365. // arr.push(i);
  1366. // }
  1367. // console.log("转换成16进制数组:", arr);
  1368. // if (tcpObject.responseCallback) {
  1369. // tcpObject.responseCallback({
  1370. // data: arr,
  1371. // });
  1372. // }
  1373. // }
  1374. // 设置连接超时
  1375. setTimeout(() => {
  1376. if (!tcpObject.isConnected) {
  1377. throw new Error("连接超时");
  1378. return;
  1379. }
  1380. }, TIME_OUT);
  1381. }
  1382. );
  1383. };
  1384. // 断开连接
  1385. const disconnect = () => {
  1386. try {
  1387. TCPSocket.disconnect({
  1388. channel: "1",
  1389. });
  1390. tcpObject.isConnected = false;
  1391. console.log("✅ 连接已关闭");
  1392. } catch (e) {
  1393. console.log("⚠️ 断开连接时发生错误:", e.message);
  1394. }
  1395. };
  1396. const startTcp = async () => {
  1397. console.log("🚀 TCP客户端启动");
  1398. console.log(`目标服务器: ${tcpObject.ip}:${tcpObject.port}`);
  1399. try {
  1400. // 连接服务器
  1401. await connectTcp(tcpObject.ip, tcpObject.port);
  1402. } catch (e) {
  1403. console.log("error", e);
  1404. }
  1405. };
  1406. */
  1407. // 定时器标识(用于清除定时器)
  1408. let timer = null;
  1409. let index = 0;
  1410. // 定时添加数据的函数
  1411. const startAddDataTimer = () => {
  1412. if (timer) {
  1413. console.log("存在旧定时器,卸载旧定时器");
  1414. clearInterval(timer);
  1415. }
  1416. console.log("开始定时任务");
  1417. // 每隔5秒执行一次
  1418. timer = setInterval(() => {
  1419. if (index < testTimeData.length) {
  1420. timeData.value.push(testTimeData[index]);
  1421. console.log("新增数据:", testTimeData[index]);
  1422. // 触发图表重新绘制
  1423. drawChart();
  1424. index++;
  1425. } else {
  1426. clearInterval(timer);
  1427. }
  1428. }, 2000); // 5000毫秒 = 5秒
  1429. };
  1430. onLoad((options) => {
  1431. console.log("页面接收到的参数:", options);
  1432. // 处理通过data参数传递的复杂对象
  1433. if (options.data) {
  1434. try {
  1435. const stockData = JSON.parse(decodeURIComponent(options.data));
  1436. console.log("解析的股票数据:", stockData);
  1437. // 更新stockInformation
  1438. if (stockData) {
  1439. stockInformation.value.stockName=stockData.stockName;
  1440. stockInformation.value.stockCode=stockData.stockCode;
  1441. }
  1442. } catch (error) {
  1443. console.error("解析股票数据失败:", error);
  1444. }
  1445. }
  1446. // 处理通过stockInformation参数传递的数据(兼容globalIndex.vue的传参方式)
  1447. if (options.stockInformation) {
  1448. try {
  1449. const stockData = JSON.parse(decodeURIComponent(options.stockInformation));
  1450. console.log("解析的股票信息:", stockData);
  1451. // 更新stockInformation
  1452. if (stockData) {
  1453. stockInformation.value = {
  1454. ...stockInformation.value,
  1455. ...stockData
  1456. };
  1457. }
  1458. } catch (error) {
  1459. console.error("解析股票信息失败:", error);
  1460. }
  1461. }
  1462. // 处理简单参数
  1463. if (options.stockCode) {
  1464. stockInformation.value.stockCode = options.stockCode;
  1465. }
  1466. if (options.stockName) {
  1467. stockInformation.value.stockName = decodeURIComponent(options.stockName);
  1468. }
  1469. });
  1470. // 保存定时器,用于页面卸载时清理
  1471. onUnmounted(() => {
  1472. // disconnect();
  1473. if (timer) {
  1474. console.log("卸载定时器");
  1475. clearInterval(timer);
  1476. }
  1477. });
  1478. onMounted(async () => {
  1479. // 获取系统信息,处理高清屏
  1480. const systemInfo = uni.getSystemInfoSync();
  1481. pixelRatio.value = systemInfo.pixelRatio;
  1482. // 设置Canvas实际像素(考虑pixelRatio以获得高清效果)
  1483. // 1rpx = 设备屏幕宽度 / 750
  1484. const rpxToPx = systemInfo.windowWidth / 750;
  1485. const offsetHeight = (150 + 200 + 80 + 150 + 30) * rpxToPx; // 350rpx转换为px
  1486. const calculatedHeight = systemInfo.windowHeight - offsetHeight;
  1487. canvasWidth.value = systemInfo.windowWidth;
  1488. canvasHeight.value = Math.max(calculatedHeight, canvasHeight.value);
  1489. // startTcp();
  1490. // timeData.value = testTimeData;
  1491. timeData.value = testTimeData.slice(0, 100);
  1492. klineData.value = testKlineData;
  1493. // klineData.value = testKlineData.slice(-touchState.baseVisibleCount);
  1494. // 前一日收盘价
  1495. let prevClosePrice = timeData.value[timeData.value.length - 2].price || stockInformation.value.closePrice;
  1496. stockInformation.value.lastDayStockClosePrice = prevClosePrice;
  1497. // 当前股价
  1498. stockInformation.value.currentPrice = timeData.value[timeData.value.length - 1].price;
  1499. // 涨跌额度
  1500. stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice;
  1501. // 涨跌幅度
  1502. stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100;
  1503. // 成交量
  1504. stockInformation.value.volume = timeData.value[timeData.value.length - 1].volume;
  1505. text[0][1].value = utils.formatPrice(stockInformation.value.currentPrice);
  1506. text[1][0].value = utils.formatStockNumber(stockInformation.value.volume);
  1507. await nextTick();
  1508. setTimeout(() => {
  1509. initCanvas();
  1510. }, 200);
  1511. });
  1512. </script>
  1513. <style>
  1514. .container {
  1515. width: 100%;
  1516. min-height: 100vh;
  1517. background-color: #f6f6f6;
  1518. }
  1519. .title-container {
  1520. width: 100%;
  1521. height: 150rpx;
  1522. background-color: white;
  1523. display: flex;
  1524. flex-direction: column;
  1525. justify-content: flex-end;
  1526. }
  1527. .title {
  1528. /* border: 1px solid #ff0000; */
  1529. position: relative;
  1530. /* 关键:作为绝对定位的父容器 */
  1531. width: 100%;
  1532. height: 80rpx;
  1533. display: flex;
  1534. justify-content: center;
  1535. align-items: center;
  1536. }
  1537. .back-homepage-btn {
  1538. margin-left: 40rpx;
  1539. margin-right: auto;
  1540. height: 30rpx;
  1541. width: 20rpx;
  1542. z-index: 1;
  1543. }
  1544. .mid-title {
  1545. position: absolute;
  1546. left: 0;
  1547. right: 0;
  1548. display: flex;
  1549. justify-content: center;
  1550. align-items: center;
  1551. }
  1552. .left-page {
  1553. height: 40rpx;
  1554. width: 40rpx;
  1555. }
  1556. .right-page {
  1557. height: 40rpx;
  1558. width: 40rpx;
  1559. }
  1560. .stock-id {
  1561. margin: 0rpx 40rpx;
  1562. display: flex;
  1563. flex-direction: column;
  1564. text-align: center;
  1565. }
  1566. .stock-name {
  1567. font-weight: bold;
  1568. }
  1569. .stock-code {
  1570. font-size: 0.8rem;
  1571. font-weight: bold;
  1572. color: #a1a1a1;
  1573. }
  1574. .search {
  1575. height: 40rpx;
  1576. width: 40rpx;
  1577. }
  1578. .more {
  1579. height: 100%;
  1580. display: flex;
  1581. justify-content: center;
  1582. align-items: center;
  1583. margin-right: 40rpx;
  1584. margin-left: 20rpx;
  1585. }
  1586. .body {
  1587. overflow: auto;
  1588. height: calc(100vh - 305rpx);
  1589. /* border: 1px solid red; */
  1590. }
  1591. .stock-information {
  1592. background-color: white;
  1593. width: 100%;
  1594. height: 200rpx;
  1595. margin: 10rpx 0rpx;
  1596. display: flex;
  1597. align-items: center;
  1598. position: relative;
  1599. /* 为伪元素定位做准备 */
  1600. }
  1601. /* 右下角黑色三角形 */
  1602. .stock-information::after {
  1603. content: "";
  1604. position: absolute;
  1605. bottom: 0;
  1606. right: 0;
  1607. width: 0;
  1608. height: 0;
  1609. border-left: 20rpx solid transparent;
  1610. border-bottom: 20rpx solid #6a6a6a;
  1611. }
  1612. .stock-detail-container {
  1613. position: absolute;
  1614. top: 100%;
  1615. /* 在父容器下方 */
  1616. left: 0;
  1617. /* 从左边开始 */
  1618. right: 0;
  1619. /* 到右边结束 */
  1620. width: 100%;
  1621. /* height: 300rpx; 使用固定高度替代calc,避免计算问题 */
  1622. height: calc(100vh - 515rpx);
  1623. display: flex;
  1624. flex-direction: column;
  1625. background-color: rgba(0, 0, 0, 0.6);
  1626. z-index: 100;
  1627. box-sizing: border-box;
  1628. pointer-events: auto;
  1629. }
  1630. .stock-detail {
  1631. border: 1px solid #cacaca;
  1632. width: 100%;
  1633. display: flex;
  1634. background-color: white;
  1635. padding: 10rpx 0;
  1636. }
  1637. .first-column,
  1638. .second-column {
  1639. width: 50%;
  1640. height: 100%;
  1641. display: flex;
  1642. flex-direction: column;
  1643. color: #6a6a6a;
  1644. font-size: 0.8rem;
  1645. }
  1646. .first-column-data,
  1647. .second-column-data {
  1648. display: flex;
  1649. justify-content: space-between;
  1650. align-items: center;
  1651. padding: 10rpx 40rpx;
  1652. }
  1653. .stock-detail-title {
  1654. color: #6a6a6a;
  1655. }
  1656. .stock-detail-value {
  1657. color: black;
  1658. }
  1659. .price-up {
  1660. color: #10b981;
  1661. }
  1662. .price-down {
  1663. color: #ef4444;
  1664. }
  1665. .price-none {
  1666. color: black;
  1667. }
  1668. .stock-current-data {
  1669. width: 30%;
  1670. height: 70%;
  1671. display: flex;
  1672. flex-direction: column;
  1673. }
  1674. .stock-current-price {
  1675. font-weight: bold;
  1676. font-size: 1.2rem;
  1677. width: 100%;
  1678. height: 50%;
  1679. display: flex;
  1680. justify-content: center;
  1681. align-items: center;
  1682. }
  1683. .stock-current-other {
  1684. width: 100%;
  1685. height: 50%;
  1686. display: flex;
  1687. font-weight: bold;
  1688. font-size: 0.6rem;
  1689. }
  1690. .stock-current-value {
  1691. width: 50%;
  1692. height: 100%;
  1693. display: flex;
  1694. justify-content: center;
  1695. align-items: center;
  1696. }
  1697. .stock-current-ratio {
  1698. width: 50%;
  1699. height: 100%;
  1700. display: flex;
  1701. justify-content: center;
  1702. align-items: center;
  1703. }
  1704. .stock-other-data {
  1705. height: 100%;
  1706. width: 70%;
  1707. display: flex;
  1708. flex-direction: column;
  1709. }
  1710. .first-line,
  1711. .second-line,
  1712. .third-line {
  1713. display: flex;
  1714. align-items: center;
  1715. width: 100%;
  1716. height: 33%;
  1717. /* font-weight: bold; */
  1718. font-size: 0.8rem;
  1719. }
  1720. .value {
  1721. margin-left: auto;
  1722. margin-right: 20rpx;
  1723. }
  1724. .high-price,
  1725. .volume,
  1726. .volume-ratio,
  1727. .low-price,
  1728. .amount,
  1729. .market-earn,
  1730. .open-price,
  1731. .turnover-ratio,
  1732. .market-value {
  1733. display: flex;
  1734. flex: 1;
  1735. }
  1736. .stock-chart {
  1737. width: 100%;
  1738. }
  1739. .stock-kline-tab {
  1740. display: flex;
  1741. width: 100%;
  1742. height: 80rpx;
  1743. /* border: 1px solid black; */
  1744. }
  1745. .tab-day,
  1746. .tab-month,
  1747. .tab-time,
  1748. .tab-week,
  1749. .tab-more,
  1750. .tab-setting {
  1751. display: flex;
  1752. justify-content: center;
  1753. align-items: center;
  1754. flex: 1;
  1755. font-size: 0.8rem;
  1756. color: #6a6a6a;
  1757. position: relative;
  1758. }
  1759. /* 向下小三角样式 */
  1760. .arrow-down {
  1761. width: 0;
  1762. height: 0;
  1763. border-left: 10rpx solid transparent;
  1764. border-right: 10rpx solid transparent;
  1765. border-bottom: 12rpx solid currentColor;
  1766. margin-left: 8rpx;
  1767. display: inline-block;
  1768. }
  1769. .arrow-up {
  1770. width: 0;
  1771. height: 0;
  1772. border-left: 10rpx solid transparent;
  1773. border-right: 10rpx solid transparent;
  1774. border-top: 12rpx solid currentColor;
  1775. margin-left: 8rpx;
  1776. display: inline-block;
  1777. }
  1778. .arrow-left {
  1779. width: 0;
  1780. height: 0;
  1781. border-top: 10rpx solid transparent;
  1782. border-bottom: 10rpx solid transparent;
  1783. border-right: 12rpx solid currentColor;
  1784. margin-left: 8rpx;
  1785. display: inline-block;
  1786. }
  1787. .arrow-right {
  1788. width: 0;
  1789. height: 0;
  1790. border-top: 10rpx solid transparent;
  1791. border-bottom: 10rpx solid transparent;
  1792. border-left: 12rpx solid currentColor;
  1793. margin-left: 8rpx;
  1794. display: inline-block;
  1795. }
  1796. .moreTabsContainer {
  1797. width: 100%;
  1798. display: flex;
  1799. gap: 5rpx;
  1800. margin-bottom: 5rpx;
  1801. }
  1802. .moreTabItem {
  1803. flex: 1;
  1804. padding: 10rpx 20rpx;
  1805. border-radius: 10rpx;
  1806. background-color: white;
  1807. font-size: 0.85rem;
  1808. color: #6a6a6a;
  1809. text-align: center;
  1810. }
  1811. .tab-selected {
  1812. color: #db1f1d;
  1813. }
  1814. .tab-selected::after {
  1815. content: "";
  1816. position: absolute;
  1817. bottom: 10rpx;
  1818. left: 50%;
  1819. transform: translateX(-50%);
  1820. width: 40rpx;
  1821. height: 5rpx;
  1822. background-color: #db1f1d;
  1823. }
  1824. .tab-setting-img {
  1825. width: 30rpx;
  1826. height: 30rpx;
  1827. }
  1828. .time-chart-container {
  1829. width: 100%;
  1830. min-height: 400rpx;
  1831. background-color: #ffffff;
  1832. box-sizing: border-box;
  1833. }
  1834. .stock-chart {
  1835. display: flex;
  1836. flex-direction: column;
  1837. align-items: center;
  1838. }
  1839. .kline-chart-container {
  1840. width: 100%;
  1841. height: 400rpx;
  1842. background-color: #ffffff;
  1843. padding: 20rpx;
  1844. box-sizing: border-box;
  1845. display: flex;
  1846. justify-content: center;
  1847. align-items: center;
  1848. }
  1849. .bottomTool {
  1850. width: 100%;
  1851. height: 150rpx;
  1852. position: fixed;
  1853. bottom: 0;
  1854. background-color: white;
  1855. display: flex;
  1856. box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
  1857. }
  1858. .index,
  1859. .function,
  1860. .favorites {
  1861. flex: 1;
  1862. display: flex;
  1863. justify-content: center;
  1864. align-items: center;
  1865. flex-direction: column;
  1866. font-size: 12px;
  1867. transition: all 0.2s ease;
  1868. }
  1869. .index:active,
  1870. .function:active,
  1871. .favorites:active {
  1872. background-color: rgba(99, 99, 99, 0.5);
  1873. transform: scale(0.95);
  1874. transition: all 0.1s ease;
  1875. }
  1876. .icon {
  1877. width: 40rpx;
  1878. height: 40rpx;
  1879. }
  1880. </style>