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.

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