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.

2912 lines
87 KiB

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