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.
 
 
 
 
 

2259 lines
62 KiB

<!-- @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" 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 === 3">
<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 class="tcpMsg" v-for="item in tcpMessages" :key="item">
{{ item }}
</view>
</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 tcpMessages = ref([]);
const tcpStockData = ref({
count: 0,
data: {},
stock_count: 0,
timestamp: "",
type: "",
});
const currentStockInfo = ref({
stock_name: "未知股票",
current_price: "0.00",
change: "0.00%",
change_value: 0,
change_percent: 0,
});
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 == 1) {
sendTcpMessage("init_real_time");
}
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 {
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",
};
console.log("0000");
tcpMessages.value.push(messageObj);
// console.log('收到TCP消息:', messageObj)
console.log("home开始调用parseStockData", messageObj);
// 解析股票数据
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: "20251001",
end_date: "20251023",
};
break;
case "daily_one_minutes_data":
messageData = {
command: "daily_one_minutes_data",
stock_code: "000001.SZ",
};
break;
case "daily_five_minutes_data":
messageData = {
command: "daily_five_minutes_data",
stock_code: "000001.SZ",
};
break;
case "daily_fifteen_minutes_data":
messageData = {
command: "daily_fifteen_minutes_data",
stock_code: "000001.SZ",
};
break;
case "daily_thirty_minutes_data":
messageData = {
command: "daily_thirty_minutes_data",
stock_code: "000001.SZ",
};
break;
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: 1500,
});
return;
} else {
try {
// 发送消息
const success = tcpConnection.send(messageData);
if (success) {
console.log("home发送TCP消息:", messageData);
uni.showToast({
title: "消息发送成功",
icon: "success",
duration: 1500,
});
}
} catch (error) {
console.error("发送TCP消息时出错:", error);
uni.showToast({
title: "消息发送失败",
icon: "none",
duration: 1500,
});
}
}
};
// 清空消息记录
const clearTcpMessages = () => {
tcpMessages.value = [];
uni.showToast({
title: "消息记录已清空",
icon: "success",
duration: 1500,
});
};
// 获取TCP连接状态
const getTcpStatus = () => {
const status = tcpConnection.getConnectionStatus();
uni.showModal({
title: "TCP连接状态",
content: `当前状态: ${status ? "已连接" : "未连接"}\n消息数量: ${tcpMessages.value.length}`,
showCancel: false,
});
};
let isMorePacket = false;
let receivedMessage;
// 解析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) {
if (typeof message === "string" && message.includes("init_real_data_start")) {
console.log("开始接受分包数据");
receivedMessage = "";
} else {
console.log("接收分包数据过程中");
}
isMorePacket = true;
receivedMessage += message;
// 如果当前消息包含},说明收到JSON字符串结尾,结束接收,开始解析
if (receivedMessage.includes("init_real_data_complete")) {
console.log("接受分包数据结束");
isMorePacket = false;
console.log("展示数据", receivedMessage);
// 获取JSON字符串的开头和结尾的坐标
let jsonStartIndex = 0;
let jsonEndIndex = receivedMessage.indexOf("init_real_data_complete");
let jsonStartCount = 0;
let jsonEndCount = 0;
for (let i = 0; i < receivedMessage.length - 1; ++i) {
if (receivedMessage[i] == "{") {
jsonStartCount++;
if (jsonStartCount == 2) {
jsonStartIndex = i;
break;
}
}
}
for (let i = receivedMessage.indexOf("init_real_data_complete"); i >= 0; --i) {
if (receivedMessage[i] == "}" || i == jsonStartIndex) {
jsonEndCount++;
if (jsonEndCount == 1) {
jsonEndIndex = i;
break;
}
}
}
// 检查JSON字符串是否有效
if (jsonStartIndex >= jsonEndIndex) {
throw new Error("JSON字符串格式错误");
}
console.log("检测到JSON字符串,开始解析");
parsedMessage = JSON.parse(receivedMessage.substring(jsonStartIndex, jsonEndIndex + 1));
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();
}
}
// 通过了JSON解析判断,说明返回的数据是需要的正确数据,进行股票实时数据检查
console.log("开始处理解析后的数据");
// 检查是否是股票数据(支持batch_data_chunk和batch_realtime_data两种类型)
if ((parsedMessage.type === "batch_data_chunk" || parsedMessage.type === "batch_realtime_data") && parsedMessage.data) {
console.log("开始更新TCP股票数据存储");
// 更新TCP股票数据存储
tcpStockData.value = {
count: parsedMessage.count || 0,
data: parsedMessage.data || {},
stock_count: parsedMessage.stock_count || 0,
timestamp: parsedMessage.timestamp || "",
type: parsedMessage.type || "",
};
// 获取第一个股票的数据用于显示
const stockCodes = Object.keys(parsedMessage.data);
if (stockCodes.length > 0) {
const firstStockCode = stockCodes[0];
// 检查数据结构
if (parsedMessage.data[firstStockCode] && Array.isArray(parsedMessage.data[firstStockCode]) && parsedMessage.data[firstStockCode].length > 0) {
const stockData = parsedMessage.data[firstStockCode][0]; // 取第一条数据
if (stockData && stockData.current_price !== undefined && stockData.pre_close !== undefined) {
// 计算涨跌幅
const changeValue = stockData.current_price - stockData.pre_close;
const changePercent = ((changeValue / stockData.pre_close) * 100).toFixed(2);
const changeSign = changeValue >= 0 ? "+" : "";
// 更新当前显示的股票信息
currentStockInfo.value = {
stock_name: stockData.stock_name || "未知股票",
current_price: stockData.current_price ? stockData.current_price.toFixed(2) : "0.00",
change: `${changeSign}${changePercent}%`,
change_value: changeValue,
change_percent: parseFloat(changePercent),
};
console.log("股票数据更新成功:", currentStockInfo.value);
}
}
}
} else {
console.log("不是batch_data_chunk或batch_realtime_data类型的消息,跳过处理");
}
} 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>