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.
 
 
 

716 lines
20 KiB

<template>
<div ref="bottomRadarRef" id="bottomRadarChart"></div>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue'
import { createChartResizeHandler, applyResponsiveStyles } from '@/utils/chartResize.js'
import * as echarts from 'echarts'
const bottomRadarRef = ref(null)
let bottomRadarChart = null
let resizeHandler = null
function initEmotionalBottomRadar(KlineData, barAndLineData) {
// 如果已存在图表实例,先销毁
if (bottomRadarChart) {
bottomRadarChart.dispose()
bottomRadarChart = null
}
let bottomRadarChartDom = document.getElementById('bottomRadarChart')
bottomRadarChart = echarts.init(bottomRadarChartDom)
// 日期-作为x轴
let dateArray = barAndLineData.map(subArray => subArray[0])
// k线数据格式:['2025/04/24', 250.5, 259.51, 249.2, 259.54, 274.899, 0.685, 258.354]
// 原始数据:索引1=开盘价, 索引2=收盘价, 索引3=最低价, 索引4=最高价
// ECharts candlestick需要:[开盘, 收盘, 最低, 最高]
let kLineDataArray = KlineData.map(subArray => [
subArray[1], // 开盘价
subArray[2], // 收盘价
subArray[3], // 最低价
subArray[4] // 最高价
])
// 计算K线数据的最小值,用于设置y轴起始值
let allKlineValues = []
kLineDataArray.forEach(item => {
if (Array.isArray(item) && item.length >= 4) {
// K线数据格式:[开盘价, 收盘价, 最低价, 最高价]
allKlineValues.push(item[0], item[1], item[2], item[3])
}
})
// 找到最小值和最大值
let validValues = allKlineValues.filter(val => typeof val === 'number' && !isNaN(val))
let minValue = Math.min(...validValues)
let maxValue = Math.max(...validValues)
// 最小值向下取整,最大值向上取整
let yAxisMin = Math.floor(minValue)
let yAxisMax = Math.ceil(maxValue)
// 红线,取第二个值
let redLineDataArray = barAndLineData.map(subArray => subArray[1])
// 色块数据格式化
let barTotalDataArray = barAndLineData.map(subArray => subArray.slice(2, 6))
// 删掉
// barTotalDataArray[0] = [0, 0, 0, 0]
// barTotalDataArray[8] = [1, 1, 1, 1]
// barTotalDataArray[9] = [0, 0, 1, 1]
// barTotalDataArray[13] = [1, 0, 1, 0]
// barTotalDataArray[16] = [0, 0, 1, 0]
// 黄色块、黄色加仓资金、紫色柱子、蓝色柱子
let yellowBlockDataArray = []
let yellowLineDataArray = []
let purpleLineDataArray = []
let blueLineDataArray = []
let transparentFillingDataArray = []
// 黄色块:为1 0-4显示柱体
// 黄色加仓资金文字:为1 在4的位置展示文字
// 紫色柱子:为1 1-80显示柱体
// 蓝色柱子:为1 0-40显示柱体
// 因数据要互相叠加展示,所以需要数据处理。base适用于 ECharts 4.x 及以上版本
barTotalDataArray.forEach((item) => {
if (item[0]) {
yellowBlockDataArray.push(4)
if (item[3]) {
// 40-4
blueLineDataArray.push(36)
if (item[2]) {
// 80-40
purpleLineDataArray.push(40)
transparentFillingDataArray.push(0)
} else {
purpleLineDataArray.push(0)
transparentFillingDataArray.push(0)
}
} else {
blueLineDataArray.push(0)
if (item[2]) {
// 80-4
purpleLineDataArray.push(76)
transparentFillingDataArray.push(0)
} else {
purpleLineDataArray.push(0)
transparentFillingDataArray.push(0)
}
}
} else if (!item[0]) {
yellowBlockDataArray.push(0)
if (item[3]) {
blueLineDataArray.push(40)
if (item[2]) {
// 80-40
purpleLineDataArray.push(40)
transparentFillingDataArray.push(0)
} else {
purpleLineDataArray.push(0)
transparentFillingDataArray.push(0)
}
} else {
blueLineDataArray.push(0)
if (item[2]) {
// 80-1,base为1
purpleLineDataArray.push(79)
transparentFillingDataArray.push(1)
} else {
purpleLineDataArray.push(0)
transparentFillingDataArray.push(0)
}
}
}
// 加仓资金
if (item[1]) {
yellowLineDataArray.push(1)
} else if (!item[1]) {
yellowLineDataArray.push(0)
}
})
// 配置图表选项,很多操作和展示已限制,如果需要需放开
let option = {
// backgroundColor: '#000046', // 设置整个图表的背景色
tooltip: {
show: true, // 启用tooltip显示
trigger: 'axis',
triggerOn: 'mousemove',
confine: true,
axisPointer: {
type: 'cross',
crossStyle: {
color: '#fff',
width: 1,
type: 'solid'
},
lineStyle: {
color: '#fff',
width: 1,
type: 'solid'
},
label: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
borderColor: '#fff',
borderWidth: 1
}
},
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#fff',
borderWidth: 1,
padding: 10,
textStyle: {
color: '#fff',
fontSize: 12
},
formatter: function (params) {
if (!params || params.length === 0) return ''
// 检查是否有第二个或第三个网格的数据,如果有则不显示tooltip
let hasSecondOrThirdGrid = params.some(param => {
return (param.seriesName === '红线' && param.axisIndex === 1) ||
(param.axisIndex === 2) ||
(param.seriesName !== 'K线' && param.seriesName !== '基础base')
})
// 如果鼠标悬浮在第二个或第三个网格上,不显示tooltip
if (hasSecondOrThirdGrid && !params.some(param => param.seriesType === 'candlestick')) {
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') {
// ECharts candlestick的value格式:[开盘, 收盘, 最低, 最高]
let candlestickData = param.value
// 确保数据有效性
if (!Array.isArray(candlestickData) || candlestickData.length < 4) {
return ''
}
let openPrice = candlestickData[1] // 开盘价
let closePrice = candlestickData[2] // 收盘价
let lowPrice = candlestickData[3] // 最低价
let highPrice = candlestickData[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(1)}</div>`
result += `<div style="color: #fff;">收盘: ${closePrice.toFixed(1)}</div>`
result += `<div style="color: #fff;">最低: ${lowPrice.toFixed(1)}</div>`
result += `<div style="color: #fff;">最高: ${highPrice.toFixed(1)}</div>`
result += `<div style="color: ${changeColor};">涨跌: ${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</div>`
result += `</div>`
} else if (param.seriesName === '红线') {
result += `<div style="color: #ef232a; margin-bottom: 4px;">${param.seriesName}: ${value}</div>`
} else if (param.seriesName !== '基础base' && value > 0) {
result += `<div style="color: ${color}; margin-bottom: 4px;">${param.seriesName}: ${value}</div>`
}
})
return result
}
},
legend: {
// data: ['K线', '红线', '色块'], 不要展示图例
type: 'scroll',
pageButtonItemGap: 2,
pageButtonPosition: 'end',
textStyle: {
color: '#666'
}
},
grid: [
{
left: '10%',
right: '3%',
top: '20px',
bottom: '50%',
height: '300px',
width: '85%'
// containLabel: true
},
{
left: '10%',
right: '3%',
top: '320px',
bottom: '25%',
height: '300px',
width: '85%'
// containLabel: true
},
{
left: '10%',
right: '3%',
top: '620px',
bottom: '50px',
height: '300px',
width: '85%'
// containLabel: true
}
],
xAxis: [
{
type: 'category',
data: dateArray,
gridIndex: 0,
boundaryGap: true, // 保持间距,不要离y轴太近,不然重叠了
axisLine: {
// show: false,
lineStyle: {
color: 'white', // x轴线颜色
}
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false // 不要x轴的分割线
},
axisPointer: {
link: {
xAxisIndex: 'all'
},
}
},
{
type: 'category',
data: dateArray,
gridIndex: 1,
boundaryGap: true,
axisLine: {
// show: false,
lineStyle: {
// color: '#008000'
color: 'white'
}
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
},
axisPointer: {
link: {
xAxisIndex: 'all'
}
}
},
{
type: 'category',
data: dateArray,
gridIndex: 2,
axisLine: {
lineStyle: {
color: 'white'
}
},
axisTick: {
show: true, // 显示刻度线
alignWithLabel: true,
lineStyle: {
color: '#999', // 颜色
width: 1, // 宽度
type: 'solid' // 线样式(solid/dashed/dotted)
}
},
axisLabel: {
color: 'white',
interval: 'auto', // 自动计算显示间隔,只显示部分日期但覆盖所有范围
rotate: 0 // 取消倾斜角度
},
splitLine: {
show: false
},
axisPointer: {
link: {
xAxisIndex: 'all'
}
}
}
],
yAxis: [
{
type: 'value',
gridIndex: 0,
splitNumber: 4,
min: yAxisMin, // 设置y轴最小值为数据最小值向下取整
max: yAxisMax, // 设置y轴最大值为数据最大值向上取整
axisLine: {
lineStyle: {
color: 'white' // y轴坐标轴颜色
}
},
axisTick: {
show: true
},
axisLabel: {
width: 50, // 宽度限制
color: 'white',
formatter: function (value, index) {
return value.toFixed(2)
}
},
splitLine: {
show: false,
lineStyle: {
color: '#837b7b',
type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
}
},
scale: true, // 不强制包含0,不然k线图底部空余太多
},
{
type: 'value',
gridIndex: 1,
splitNumber: 3,
axisLine: {
lineStyle: {
color: 'white'
}
},
axisTick: {
show: true
},
splitNumber: 5, // 刻度数量
axisLabel: {
width: 50, // 宽度限制
color: 'white',
formatter: function (value, index) {
// 如果没有刻度数量,其他方法获取不到y轴刻度总长
if (index === 0) {
return '0'
} else if (index === 5) {
return ''
}
return value
}
},
splitLine: {
show: false,
lineStyle: {
color: '#837b7b',
type: 'dotted'
}
},
},
{
type: 'value',
gridIndex: 2,
splitNumber: 2,
axisLine: {
lineStyle: {
color: 'white'
}
},
axisTick: {
show: true
},
splitNumber: 5, // 刻度数量
axisLabel: {
width: 50, // 宽度限制
color: 'white',
formatter: function (value, index) {
if (index === 5) {
return ''
}
return value
}
},
splitLine: {
show: false,
lineStyle: {
color: '#837b7b',
type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
}
},
splitNumber: 5,
min: function (value) {
return 0 // 最小值
},
max: function (value) {
return value.max + 10 // 比最大值高10, 避免最高点和上一个图表x轴重合
}
}
],
dataZoom: [
{
type: 'slider',
xAxisIndex: [0, 1, 2],
start: 0,
end: 100,
show: true,
bottom: window.innerWidth > 768 ? 80 : 50,
height: 20,
borderColor: '#CFD6E3',
fillerColor: 'rgba(135, 175, 274, 0.2)',
handleStyle: {
color: '#CFD6E3'
},
textStyle: {
color: '#fff'
},
dataBackground: {
lineStyle: {
color: '#CFD6E3'
},
areaStyle: {
color: 'rgba(241,243,247,0.5)'
}
}
},
{
type: 'inside',
xAxisIndex: [0, 1, 2],
start: 0,
end: 100,
zoomOnMouseWheel: true,
moveOnMouseMove: true,
moveOnMouseWheel: false
}
],
series: [
{
name: 'K线',
type: 'candlestick',
data: kLineDataArray,
xAxisIndex: 0,
yAxisIndex: 0,
itemStyle: {
color: '#14b143', // 开盘价 > 收盘价时为绿色
color0: '#ef232a', // 开盘价 < 收盘价时为红色
borderColor: '#14b143',
borderColor0: '#ef232a',
normal: {
color: '#14b143', // 开盘价 > 收盘价时为绿色
color0: '#ef232a', // 开盘价 < 收盘价时为红色
borderColor: '#14b143',
borderColor0: '#ef232a',
opacity: function (params) {
// K线数据格式:[开,收,低,高] 收盘价 > 开盘价时为阳线,设置边框不透明、填充透明
return params.data[1] > params.data[0] ? 0 : 1
}
}
}
},
{
name: '红线',
type: 'line',
data: redLineDataArray,
xAxisIndex: 1,
yAxisIndex: 1,
symbol: 'none',
sampling: 'average',
itemStyle: {
normal: {
color: '#ef232a'
}
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(33, 150, 243, 0.4)' },
{ offset: 1, color: 'rgba(33, 150, 243, 0)' }
]
}
}
},
{
name: '基础base',
type: 'bar',
stack: 'total',
// barGap: '-100%', // 重叠
xAxisIndex: 2,
yAxisIndex: 2,
barCategoryGap: '0%',
itemStyle: {
normal: {
color: '#ffffff',
borderWidth: 0,
}
},
data: transparentFillingDataArray,
},
{
name: '黄色',
type: 'bar',
stack: 'total',
// barGap: '-100%', // 重叠
xAxisIndex: 2,
yAxisIndex: 2,
barCategoryGap: '0%', // 类目间柱条间距为0
itemStyle: {
normal: {
color: 'rgba(255, 255, 0, 1)',
borderWidth: 0,
// 加仓资金的文字显示
label: {
show: (params) => {
return yellowLineDataArray[params.dataIndex] > 0
},
position: 'top',
textStyle: {
color: 'rgba(255, 255, 0, 1)'
},
formatter: (params) => {
return yellowLineDataArray[params.dataIndex] > 0 ? '加仓资金' : ''
}
}
}
},
data: yellowBlockDataArray,
},
{
name: '蓝色',
type: 'bar',
stack: 'total',
xAxisIndex: 2,
yAxisIndex: 2,
barCategoryGap: '0%', // 类目间柱条间距为0
label: {
show: true,
position: 'inside'
},
itemStyle: {
normal: {
color: 'rgba(34, 196, 190, 1)',
borderWidth: 0
}
},
data: blueLineDataArray
},
{
name: '紫色',
type: 'bar',
stack: 'total',
xAxisIndex: 2,
yAxisIndex: 2,
barCategoryGap: '0%', // 类目间柱条间距为0
label: {
show: true,
position: 'inside'
},
itemStyle: {
normal: {
color: 'rgba(191, 87, 222, 1)',
borderWidth: 0
}
},
data: purpleLineDataArray
},
]
}
// 使用配置项显示图表
bottomRadarChart.setOption(option)
// 应用响应式样式
if (bottomRadarRef.value) {
applyResponsiveStyles(bottomRadarRef.value);
}
// 创建响应式处理器
if (resizeHandler) {
resizeHandler.cleanup();
}
resizeHandler = createChartResizeHandler({
chart: bottomRadarChart,
container: bottomRadarRef.value,
option: option,
beforeResize: adjustGridHeight,
name: '情绪探底雷达图表'
});
// 立即触发一次resize以确保初始布局正确
setTimeout(() => {
if (resizeHandler) {
resizeHandler.triggerResize();
}
}, 100);
function adjustGridHeight() {
if (window.innerWidth <= 768) {
option.grid[0].height = '150px'
option.grid[1].height = '150px'
option.grid[2].height = '150px'
option.grid[0].left = '15%'
option.grid[1].left = '15%'
option.grid[2].left = '15%'
option.grid[1].top = '170px'
option.grid[2].top = '320px'
option.grid[0].width = '80%'
option.grid[1].width = '80%'
option.grid[2].width = '80%'
}
bottomRadarChart.setOption(option)
}
// 初始化时调整高度
adjustGridHeight()
}
// 暴露给父级
defineExpose({
initEmotionalBottomRadar
})
onBeforeUnmount(() => {
// 清理响应式处理器
if (resizeHandler) {
resizeHandler.cleanup();
resizeHandler = null;
}
// 组件卸载时销毁图表
if (bottomRadarChart) {
bottomRadarChart.dispose();
bottomRadarChart = null;
}
})
</script>
<style>
#bottomRadarChart {
width: 100%;
height: 1040px;
box-sizing: border-box;
overflow: hidden;
margin: 0px auto !important;
padding: 0;
}
/* 手机端适配样式 */
@media only screen and (max-width: 768px) {
#bottomRadarChart {
width: 90% !important;
height: 560px;
padding: 0;
}
}
</style>