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.
974 lines
30 KiB
974 lines
30 KiB
<template>
|
|
<div ref="qxnlzhqEchartsRef" class="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: '13%',
|
|
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: '13%', // 向右调整位置
|
|
// top: getNumberTop(min, max, region.max),
|
|
// // top: 100,
|
|
// style: {
|
|
// text: region.max.toString(),
|
|
// fill: region.NumberColor,
|
|
// fontSize: 12,
|
|
// fontWeight: "bold",
|
|
// },
|
|
// 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: "#F5D6FF",
|
|
fontColor: '#A7691C',
|
|
NumberColor: 'white',
|
|
},
|
|
{
|
|
min: qxnlzhqData.zc,
|
|
max: qxnlzhqData.ht,
|
|
name: "【认知潜伏区】",
|
|
color: "#FFF6C4",
|
|
fontColor: '#A7691C',
|
|
NumberColor: 'white',
|
|
},
|
|
{
|
|
min: qxnlzhqData.ht,
|
|
max: qxnlzhqData.qs,
|
|
name: "【多空消化区】",
|
|
color: {
|
|
type: 'linear',
|
|
x: 0,
|
|
y: 0,
|
|
x2: 1,
|
|
y2: 0,
|
|
colorStops: [
|
|
{ offset: 0, color: '#D7FF9B' },
|
|
{ offset: 1, color: '#CEFF85' }
|
|
]
|
|
},
|
|
fontColor: '#A7691C',
|
|
NumberColor: 'white',
|
|
},
|
|
{
|
|
min: qxnlzhqData.qs,
|
|
max: qxnlzhqData.tp,
|
|
name: "【共识加速区】",
|
|
color: "#FFDC8F",
|
|
fontColor: '#A7691C',
|
|
NumberColor: 'white',
|
|
},
|
|
{
|
|
min: qxnlzhqData.tp,
|
|
max: qxnlzhqData.js,
|
|
name: "【情绪临界区】",
|
|
color: "#FFC0AA",
|
|
fontColor: '#2D2D89',
|
|
NumberColor: 'white',
|
|
},
|
|
];
|
|
// gg yl为-1 不绘制部分图表
|
|
if (Number(qxnlzhqData.yl) != -1) {
|
|
regions.push(
|
|
{
|
|
min: qxnlzhqData.js,
|
|
max: qxnlzhqData.yl,
|
|
name: "【杠杆失衡区】",
|
|
color: "#51C3F9",
|
|
fontColor: '#2D2D89',
|
|
NumberColor: 'white',
|
|
},
|
|
)
|
|
}
|
|
if (Number(qxnlzhqData.gg) != -1) {
|
|
regions.push(
|
|
{
|
|
min: qxnlzhqData.yl,
|
|
max: qxnlzhqData.gg,
|
|
name: "【情绪熔断区】",
|
|
color: "#D0A7FF",
|
|
fontColor: '#2D2D89',
|
|
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);
|
|
|
|
// 找到最高价的最大值及其对应的索引
|
|
let maxHighPrice = -Infinity;
|
|
let maxHighPriceIndex = -1;
|
|
kline.forEach((item, index) => {
|
|
const highPrice = item[4]; // 最高价在数组的第5个位置(索引4)
|
|
if (highPrice > maxHighPrice) {
|
|
maxHighPrice = highPrice;
|
|
maxHighPriceIndex = index;
|
|
}
|
|
});
|
|
// 计算止盈止损价格
|
|
const stopProfitPrice = Number(qxnlzhqData.cc) * 1.05; // 止盈价
|
|
const stopLossPrice = Number(qxnlzhqData.cc) * 0.97; // 止损价
|
|
|
|
// 计算最后一根K线的收盘价
|
|
const lastClosePrice = mixData[mixData.length - 1].value[2];
|
|
|
|
// 动态调整标记位置以避免与止盈止损线重叠
|
|
const priceBuffer = (dataMax - dataMin) * 0.02; // 2%的缓冲区间
|
|
|
|
// 最高价标记位置调整
|
|
let maxPricePosition = 'top';
|
|
let maxPriceOffset = [0, -5];
|
|
if (Math.abs(maxHighPrice - stopProfitPrice) < priceBuffer) {
|
|
maxPriceOffset = [0, -25]; // 增加偏移避免重叠
|
|
}
|
|
|
|
// 收盘价标记位置调整
|
|
let closePricePosition = 'bottom';
|
|
let closePriceOffset = [0, 15];
|
|
if (Math.abs(lastClosePrice - stopLossPrice) < priceBuffer) {
|
|
closePriceOffset = [0, 25]; // 增加偏移避免重叠
|
|
}
|
|
if (Math.abs(lastClosePrice - stopProfitPrice) < priceBuffer) {
|
|
closePriceOffset = [0, 25]; // 增加偏移避免重叠
|
|
}
|
|
// 确定起始和结束位置
|
|
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'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// 检查DOM元素是否存在
|
|
if (!qxnlzhqEchartsRef.value) {
|
|
console.error('emoEnergyConverter: DOM元素未找到,无法初始化图表');
|
|
return;
|
|
}
|
|
|
|
// 初始化图表
|
|
qxnlzhqEchartsInstance = echarts.init(qxnlzhqEchartsRef.value);
|
|
let option;
|
|
// 设置图表配置
|
|
option = {
|
|
tooltip: {
|
|
show: true,
|
|
trigger: 'axis',
|
|
confine: true, // 限制tooltip在图表区域内
|
|
position: function (point, params, dom, rect, size) {
|
|
// 获取图表容器大小
|
|
const chartWidth = size.viewSize[0];
|
|
const chartHeight = size.viewSize[1];
|
|
const tooltipWidth = size.contentSize[0];
|
|
const tooltipHeight = size.contentSize[1];
|
|
|
|
// 检测是否为移动设备
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
if (isMobile) {
|
|
// 移动端:固定在顶部中央
|
|
return {
|
|
top: 10,
|
|
left: Math.max(10, (chartWidth - tooltipWidth) / 2)
|
|
};
|
|
} else {
|
|
// 桌面端:智能定位
|
|
let x = point[0];
|
|
let y = point[1];
|
|
|
|
// 防止tooltip超出右边界
|
|
if (x + tooltipWidth > chartWidth) {
|
|
x = chartWidth - tooltipWidth - 10;
|
|
}
|
|
|
|
// 防止tooltip超出下边界
|
|
if (y + tooltipHeight > chartHeight) {
|
|
y = chartHeight - tooltipHeight - 10;
|
|
}
|
|
|
|
return [Math.max(10, x), Math.max(10, y)];
|
|
}
|
|
},
|
|
axisPointer: {
|
|
type: 'cross',
|
|
lineStyle: {
|
|
color: 'grey',
|
|
width: 1,
|
|
type: 'dashed'
|
|
},
|
|
label: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
color: '#fff',
|
|
borderColor: '#fff',
|
|
borderWidth: 1
|
|
}
|
|
},
|
|
backgroundColor: '#E8E8F2',
|
|
borderColor: '#fff',
|
|
borderWidth: 1,
|
|
padding: [8, 12],
|
|
textStyle: {
|
|
color: '#000',
|
|
fontSize: window.innerWidth <= 768 ? 10 : 12 // 移动端使用更小字体
|
|
},
|
|
extraCssText: window.innerWidth <= 768 ?
|
|
'max-width: 280px; word-wrap: break-word; white-space: normal; box-shadow: 0 2px 8px rgba(0,0,0,0.3);' :
|
|
'max-width: 350px; word-wrap: break-word; white-space: normal; box-shadow: 0 2px 8px rgba(0,0,0,0.3);',
|
|
formatter: function (params) {
|
|
if (!params || params.length === 0) return ''
|
|
|
|
const isMobile = window.innerWidth <= 768;
|
|
const fontSize = isMobile ? '10px' : '12px';
|
|
const lineHeight = isMobile ? '1.3' : '1.5';
|
|
const marginBottom = isMobile ? '4px' : '6px';
|
|
|
|
let result = `<div style="font-weight: bold; color: #000; margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">${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 previousClosePrice = null;
|
|
const currentIndex = param.dataIndex;
|
|
if (currentIndex > 0 && mixData[currentIndex - 1]) {
|
|
previousClosePrice = mixData[currentIndex - 1].value[2]; // 前一日收盘价
|
|
}
|
|
|
|
let priceChange, changePercent;
|
|
if (previousClosePrice !== null && typeof previousClosePrice === 'number') {
|
|
// 使用前一日收盘价计算涨跌幅
|
|
priceChange = closePrice - previousClosePrice;
|
|
changePercent = ((priceChange / previousClosePrice) * 100).toFixed(2);
|
|
} else {
|
|
// 如果没有前一日数据,使用开盘价计算(兜底方案)
|
|
priceChange = closePrice - openPrice;
|
|
changePercent = ((priceChange / openPrice) * 100).toFixed(2);
|
|
}
|
|
let changeColor = priceChange >= 0 ? '#32B520' : '#D8001B'
|
|
|
|
if (isMobile) {
|
|
// 移动端简化显示
|
|
result += `<div style="margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">`
|
|
result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>开盘价:</span><span>${openPrice.toFixed(2)}</span></div>`
|
|
result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>收盘价:</span><span>${closePrice.toFixed(2)}</span></div>`
|
|
result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>最低价:</span><span>${lowPrice.toFixed(2)}</span></div>`
|
|
result += `<div style="color: #000; display: flex; justify-content: space-between;"><span>最高价:</span><span>${highPrice.toFixed(2)}</span></div>`
|
|
result += `<div style="color: ${changeColor}; display: flex; justify-content: space-between;"><span>涨跌:</span><span>${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</span></div>`
|
|
result += `</div>`
|
|
} else {
|
|
// 桌面端完整显示
|
|
result += `<div style="margin-bottom: ${marginBottom}; font-size: ${fontSize}; line-height: ${lineHeight};">`
|
|
result += `<div style="color: #000;">开盘价: ${openPrice.toFixed(2)}</div>`
|
|
result += `<div style="color: #000;">收盘价: ${closePrice.toFixed(2)}</div>`
|
|
result += `<div style="color: #000;">最低价: ${lowPrice.toFixed(2)}</div>`
|
|
result += `<div style="color: #000;">最高价: ${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: #FC0000; margin-bottom: 2px; font-size: ${fontSize}; line-height: ${lineHeight};">${isMobile ? '止盈' : param.seriesName}: ${value.toFixed(2)}</div>`
|
|
} else if (param.seriesName === '止损线' && value !== null && value !== undefined && typeof value === 'number') {
|
|
result += `<div style="color: #002DFF; margin-bottom: 2px; font-size: ${fontSize}; line-height: ${lineHeight};">${isMobile ? '止损' : param.seriesName}: ${value.toFixed(2)}</div>`
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
},
|
|
dataZoom: [
|
|
{
|
|
type: 'slider',
|
|
xAxisIndex: 0,
|
|
start: 50,
|
|
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: 50,
|
|
end: 100,
|
|
zoomOnMouseWheel: true,
|
|
moveOnMouseMove: true,
|
|
moveOnMouseWheel: false
|
|
}
|
|
],
|
|
xAxis: {
|
|
type: "category",
|
|
data: [...mixData.map((item) => item.date), '', '', ''], // 在末尾添加三个空占位符,留出三根K线的距离
|
|
axisLabel: {
|
|
rotate: 0, // 取消倾斜角度
|
|
color: "white",
|
|
interval: 'auto' // 自动计算显示间隔,只显示部分日期但覆盖所有范围
|
|
},
|
|
axisLine: {
|
|
// show: false,
|
|
lineStyle: {
|
|
color: 'white', // x轴线颜色
|
|
}
|
|
},
|
|
axisTick: {
|
|
show: true,
|
|
alignWithLabel: true, // 刻度线与标签对齐
|
|
lineStyle: {
|
|
color: "white", // 与十字线颜色保持一致
|
|
width: 1,
|
|
type: "dashed" // 与十字线样式保持一致
|
|
}
|
|
},
|
|
},
|
|
yAxis: {
|
|
scale: true,
|
|
axisLine: {
|
|
// show: false,
|
|
lineStyle: {
|
|
color: 'white', // y轴线颜色
|
|
width: 3
|
|
}
|
|
},
|
|
splitLine: {
|
|
show: false,
|
|
},
|
|
axisLabel: { // 刻度标签
|
|
show: true,
|
|
color: 'white',
|
|
},
|
|
axisTick: { // 刻度线
|
|
show: true,
|
|
color: 'white',
|
|
},
|
|
min:
|
|
qxnlzhqData.dd < stopLossPrice * 0.98
|
|
? Math.floor(qxnlzhqData.dd)
|
|
: Math.floor(stopLossPrice * 0.98),
|
|
max: Math.round(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,
|
|
// 添加最高价标记
|
|
{
|
|
name: '最高价',
|
|
coord: [maxHighPriceIndex, maxHighPrice],
|
|
itemStyle: {
|
|
normal: {
|
|
color: 'transparent', // 透明标记点
|
|
borderColor: 'transparent',
|
|
borderWidth: 0
|
|
}
|
|
},
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: maxPricePosition,
|
|
formatter: `${maxHighPrice.toFixed(2)}`,
|
|
textStyle: {
|
|
color: 'rgb(0,170,255)',
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
// textBorderColor: '#000000',
|
|
textBorderWidth: 1
|
|
},
|
|
offset: maxPriceOffset
|
|
}
|
|
}
|
|
},
|
|
// 添加最后一根K线收盘价标记
|
|
{
|
|
name: '收盘价',
|
|
coord: [mixData.length - 1, mixData[mixData.length - 1].value[2]],
|
|
itemStyle: {
|
|
normal: {
|
|
color: 'transparent', // 透明标记点
|
|
borderColor: 'transparent',
|
|
borderWidth: 0
|
|
}
|
|
},
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: closePricePosition,
|
|
formatter: `${lastClosePrice.toFixed(2)}`,
|
|
textStyle: {
|
|
color: 'rgb(59,143,8)',
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
// textBorderColor: '#000000',
|
|
textBorderWidth: 1
|
|
},
|
|
offset: closePriceOffset
|
|
}
|
|
}
|
|
}
|
|
],
|
|
z: 5 // 确保标记显示在最上层
|
|
},
|
|
itemStyle: {
|
|
normal: {
|
|
// 阳线样式(收盘 > 开盘)
|
|
// color: '#14b143', // 开盘价 < 收盘价时为绿色
|
|
color: '#00AAFF',
|
|
color0: '#FF007F', // 开盘价 > 收盘价时为红色
|
|
borderColor: '#00AAFF', // 阳线边框色(绿)
|
|
borderColor0: '#FF007F', // 阴线边框色(红)
|
|
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: '#ffffff', // 白色描边
|
|
width: 6,
|
|
type: 'solid'
|
|
}
|
|
},
|
|
z: 10,
|
|
silent: true,
|
|
showInLegend: false
|
|
},
|
|
{
|
|
name: '止盈线',
|
|
type: 'line',
|
|
data: takeProfitData,
|
|
symbol: 'none',
|
|
lineStyle: {
|
|
normal: {
|
|
color: '#FF0000', // 红色
|
|
width: 2,
|
|
type: 'solid'
|
|
}
|
|
},
|
|
z: 10,
|
|
markPoint: {
|
|
symbol: 'circle',
|
|
symbolSize: 1,
|
|
data: [
|
|
{
|
|
coord: [startIndex, stopProfitPrice],
|
|
itemStyle: {
|
|
color: 'transparent'
|
|
},
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: 'left',
|
|
formatter: `止盈${stopProfitPrice.toFixed(2)}`,
|
|
textStyle: {
|
|
color: '#FF0000',
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
textBorderColor: '#ffffff',
|
|
textBorderWidth: 2
|
|
},
|
|
offset: [-10, 0]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
name: '止损线描边',
|
|
type: 'line',
|
|
data: stopLossData,
|
|
symbol: 'none',
|
|
lineStyle: {
|
|
normal: {
|
|
color: '#ffffff', // 白色描边
|
|
width: 6,
|
|
type: 'solid'
|
|
}
|
|
},
|
|
z: 10,
|
|
silent: true,
|
|
showInLegend: false
|
|
},
|
|
{
|
|
name: '止损线',
|
|
type: 'line',
|
|
data: stopLossData,
|
|
symbol: 'none',
|
|
lineStyle: {
|
|
normal: {
|
|
color: '#001EFF',
|
|
width: 2,
|
|
type: 'solid'
|
|
}
|
|
},
|
|
z: 10,
|
|
markPoint: {
|
|
symbol: 'circle',
|
|
symbolSize: 1,
|
|
data: [
|
|
{
|
|
coord: [startIndex, stopLossPrice],
|
|
itemStyle: {
|
|
color: 'transparent'
|
|
},
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: 'left',
|
|
formatter: `止损${stopLossPrice.toFixed(2)}`,
|
|
textStyle: {
|
|
color: '#001EFF',
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
textBorderColor: '#ffffff',
|
|
textBorderWidth: 2
|
|
},
|
|
offset: [-10, 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: 'bottom',
|
|
// formatter: `{text| ${mixData[mixData.length - 1].value[2].toFixed(2)}}`,
|
|
// rich: {
|
|
// text: {
|
|
// color: '#001EFF',
|
|
// fontSize: 12,
|
|
// fontWeight: 'bold',
|
|
// textBorderColor: '#ffffff',
|
|
// textBorderWidth: 2,
|
|
// }
|
|
// },
|
|
// offset: [-25, -40]
|
|
// }
|
|
// }
|
|
// }
|
|
// ]
|
|
// }
|
|
// },
|
|
// {
|
|
// 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: 'top',
|
|
// formatter: `{text| ${mixData[mixData.length - 1].value[3].toFixed(2)}}`,
|
|
// rich: {
|
|
// text: {
|
|
// color: '#FF0000',
|
|
// fontSize: 12,
|
|
// fontWeight: 'bold',
|
|
// textBorderColor: '#ffffff',
|
|
// textBorderWidth: 2,
|
|
// }
|
|
// },
|
|
// offset: [-25, 40]
|
|
// }
|
|
// }
|
|
// }
|
|
// ]
|
|
// }
|
|
// }
|
|
],
|
|
grid: {
|
|
left: window.innerWidth >= 768 ? "13%" : "18%",
|
|
right: "10",
|
|
top: '10',
|
|
bottom: "60",
|
|
containLabel: false,
|
|
width: window.innerWidth >= 768 ? '80%': '70%',
|
|
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: 542px;
|
|
margin: 0;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 手机端适配样式 */
|
|
@media only screen and (max-width: 768px) {
|
|
.qxnlzhqEcharts {
|
|
width: 100%;
|
|
height: 300px;
|
|
/* margin: 0; */
|
|
}
|
|
|
|
/* 移动端tooltip优化 */
|
|
:deep(.echarts-tooltip) {
|
|
max-width: 280px !important;
|
|
font-size: 10px !important;
|
|
line-height: 1.3 !important;
|
|
padding: 8px 10px !important;
|
|
word-wrap: break-word !important;
|
|
white-space: normal !important;
|
|
box-sizing: border-box !important;
|
|
}
|
|
|
|
/* 确保tooltip不会超出屏幕 */
|
|
:deep(.echarts-tooltip-content) {
|
|
max-width: 100% !important;
|
|
overflow: hidden !important;
|
|
}
|
|
}
|
|
</style>
|