|
|
<!-- @format -->
<template> <!-- 容器 --> <view class="container"> <!-- 标题 --> <view class="title-container"> <view class="title"> <image class="back-homepage-btn" src="/static/marketSituation-image/back.png" mode="返回按钮" @click="backToHomepage()"> </image> <view class="mid-title"> <view class="arrow-left"> </view> <view class="stock-id"> <view class="stock-name"> {{ stockInformation.stockName }} </view> <view class="stock-code"> {{ stockInformation.stockCode }} </view> </view> <view class="arrow-right"> </view> </view> <image class="search" src="/static/marketSituation-image/search.png" mode="搜索" @click="search()"> </image> <view class="more" @click="getMore()">···</view> </view> </view>
<view class="body"> <!-- 股票信息栏 --> <view class="stock-information" @click="openStockDetail()"> <view v-if="isStockDetail" class="stock-detail-container" @click.stop @click="closeStockDetail()"> <view class="stock-detail" @click.stop> <view class="first-column"> <view class="first-column-data" v-for="item in firstColumData" :key="item"> <view class="stock-detail-title"> {{ item.title }} </view> <view class="stock-detail-value"> {{ item.value }} </view> </view> </view> <view class="second-column"> <view class="second-column-data" v-for="item in secondColumnData" :key="item"> <view class="stock-detail-title"> {{ item.title }} </view> <view class="stock-detail-value"> {{ item.value }} </view> </view> </view> </view> </view> <view class="stock-current-data"> <view class="stock-current-price" :class="confirmStockColor(stockInformation.currentPrice, stockInformation.lastDayStockClosePrice)"> {{ Number(stockInformation.currentPrice).toFixed(2) }} </view> <view class="stock-current-other"> <view class="stock-current-value" :class="confirmStockColor(stockInformation.currentValue)"> {{ Number(stockInformation.currentValue).toFixed(2) }} </view> <view class="stock-current-ratio" :class="confirmStockColor(stockInformation.currentRatio)"> {{ Number(stockInformation.currentRatio).toFixed(2) }}% </view> </view> </view> <view class="stock-other-data"> <view class="first-line"> <view class="high-price"> 高 <view class="value" :class="confirmStockColor(stockInformation.highPrice, stockInformation.lastDayStockClosePrice)"> {{ Number(stockInformation.highPrice).toFixed(2) }} </view> </view>
<view class="volume"> 量 <view class="value"> {{ utils.formatStockNumber(stockInformation.volume, 2) }} </view> </view> <view class="volume-ratio"> 量比 <view class="value"> {{ Number(stockInformation.volumeRatio).toFixed(2) }} </view> </view> </view> <view class="second-line"> <view class="low-price"> 低 <view class="value" :class="confirmStockColor(stockInformation.lowPrice, stockInformation.lastDayStockClosePrice)"> {{ Number(stockInformation.lowPrice).toFixed(2) }} </view> </view> <view class="amount"> 额 <view class="value"> {{ utils.formatStockNumber(stockInformation.amount, 2) }} </view> </view> <view class="market-earn"> 市盈 <view class="value"> {{ Number(stockInformation.marketEarn).toFixed(2) }} </view> </view> </view> <view class="third-line"> <view class="open-price"> 开 <view class="value" :class="confirmStockColor(stockInformation.openPrice, stockInformation.lastDayStockClosePrice)"> {{ Number(stockInformation.openPrice).toFixed(2) }} </view> </view> <view class="turnover-ratio"> 换 <view class="value"> {{ Number(stockInformation.turnoverRatio).toFixed(2) }}% </view> </view> <view class="market-value"> 市值 <view class="value"> {{ utils.formatStockNumber(stockInformation.marketValue, 2) }} </view> </view> </view> </view> </view> <!-- 股票图表 --> <view class="stock-chart"> <view class="stock-kline-tab"> <!-- 1:分时 2:日K 3:周K 4:月K --> <view class="tab-time" :class="{ 'tab-selected': klineTab == 1 }" @click="selectKlineTab(1)"> 分时 </view> <view class="tab-day" :class="{ 'tab-selected': klineTab == 2 }" @click="selectKlineTab(2)"> 日K </view> <view class="tab-week" :class="{ 'tab-selected': klineTab == 3 }" @click="selectKlineTab(3)"> 周K </view> <view class="tab-month" :class="{ 'tab-selected': klineTab == 4 }" @click="selectKlineTab(4)"> 月K </view> <view class="tab-more" :class="{ 'tab-selected': klineTab != 1 && klineTab != 2 && klineTab != 3 && klineTab != 4 }" @click="isMoreTabs ? closeMoreTabs() : openMoreTabs()"> <view class="more-no-choose" v-if="klineTab == 1 || klineTab == 2 || klineTab == 3 || klineTab == 4"> 更多 </view> <view class="more-choose" v-else> {{ chooseTabName(klineTab) }} </view> <view :class="[isMoreTabs ? 'arrow-down' : 'arrow-up']"> </view> </view> <view class="tab-setting"> <image class="tab-setting-img" src="/static/marketSituation-image/marketCondition-image/setting2.png" mode="设置"></image> </view> </view> <view v-if="isMoreTabs" class="moreTabsContainer"> <view v-for="item in moreTabsData" :key="item" class="moreTabItem" :class="{ 'tab-selected': klineTab == item.value }" @click="selectMoreTab(item.value)"> {{ item.title }} </view> </view> <view class="stock-kline"> <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"> <!-- 主图Canvas --> <canvas canvas-id="stockChart" class="stock-chart" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', pointerEvents: 'none', }" will-read-frequently="true" ></canvas> <!-- 动态数据Canvas层 --> <canvas canvas-id="dynamicCanvas" id="dynamicCanvas" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', pointerEvents: 'none', position: 'absolute', top: '0', left: '0', }" will-read-frequently="true" ></canvas> <!-- 十字准心Canvas层 --> <canvas canvas-id="crosshairCanvas" id="crosshairCanvas" :width="canvasWidth" :height="canvasHeight" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'absolute', top: '0', left: '0', }" will-read-frequently="true" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" ></canvas> </view> <!-- K线图区域 --> <view class="test" v-else-if="klineTab === 10"> <button @click="startTcp()">接收消息</button> <button @click="sendStopTimeData()">停止消息</button> <button @click="sendTcpMessage('real_time')">实时行情推送</button> <button @click="sendTcpMessage('init_real_time')">初始化获取行情历史数据</button> <button @click="sendTcpMessage('stop_real_time')">停止实时推送</button> </view> <view v-else class="kline-chart-container"> <text>K线图开发中...</text> </view> </view> </view> </view>
<view class="bottomTool"> <view class="index"> <image class="icon" src="/static/marketSituation-image/marketCondition-image/index.png" mode="指标仓库图标"> </image> 指标仓库 </view> <view class="function"> <image class="icon" src="/static/marketSituation-image/marketCondition-image/function.png" mode="功能图标"> </image> 功能 </view> <view class="favorites"> <image class="icon" src="/static/marketSituation-image/marketCondition-image/favorites.png" mode="加自选图标"></image> 加自选 </view> </view> </view></template>
<script setup>import { ref, reactive, computed, onMounted, watch, nextTick, onUnmounted, getCurrentInstance } from "vue";import { onLoad } from "@dcloudio/uni-app";const instance = getCurrentInstance();import { prevClosePrice, timeData as testTimeData, klineData as testKlineData } from "@/common/stockTimeInformation.js";import { throttle } from "@/common/util.js";import { HCharts } from "@/common/canvasMethod.js";import tcpConnection, { TCPConnection, TCP_CONFIG } from "@/api/tcpConnection.js";
// TCP相关响应式变量
const tcpConnected = ref(false);const connectionListener = ref(null);const messageListener = ref(null);
// 股票信息栏变量
const stockInformation = ref({ stockName: "----", //股票名称
stockCode: "------", //股票代码
lastDayStockClosePrice: 0.0, //前一日收盘价
currentPrice: 0.0, //当前股价
currentValue: 0.0, //涨跌额度
currentRatio: 0.0, //涨跌幅度
highPrice: 0.0, //最高价
lowPrice: 0.0, //最低价
openPrice: 0.0, //开盘价
closePrice: 0.0, //收盘价
volume: 0.0, //成交量
volumeRatio: 0.0, //成交量比
amount: 0.0, //成交额
marketEarn: 0.0, //市盈
turnoverRatio: 0.0, //换手率
marketValue: 0.0, //市值
});
// 是否展开股票信息细节栏的判断变量
const isStockDetail = ref(false);
// 股票信息细节内容变量
const firstColumData = [ { title: "振幅", value: 0, }, { title: "现手", value: 0, }, { title: "内盘", value: 0, }, { title: "外盘", value: 0, }, { title: "流通股", value: 0, }, { title: "每股净资产", value: 0, }, { title: "净资产收益率", value: 0, }, { title: "总股本", value: 0, }, { title: "总资本", value: 0, }, { title: "总市值", value: 0, }, { title: "一个月最高", value: 0, }, { title: "一个月最低", value: 0, },];const secondColumnData = [ { title: "均价", value: 0, }, { title: "昨收", value: 0, }, { title: "委比", value: 0, }, { title: "委买", value: 0, }, { title: "委卖", value: 0, }, { title: "市盈利(静)", value: 0, }, { title: "市盈利(动)", value: 0, }, { title: "市净率", value: 0, }, { title: "涨停价", value: 0, }, { title: "跌停价", value: 0, }, { title: "一年最高", value: 0, }, { title: "一年最低", value: 0, },];
// 是否展开更多Tab的判断变量
const isMoreTabs = ref(false);
const moreTabsData = ref([ { title: "1分", value: 5, }, { title: "5分", value: 6, }, { title: "15分", value: 7, }, { title: "30分", value: 8, }, { title: "60分", value: 9, }, { title: "季K", value: 10, }, { title: "年K", value: 11, },]);
// 股票当前选中的K线类型Tab
// 1:分时 2:日K 3:周K 4:月K
const klineTab = ref(1);const startTcp = () => { try { removeTcpListeners(); disconnectTcp(); initTcpListeners(); connectTcp(); } catch (error) { console.error("建立连接并设置监听:", error); uni.showToast({ title: "建立连接并设置监听", icon: "none", duration: 1500, }); }};
const sendStopTimeData = () => { disconnectTcp(); removeTcpListeners();};
// 确定股票数据的颜色方法
const confirmStockColor = (price, lastDayStockClosePrice) => { if (typeof lastDayStockClosePrice === "undefined") { if (price == 0) { return "price-none"; } else if (price > 0) { return "price-up"; } else { return "price-down"; } } else { if (price == lastDayStockClosePrice) { return "price-none"; } else if (price > lastDayStockClosePrice) { return "price-up"; } else { return "price-down"; } }};
// 股票K线类型方法
const selectKlineTab = (tabId) => { klineTab.value = tabId; if (klineTab.value) { sendTcpMessage("stop_real_time"); } switch (klineTab.value) { case 1: sendTcpMessage("init_real_time"); break; case 2: sendTcpMessage("daily_data"); break; case 3: sendTcpMessage("weekly_data"); break; case 4: sendTcpMessage("monthly_data"); break; case 5: sendTcpMessage("daily_one_minutes_data"); break; case 6: sendTcpMessage("daily_five_minutes_data"); break; case 7: sendTcpMessage("daily_fifteen_minutes_data"); break; case 8: sendTcpMessage("daily_thirty_minutes_data"); break; case 9: sendTcpMessage("daily_sixty_minutes_data"); break; case 10: uni.showToast({ title: "暂无季K数据", icon: "none", duration: 2000, }); break; case 11: uni.showToast({ title: "暂无年K数据", icon: "none", duration: 2000, }); break; default: break; } initCanvas(); // startAddDataTimer();
};// 返回按钮
const backToHomepage = () => { const pages = getCurrentPages(); if (pages.length > 1) { uni.navigateBack(); } else { // 如果没有上一页,跳转到首页
uni.reLaunch({ url: "/pages/home/home", }); }};
const openStockDetail = () => { isStockDetail.value = true;};
const closeStockDetail = () => { isStockDetail.value = false;};
const openMoreTabs = () => { isMoreTabs.value = true;};
const closeMoreTabs = () => { isMoreTabs.value = false;};
const selectMoreTab = (value) => { selectKlineTab(value);};
const chooseTabName = () => { if (klineTab.value === 5) { return "1分"; } else if (klineTab.value === 6) { return "5分"; } else if (klineTab.value === 7) { return "15分"; } else if (klineTab.value === 8) { return "30分"; } else if (klineTab.value === 9) { return "60分"; } else if (klineTab.value === 10) { return "季K"; } else if (klineTab.value === 11) { return "年K"; }};
// 画布对象
const canvasWidth = ref(100);const canvasHeight = ref(100);const CANVAS_BACKGROUND_COLOR = "#fff";const TEXT_COLOR1 = "#000";const TEXT_COLOR2 = "red";const LINE_COLOR = "#e5e5e5";const timeChartObject = ref({ min: 0, max: 0,});const klineChartObject = ref({ min: 0, max: 0,});
const chartRange = ref();
// 开盘时间
const openTime = "09:30";// 收盘时间
const closeTime = "15:00";
const ctx = ref(null);const dynamicCtx = ref(null); // 动态Canvas上下文
const crosshairCtx = ref(null); // 十字准心Canvas上下文
const pixelRatio = ref(1);// 触屏对象
const touchState = reactive({ startX: 0, startY: 0, isMoving: false, moveDistance: 0, moveThreshold: 10, //移动阈值(用于判断是否在点击)
scale: 1, // 当前缩放比例
minScale: 0.3, // 最小缩放比例
maxScale: 5, // 最大缩放比例
baseVisibleCount: 40, // 基准可见K线数量
offset: 0, // 数据偏移量
isZooming: false, // 是否正在缩放
initialDistance: 0, // 初始两指距离
initialScale: 1, // 初始缩放比例
});// 计算当前可见的K线数量
const visibleCount = computed(() => { return Math.floor(touchState.baseVisibleCount / touchState.scale);});// 计算当前显示的数据范围
const visibleDataRange = computed(() => { const start = Math.max(0, klineData.value.length - visibleCount.value - touchState.offset); const end = Math.min(klineData.value.length, start + visibleCount.value); return { start, end, };});// 获取当前可见的数据
const visibleKlineData = computed(() => { const { start, end } = visibleDataRange.value; return klineData.value.slice(start, end);});// 计算两点之间的距离
const getDistance = (touch1, touch2) => { const dx = touch1.x - touch2.x; const dy = touch1.y - touch2.y; return Math.sqrt(dx * dx + dy * dy);};
// 十字准线相关状态
const crosshair = reactive({ show: false, x: 0, y: 0, currentData: null, snapToData: true,});// 绘制网格和坐标轴
const grid = [ { top: 20, bottom: canvasHeight.value * 0.5, left: 5, right: 5, lineColor: LINE_COLOR, lineWidth: 1, horizontalLineNum: 5, verticalLineNum: 5, }, { top: canvasHeight.value * 0.5 + 20, bottom: 20, left: 5, right: 5, lineColor: LINE_COLOR, lineWidth: 1, horizontalLineNum: 3, verticalLineNum: 5, },];
// 绘制网格和坐标轴
const dayGrid = [ { top: 20, bottom: canvasHeight.value * 0.5, left: 5, right: 5, lineColor: LINE_COLOR, lineWidth: 1, horizontalLineNum: 5, verticalLineNum: 2, }, { top: canvasHeight.value * 0.5 + 20, bottom: 20, left: 5, right: 5, lineColor: LINE_COLOR, lineWidth: 1, horizontalLineNum: 3, verticalLineNum: 2, }, { top: canvasHeight.value * 0.5 + 20, bottom: 20, left: 5, right: 5, lineColor: LINE_COLOR, lineWidth: 1, horizontalLineNum: 3, verticalLineNum: 2, },];// 工具函数
const utils = { // 格式化价格
formatPrice(price) { return price.toFixed(2); },
// 计算数据范围
calculateDataRange(data, key) { if (!data || data.length === 0) { return { min: 0, max: 0, }; } const values = data.map((item) => item[key]); return { min: Math.min(...values), max: Math.max(...values), }; }, // 计算标签
calculateLabel(data, type = 2, preClosePrice = 0, key, num) { let label = []; if (key === "price") { // 分时价格区间
if (type == 1) { const priceRange = utils.calculateDataRange(data, "price"); const theMost = Math.max(priceRange.max - preClosePrice, preClosePrice - priceRange.min); const mid = (num - 1) / 2; // 计算分时价格标签
label[mid] = { value: utils.formatPrice(preClosePrice), ratio: utils.formatPrice(0) + "%", }; for (let i = 0; i < mid; i++) { label[i] = { value: utils.formatPrice(preClosePrice + (theMost * (mid - i)) / mid), ratio: utils.formatPrice((100 * (theMost * (mid - i))) / mid / preClosePrice) + "%", };
label[num - 1 - i] = { value: utils.formatPrice(preClosePrice - (theMost * (mid - i)) / mid), ratio: utils.formatPrice((-1 * 100 * (theMost * (mid - i))) / mid / preClosePrice) + "%", }; } chartRange.value.push({ max: preClosePrice + theMost, min: preClosePrice - theMost, }); // timeChartObject.value.max = preClosePrice + theMost;
// timeChartObject.value.min = preClosePrice - theMost;
return label; } else { const highPriceRange = utils.calculateDataRange(data, "high"); const lowPriceRange = utils.calculateDataRange(data, "low"); const priceRange = { max: highPriceRange.max * 1.01, min: lowPriceRange.min * 0.99, }; const priceDiff = priceRange.max - priceRange.min; for (let i = 0; i < num; ++i) { label[i] = { value: utils.formatPrice(priceRange.max - (i * priceDiff) / (num - 1)), }; } chartRange.value.push(priceRange); // klineChartObject.value.max = highPriceRange.max * 1.01;
// klineChartObject.value.min = lowPriceRange.min * 0.99;
return label; } } else if (key === "volume") { const volumeRange = utils.calculateDataRange(data, "volume"); chartRange.value.push({ max: volumeRange.max, min: 0, }); label[0] = { value: utils.formatPrice(volumeRange.max), }; label[1] = { value: utils.formatPrice(0), }; return label; } return null; }, // 线性插值
lerp(start, end, factor) { return start + (end - start) * factor; },
// 根据X坐标找到最近的数据点
findNearestDataPoint(x, pointLen, data, grid, offset) { if (!data.length) return null; const width = canvasWidth.value; const height = canvasHeight.value; // 计算每个数据点的X坐标间隔
// 倒推 const x=5+(index*(width-10)/pointLen) 已知x求index
const xStep = width - grid[0].left - grid[0].right;
// 计算触摸点对应的数据索引
const touchX = (x - grid[0].left - offset) * pointLen; let nearestIndex = Math.round(touchX / xStep); let dataX; // 确保索引在有效范围内
if (nearestIndex >= 0 && nearestIndex <= data.length - 1) { dataX = offset + grid[0].left + (nearestIndex * (width - grid[0].left - grid[0].right)) / pointLen; } else { dataX = x; } nearestIndex = Math.max(0, Math.min(nearestIndex, data.length - 1));
return { ...data[nearestIndex], index: nearestIndex, x: dataX, }; },
// 根据Y坐标计算价格
calculatePriceFromY(y, data, grid) { if (!data.length) return 0;
const width = canvasWidth.value; const height = canvasHeight.value; // 上下边距1
const topPadding1 = 20; const bottomPadding1 = height * 0.4; // 上下边距2
const topPadding2 = height - bottomPadding1 + 40; const bottomPadding2 = 5; // 左右边距
const verticalPadding = 5;
let chartY; let price;
for (let i = 0; i < grid.length; i++) { if (y >= grid[i].top && y <= height - grid[i].bottom) { const chartDiff = chartRange.value[i].max - chartRange.value[i].min; chartY = y - grid[i].top; price = chartRange.value[i].max - (chartY / (height - grid[i].bottom - grid[i].top)) * chartDiff; break; } }
// if (y >= topPadding1 && y <= height - bottomPadding1) {
// const priceDiff = priceRange.max - priceRange.min;
// chartY = y - topPadding1;
// price = priceRange.max - (chartY / (height - topPadding1 - bottomPadding1)) * priceDiff;
// } else if (y >= topPadding2 && y <= height - bottomPadding2) {
// const volumeRange = utils.calculateDataRange(data, "volume");
// const volumeDiff = volumeRange.max - 0;
// chartY = y - topPadding2;
// price = volumeRange.max - (chartY / (height - topPadding2 - bottomPadding2)) * volumeDiff;
// }
return price; }, // 股市数值格式化方法
formatStockNumber(value, decimalPlaces = 2) { const num = Number(value); if (isNaN(num)) return "0";
const absNum = Math.abs(num); const sign = num < 0 ? "-" : "";
if (absNum >= 1000000000000) { // 万亿级别
return sign + (absNum / 1000000000000).toFixed(decimalPlaces) + "万亿"; } else if (absNum >= 100000000) { // 亿级别
return sign + (absNum / 100000000).toFixed(decimalPlaces) + "亿"; } else if (absNum >= 10000) { // 万级别
return sign + (absNum / 10000).toFixed(decimalPlaces) + "万"; } else { // 小于万的直接显示
return sign + absNum.toFixed(decimalPlaces); } },};
let text = [ [ { name: "领先", value: "暂无数据", color: "red", }, { name: "价", value: utils.formatPrice(stockInformation.value.currentPrice), color: "black", }, ], [ { name: "量", value: utils.formatStockNumber(stockInformation.value.volume), color: "green", }, { name: "额", value: "暂无数据", color: "black", }, ],];// 示例数据
const timeData = ref([]);
const klineData = ref([]);
const initCanvas = async () => { try { crosshair.show = false;
grid[0].bottom = canvasHeight.value * 0.4; grid[1].top = canvasHeight.value * 0.6 + 30;
dayGrid[0].top = 20; dayGrid[0].bottom = canvasHeight.value * 0.6; dayGrid[1].top = canvasHeight.value * 0.4 + 30; dayGrid[1].bottom = canvasHeight.value * 0.3; dayGrid[2].top = canvasHeight.value * 0.7 + 20; dayGrid[2].bottom = 20;
// 等待DOM更新
await nextTick(); ctx.value = uni.createCanvasContext("stockChart", instance.proxy); // 初始化动态数据Canvas上下文
dynamicCtx.value = uni.createCanvasContext("dynamicCanvas", instance.proxy); // 初始化十字准心Canvas上下文
crosshairCtx.value = uni.createCanvasContext("crosshairCanvas", instance.proxy);
if (ctx.value) { // 设置Canvas的内部绘图区域尺寸
ctx.value.canvas = { width: canvasWidth.value, height: canvasHeight.value, }; } else { console.warn("Canvas上下文未初始化,跳过绘制"); } // 确保Canvas上下文知道正确的尺寸
if (dynamicCtx.value) { // 设置Canvas的内部绘图区域尺寸
dynamicCtx.value.canvas = { width: canvasWidth.value, height: canvasHeight.value, }; } else { console.warn("动态Canvas上下文未初始化,跳过绘制"); } if (crosshairCtx.value) { // 设置Canvas的内部绘图区域尺寸
crosshairCtx.value.canvas = { width: canvasWidth.value, height: canvasHeight.value, }; } else { console.warn("十字准心Canvas上下文未初始化,跳过绘制"); } // 等待DOM更新
await nextTick(); console.log("ctx", ctx.value); drawChart(); } catch (error) { console.error("初始化Canvas失败:", error); }};
// 绘制图表主函数-设置基础数据
const drawChart = () => { if (!ctx.value || !dynamicCtx.value || !crosshairCtx.value) { console.warn("Canvas上下文未初始化,跳过绘制"); return; } const data = klineTab.value == 1 ? timeData.value : klineData.value; chartRange.value = []; // 清除画布
// HCharts.setCanvasColor(ctx.value, width, height, CANVAS_BACKGROUND_COLOR);
// HCharts.setCanvasColor(dynamicCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
// HCharts.setCanvasColor(crosshairCtx.value, width, height, CANVAS_BACKGROUND_COLOR);
// 根据当前标签绘制对应图表
if (klineTab.value == 1) { console.log("stockInfomaton.lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice); // 设置标签样式
const label = [ { text: utils.calculateLabel(data, 1, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum), color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2], fontSize: 12, lineStyle: ["solid", "solid", "solid", "solid", "solid"], onlyTwo: false, }, { text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum), color: [TEXT_COLOR1, TEXT_COLOR1], lineStyle: ["solid", "solid", "solid"], fontSize: 12, onlyTwo: true, }, ]; // 把label加进grid中
grid[0].label = label[0]; grid[1].label = label[1];
drawTimeChart(); } else { // 设置标签样式
const label = [ { text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "price", grid[0].horizontalLineNum), color: [TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR1, TEXT_COLOR2], lineStyle: ["solid", "dash", "dash", "dash", "solid"], fontSize: 12, onlyTwo: false, }, { text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum), color: [TEXT_COLOR1, TEXT_COLOR1], lineStyle: ["solid", "dash", "solid"], fontSize: 12, onlyTwo: true, }, { text: utils.calculateLabel(data, 2, stockInformation.value.lastDayStockClosePrice, "volume", grid[1].horizontalLineNum), color: [TEXT_COLOR1, TEXT_COLOR1], lineStyle: ["solid", "dash", "solid"], fontSize: 12, onlyTwo: true, }, ]; // 把label加进grid中
dayGrid[0].label = label[0]; dayGrid[1].label = label[1]; dayGrid[2].label = label[2]; // HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, dayGrid, data[0].date, data[data.length - 1].date);
drawKLineChart(); }
// crosshairCtx.value.draw();
};const throttledDrawChart = throttle(drawChart, 50);
// // 绘制分时图
const drawTimeChart = () => { drawCtxChart(grid); drawDynamicCtxChart();};
// // 绘制K线图
const drawKLineChart = () => { drawCtxChart(dayGrid); drawDynamicCtxChart();};
const drawCtxChart = (paramGrid) => { // 检查Canvas上下文是否已初始化
if (!ctx.value) { console.warn("Canvas上下文未初始化,跳过绘制"); return; } // 绘制网格
HCharts.drawGrid(ctx.value, canvasWidth.value, canvasHeight.value, paramGrid, openTime, closeTime); //执行绘制
ctx.value.draw();};const drawDynamicCtxChart = () => { if (klineTab.value == 1) { //绘制价格曲线
HCharts.drawPriceLine(dynamicCtx.value, canvasWidth.value, canvasHeight.value, timeData.value, grid, chartRange.value[0]); //绘制成交量
HCharts.drawVolume( dynamicCtx.value, canvasWidth.value, canvasHeight.value, timeData.value, 2, 240, grid, { max: utils.calculateDataRange(timeData.value, "volume").max, min: 0, }, 0 ); HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, grid); HCharts.drawTopPriceDisplay(crosshairCtx.value, grid, text); } else { drawKLine(dynamicCtx.value); //绘制成交量
HCharts.drawVolume( dynamicCtx.value, canvasWidth.value, canvasHeight.value, klineData.value, 2, klineData.value.length, dayGrid, { max: utils.calculateDataRange(klineData.value, "volume").max, min: 0, }, touchState.offset ); //绘制成交量
HCharts.drawVolume( dynamicCtx.value, canvasWidth.value, canvasHeight.value, klineData.value, 3, klineData.value.length, dayGrid, { max: utils.calculateDataRange(klineData.value, "volume").max, min: 0, }, touchState.offset ); HCharts.drawAxisLabels(dynamicCtx.value, canvasWidth.value, canvasHeight.value, dayGrid); // drawMovingAverage();
HCharts.drawTopPriceDisplay(crosshairCtx.value, dayGrid, []); } //执行绘制
dynamicCtx.value.draw(false); crosshairCtx.value.draw();};
const throttledDrawDynamicCtxChart = throttle(drawDynamicCtxChart, 50);// const throttledDrawKLineChart = throttle(drawKLineChart, 10);
// 绘制K线
const drawKLine = (ctx) => { const data = klineData.value; if (!data.length) return;
const width = canvasWidth.value; const height = canvasHeight.value;
// 计算价格范围
const priceRange = chartRange.value[0]; const priceDiff = priceRange.max - priceRange.min;
const areaWidth = (width - dayGrid[0].left - dayGrid[0].right) / data.length; const candleWidth = areaWidth * 0.6;
data.forEach((item, index) => { const x = touchState.offset + dayGrid[0].left + (index * (width - dayGrid[0].left - dayGrid[0].right)) / data.length;
// 计算坐标
const highY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.high - priceRange.min) / priceDiff); const lowY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.low - priceRange.min) / priceDiff); const openY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.open - priceRange.min) / priceDiff); const closeY = dayGrid[0].top + (height - dayGrid[0].top - dayGrid[0].bottom) * (1 - (item.close - priceRange.min) / priceDiff);
// 判断涨跌
const isRise = item.close >= item.open; const color = isRise ? "green" : "red";
// 绘制上下影线
ctx.setStrokeStyle(color); ctx.setLineWidth(1); ctx.beginPath(); ctx.moveTo(x, highY); ctx.lineTo(x, lowY); ctx.stroke();
// 绘制K线实体
const entityTop = isRise ? closeY : openY; const entityBottom = isRise ? openY : closeY; const entityHeight = Math.max(Math.abs(entityBottom - entityTop), 1);
ctx.setFillStyle(color); ctx.fillRect(x - candleWidth / 2, entityTop, candleWidth, entityHeight); });};
// 绘制移动平均线
const drawMovingAverage = () => { const data = klineData.value; if (!data.length) return;
const ma5 = calculateMA(data, 5); const ma10 = calculateMA(data, 10);
drawMALine(ma5, "#fadb14", "MA5"); // 黄色
drawMALine(ma10, "#eb2f96", "MA10"); // 粉色
};
// 计算移动平均线
const calculateMA = (data, period) => { const result = []; for (let i = period - 1; i < data.length; i++) { let sum = 0; for (let j = 0; j < period; j++) { sum += data[i - j].close; } result.push({ index: i, value: sum / period, }); } return result;};
// 绘制均线
const drawMALine = (maData, color, label) => { if (!maData.length) return;
const width = canvasWidth.value; const height = canvasHeight.value; const data = klineData.value;
// 计算价格范围
const highs = data.map((item) => item.high); const lows = data.map((item) => item.low); const priceRange = chartRange.value[0]; const priceDiff = priceRange.max - priceRange.min;
ctx.value.setStrokeStyle(color); ctx.value.setLineWidth(1.5); ctx.value.beginPath();
maData.forEach((point, idx) => { const x = 40 + (point.index * (width - 60)) / (data.length - 1); const y = 20 + (height - 60) * (1 - (point.value - priceRange.min) / priceDiff);
if (idx === 0) { ctx.value.moveTo(x, y); } else { ctx.value.lineTo(x, y); } });
ctx.value.stroke();
// 绘制图例
if (maData.length > 0) { const lastPoint = maData[maData.length - 1]; const x = 40 + (lastPoint.index * (width - 60)) / (data.length - 1); const y = 20 + (height - 60) * (1 - (lastPoint.value - priceRange.min) / priceDiff);
HCharts.drawText(ctx.value`${label}: ${utils.formatPrice(lastPoint.value)}`, x + 10, y - 5, 12, color); }};// 触摸事件处理
const touchStart = (e) => { e.preventDefault(); // 阻止页面滚动等默认行为
if (typeof e.touches[1] === "undefined") { touchState.startX = e.touches[0].x; touchState.startY = e.touches[0].y; touchState.isMoving = false; } else if (typeof e.touches[1] !== "undefined" && !crosshair.show) { // touchState.startX1 = e.touches[0].x;
// touchState.startY1 = e.touches[0].y;
// touchState.startX2 = e.touches[1].x;
// touchState.startY2 = e.touches[1].y;
// touchState.isMoving = false;
}};
const touchMove = (e) => { touchState.isMoving = true; // 计算移动距离
const currentX = e.touches[0].x; const currentY = e.touches[0].y; const deltaX = currentX - touchState.startX; const deltaY = currentY - touchState.startY; touchState.moveDistance = Math.max(touchState.moveDistance, Math.sqrt(deltaX * deltaX + deltaY * deltaY)); if (crosshair.show) { if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) { throttledUpdateCrosshair(currentX, currentY); } else { // 如果移出图表区域,隐藏十字准线
// crosshair.show = false;
if (klineTab.value === 1) { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text); } else { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []); } } } else { return; if (klineTab.value === 2) { // if(currentY)
if (currentX < touchState.startX) { touchState.offset += currentX - touchState.startX; throttledDrawDynamicCtxChart(); touchState.startX = currentX; } else { touchState.offset += currentX - touchState.startX; throttledDrawDynamicCtxChart(); touchState.startX = currentX; } if (canvasWidth.value + touchState.offset < 30) { touchState.offset = 0; } } }};
const touchEnd = (e) => { // 触摸结束,隐藏十字准线
if (touchState.moveDistance < touchState.moveThreshold) { // 移动距离小于阈值,认为是点击事件
crosshair.show = !crosshair.show; }
if (crosshair.show) { const currentX = e.changedTouches[0].x; const currentY = e.changedTouches[0].y; if (isInChartArea(currentX, currentY, klineTab.value === 1 ? grid : dayGrid)) { throttledUpdateCrosshair(currentX, currentY); } } if (klineTab.value === 1) { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text); } else { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []); } // 重置移动距离
touchState.moveDistance = 0;};
// 检查坐标是否在图表区域内
const isInChartArea = (x, y, grid) => { const width = canvasWidth.value; const height = canvasHeight.value;
if (x < grid[0].left || x > width - grid[0].right) return false;
for (let i = 0; i < grid.length; i++) { if (y >= grid[i].top && y <= height - grid[i].bottom) return true; } return false;};
// 更新十字准线位置和数据
const updateCrosshair = (x, y) => { if (!crosshair.show) return; // 更新Y坐标以匹配实际价格
const data = klineTab.value === 1 ? timeData.value : klineData.value;
const width = canvasWidth.value; const height = canvasHeight.value;
crosshair.x = x; crosshair.y = y; const nearestData = utils.findNearestDataPoint(x, klineTab.value === 1 ? 240 : data.length, data, klineTab.value === 1 ? grid : dayGrid, klineTab.value === 1 ? 0 : touchState.offset); crosshair.x = nearestData.x; stockInformation.value.currentPrice = nearestData.price || nearestData.close; stockInformation.value.volume = nearestData.volume; // 涨跌额度
if (klineTab.value == 1) { stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice; stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100; } else { stockInformation.value.openPrice = nearestData.open; stockInformation.value.highPrice = nearestData.high; stockInformation.value.lowPrice = nearestData.low; if (nearestData.index != 0) { stockInformation.value.lastDayStockClosePrice = data[nearestData.index - 1].close; stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice; stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100; } } if (klineTab.value === 1 && crosshair.snapToData) { // 吸附到最近的数据点
if (nearestData) { crosshair.currentData = nearestData; const priceDiff = chartRange.value[0].max - chartRange.value[0].min; crosshair.y = grid[0].top + (height - grid[0].bottom - grid[0].top) * (1 - (nearestData.price - chartRange.value[0].min) / priceDiff); } } else { // 自由移动模式
const currentPrice = utils.calculatePriceFromY(y, data, klineTab.value === 1 ? grid : dayGrid); crosshair.currentData = { ...nearestData, price: currentPrice, }; }
if (crosshair?.currentData?.volume) { text[0][1].value = crosshair.currentData.price; text[1][0].value = crosshair.currentData.volume; } // 只重绘十字准心,不重绘整个图表
if (klineTab.value === 1) { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, grid, crosshair, text); } else { HCharts.drawCrosshair(crosshairCtx.value, canvasWidth.value, canvasHeight.value, dayGrid, crosshair, []); }};
const throttledUpdateCrosshair = throttle(updateCrosshair, 10);
// 缩放控制函数
const zoomIn = () => { touchState.scale = Math.min(touchState.maxScale, touchState.scale * 1.2); drawChart();};
const zoomOut = () => { touchState.scale = Math.max(touchState.minScale, touchState.scale / 1.2); drawChart();};
// 监听数据变化
// watch([timeData, klineData], () => {
// console.log("数据变化");
// drawChart();
// });
watch(klineTab, () => { console.log("标签页变化"); drawChart();});
// 初始化TCP监听器
const initTcpListeners = () => { // 创建连接状态监听器并保存引用
connectionListener.value = (status, result) => { tcpConnected.value = status === "connected"; console.log("TCP连接状态变化:", status, tcpConnected.value);
// 显示连接状态提示
uni.showToast({ title: status === "connected" ? "TCP连接成功" : "TCP连接断开", icon: status === "connected" ? "success" : "none", duration: 2000, });
if (status === "connected") { if (klineTab.value == 1) { sendTcpMessage("init_real_time"); } } };
// 创建消息监听器并保存引用
messageListener.value = (type, message, parsedArray) => { const messageObj = { type: type, content: message, parsedArray: parsedArray, timestamp: new Date().toLocaleTimeString(), direction: "received", };
// 解析股票数据
parseStockData(message); };
// 注册监听器
tcpConnection.onConnectionChange(connectionListener.value); tcpConnection.onMessage(messageListener.value);};
// 连接TCP服务器
const connectTcp = () => { console.log("开始连接TCP服务器..."); tcpConnection.connect();};
// 断开TCP连接
const disconnectTcp = () => { console.log("断开TCP连接..."); tcpConnection.disconnect(); tcpConnected.value = false;};
// 发送TCP消息
const sendTcpMessage = (command) => { let messageData; switch (command) { // 实时行情推送
case "real_time": messageData = { command: "real_time", stock_code: "SH.000001", }; break; // 初始化获取行情历史数据
case "init_real_time": messageData = { command: "init_real_time", stock_code: "SH.000001", }; break; case "stop_real_time": messageData = { command: "stop_real_time", }; break; // 股票列表
case "stock_list": messageData = { command: "stock_list", }; break; // 日线数据
case "daily_data": messageData = { command: "daily_data", stock_code: "GBPAUD.FXCM", start_date: "20251001", end_date: "20251023", }; break; // 周线数据
case "weekly_data": messageData = { command: "weekly_data", stock_code: "000001.SZ", start_date: "2024912", end_date: "20251029", }; break; // 周线数据
case "monthly_data": messageData = { command: "monthly_data", stock_code: "000001.SZ", start_date: "2024912", end_date: "20251029", }; break; // 1分钟线数据
case "daily_one_minutes_data": messageData = { command: "daily_one_minutes_data", stock_code: "000001.SZ", }; break; // 5分钟线数据
case "daily_five_minutes_data": messageData = { command: "daily_five_minutes_data", stock_code: "000001.SZ", }; break; // 15分钟线数据
case "daily_fifteen_minutes_data": messageData = { command: "daily_fifteen_minutes_data", stock_code: "000001.SZ", }; break; // 30分钟线数据
case "daily_thirty_minutes_data": messageData = { command: "daily_thirty_minutes_data", stock_code: "000001.SZ", }; break; // 60分钟线数据
case "daily_sixty_minutes_data": messageData = { command: "daily_sixty_minutes_data", stock_code: "000001.SZ", }; break; case "batch_real_time": messageData = { command: "batch_real_time", stock_codes: ["SH.000001", "SH.000002", "SH.000003", "SH.000004", "SH.000005"], }; break; case "help": messageData = { command: "help", }; break; } if (!messageData) { uni.showToast({ title: "命令不存在", icon: "none", duration: 1000, }); return; } else { try { // 发送消息
const success = tcpConnection.send(messageData); if (success) { console.log("home发送TCP消息:", messageData); uni.showToast({ title: "消息发送成功", icon: "success", duration: 1000, }); } } catch (error) { console.error("发送TCP消息时出错:", error); uni.showToast({ title: "消息发送失败", icon: "none", duration: 1000, }); } }};
// 获取TCP连接状态
const getTcpStatus = () => { const status = tcpConnection.getConnectionStatus(); uni.showModal({ title: "TCP连接状态", content: `当前状态: ${status ? "已连接" : "未连接"}`, showCancel: false, });};
let isMorePacket = { init_real_time: false, daily_data: false, weekly_data: false, monthly_data: false, daily_one_minutes_data: false, daily_five_minutes_data: false, daily_fifteen_minutes_data: false, daily_thirty_minutes_data: false, daily_sixty_minutes_data: false,};let receivedMessage;const findJsonPacket = (message, command) => { let jsonStartIndex = 0; let jsonEndIndex = message.indexOf(command); let jsonStartCount = 0; let jsonEndCount = 0;
for (let i = 0; i < message.length - 1; ++i) { if (message[i] == "{") { jsonStartCount++; if (jsonStartCount == 2) { jsonStartIndex = i; break; } } }
for (let i = message.indexOf(command); i >= 0; --i) { if (message[i] == "}" || i == jsonStartIndex) { jsonEndCount++; if (jsonEndCount == 1) { jsonEndIndex = i; break; } } }
// 检查JSON字符串是否有效
if (jsonStartIndex >= jsonEndIndex) { return { error: true }; } return { json: JSON.parse(message.substring(jsonStartIndex, jsonEndIndex + 1)) };};// 解析TCP股票数据
const parseStockData = (message) => { try { console.log("进入parseStockData, message类型:", typeof message);
let parsedMessage; // 如果isMorePacket是true,说明正在接受分包数据,无条件接收
// 如果message是字符串且以{开头,说明是JSON字符串,需要解析
// 如果不属于以上两种情况,说明是普通字符串,不预解析
if (message.includes("欢迎连接到股票数据服务器")) { console.log("服务器命令列表,不予处理"); return; } if ((typeof message === "string" && message.includes("init_real_data_start")) || isMorePacket.init_real_time) { if (typeof message === "string" && message.includes("init_real_data_start")) { console.log("开始接受分包数据"); receivedMessage = ""; } else { console.log("接收分包数据过程中"); } isMorePacket.init_real_time = true; receivedMessage += message; // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
if (receivedMessage.includes("init_real_data_complete")) { console.log("接受分包数据结束"); isMorePacket.init_real_time = false;
console.log("展示数据", receivedMessage);
const result = findJsonPacket(receivedMessage, "init_real_data_complete"); if (result.error) { throw new Error("解析JSON字符串失败"); } else { parsedMessage = result.json; console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage); if (parsedMessage.type === "daily_data") { timeData.value = parsedMessage.data; stockInformation.value.lastDayStockClosePrice = parsedMessage.pre_close; console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice); drawChart(); sendTcpMessage("stop_real_time"); sendTcpMessage("real_time"); } } } } else if ((typeof message === "string" && message.includes("daily_data_start")) || isMorePacket.daily_data) { if (typeof message === "string" && message.includes("daily_data_start")) { console.log("开始接受分包数据"); receivedMessage = ""; } else { console.log("接收分包数据过程中"); } isMorePacket.daily_data = true; receivedMessage += message; // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
if (receivedMessage.includes("daily_data_complete")) { console.log("接受分包数据结束"); isMorePacket.daily_data = false;
console.log("展示数据", receivedMessage);
const result = findJsonPacket(receivedMessage, "daily_data_complete"); if (result.error) { throw new Error("解析JSON字符串失败"); } else { parsedMessage = result.json; console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage); if (parsedMessage.type === "daily_data") { klineData.value = parsedMessage.data.map((item) => ({ open: item.ask_open, close: item.ask_close, high: item.ask_high, low: item.ask_low, volume: item.tick_qty, 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, })); stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close; touchState.offset = canvasWidth.value / klineData.value.length / 2; console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice); drawChart(); } } } } else if ((typeof message === "string" && message.includes("weekly_data_start")) || isMorePacket.weekly_data) { if (typeof message === "string" && message.includes("weekly_data_start")) { console.log("开始接受分包数据"); receivedMessage = ""; } else { console.log("接收分包数据过程中"); } isMorePacket.weekly_data = true; receivedMessage += message; // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
if (receivedMessage.includes("weekly_data_complete")) { console.log("接受分包数据结束"); isMorePacket.weekly_data = false;
console.log("展示数据", receivedMessage);
const result = findJsonPacket(receivedMessage, "weekly_data_complete"); if (result.error) { throw new Error("解析JSON字符串失败"); } else { parsedMessage = result.json; console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage); if (parsedMessage.type === "weekly_data") { klineData.value = parsedMessage.data.map((item) => ({ open: item.bid_open, close: item.bid_close, high: item.bid_high, low: item.bid_low, volume: item.vol, amount: item.amount, 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, })); stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close; touchState.offset = canvasWidth.value / klineData.value.length / 2; console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice); drawChart(); } } } } else if ((typeof message === "string" && message.includes("daily_one_minutes_data_start")) || isMorePacket.daily_one_minutes_data) { if (typeof message === "string" && message.includes("daily_one_minutes_data_start")) { console.log("开始接受分包数据"); receivedMessage = ""; } else { console.log("接收分包数据过程中"); } isMorePacket.daily_one_minutes_data = true; receivedMessage += message; // 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
if (receivedMessage.includes("daily_one_minutes_data_complete")) { console.log("接受分包数据结束"); isMorePacket.daily_one_minutes_data = false;
console.log("展示数据", receivedMessage);
const result = findJsonPacket(receivedMessage, "daily_one_minutes_data_complete"); if (result.error) { throw new Error("解析JSON字符串失败"); } else { parsedMessage = result.json; console.log("JSON解析成功,解析后类型:", typeof parsedMessage, parsedMessage); if (parsedMessage.type === "daily_one_minutes_data") { // klineData.value = parsedMessage.data.map((item) => ({
// open: item.open,
// close: item.close,
// high: item.high,
// low: item.low,
// volume: item.volume,
// amount: item.amount,
// date: item.time,
// }));
stockInformation.value.lastDayStockClosePrice = klineData.value[klineData.value.length - 2].close; touchState.offset = canvasWidth.value / klineData.value.length / 2; console.log("lastDayStockClosePrice", stockInformation.value.lastDayStockClosePrice); drawChart(); } } } } else { // 没有通过JSON解析判断,说明不是需要的数据
console.log("不是需要的数据,不做处理"); } } catch (error) { console.error("解析TCP股票数据失败:", error.message); console.error("错误详情:", error); }};
// 移除TCP监听器
const removeTcpListeners = () => { if (connectionListener.value) { tcpConnection.removeConnectionListener(connectionListener.value); connectionListener.value = null; console.log("已移除TCP连接状态监听器"); }
if (messageListener.value) { tcpConnection.removeMessageListener(messageListener.value); messageListener.value = null; console.log("已移除TCP消息监听器"); }};
// 定时器标识(用于清除定时器)
let timer = null;let index = 0;// 定时添加数据的函数
const startAddDataTimer = () => { if (timer) { console.log("存在旧定时器,卸载旧定时器"); clearInterval(timer); } console.log("开始定时任务"); // 每隔5秒执行一次
timer = setInterval(() => { if (index < testTimeData.length) { timeData.value.push(testTimeData[index]); console.log("新增数据:", testTimeData[index]); // 触发图表重新绘制
drawChart(); index++; } else { clearInterval(timer); } }, 2000); // 5000毫秒 = 5秒
};
onLoad((options) => { console.log("页面接收到的参数:", options);
// 处理通过data参数传递的复杂对象
if (options.data) { try { const stockData = JSON.parse(decodeURIComponent(options.data)); console.log("解析的股票数据:", stockData);
// 更新stockInformation
if (stockData) { stockInformation.value.stockName = stockData.stockName; stockInformation.value.stockCode = stockData.stockCode; } } catch (error) { console.error("解析股票数据失败:", error); } }
// 处理通过stockInformation参数传递的数据(兼容globalIndex.vue的传参方式)
if (options.stockInformation) { try { const stockData = JSON.parse(decodeURIComponent(options.stockInformation)); console.log("解析的股票信息:", stockData);
// 更新stockInformation
if (stockData) { stockInformation.value = { ...stockInformation.value, ...stockData, }; } } catch (error) { console.error("解析股票信息失败:", error); } }
// 处理简单参数
if (options.stockCode) { stockInformation.value.stockCode = options.stockCode; } if (options.stockName) { stockInformation.value.stockName = decodeURIComponent(options.stockName); }});
// 保存定时器,用于页面卸载时清理
onUnmounted(() => { removeTcpListeners(); disconnect(); if (timer) { console.log("卸载定时器"); clearInterval(timer); }});
onMounted(async () => { try { console.log("步骤1: 初始化系统信息"); const systemInfo = uni.getSystemInfoSync(); pixelRatio.value = systemInfo.pixelRatio; // 设置Canvas实际像素(考虑pixelRatio以获得高清效果)
// 1rpx = 设备屏幕宽度 / 750
const rpxToPx = systemInfo.windowWidth / 750; const offsetHeight = (150 + 200 + 80 + 150 + 30) * rpxToPx; // 350rpx转换为px
const calculatedHeight = systemInfo.windowHeight - offsetHeight; canvasWidth.value = systemInfo.windowWidth; canvasHeight.value = Math.max(calculatedHeight, canvasHeight.value);
initTcpListeners(); await nextTick(); // 开始连接
startTcp();
if (timeData.value && timeData.value.length > 0) { // 当前股价
stockInformation.value.currentPrice = timeData.value[timeData.value.length - 1].price; // 涨跌额度
stockInformation.value.currentValue = stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice; // 涨跌幅度
stockInformation.value.currentRatio = ((stockInformation.value.currentPrice - stockInformation.value.lastDayStockClosePrice) / stockInformation.value.lastDayStockClosePrice) * 100; // 成交量
stockInformation.value.volume = timeData.value[timeData.value.length - 1].volume; text[0][1].value = utils.formatPrice(stockInformation.value.currentPrice); text[1][0].value = utils.formatStockNumber(stockInformation.value.volume); } else { console.warn("没有时间数据,跳过股票信息计算"); } await nextTick(); initCanvas(); console.log("所有初始化步骤完成"); } catch (error) { console.error("初始化过程中出现错误:", error); }});</script>
<style>.container { width: 100%; min-height: 100vh; background-color: #f6f6f6;}
.title-container { width: 100%; height: 150rpx; background-color: white; display: flex; flex-direction: column; justify-content: flex-end;}
.title { /* border: 1px solid #ff0000; */ position: relative; /* 关键:作为绝对定位的父容器 */ width: 100%; height: 80rpx; display: flex; justify-content: center; align-items: center;}
.back-homepage-btn { margin-left: 40rpx; margin-right: auto; height: 30rpx; width: 20rpx; z-index: 1;}
.mid-title { position: absolute; left: 0; right: 0; display: flex; justify-content: center; align-items: center;}
.left-page { height: 40rpx; width: 40rpx;}
.right-page { height: 40rpx; width: 40rpx;}
.stock-id { margin: 0rpx 40rpx; display: flex; flex-direction: column; text-align: center;}
.stock-name { font-weight: bold;}
.stock-code { font-size: 0.8rem; font-weight: bold; color: #a1a1a1;}
.search { height: 40rpx; width: 40rpx;}
.more { height: 100%; display: flex; justify-content: center; align-items: center; margin-right: 40rpx; margin-left: 20rpx;}
.body { overflow: auto; height: calc(100vh - 305rpx); /* border: 1px solid red; */}
.stock-information { background-color: white; width: 100%; height: 200rpx; margin: 10rpx 0rpx; display: flex; align-items: center; position: relative; /* 为伪元素定位做准备 */}
/* 右下角黑色三角形 */.stock-information::after { content: ""; position: absolute; bottom: 0; right: 0; width: 0; height: 0; border-left: 20rpx solid transparent; border-bottom: 20rpx solid #6a6a6a;}
.stock-detail-container { position: absolute; top: 100%; /* 在父容器下方 */ left: 0; /* 从左边开始 */ right: 0; /* 到右边结束 */ width: 100%; /* height: 300rpx; 使用固定高度替代calc,避免计算问题 */ height: calc(100vh - 515rpx); display: flex; flex-direction: column; background-color: rgba(0, 0, 0, 0.6); z-index: 100; box-sizing: border-box; pointer-events: auto;}
.stock-detail { border: 1px solid #cacaca; width: 100%; display: flex; background-color: white; padding: 10rpx 0;}
.first-column,.second-column { width: 50%; height: 100%; display: flex; flex-direction: column; color: #6a6a6a; font-size: 0.8rem;}
.first-column-data,.second-column-data { display: flex; justify-content: space-between; align-items: center; padding: 10rpx 40rpx;}
.stock-detail-title { color: #6a6a6a;}
.stock-detail-value { color: black;}
.price-up { color: #10b981;}
.price-down { color: #ef4444;}
.price-none { color: black;}
.stock-current-data { width: 30%; height: 70%; display: flex; flex-direction: column;}
.stock-current-price { font-weight: bold; font-size: 1.2rem; width: 100%; height: 50%; display: flex; justify-content: center; align-items: center;}
.stock-current-other { width: 100%; height: 50%; display: flex; font-weight: bold; font-size: 0.6rem;}
.stock-current-value { width: 50%; height: 100%; display: flex; justify-content: center; align-items: center;}
.stock-current-ratio { width: 50%; height: 100%; display: flex; justify-content: center; align-items: center;}
.stock-other-data { height: 100%; width: 70%; display: flex; flex-direction: column;}
.first-line,.second-line,.third-line { display: flex; align-items: center; width: 100%; height: 33%; /* font-weight: bold; */ font-size: 0.8rem;}
.value { margin-left: auto; margin-right: 20rpx;}
.high-price,.volume,.volume-ratio,.low-price,.amount,.market-earn,.open-price,.turnover-ratio,.market-value { display: flex; flex: 1;}
.stock-chart { width: 100%;}
.stock-kline-tab { display: flex; width: 100%; height: 80rpx; /* border: 1px solid black; */}
.tab-day,.tab-month,.tab-time,.tab-week,.tab-more,.tab-setting { display: flex; justify-content: center; align-items: center; flex: 1; font-size: 0.8rem; color: #6a6a6a; position: relative;}
/* 向下小三角样式 */.arrow-down { width: 0; height: 0; border-left: 10rpx solid transparent; border-right: 10rpx solid transparent; border-bottom: 12rpx solid currentColor; margin-left: 8rpx; display: inline-block;}
.arrow-up { width: 0; height: 0; border-left: 10rpx solid transparent; border-right: 10rpx solid transparent; border-top: 12rpx solid currentColor; margin-left: 8rpx; display: inline-block;}
.arrow-left { width: 0; height: 0; border-top: 10rpx solid transparent; border-bottom: 10rpx solid transparent; border-right: 12rpx solid currentColor; margin-left: 8rpx; display: inline-block;}
.arrow-right { width: 0; height: 0; border-top: 10rpx solid transparent; border-bottom: 10rpx solid transparent; border-left: 12rpx solid currentColor; margin-left: 8rpx; display: inline-block;}
.moreTabsContainer { width: 100%; display: flex; gap: 5rpx; margin-bottom: 5rpx;}
.moreTabItem { flex: 1; padding: 10rpx 20rpx; border-radius: 10rpx; background-color: white; font-size: 0.85rem; color: #6a6a6a; text-align: center;}
.tab-selected { color: #db1f1d;}
.tab-selected::after { content: ""; position: absolute; bottom: 10rpx; left: 50%; transform: translateX(-50%); width: 40rpx; height: 5rpx; background-color: #db1f1d;}
.tab-setting-img { width: 30rpx; height: 30rpx;}
.time-chart-container { width: 100%; min-height: 400rpx; background-color: #ffffff; box-sizing: border-box;}
.stock-chart { display: flex; flex-direction: column; align-items: center;}
.kline-chart-container { width: 100%; height: 400rpx; background-color: #ffffff; padding: 20rpx; box-sizing: border-box; display: flex; justify-content: center; align-items: center;}
.bottomTool { width: 100%; height: 150rpx; position: fixed; bottom: 0; background-color: white; display: flex; box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);}
.index,.function,.favorites { flex: 1; display: flex; justify-content: center; align-items: center; flex-direction: column; font-size: 12px; transition: all 0.2s ease;}
.index:active,.function:active,.favorites:active { background-color: rgba(99, 99, 99, 0.5); transform: scale(0.95); transition: all 0.1s ease;}
.icon { width: 40rpx; height: 40rpx;}</style>
|