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.
 
 
 

740 lines
21 KiB

<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>