|
|
<template> <div ref="qxnlzhqEchartsRef" id="qxnlzhqEcharts"></div> </template>
<script setup> import { ref, onMounted, onBeforeUnmount, toRef, reactive } from "vue"; import * as echarts from "echarts";
defineExpose({ initQXNLZHEcharts });
let qxnlzhqEchartsRef = ref(null); let qxnlzhqEchartsInstance = null;
let regions = reactive([]); const dataMax=ref(null) // 设置区域名称 位置
function getNameTop(min, max, regionMiidle) { // 获取整个图表的高度
const chartHeight = qxnlzhqEchartsInstance.getHeight(); // 60: 为x轴占的高度
return (max - Number(regionMiidle)) / (max - min) * (chartHeight - 60) } // 设置区域最大值 位置
function getNumberTop(min, max, regionMax) { // 获取整个图表的高度
const chartHeight = qxnlzhqEchartsInstance.getHeight(); // 60: 为x轴占的高度
return (max - Number(regionMax)) / (max - min) * (chartHeight - 60) }
// 生成图形标注(核心逻辑)
const generateGraphics = (min, max) => { let hasPartialVisible = false; // 标记是否已经遇到第一个部分可见的区域
return regions.flatMap((region) => { if(!region.min || !region.max) return []; const middleY = (Number(region.min) + Number(region.max)) / 2; const safeY = Math.max(min, Math.min(middleY, max*0.99)); // 检查区域是否完全可见
const isFullyVisible = region.min >= min && region.max <= max; // 检查区域是否部分可见
const isPartiallyVisible = (region.min < max && region.max > min) && !isFullyVisible; // 如果已经有一个部分可见的区域名称显示了,就不再显示其他部分可见的区域名称
if (isPartiallyVisible && hasPartialVisible) { return []; } // 如果是第一个部分可见的区域,设置标记
if (isPartiallyVisible) { hasPartialVisible = true; } const graphics = []; // 区域名称(中间位置)
if (isFullyVisible || isPartiallyVisible) { graphics.push({ type: "text", left: '10%', top: getNameTop(min, max, safeY), style: { text: region.name, fill: region.fontColor, fontSize: 14, fontWeight: "bold", }, z: 3, }); } // y轴数值(顶部位置)
if (isFullyVisible) { graphics.push({ type: "text", left: '5%', // 向右调整位置
top: getNumberTop(min, max, region.max), // top: 100,
style: { text: region.max.toString(), fill: region.NumberColor, fontSize: 12, }, z: 3, }); } return graphics; }); };
function initQXNLZHEcharts(kline, qxnlzhqData) { // 测试数据 !!! 删掉
// qxnlzhqData.topxh = ["2025/04/04", "2025/04/15"]
// qxnlzhqData.lowxh = ["2025/04/08", "2025/04/18"]
// qxnlzhqData.qixh = ["2025/04/10", "2025/04/21"]
if (qxnlzhqEchartsInstance) { qxnlzhqEchartsInstance.dispose(); } // 数据
let mixData = []; kline.forEach((element) => { let date = element[0]; let value = [element[1], element[2], element[3], element[4]]; mixData.push({ date, value, }); }); // 动态区域配置
// dd到zc 低吸区------情绪冰点区 ; zc到ht 关注区------认知潜伏区; ht到qs 回调区------多空消化区 ; qs到tp 拉升区------共识加速区;
// tp到js 突破区------情绪临界区 ; js到yl 警示区-------杠杆失衡区 ; yl到gg 风险区-------情绪熔断区;
regions = [ { min: qxnlzhqData.dd, max: qxnlzhqData.zc, name: "【情绪冰点区】", color: "#FF9F9F", fontColor: '#666666', NumberColor: 'white', }, { min: qxnlzhqData.zc, max: qxnlzhqData.ht, name: "【认知潜伏区】", color: "#FFCB75", fontColor: '#666666', NumberColor: 'white', }, { min: qxnlzhqData.ht, max: qxnlzhqData.qs, name: "【多空消化区】", color: "#D7E95D", fontColor: '#666666', NumberColor: 'white', }, { min: qxnlzhqData.qs, max: qxnlzhqData.tp, name: "【共识加速区】", color: "#A0F56F", fontColor: '#666666', NumberColor: 'white', }, { min: qxnlzhqData.tp, max: qxnlzhqData.js, name: "【情绪临界区】", color: "#87F3CD", fontColor: '#666666', NumberColor: 'white', }, ]; // gg yl为-1 不绘制部分图表
if (Number(qxnlzhqData.yl) != -1) { regions.push( { min: qxnlzhqData.js, max: qxnlzhqData.yl, name: "【杠杆失衡区】", color: "#51C3F9", fontColor: '#666666', NumberColor: 'white', }, ) } if (Number(qxnlzhqData.gg) != -1) { regions.push( { min: qxnlzhqData.yl, max: qxnlzhqData.gg, name: "【情绪熔断区】", color: "#D0A7FF", fontColor: '#666666', NumberColor: 'white', }, ) }
// 计算动态的y轴范围
const priceValues = kline.flatMap(item => [item[1], item[2], item[3], item[4]]); const dataMin = Math.min(...priceValues); const dataMax = Math.max(...priceValues); // 获取最后一根K线数据
const lastKLine = mixData[mixData.length - 1]; const lastHigh = lastKLine.value[2]; // 最高价
const lastLow = lastKLine.value[3]; // 最低价
// 计算止盈止损价格
const stopProfitPrice = Number(qxnlzhqData.cc) * 1.05; // 止盈价
const stopLossPrice = Number(qxnlzhqData.cc) * 0.97; // 止损价
// 确定起始和结束位置
const startIndex = Math.max(0, mixData.length - 17); // 创建完整数据数组
const takeProfitData = new Array(mixData.length).fill(null); const stopLossData = new Array(mixData.length).fill(null); // 填充显示区域的数据
for (var i = startIndex; i < mixData.length; i++) { takeProfitData[i] = stopProfitPrice; stopLossData[i] = stopLossPrice; }
// topxh、lowxh、qixh 对应k线染色
// 创建中间区域数据
const middleRangeData = []; const middleRangeData1 = []; const markPointData = []; mixData.forEach((item, index) => { const [open, close, low, high] = item.value; const rangeHeight = high - low; // const middleThirdStart = low + rangeHeight * (1/3);
// const middleThirdEnd = low + rangeHeight * (2/3);
let color = null;
if (qxnlzhqData.topxh.includes(item.date)) { color = '#000000'; // 黑色
} else if (qxnlzhqData.lowxh.includes(item.date)) { color = '#1E90FF'; // 蓝色
}
// 添加中间区域数据
if (color) { middleRangeData.push({ value: [index, close > open ? (close - open) : (open - close)], // 修正数据格式
itemStyle: { normal: { color: color } }, }); middleRangeData1.push({ value: [index, close > open ? open : close], // 修正数据格式
itemStyle: { normal: { color: 'transparent' } }, }); } else { middleRangeData.push(null); middleRangeData1.push(null); }
// 添加文字标记数据
if (qxnlzhqData.qixh.includes(item.date)) { markPointData.push({ name: '起', coord: [index, (open + close) / 2], itemStyle: { normal: { color: 'rgba(0,0,0,0)' // 标记点透明
} }, label: { normal: { show: true, position: 'inside', formatter: '起', textStyle: { color: '#FF0000', fontSize: 10, // fontWeight: 'bold'
} } } }); } });
// 初始化图表
qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value); let option; // 设置图表配置
option = { tooltip: { show: true, trigger: 'axis', axisPointer: { type: 'line', lineStyle: { color: '#fff', width: 1, type: 'solid' }, label: { backgroundColor: 'rgba(0, 0, 0, 0.8)', color: '#fff', borderColor: '#fff', borderWidth: 1 } }, backgroundColor: '#646E71', borderColor: '#fff', borderWidth: 1, padding: 10, textStyle: { color: '#fff', fontSize: 12 }, formatter: function (params) { if (!params || params.length === 0) return '' let result = `<div style="font-weight: bold; color: #fff; margin-bottom: 8px;">${params[0].name}</div>`
params.forEach(param => { let value = param.value let color = param.color
if (param.seriesType === 'candlestick') { let openPrice = value[1] // 开盘价
let closePrice = value[2] // 收盘价
let lowPrice = value[3] // 最低价
let highPrice = value[4] // 最高价
// 检查数据有效性
if (typeof openPrice !== 'number' || typeof closePrice !== 'number' || typeof lowPrice !== 'number' || typeof highPrice !== 'number') { return '' } let priceChange = closePrice - openPrice let changePercent = ((priceChange / openPrice) * 100).toFixed(2) let changeColor = priceChange >= 0 ? '#14b143' : '#ef232a' result += `<div style="margin-bottom: 6px;">` // result += `<div style="color: #fff; font-weight: bold;">${param.seriesName}</div>`
result += `<div style="color: #fff;">开盘: ${openPrice.toFixed(2)}</div>` result += `<div style="color: #fff;">收盘: ${closePrice.toFixed(2)}</div>` result += `<div style="color: #fff;">最低: ${lowPrice.toFixed(2)}</div>` result += `<div style="color: #fff;">最高: ${highPrice.toFixed(2)}</div>` result += `<div style="color: ${changeColor};">涨跌: ${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</div>` result += `</div>` } else if (param.seriesName === '止盈线' && value !== null && value !== undefined && typeof value === 'number') { result += `<div style="color: #FF0000; margin-bottom: 4px;">${param.seriesName}: ${value.toFixed(2)}</div>` } else if (param.seriesName === '止损线' && value !== null && value !== undefined && typeof value === 'number') { result += `<div style="color: #001EFF; margin-bottom: 4px;">${param.seriesName}: ${value.toFixed(2)}</div>` } })
return result } }, dataZoom: [ { type: 'slider', xAxisIndex: 0, start: 0, end: 100, show: true, bottom: 10, height: 20, borderColor: '#fff', fillerColor: 'rgba(255, 255, 255, 0.2)', handleStyle: { color: '#fff', borderColor: '#fff' }, textStyle: { color: '#fff' } }, { type: 'inside', xAxisIndex: 0, start: 0, end: 100, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: false } ], xAxis: { type: "category", data: mixData.map((item) => item.date), axisLabel: { rotate: 0, // 取消倾斜角度
color: "white", interval: 'auto' // 自动计算显示间隔,只显示部分日期但覆盖所有范围
}, axisLine: { // show: false,
lineStyle: { color: 'white', // x轴线颜色
} }, }, yAxis: { scale: true, axisLine: { // show: false,
lineStyle: { color: 'white', // x轴线颜色
} }, splitLine: { show: false, }, axisLabel: { // 刻度标签
show: false // 不显示刻度标签
}, axisTick: { // 刻度线
show: false // 不显示刻度线
}, min: qxnlzhqData.dd < stopLossPrice * 0.98 ? Math.floor(qxnlzhqData.dd) : Math.floor(stopLossPrice * 0.98), max: Math.max(Math.ceil(dataMax * 1.02), (qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02)), stopProfitPrice * 1.02), }, // 自定义区域名称和区域范围值 位置
graphic: generateGraphics(qxnlzhqData.dd < stopLossPrice * 0.98 ? Math.floor(qxnlzhqData.dd) : Math.floor(stopLossPrice * 0.98), Math.max(Math.ceil(dataMax * 1.02), (qxnlzhqData.yl > 0 ? qxnlzhqData.yl : Math.ceil(dataMax * 1.02)), stopProfitPrice * 1.02),), series: [ { type: "candlestick", data: mixData.map((item) => item.value), z: 1, clip: true, markPoint: { symbol: 'circle', symbolSize: 10, data: markPointData, z: 5 // 确保标记显示在最上层
}, itemStyle: { normal: { // 阳线样式(收盘 > 开盘)
// color: '#14b143', // 开盘价 < 收盘价时为绿色
color: 'rgba(0,0,0,0)', color0: '#ef232a', // 开盘价 > 收盘价时为红色
borderColor: '#14b143', // 阳线边框色(绿)
borderColor0: '#ef232a', // 阴线边框色(红)
borderWidth: 1.5 } }, // 实现 分区域背景色
markArea: { silent: true, data: regions.map((region) => [ { yAxis: region.min, itemStyle: { normal: { color: region.color } }, }, { yAxis: region.max }, ]), }, }, { name: '中间区域', type: 'bar', stack: 'total', data: middleRangeData1, barWidth: '20%', barCategoryGap: '40%', itemStyle: { normal: { color: 'rgba(0,0,0,0)' // 默认透明
} }, z: 2 }, // 中间区域染色
{ name: '中间区域', type: 'bar', stack: 'total', data: middleRangeData, barWidth: '20%', barCategoryGap: '40%', itemStyle: { normal: { color: 'rgba(0,0,0,0)' // 默认透明
} }, z: 2 }, { name: '止盈线', type: 'line', data: takeProfitData, symbol: 'none', lineStyle: { normal: { color: '#FF0000', // 蓝色
width: 2, type: 'solid' } }, markPoint: { symbol: 'circle', symbolSize: 1, data: [ { coord: [mixData.map((item) => item.value).length - 1, stopProfitPrice], itemStyle: { color: '#ff80ff' }, label: { normal: { show: true, position: 'top', formatter: `{text|止盈}`, rich: { text: { color: '#FF0000', fontSize: 14, fontWeight: 'bold' } }, offset: [-20, 0] } } } ] } }, { name: '止损线', type: 'line', data: stopLossData, symbol: 'none', lineStyle: { normal: { color: '#001EFF', width: 2, type: 'solid' } }, markPoint: { symbol: 'circle', symbolSize: 1, data: [ { coord: [mixData.map((item) => item.value).length - 1, stopLossPrice], itemStyle: { color: '#080bfd' }, label: { normal: { show: true, position: 'bottom', formatter: `{text|止损}`, rich: { text: { color: '#001EFF', fontSize: 14, fontWeight: 'bold' } }, offset: [-20, 0] } } } ] } }, { name: '最低价', type: 'line', symbol: 'none', lineStyle: { normal: { color: 'transparent', width: 0 } }, markPoint: { symbol: 'circle', symbolSize: 1, data: [ { coord: [mixData.length - 1, mixData[mixData.length - 1].value[2]], itemStyle: { color: 'transparent' }, label: { normal: { show: true, position: 'top', formatter: `{text|${mixData[mixData.length - 1].value[2].toFixed(2)}}`, rich: { text: { color: '#001EFF', fontSize: 12, fontWeight: 'bold', textBorderColor: '#ffffff', textBorderWidth: 2, } }, offset: [20, 10] } } } ] } }, { name: '最高价', type: 'line', symbol: 'none', lineStyle: { normal: { color: 'transparent', width: 0 } }, markPoint: { symbol: 'circle', symbolSize: 1, data: [ { coord: [mixData.length - 1, mixData[mixData.length - 1].value[3]], itemStyle: { color: 'transparent' }, label: { normal: { show: true, position: 'bottom', formatter: `{text|${mixData[mixData.length - 1].value[3].toFixed(2)}}`, rich: { text: { color: '#FF0000', fontSize: 12, fontWeight: 'bold', textBorderColor: '#ffffff', textBorderWidth: 2, } }, offset: [20, -10] } } } ] } } ], grid: { left: "10%", right: "10", top: '10', bottom: "60", containLabel: false, width: '85%', height: 'auto', overflow: 'hidden' }, }; // 应用配置
qxnlzhqEchartsInstance.setOption(option); // 防抖函数,避免频繁触发resize
const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // 监听窗口大小变化,调整图表尺寸
const resizeHandler = debounce(() => { if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) { try { qxnlzhqEchartsInstance.resize(); console.log('情绪能量转化器图表已重新调整大小'); } catch (error) { console.error('情绪能量转化器图表resize失败:', error); } } }, 100); // 100ms防抖延迟
// 移除之前的监听器(如果存在)
if (window.emoEnergyConverterResizeHandler) { window.removeEventListener('resize', window.emoEnergyConverterResizeHandler); } // 添加新的监听器
window.addEventListener('resize', resizeHandler); // 存储resize处理器以便后续清理
window.emoEnergyConverterResizeHandler = resizeHandler; // 添加容器大小监听器
if (qxnlzhqEchartsRef.value && window.ResizeObserver) { const containerObserver = new ResizeObserver(debounce(() => { if (qxnlzhqEchartsInstance && !qxnlzhqEchartsInstance.isDisposed()) { try { qxnlzhqEchartsInstance.resize(); console.log('情绪能量转化器容器大小变化,图表已调整'); } catch (error) { console.error('情绪能量转化器容器resize失败:', error); } } }, 100)); containerObserver.observe(qxnlzhqEchartsRef.value); window.emoEnergyConverterContainerObserver = containerObserver; } }
onBeforeUnmount(() => { // 组件卸载时销毁图表
if (qxnlzhqEchartsInstance) { qxnlzhqEchartsInstance.dispose(); qxnlzhqEchartsInstance = null; } // 移除窗口resize监听器
if (window.emoEnergyConverterResizeHandler) { window.removeEventListener('resize', window.emoEnergyConverterResizeHandler); window.emoEnergyConverterResizeHandler = null; } // 清理容器观察器
if (window.emoEnergyConverterContainerObserver) { window.emoEnergyConverterContainerObserver.disconnect(); window.emoEnergyConverterContainerObserver = null; } }); </script> <style scoped> #qxnlzhqEcharts { width: 100%; height: 700px; margin: 0; box-sizing: border-box; overflow: hidden; }
/* 手机端适配样式 */ @media only screen and (max-width: 768px) { #qxnlzhqEcharts { width: 100%; height: 300px; margin: 0; }
} </style>
|