|
|
@ -3,102 +3,424 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup> |
|
|
|
import { ref, onMounted, onBeforeUnmount, toRef } from 'vue'; |
|
|
|
import * as echarts from 'echarts'; |
|
|
|
import { ref, onMounted, onBeforeUnmount, toRef, reactive } from "vue"; |
|
|
|
import * as echarts from "echarts"; |
|
|
|
|
|
|
|
defineExpose({initQXNLZHEcharts}) |
|
|
|
defineExpose({ initQXNLZHEcharts }); |
|
|
|
|
|
|
|
const qxnlzhqEchartsRef = ref(null); |
|
|
|
let qxnlzhqEchartsRef = ref(null); |
|
|
|
let qxnlzhqEchartsInstance = null; |
|
|
|
|
|
|
|
const data = ref({ |
|
|
|
echartsData:{} |
|
|
|
}) |
|
|
|
let { |
|
|
|
echartsData |
|
|
|
} = toRef(data.value) |
|
|
|
let regions = reactive([]); |
|
|
|
|
|
|
|
// 设置区域名称 位置 |
|
|
|
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) => { |
|
|
|
return regions.flatMap((region) => { |
|
|
|
const middleY = (Number(region.min) + Number(region.max)) / 2; |
|
|
|
return [ |
|
|
|
// 区域名称(中间位置) |
|
|
|
{ |
|
|
|
type: "text", |
|
|
|
left: '17', // 靠近y轴内侧 |
|
|
|
top: getNameTop(min, max, middleY), |
|
|
|
style: { |
|
|
|
text: region.name, |
|
|
|
fill: region.fontColor, |
|
|
|
fontSize: 14, |
|
|
|
fontWeight: "bold", |
|
|
|
}, |
|
|
|
z: 3, |
|
|
|
}, |
|
|
|
// y轴数值(顶部位置) |
|
|
|
{ |
|
|
|
type: "text", |
|
|
|
left: '25', |
|
|
|
top: getNumberTop(min, max, region.max), |
|
|
|
// top: 100, |
|
|
|
style: { |
|
|
|
text: region.max.toString(), |
|
|
|
fill: region.NumberColor, |
|
|
|
fontSize: 12, |
|
|
|
}, |
|
|
|
z: 3, |
|
|
|
}, |
|
|
|
]; |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
function initQXNLZHEcharts(kline, qxnlzhqData) { |
|
|
|
console.log('kline', kline) |
|
|
|
console.log('qxnlzhqData', qxnlzhqData) |
|
|
|
let mixData = [] |
|
|
|
kline.forEach(element => { |
|
|
|
let date = element[0] |
|
|
|
let value = [element[1],element[2],element[3],element[4]] |
|
|
|
// 测试数据 !!! 删掉 |
|
|
|
// 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 |
|
|
|
}) |
|
|
|
value, |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// 动态区域配置 |
|
|
|
// dd到zc 低吸区------情绪冰点区 ; zc到ht 关注区------认知潜伏区; ht到qs 回调区------多空消化区 ; qs到tp 拉升区------共识加速区; |
|
|
|
// tp到js 突破区------情绪临界区 ; js到yl 警示区-------杠杆失衡区 ; yl到gg 风险区-------情绪熔断区; |
|
|
|
regions = [ |
|
|
|
{ |
|
|
|
min: qxnlzhqData.dd, |
|
|
|
max: qxnlzhqData.zc, |
|
|
|
name: "【情绪冰点区】", |
|
|
|
color: "#e7a5d6", |
|
|
|
fontColor: '#000', |
|
|
|
NumberColor: 'blue', |
|
|
|
}, |
|
|
|
{ |
|
|
|
min: qxnlzhqData.zc, |
|
|
|
max: qxnlzhqData.ht, |
|
|
|
name: "【认知潜伏区】", |
|
|
|
color: "#f36587", |
|
|
|
fontColor: '#000', |
|
|
|
NumberColor: 'blue', |
|
|
|
}, |
|
|
|
{ |
|
|
|
min: qxnlzhqData.ht, |
|
|
|
max: qxnlzhqData.qs, |
|
|
|
name: "【多空消化区】", |
|
|
|
color: "#e99883", |
|
|
|
fontColor: '#000', |
|
|
|
NumberColor: 'blue', |
|
|
|
}, |
|
|
|
{ |
|
|
|
min: qxnlzhqData.qs, |
|
|
|
max: qxnlzhqData.tp, |
|
|
|
name: "【共识加速区】", |
|
|
|
color: "#f0db84", |
|
|
|
fontColor: '#000', |
|
|
|
NumberColor: 'red', |
|
|
|
}, |
|
|
|
{ |
|
|
|
min: qxnlzhqData.tp, |
|
|
|
max: qxnlzhqData.js, |
|
|
|
name: "【情绪临界区】", |
|
|
|
color: "#dbeee3", |
|
|
|
fontColor: 'red', |
|
|
|
NumberColor: 'red', |
|
|
|
}, |
|
|
|
]; |
|
|
|
// gg yl为-1 不绘制部分图表 |
|
|
|
if (Number(qxnlzhqData.yl) != -1) { |
|
|
|
regions.push( |
|
|
|
{ |
|
|
|
min: qxnlzhqData.js, |
|
|
|
max: qxnlzhqData.yl, |
|
|
|
name: "【杠杆失衡区】", |
|
|
|
color: "#9ac2d8", |
|
|
|
fontColor: 'red', |
|
|
|
NumberColor: 'red', |
|
|
|
}, |
|
|
|
) |
|
|
|
} |
|
|
|
if (Number(qxnlzhqData.gg) != -1) { |
|
|
|
regions.push( |
|
|
|
{ |
|
|
|
min: qxnlzhqData.yl, |
|
|
|
max: qxnlzhqData.gg, |
|
|
|
name: "【情绪熔断区】", |
|
|
|
color: "#bce283", |
|
|
|
fontColor: 'red', |
|
|
|
NumberColor: 'red', |
|
|
|
}, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// 计算止盈止损价格 |
|
|
|
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' |
|
|
|
} |
|
|
|
}, |
|
|
|
}); |
|
|
|
console.log('mixData---',mixData) |
|
|
|
} 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; |
|
|
|
// 设置图表配置 |
|
|
|
const option = { |
|
|
|
option = { |
|
|
|
xAxis: { |
|
|
|
type: 'category', |
|
|
|
data: mixData.map(item => item.date), |
|
|
|
type: "category", |
|
|
|
data: mixData.map((item) => item.date), |
|
|
|
axisLabel: { |
|
|
|
rotate: 45, // 日期旋转45度防止重叠 |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
yAxis: { |
|
|
|
scale: true, |
|
|
|
splitLine: { |
|
|
|
show: false |
|
|
|
show: false, |
|
|
|
}, |
|
|
|
axisLabel: { |
|
|
|
// formatter: '{value}' |
|
|
|
} |
|
|
|
axisLabel: { // 刻度标签 |
|
|
|
show: false // 不显示刻度标签 |
|
|
|
}, |
|
|
|
axisTick: { // 刻度线 |
|
|
|
show: false // 不显示刻度线 |
|
|
|
}, |
|
|
|
min: |
|
|
|
qxnlzhqData.dd < stopLossPrice * 0.98 |
|
|
|
? Math.floor(qxnlzhqData.dd) |
|
|
|
: Math.floor(stopLossPrice * 0.98), |
|
|
|
max: |
|
|
|
qxnlzhqData.yl > stopProfitPrice * 1.02 |
|
|
|
? Math.ceil(qxnlzhqData.yl) |
|
|
|
: Math.ceil(stopProfitPrice * 1.02), |
|
|
|
}, |
|
|
|
// 自定义区域名称和区域范围值 位置 |
|
|
|
graphic: generateGraphics(qxnlzhqData.dd < stopLossPrice * 0.98 |
|
|
|
? Math.floor(qxnlzhqData.dd) |
|
|
|
: Math.floor(stopLossPrice * 0.98),qxnlzhqData.yl > stopProfitPrice * 1.02 |
|
|
|
? Math.ceil(qxnlzhqData.yl) |
|
|
|
: Math.ceil(stopProfitPrice * 1.02)), |
|
|
|
series: [ |
|
|
|
{ |
|
|
|
type: "candlestick", |
|
|
|
data: mixData.map((item) => item.value), |
|
|
|
z: 1, |
|
|
|
markPoint: { |
|
|
|
symbol: 'circle', |
|
|
|
symbolSize: 10, |
|
|
|
data: markPointData, |
|
|
|
z: 5 // 确保标记显示在最上层 |
|
|
|
}, |
|
|
|
series: [{ |
|
|
|
type: 'candlestick', |
|
|
|
data: mixData.map(item => item.value), |
|
|
|
itemStyle: { |
|
|
|
// // 阳线样式(收盘 > 开盘) |
|
|
|
// color: '#ff0783', // 阳线边框色(红) |
|
|
|
// borderColor: '#ff0783', |
|
|
|
// color0: 'transparent', // 阳线填充色(透明实现空心效果) |
|
|
|
// borderColor0: '#ff0783', |
|
|
|
|
|
|
|
// // 阴线样式(收盘 < 开盘) |
|
|
|
// color: '#008080', // 阴线填充色(绿) |
|
|
|
// borderColor: '#008080', |
|
|
|
// color0: '#008080', // 阴线边框色(同填充色实现实心) |
|
|
|
// borderColor0: '#008080', |
|
|
|
|
|
|
|
// 鼠标悬停样式 |
|
|
|
// emphasis: { |
|
|
|
// color: '#ff6666', // 阳线悬停边框色 |
|
|
|
// borderColor: '#ff6666', |
|
|
|
// color0: 'rgba(255,102,102,0.3)', // 阳线悬停轻微填充 |
|
|
|
// borderColor0: '#ff6666', |
|
|
|
|
|
|
|
// color: '#66cc99', // 阴线悬停色 |
|
|
|
// borderColor: '#66cc99', |
|
|
|
// color0: '#66cc99', |
|
|
|
// borderColor0: '#66cc99' |
|
|
|
// } |
|
|
|
normal: { |
|
|
|
// 阳线样式(收盘 > 开盘) |
|
|
|
color: '#ef232a', // 阴线色(实际用不到) |
|
|
|
color0: '#14b143', // 阳线色(实际用不到) |
|
|
|
borderColor: '#ef232a', // 阳线边框色(红) |
|
|
|
borderColor0: '#14b143', // 阴线边框色(绿) |
|
|
|
// 关键:通过 opacity 控制空心效果 |
|
|
|
opacity: function(params) { |
|
|
|
// 收盘价 > 开盘价时为阳线,设置边框不透明、填充透明 |
|
|
|
return params.data[2] > params.data[1] ? 0 : 1; |
|
|
|
color: 'transparent', // 阳线色 |
|
|
|
color0: '#008080', // 阴线色 |
|
|
|
borderColor: '#ff0783', // 阳线边框色(红) |
|
|
|
borderColor0: '#008080', // 阴线边框色(绿) |
|
|
|
} |
|
|
|
}, |
|
|
|
// 实现 分区域背景色 |
|
|
|
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: '10%', |
|
|
|
barCategoryGap: '45%', |
|
|
|
itemStyle: { |
|
|
|
normal: { |
|
|
|
color: 'rgba(0,0,0,0)' // 默认透明 |
|
|
|
} |
|
|
|
}, |
|
|
|
z:2 |
|
|
|
}, |
|
|
|
// 中间区域染色 |
|
|
|
{ |
|
|
|
name: '中间区域', |
|
|
|
type: 'bar', |
|
|
|
stack: 'total', |
|
|
|
data: middleRangeData, |
|
|
|
barWidth: '10%', |
|
|
|
barCategoryGap: '45%', |
|
|
|
itemStyle: { |
|
|
|
normal: { |
|
|
|
color: 'rgba(0,0,0,0)' // 默认透明 |
|
|
|
} |
|
|
|
}], |
|
|
|
grid: { |
|
|
|
left: '3%', |
|
|
|
right: '4%', |
|
|
|
bottom: '4%', // 为dataZoom留出空间 |
|
|
|
containLabel: true |
|
|
|
}, |
|
|
|
z:2 |
|
|
|
}, |
|
|
|
{ |
|
|
|
name: '止盈线', |
|
|
|
type: 'line', |
|
|
|
data: takeProfitData, |
|
|
|
symbol: 'none', |
|
|
|
lineStyle: { |
|
|
|
normal: { |
|
|
|
color: '#ff80ff', // 蓝色 |
|
|
|
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: 'bottom', |
|
|
|
formatter: `{text|止盈: ${stopProfitPrice}}`, |
|
|
|
rich: { |
|
|
|
text: { |
|
|
|
color: '#820a06', |
|
|
|
fontSize: 14, |
|
|
|
fontWeight: 'bold' |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
} |
|
|
|
}, |
|
|
|
{ |
|
|
|
name: '止损线', |
|
|
|
type: 'line', |
|
|
|
data: stopLossData, |
|
|
|
symbol: 'none', |
|
|
|
lineStyle: { |
|
|
|
normal: { |
|
|
|
color: '#080bfd', // 红色 |
|
|
|
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|止损: ${stopLossPrice}}`, |
|
|
|
rich: { |
|
|
|
text: { |
|
|
|
color: '#080bfd', |
|
|
|
fontSize: 14, |
|
|
|
fontWeight: 'bold' |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
] |
|
|
|
} |
|
|
|
}, |
|
|
|
], |
|
|
|
grid: { |
|
|
|
left: "0", |
|
|
|
right: "10", |
|
|
|
top: '10', |
|
|
|
bottom: "0", |
|
|
|
containLabel: true, |
|
|
|
}, |
|
|
|
}; |
|
|
|
// 应用配置 |
|
|
|
qxnlzhqEchartsInstance.setOption(option); |
|
|
@ -115,6 +437,5 @@ onBeforeUnmount(() => { |
|
|
|
#qxnlzhqEcharts { |
|
|
|
width: 100%; |
|
|
|
height: 600px; |
|
|
|
border: 1px solid red; |
|
|
|
} |
|
|
|
</style> |