Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
水玉婷
ai-wechat
Commits
e1ea9e38
Commit
e1ea9e38
authored
Dec 26, 2025
by
水玉婷
Browse files
feat:添加折线图
parent
85a6b6c0
Changes
8
Hide whitespace changes
Inline
Side-by-side
src/views/components/AiChat.vue
View file @
e1ea9e38
...
@@ -39,7 +39,7 @@
...
@@ -39,7 +39,7 @@
<
template
v-for=
"(item, i) in msg.contentBlocks"
:key=
"i"
>
<
template
v-for=
"(item, i) in msg.contentBlocks"
:key=
"i"
>
<!-- 图表内容块 -->
<!-- 图表内容块 -->
<div
v-if=
"item.chartData"
class=
"chart-block"
>
<div
v-if=
"item.chartData"
class=
"chart-block"
>
<ChartComponent
:chart-data=
"item.chartData"
:chart-type=
"item.chartType ||
3
"
<ChartComponent
:chart-data=
"item.chartData"
:chart-type=
"item.chartType ||
'column'
"
:title=
"item.chartData.title || '图表数据'"
/>
:title=
"item.chartData.title || '图表数据'"
/>
</div>
</div>
<!-- 音频内容块 -->
<!-- 音频内容块 -->
...
@@ -332,6 +332,10 @@ const sseService = createSSEService({
...
@@ -332,6 +332,10 @@ const sseService = createSSEService({
onReconnect
:
(
newDialogSessionId
)
=>
{
onReconnect
:
(
newDialogSessionId
)
=>
{
console
.
log
(
'
🔄 SSE重连成功,新的dialogSessionId:
'
,
newDialogSessionId
);
console
.
log
(
'
🔄 SSE重连成功,新的dialogSessionId:
'
,
newDialogSessionId
);
dialogSessionId
.
value
=
newDialogSessionId
;
dialogSessionId
.
value
=
newDialogSessionId
;
// 标记重连成功,避免心跳检测重复触发重连
sseService
.
markReconnectSuccess
();
// 优化:只在有错误消息的情况下显示重连成功消息
// 优化:只在有错误消息的情况下显示重连成功消息
if
(
lastNetworkStatusMessage
.
value
?.
type
===
'
error
'
)
{
if
(
lastNetworkStatusMessage
.
value
?.
type
===
'
error
'
)
{
addNetworkStatusMessage
(
'
success
'
,
'
服务重连成功,对话已恢复!
'
);
addNetworkStatusMessage
(
'
success
'
,
'
服务重连成功,对话已恢复!
'
);
...
@@ -405,6 +409,19 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
...
@@ -405,6 +409,19 @@ const validateMessageParams = (type: MessageType, params: MessageParams): boolea
// 统一发送消息函数
// 统一发送消息函数
const
sendMessage
=
async
(
type
:
MessageType
=
'
text
'
,
params
:
MessageParams
=
{})
=>
{
const
sendMessage
=
async
(
type
:
MessageType
=
'
text
'
,
params
:
MessageParams
=
{})
=>
{
// 如果消息文本为空且是文本类型,则延迟1秒后模拟折线图消息进行测试
if
(
type
===
'
text
'
&&
!
messageText
.
value
.
trim
()
&&
!
params
.
message
)
{
loading
.
value
=
true
;
console
.
log
(
'
📊 检测到空消息,1秒后发送折线图测试...
'
);
setTimeout
(()
=>
{
simulateLineChartMessage
();
loading
.
value
=
false
;
},
1000
);
return
;
}
loading
.
value
=
true
;
loading
.
value
=
true
;
const
{
message
,
audioUrl
,
durationTime
}
=
params
;
const
{
message
,
audioUrl
,
durationTime
}
=
params
;
...
@@ -650,6 +667,50 @@ onMounted(() => {
...
@@ -650,6 +667,50 @@ onMounted(() => {
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
closeSSE
();
closeSSE
();
});
});
// 模拟折线图消息
const
simulateLineChartMessage
=
()
=>
{
console
.
log
(
'
模拟折线图消息
'
);
// 使用真实医院数据创建折线图数据
const
lineChartData
=
{
title
:
"
2023年第三季度武汉各医院每月住院患者总数及费用汇总
"
,
dimFields
:
[
'
月份
'
,
'
地区
'
],
indexFields
:
[
'
用户总数
'
,
'
费用汇总
'
],
rows
:
[
{
'
月份
'
:
'
1月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
100
,
'
费用汇总
'
:
5000
},
{
'
月份
'
:
'
2月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
150
,
'
费用汇总
'
:
6000
},
{
'
月份
'
:
'
3月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
120
,
'
费用汇总
'
:
5500
},
{
'
月份
'
:
'
4月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
180
,
'
费用汇总
'
:
8000
},
{
'
月份
'
:
'
5月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
200
,
'
费用汇总
'
:
9000
},
{
'
月份
'
:
'
6月
'
,
'
地区
'
:
'
北京
'
,
'
用户总数
'
:
250
,
'
费用汇总
'
:
11000
},
{
'
月份
'
:
'
1月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
80
,
'
费用汇总
'
:
4000
},
{
'
月份
'
:
'
2月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
120
,
'
费用汇总
'
:
5000
},
{
'
月份
'
:
'
3月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
100
,
'
费用汇总
'
:
4500
},
{
'
月份
'
:
'
4月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
150
,
'
费用汇总
'
:
7000
},
{
'
月份
'
:
'
5月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
180
,
'
费用汇总
'
:
8000
},
{
'
月份
'
:
'
6月
'
,
'
地区
'
:
'
上海
'
,
'
用户总数
'
:
220
,
'
费用汇总
'
:
10000
},
{
'
月份
'
:
'
1月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
60
,
'
费用汇总
'
:
3000
},
{
'
月份
'
:
'
2月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
90
,
'
费用汇总
'
:
4000
},
{
'
月份
'
:
'
3月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
80
,
'
费用汇总
'
:
3500
},
{
'
月份
'
:
'
4月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
120
,
'
费用汇总
'
:
6000
},
{
'
月份
'
:
'
5月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
150
,
'
费用汇总
'
:
7000
},
{
'
月份
'
:
'
6月
'
,
'
地区
'
:
'
广州
'
,
'
用户总数
'
:
180
,
'
费用汇总
'
:
9000
}
],
chartType
:
'
line
'
// 将chartType放在message对象内部
};
// 正确的SSE消息格式:status 3(图表数据),type 2(表格数据)
const
simulatedMessage
=
{
message
:
lineChartData
,
status
:
3
,
// 图表数据
type
:
2
// 表格数据
};
// 调用handleSSEMessage处理模拟消息
handleSSEMessage
(
simulatedMessage
);
console
.
log
(
'
折线图消息已发送,使用正确的SSE格式:status=3, type=2
'
);
};
</
script
>
</
script
>
<
style
lang=
"less"
scoped
>
<
style
lang=
"less"
scoped
>
@import './style.less';
@import './style.less';
...
...
src/views/components/ChartComponent.vue
View file @
e1ea9e38
<
template
>
<
template
>
<div
class=
"message-chart"
>
<div
class=
"chart-component"
>
<div
class=
"chart-title"
>
{{
title
}}
</div>
<ColumnChart
<div
v-if=
"isEmpty"
class=
"chart-empty"
>
v-if=
"chartType === CHART_TYPES.COLUMN"
<div
class=
"empty-icon"
>
📊
</div>
:chart-data=
"chartData"
<div
class=
"empty-text"
>
暂无数据
</div>
:title=
"title"
<div
class=
"empty-desc"
>
当前查询条件下没有找到相关数据
</div>
:width=
"width"
:height=
"height"
/>
<LineChart
v-else-if=
"chartType === CHART_TYPES.LINE"
:chart-data=
"chartData"
:title=
"title"
:width=
"width"
:height=
"height"
/>
<div
v-else
class=
"chart-error"
>
不支持的图表类型:
{{
chartType
}}
</div>
</div>
<div
v-else
ref=
"chartContainer"
class=
"chart-container"
></div>
<div
v-if=
"error"
class=
"chart-error"
>
{{
error
}}
</div>
</div>
</div>
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
onMounted
,
onUnmounted
,
watch
,
nextTick
}
from
'
vue
'
;
import
ColumnChart
from
'
./ColumnChart.vue
'
;
import
*
as
echarts
from
'
echarts
'
;
import
LineChart
from
'
./LineChart.vue
'
;
// 在模块作用域中定义图表类型常量
const
CHART_TYPES
=
{
COLUMN
:
'
column
'
,
LINE
:
'
line
'
};
// 定义组件属性
// 定义组件属性
interface
Props
{
interface
Props
{
chartData
:
any
;
chartData
:
any
;
chartType
?:
any
;
chartType
?:
number
|
string
;
title
?:
string
;
title
?:
string
;
width
?:
number
|
string
;
width
?:
number
|
string
;
height
?:
number
|
string
;
height
?:
number
|
string
;
}
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
chartType
:
{
type
:
'
column
'
}
,
chartType
:
'
column
'
,
title
:
'
数据图表
'
,
title
:
'
数据图表
'
,
width
:
'
100%
'
,
width
:
'
100%
'
,
height
:
'
auto
'
// 设置为auto,完全自适应
height
:
'
auto
'
});
const
chartContainer
=
ref
<
HTMLElement
>
();
const
chartInstance
=
ref
<
echarts
.
ECharts
|
null
>
(
null
);
const
error
=
ref
<
string
>
(
''
);
const
isEmpty
=
ref
<
boolean
>
(
false
);
// ========== 工具函数抽离 ==========
/**
* 检查数据是否为空
*/
const
checkDataEmpty
=
(
data
:
any
):
boolean
=>
{
if
(
!
data
)
return
true
;
if
(
!
data
.
rows
||
!
Array
.
isArray
(
data
.
rows
))
return
true
;
if
(
data
.
rows
.
length
===
0
)
return
true
;
// 检查所有行是否都是空数据
const
hasValidData
=
data
.
rows
.
some
((
row
:
any
)
=>
{
if
(
data
.
indexFields
&&
Array
.
isArray
(
data
.
indexFields
))
{
return
data
.
indexFields
.
some
((
field
:
string
)
=>
{
const
value
=
row
[
field
];
return
value
!==
null
&&
value
!==
undefined
&&
value
!==
''
&&
!
isNaN
(
Number
(
value
));
});
}
return
false
;
});
return
!
hasValidData
;
};
/**
* 数字格式化工具
*/
const
formatNumber
=
(
value
:
any
):
string
=>
{
if
(
value
===
null
||
value
===
undefined
||
isNaN
(
value
)
||
value
===
''
)
{
return
'
0
'
;
}
const
numValue
=
Number
(
value
);
if
(
isNaN
(
numValue
))
return
'
0
'
;
if
(
numValue
===
0
)
return
'
0
'
;
const
roundedValue
=
Math
.
ceil
(
numValue
*
100
)
/
100
;
if
(
Math
.
abs
(
roundedValue
)
>=
100000000
)
{
return
`
${(
roundedValue
/
100000000
).
toFixed
(
2
)}
亿`
;
}
else
if
(
Math
.
abs
(
roundedValue
)
>=
10000
)
{
return
`
${(
roundedValue
/
10000
).
toFixed
(
2
)}
万`
;
}
else
{
return
roundedValue
.
toFixed
(
2
);
}
};
/**
* 获取颜色方案
*/
const
getColors
=
()
=>
[
'
#1890ff
'
,
'
#52c41a
'
,
'
#faad14
'
,
'
#f5222d
'
,
'
#722ed1
'
,
'
#fa8c16
'
];
/**
* 获取tooltip数据值
*/
const
getTooltipValue
=
(
param
:
any
):
any
=>
{
if
(
param
.
data
!==
undefined
&&
param
.
data
!==
null
)
return
param
.
data
;
if
(
param
.
value
!==
undefined
&&
param
.
value
!==
null
)
return
param
.
value
;
return
null
;
};
/**
* 数据格式化工具
*/
const
formatData
=
(
data
:
any
)
=>
{
if
(
data
&&
data
.
rows
&&
Array
.
isArray
(
data
.
rows
)
&&
data
.
dimFields
&&
data
.
indexFields
)
{
const
{
rows
,
dimFields
,
indexFields
}
=
data
;
if
(
rows
.
length
===
0
)
{
return
{
isEmpty
:
true
,
data
:
[],
chartType
:
3
};
}
const
chartConfig
:
any
=
{
data
:
rows
,
chartType
:
3
};
// 维度字段处理
if
(
dimFields
.
length
===
1
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
false
;
}
else
if
(
dimFields
.
length
>=
2
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
groupField
=
dimFields
[
1
];
chartConfig
.
isGroup
=
true
;
}
// 指标字段处理
if
(
indexFields
.
length
===
1
)
{
chartConfig
.
yField
=
indexFields
[
0
];
chartConfig
.
isMultiY
=
false
;
}
else
if
(
indexFields
.
length
>=
2
)
{
chartConfig
.
isMultiY
=
true
;
chartConfig
.
yFields
=
indexFields
;
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
dimFields
.
length
>=
2
;
}
return
chartConfig
;
}
else
{
throw
new
Error
(
'
不支持的数据格式,请使用新的结构化数据格式
'
);
}
};
// ========== 图表配置生成器 ==========
/**
* 基础图表配置
*/
const
getBaseChartConfig
=
()
=>
({
responsive
:
true
,
animation
:
true
,
animationDuration
:
500
,
animationEasing
:
'
cubicOut
'
as
const
,
grid
:
{
left
:
'
3%
'
,
right
:
'
3%
'
,
bottom
:
'
3%
'
,
top
:
'
70px
'
,
// 改为固定值,为legend留出空间
containLabel
:
true
}
});
/**
* 基础tooltip配置
*/
const
getBaseTooltipConfig
=
()
=>
({
trigger
:
'
item
'
as
const
,
backgroundColor
:
'
rgba(255, 255, 255, 0.95)
'
,
borderColor
:
'
#e8e8e8
'
,
borderWidth
:
1
,
textStyle
:
{
color
:
'
#333
'
,
fontSize
:
12
}
});
/**
* 基础x轴配置
*/
const
getXAxisConfig
=
(
xAxisData
:
string
[])
=>
({
type
:
'
category
'
as
const
,
data
:
xAxisData
,
axisLabel
:
{
interval
:
0
,
rotate
:
xAxisData
.
length
>
6
?
45
:
0
,
margin
:
8
,
fontSize
:
12
}
});
/**
* 基础y轴配置
*/
const
getYAxisConfig
=
(
name
?:
string
)
=>
({
type
:
'
value
'
as
const
,
...(
name
&&
{
name
}),
axisLabel
:
{
formatter
:
(
value
:
number
)
=>
formatNumber
(
value
)
},
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
}
});
/**
* 双y轴配置
*/
const
getDualYAxisConfig
=
(
yFields
:
string
[])
=>
[
{
...
getYAxisConfig
(
yFields
[
0
]),
position
:
'
left
'
as
const
,
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
}
},
{
...
getYAxisConfig
(
yFields
[
1
]),
position
:
'
right
'
as
const
,
splitLine
:
{
lineStyle
:
{
type
:
'
solid
'
as
const
,
color
:
'
#e8e8e8
'
,
width
:
1
}
}
}
];
/**
* 基础系列配置
*/
const
getBaseSeriesConfig
=
(
name
:
string
,
data
:
any
[],
color
:
string
,
yAxisIndex
=
0
)
=>
({
name
,
type
:
'
bar
'
as
const
,
data
,
yAxisIndex
,
itemStyle
:
{
color
},
barWidth
:
'
auto
'
as
const
,
barGap
:
'
30%
'
,
barCategoryGap
:
'
40%
'
});
// ========== 图表类型配置生成器 ==========
/**
* 单指标柱状图配置生成器
*/
const
createSingleColumnOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
getBaseSeriesConfig
(
group
,
groupData
,
colors
[
index
%
colors
.
length
]);
});
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
(),
formatter
:
createTooltipFormatter
()
},
legend
:
{
data
:
groups
,
orient
:
'
horizontal
'
,
left
:
'
center
'
,
top
:
'
0
'
,
padding
:
[
15
,
20
,
20
,
25
],
// 增加四周的padding
itemGap
:
15
,
type
:
'
scroll
'
,
textStyle
:
{
fontSize
:
12
},
pageTextStyle
:
{
fontSize
:
10
},
pageIconSize
:
12
,
pageButtonItemGap
:
5
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
};
}
else
{
const
seriesData
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
(),
formatter
:
createTooltipFormatter
()
},
legend
:
{
show
:
false
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yField
,
seriesData
,
colors
[
0
])]
};
}
};
/**
* 双轴柱状图配置生成器
*/
const
createDualColumnOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series1
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
0
]}
`
,
groupData
,
colors
[
index
%
colors
.
length
],
0
);
});
const
series2
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
1
]}
`
,
groupData
,
colors
[(
index
+
3
)
%
colors
.
length
],
1
);
});
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
(),
formatter
:
createTooltipFormatter
()
},
legend
:
{
data
:
[...
series1
,
...
series2
].
map
(
s
=>
s
.
name
),
orient
:
'
horizontal
'
,
left
:
'
center
'
,
top
:
'
0
'
,
padding
:
[
15
,
20
,
20
,
25
],
// 增加四周的padding
itemGap
:
15
,
type
:
'
scroll
'
,
textStyle
:
{
fontSize
:
12
},
pageTextStyle
:
{
fontSize
:
10
},
pageIconSize
:
12
,
pageButtonItemGap
:
5
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[...
series1
,
...
series2
]
};
}
else
{
const
series1Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
const
series2Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
(),
formatter
:
createTooltipFormatter
()
},
legend
:
{
data
:
[
chartConfig
.
yFields
[
0
],
chartConfig
.
yFields
[
1
]]
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yFields
[
0
],
series1Data
,
colors
[
0
],
0
),
getBaseSeriesConfig
(
chartConfig
.
yFields
[
1
],
series2Data
,
colors
[
1
],
1
)
]
};
}
};
/**
* 创建tooltip格式化器
*/
const
createTooltipFormatter
=
()
=>
{
return
(
params
:
any
)
=>
{
// 在item模式下,params是单个对象
const
dataValue
=
getTooltipValue
(
params
);
const
seriesName
=
params
.
seriesName
;
const
xAxisValue
=
params
.
name
||
params
.
axisValue
;
if
(
dataValue
===
null
||
dataValue
===
undefined
)
return
''
;
const
formattedValue
=
formatNumber
(
dataValue
);
return
`
${
xAxisValue
}
<br/>
${
seriesName
}
:
${
formattedValue
}
`
;
};
};
// ========== 响应式处理逻辑 ==========
/**
* 创建响应式处理函数
*/
const
createResizeHandler
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
let
resizeTimer
:
NodeJS
.
Timeout
;
return
()
=>
{
clearTimeout
(
resizeTimer
);
resizeTimer
=
setTimeout
(()
=>
{
if
(
chartInstance
.
value
&&
chartContainer
.
value
)
{
const
newOption
=
chartTypeForLogic
===
'
column
'
?
createSingleColumnOption
(
chartConfig
)
:
createDualColumnOption
(
chartConfig
);
// 完全自适应配置
chartInstance
.
value
.
setOption
({
...
newOption
,
animation
:
false
});
chartInstance
.
value
.
resize
();
// ECharts会自动适应容器大小
}
},
100
);
};
};
/**
* 初始化图表大小监听
*/
const
initChartResizeListener
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
const
handleResize
=
createResizeHandler
(
chartConfig
,
chartTypeForLogic
);
// 窗口大小变化监听
window
.
addEventListener
(
'
resize
'
,
handleResize
);
// ResizeObserver监听
if
(
typeof
ResizeObserver
!==
'
undefined
'
&&
chartContainer
.
value
)
{
const
resizeObserver
=
new
ResizeObserver
(
handleResize
);
resizeObserver
.
observe
(
chartContainer
.
value
);
onUnmounted
(()
=>
{
resizeObserver
.
disconnect
();
});
}
// 清理函数
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
resize
'
,
handleResize
);
});
};
// ========== 主图表创建函数 ==========
/**
* 创建ECharts图表
*/
const
createChart
=
()
=>
{
try
{
if
(
!
chartContainer
.
value
)
return
;
// 检查数据是否为空
isEmpty
.
value
=
checkDataEmpty
(
props
.
chartData
);
if
(
isEmpty
.
value
)
{
// 清理之前的图表实例
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
chartInstance
.
value
=
null
;
}
error
.
value
=
''
;
return
;
}
const
chartConfig
=
formatData
(
props
.
chartData
);
// 根据数据特征自动选择图表类型
const
chartTypeForLogic
=
chartConfig
.
isMultiY
&&
chartConfig
.
yFields
&&
chartConfig
.
yFields
.
length
>
1
?
'
dualColumn
'
:
'
column
'
;
// 创建ECharts实例
chartInstance
.
value
=
echarts
.
init
(
chartContainer
.
value
);
// 生成图表配置
const
option
=
chartTypeForLogic
===
'
column
'
?
createSingleColumnOption
(
chartConfig
)
:
createDualColumnOption
(
chartConfig
);
// 设置图表选项
chartInstance
.
value
.
setOption
(
option
);
error
.
value
=
''
;
// 初始化响应式监听
initChartResizeListener
(
chartConfig
,
chartTypeForLogic
);
}
catch
(
err
:
any
)
{
error
.
value
=
`图表渲染失败:
${
err
.
message
}
`
;
console
.
error
(
'
图表渲染失败:
'
,
err
);
}
};
// ========== 生命周期和响应式 ==========
// 监听数据变化
watch
(()
=>
props
.
chartData
,
createChart
);
onMounted
(()
=>
{
nextTick
(
createChart
);
});
// 组件卸载时清理
onUnmounted
(()
=>
{
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
}
});
});
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
.
message-char
t
{
.
chart-componen
t
{
margin
:
16px
0
;
margin
:
16px
0
;
/* 完全自适应,不设置固定高度 */
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框和内边距在宽度计算中 */
}
}
.chart-title
{
.chart-error
{
font-size
:
16px
;
padding
:
20px
;
font-weight
:
bold
;
text-align
:
center
;
margin-bottom
:
8px
;
color
:
#f5222d
;
color
:
#333
;
background-color
:
#fff2f0
;
width
:
100%
;
/* 标题宽度限制 */
border
:
1px
solid
#ffccc7
;
box-sizing
:
border-box
;
}
.chart-container
{
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
/* 正常图表保持宽高比自适应 */
aspect-ratio
:
3
/
2
;
/* 宽高比 3:2 */
min-height
:
200px
;
/* 最小高度确保显示效果 */
width
:
100%
;
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框在宽度计算中 */
overflow
:
hidden
;
/* 防止内容溢出导致滚动条 */
}
/* 空状态样式 */
.chart-empty
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
350px
;
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
border-radius
:
4px
;
background-color
:
#fafafa
;
color
:
#999
;
width
:
100%
;
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框在宽度计算中 */
overflow
:
hidden
;
/* 防止内容溢出 */
}
.empty-icon
{
font-size
:
48px
;
margin-bottom
:
16px
;
width
:
80px
;
height
:
80px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
#e8e8e8
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.1
);
}
.empty-text
{
font-size
:
16px
;
font-weight
:
500
;
margin-bottom
:
8px
;
color
:
#666
;
text-align
:
center
;
/* 文字居中,避免溢出 */
max-width
:
100%
;
/* 文字宽度限制 */
}
.empty-desc
{
font-size
:
14px
;
color
:
#999
;
text-align
:
center
;
/* 文字居中,避免溢出 */
max-width
:
100%
;
/* 文字宽度限制 */
}
}
</
style
>
</
style
>
\ No newline at end of file
src/views/components/ColumnChart.vue
0 → 100644
View file @
e1ea9e38
<
template
>
<div
class=
"message-chart"
>
<div
class=
"chart-title"
>
{{
title
}}
</div>
<div
v-if=
"isEmpty"
class=
"chart-empty"
>
<div
class=
"empty-icon"
>
📊
</div>
<div
class=
"empty-text"
>
暂无数据
</div>
<div
class=
"empty-desc"
>
当前查询条件下没有找到相关数据
</div>
</div>
<div
v-else
ref=
"chartContainer"
class=
"chart-container"
></div>
<div
v-if=
"error"
class=
"chart-error"
>
{{
error
}}
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
shallowRef
,
onMounted
,
onUnmounted
,
watch
,
nextTick
}
from
'
vue
'
;
import
*
as
echarts
from
'
echarts
'
;
// 定义组件属性
interface
Props
{
chartData
:
any
;
title
?:
string
;
width
?:
number
|
string
;
height
?:
number
|
string
;
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
title
:
'
柱状图
'
,
width
:
'
100%
'
,
height
:
'
auto
'
// 设置为auto,完全自适应
});
const
chartContainer
=
ref
<
HTMLElement
>
();
// 使用shallowRef避免Vue 3 Proxy代理与ECharts的兼容性问题
const
chartInstance
=
shallowRef
<
echarts
.
ECharts
|
null
>
(
null
);
const
error
=
ref
<
string
>
(
''
);
const
isEmpty
=
ref
<
boolean
>
(
false
);
// ========== 工具函数抽离 ==========
/**
* 检查数据是否为空
*/
const
checkDataEmpty
=
(
data
:
any
):
boolean
=>
{
if
(
!
data
)
return
true
;
if
(
!
data
.
rows
||
!
Array
.
isArray
(
data
.
rows
))
return
true
;
if
(
data
.
rows
.
length
===
0
)
return
true
;
// 检查所有行是否都是空数据
const
hasValidData
=
data
.
rows
.
some
((
row
:
any
)
=>
{
if
(
data
.
indexFields
&&
Array
.
isArray
(
data
.
indexFields
))
{
return
data
.
indexFields
.
some
((
field
:
string
)
=>
{
const
value
=
row
[
field
];
return
value
!==
null
&&
value
!==
undefined
&&
value
!==
''
&&
!
isNaN
(
Number
(
value
));
});
}
return
false
;
});
return
!
hasValidData
;
};
/**
* 数字格式化工具
*/
const
formatNumber
=
(
value
:
any
):
string
=>
{
if
(
value
===
null
||
value
===
undefined
||
isNaN
(
value
)
||
value
===
''
)
{
return
'
0
'
;
}
const
numValue
=
Number
(
value
);
if
(
isNaN
(
numValue
))
return
'
0
'
;
if
(
numValue
===
0
)
return
'
0
'
;
const
roundedValue
=
Math
.
ceil
(
numValue
*
100
)
/
100
;
if
(
Math
.
abs
(
roundedValue
)
>=
100000000
)
{
return
`
${(
roundedValue
/
100000000
).
toFixed
(
2
)}
亿`
;
}
else
if
(
Math
.
abs
(
roundedValue
)
>=
10000
)
{
return
`
${(
roundedValue
/
10000
).
toFixed
(
2
)}
万`
;
}
else
{
return
roundedValue
.
toFixed
(
2
);
}
};
/**
* 获取颜色方案 - 使用ECharts内置颜色方案
*/
const
getColors
=
()
=>
{
return
[
'
#1890ff
'
,
'
#52c41a
'
,
'
#faad14
'
,
'
#f5222d
'
,
'
#722ed1
'
,
'
#13c2c2
'
,
'
#fa541c
'
,
'
#eb2f96
'
,
'
#a0d911
'
,
'
#2f54eb
'
,
'
#fa8c16
'
,
'
#52c41a
'
,
'
#13c2c2
'
,
'
#1890ff
'
,
'
#722ed1
'
,
'
#faad14
'
,
'
#f5222d
'
,
'
#eb2f96
'
,
'
#a0d911
'
,
'
#2f54eb
'
,
'
#fa8c16
'
,
'
#52c41a
'
,
'
#13c2c2
'
,
'
#1890ff
'
];
};
/**
* 数据格式化工具
*/
const
formatData
=
(
data
:
any
)
=>
{
if
(
data
&&
data
.
rows
&&
Array
.
isArray
(
data
.
rows
)
&&
data
.
dimFields
&&
data
.
indexFields
)
{
const
{
rows
,
dimFields
,
indexFields
}
=
data
;
if
(
rows
.
length
===
0
)
{
return
{
isEmpty
:
true
,
data
:
[],
chartType
:
'
column
'
};
}
const
chartConfig
:
any
=
{
data
:
rows
,
chartType
:
'
column
'
};
// 维度字段处理
if
(
dimFields
.
length
===
1
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
false
;
}
else
if
(
dimFields
.
length
>=
2
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
groupField
=
dimFields
[
1
];
chartConfig
.
isGroup
=
true
;
}
// 指标字段处理
if
(
indexFields
.
length
===
1
)
{
chartConfig
.
yField
=
indexFields
[
0
];
chartConfig
.
isMultiY
=
false
;
}
else
if
(
indexFields
.
length
>=
2
)
{
chartConfig
.
isMultiY
=
true
;
chartConfig
.
yFields
=
indexFields
;
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
dimFields
.
length
>=
2
;
}
return
chartConfig
;
}
else
{
throw
new
Error
(
'
不支持的数据格式,请使用新的结构化数据格式
'
);
}
};
// ========== 图表配置生成器 ==========
/**
* 基础图表配置
*/
const
getBaseChartConfig
=
()
=>
({
responsive
:
true
,
animation
:
true
,
animationDuration
:
500
,
animationEasing
:
'
cubicOut
'
as
const
,
grid
:
{
left
:
'
3%
'
,
right
:
'
3%
'
,
bottom
:
'
3%
'
,
top
:
'
70px
'
,
// 改为固定值,为legend留出空间
containLabel
:
true
}
});
/**
* 基础tooltip配置
*/
const
getBaseTooltipConfig
=
()
=>
({
trigger
:
'
axis
'
as
const
,
axisPointer
:
{
type
:
'
line
'
,
lineStyle
:
{
color
:
'
#1890ff
'
,
width
:
2
},
label
:
{
show
:
true
,
backgroundColor
:
'
#1890ff
'
,
color
:
'
#fff
'
,
fontSize
:
12
,
padding
:
[
4
,
6
],
borderRadius
:
2
}
},
backgroundColor
:
'
rgba(255, 255, 255, 0.95)
'
,
borderColor
:
'
#e8e8e8
'
,
borderWidth
:
1
,
textStyle
:
{
color
:
'
#333
'
,
fontSize
:
12
},
confine
:
false
,
triggerOn
:
'
mousemove
'
,
show
:
true
,
alwaysShowContent
:
false
,
position
:
'
top
'
,
padding
:
8
,
extraCssText
:
'
box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 9999 !important;
'
,
formatter
:
function
(
params
:
any
)
{
if
(
!
params
||
params
.
length
===
0
)
{
return
''
;
}
const
firstParam
=
Array
.
isArray
(
params
)
?
params
[
0
]
:
params
;
const
axisValue
=
firstParam
.
axisValue
||
firstParam
.
name
||
''
;
let
result
=
`<div style="font-weight: bold; margin-bottom: 8px;">
${
axisValue
}
</div>`
;
const
paramsArray
=
Array
.
isArray
(
params
)
?
params
:
[
params
];
paramsArray
.
forEach
((
param
:
any
)
=>
{
const
value
=
param
.
value
!==
undefined
&&
param
.
value
!==
null
?
(
Array
.
isArray
(
param
.
value
)
?
formatNumber
(
param
.
value
[
param
.
componentIndex
||
0
])
:
formatNumber
(
param
.
value
))
:
'
0
'
;
const
color
=
param
.
color
||
'
#5470c6
'
;
const
seriesName
=
param
.
seriesName
||
'
数据
'
;
result
+=
`
<div style="display: flex; align-items: center; margin: 4px 0;">
<span style="display: inline-block; width: 12px; height: 12px; background-color:
${
color
}
; border-radius: 6px; margin-right: 8px;"></span>
<span style="flex: 1;">
${
seriesName
}
:</span>
<span style="font-weight: bold; margin-left: 8px;">
${
value
}
</span>
</div>
`
;
});
return
result
;
}
});
/**
* 基础x轴配置
*/
const
getXAxisConfig
=
(
xAxisData
:
string
[])
=>
({
type
:
'
category
'
as
const
,
data
:
xAxisData
,
axisLabel
:
{
interval
:
0
,
rotate
:
xAxisData
.
length
>
6
?
45
:
0
,
margin
:
8
,
fontSize
:
12
}
});
/**
* 基础y轴配置
*/
const
getYAxisConfig
=
(
name
?:
string
)
=>
({
type
:
'
value
'
as
const
,
...(
name
&&
{
name
}),
axisLabel
:
{
formatter
:
(
value
:
number
)
=>
formatNumber
(
value
)
},
axisTick
:
{
show
:
true
,
alignWithLabel
:
true
,
lineStyle
:
{
width
:
1
}
},
axisLine
:
{
show
:
true
,
lineStyle
:
{
width
:
1
}
},
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
}
});
/**
* 双y轴配置
*/
const
getDualYAxisConfig
=
(
yFields
:
string
[])
=>
[
{
...
getYAxisConfig
(
yFields
[
0
]),
position
:
'
left
'
as
const
,
nameTextStyle
:
{
align
:
'
right
'
},
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
}
},
{
...
getYAxisConfig
(
yFields
[
1
]),
position
:
'
right
'
as
const
,
nameTextStyle
:
{
align
:
'
left
'
},
splitLine
:
{
lineStyle
:
{
type
:
'
solid
'
as
const
,
color
:
'
#e8e8e8
'
,
width
:
1
}
}
}
];
/**
* 基础系列配置
*/
const
getBaseSeriesConfig
=
(
name
:
string
,
data
:
any
[],
color
:
string
,
yAxisIndex
=
0
)
=>
({
name
,
type
:
'
bar
'
as
const
,
data
,
yAxisIndex
,
itemStyle
:
{
color
},
barWidth
:
'
auto
'
as
const
,
barGap
:
'
30%
'
,
barCategoryGap
:
'
40%
'
});
// ========== 图表类型配置生成器 ==========
/**
* 单指标柱状图配置生成器
*/
const
createSingleColumnOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
getBaseSeriesConfig
(
group
,
groupData
,
colors
[
index
%
colors
.
length
]);
});
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
()
},
legend
:
{
data
:
groups
,
orient
:
'
horizontal
'
,
left
:
'
center
'
,
top
:
'
0
'
,
padding
:
[
15
,
20
,
20
,
25
],
// 增加四周的padding
itemGap
:
15
,
type
:
'
scroll
'
,
textStyle
:
{
fontSize
:
12
},
pageTextStyle
:
{
fontSize
:
10
},
pageIconSize
:
12
,
pageButtonItemGap
:
5
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
};
}
else
{
const
seriesData
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
()
},
legend
:
{
show
:
false
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yField
,
seriesData
,
colors
[
0
])]
};
}
};
/**
* 双轴柱状图配置生成器
*/
const
createDualColumnOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series1
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
0
]}
`
,
groupData
,
colors
[
index
%
colors
.
length
],
0
);
});
const
series2
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
1
]}
`
,
groupData
,
colors
[(
index
+
3
)
%
colors
.
length
],
1
);
});
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
()
},
legend
:
{
data
:
[...
series1
,
...
series2
].
map
(
s
=>
s
.
name
),
orient
:
'
horizontal
'
,
left
:
'
center
'
,
top
:
'
0
'
,
padding
:
[
15
,
20
,
20
,
25
],
// 增加四周的padding
itemGap
:
15
,
type
:
'
scroll
'
,
textStyle
:
{
fontSize
:
12
},
pageTextStyle
:
{
fontSize
:
10
},
pageIconSize
:
12
,
pageButtonItemGap
:
5
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[...
series1
,
...
series2
]
};
}
else
{
const
series1Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
const
series2Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
{
...
getBaseChartConfig
(),
tooltip
:
{
...
getBaseTooltipConfig
()
},
legend
:
{
data
:
[
chartConfig
.
yFields
[
0
],
chartConfig
.
yFields
[
1
]]
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yFields
[
0
],
series1Data
,
colors
[
0
],
0
),
getBaseSeriesConfig
(
chartConfig
.
yFields
[
1
],
series2Data
,
colors
[
1
],
1
)
]
};
}
};
// ========== 响应式处理逻辑 ==========
/**
* 创建响应式处理函数
*/
const
createResizeHandler
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
let
resizeTimer
:
NodeJS
.
Timeout
;
return
()
=>
{
clearTimeout
(
resizeTimer
);
resizeTimer
=
setTimeout
(()
=>
{
if
(
chartInstance
.
value
&&
chartContainer
.
value
)
{
const
newOption
=
chartTypeForLogic
===
'
column
'
?
createSingleColumnOption
(
chartConfig
)
:
createDualColumnOption
(
chartConfig
);
// 完全自适应配置
chartInstance
.
value
.
setOption
({
...
newOption
,
animation
:
false
});
chartInstance
.
value
.
resize
();
// ECharts会自动适应容器大小
}
},
100
);
};
};
/**
* 初始化图表大小监听
*/
const
initChartResizeListener
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
const
handleResize
=
createResizeHandler
(
chartConfig
,
chartTypeForLogic
);
// 窗口大小变化监听
window
.
addEventListener
(
'
resize
'
,
handleResize
);
// ResizeObserver监听
if
(
typeof
ResizeObserver
!==
'
undefined
'
&&
chartContainer
.
value
)
{
const
resizeObserver
=
new
ResizeObserver
(
handleResize
);
resizeObserver
.
observe
(
chartContainer
.
value
);
onUnmounted
(()
=>
{
resizeObserver
.
disconnect
();
});
}
// 清理函数
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
resize
'
,
handleResize
);
});
};
// ========== 主图表创建函数 ==========
/**
* 创建ECharts图表
*/
const
createChart
=
()
=>
{
if
(
!
chartContainer
.
value
)
return
;
// 检查数据是否为空
isEmpty
.
value
=
checkDataEmpty
(
props
.
chartData
);
if
(
isEmpty
.
value
)
{
// 清理之前的图表实例
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
chartInstance
.
value
=
null
;
}
error
.
value
=
''
;
return
;
}
const
chartConfig
=
formatData
(
props
.
chartData
);
// 根据数据特征自动选择图表类型
const
chartTypeForLogic
=
chartConfig
.
isMultiY
&&
chartConfig
.
yFields
&&
chartConfig
.
yFields
.
length
>
1
?
'
dualColumn
'
:
'
column
'
;
// 创建ECharts实例
chartInstance
.
value
=
echarts
.
init
(
chartContainer
.
value
);
// 生成图表配置
const
option
=
chartTypeForLogic
===
'
column
'
?
createSingleColumnOption
(
chartConfig
)
:
createDualColumnOption
(
chartConfig
);
// 设置图表选项
chartInstance
.
value
.
setOption
(
option
);
error
.
value
=
''
;
// 初始化响应式监听
initChartResizeListener
(
chartConfig
,
chartTypeForLogic
);
};
// ========== 生命周期和响应式 ==========
// 监听数据变化
watch
(()
=>
props
.
chartData
,
createChart
);
onMounted
(()
=>
{
nextTick
(
createChart
);
});
// 组件卸载时清理
onUnmounted
(()
=>
{
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
}
});
</
script
>
<
style
scoped
>
.message-chart
{
margin
:
16px
0
;
/* 完全自适应,不设置固定高度 */
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框和内边距在宽度计算中 */
}
.chart-title
{
font-size
:
16px
;
font-weight
:
bold
;
margin-bottom
:
8px
;
color
:
#333
;
width
:
100%
;
/* 标题宽度限制 */
box-sizing
:
border-box
;
text-align
:
center
;
/* 标题居中 */
}
.chart-container
{
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
/* 正常图表保持宽高比自适应 */
aspect-ratio
:
3
/
2
;
/* 宽高比 3:2 */
min-height
:
200px
;
/* 最小高度确保显示效果 */
width
:
100%
;
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框在宽度计算中 */
overflow
:
hidden
;
/* 防止内容溢出导致滚动条 */
}
/* 空状态样式 */
.chart-empty
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
350px
;
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
background-color
:
#fafafa
;
color
:
#999
;
width
:
100%
;
/* 确保宽度不超过父容器 */
box-sizing
:
border-box
;
/* 包含边框在宽度计算中 */
overflow
:
hidden
;
/* 防止内容溢出 */
}
.empty-icon
{
font-size
:
48px
;
margin-bottom
:
16px
;
width
:
80px
;
height
:
80px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
#e8e8e8
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.1
);
}
.empty-text
{
font-size
:
16px
;
font-weight
:
500
;
margin-bottom
:
8px
;
color
:
#666
;
text-align
:
center
;
/* 文字居中,避免溢出 */
max-width
:
100%
;
/* 文字宽度限制 */
}
.empty-desc
{
font-size
:
14px
;
color
:
#999
;
text-align
:
center
;
/* 文字居中,避免溢出 */
max-width
:
100%
;
/* 文字宽度限制 */
}
</
style
>
\ No newline at end of file
src/views/components/LineChart.vue
0 → 100644
View file @
e1ea9e38
<
template
>
<div
class=
"message-chart"
>
<div
class=
"chart-title"
>
{{
title
}}
</div>
<div
v-if=
"isEmpty"
class=
"chart-empty"
>
<div
class=
"empty-icon"
>
📊
</div>
<div
class=
"empty-text"
>
暂无数据
</div>
<div
class=
"empty-desc"
>
当前查询条件下没有找到相关数据
</div>
</div>
<div
v-else
ref=
"chartContainer"
class=
"chart-container"
></div>
<div
v-if=
"error"
class=
"chart-error"
>
{{
error
}}
</div>
</div>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
shallowRef
,
watch
,
onMounted
,
onUnmounted
,
nextTick
}
from
'
vue
'
;
import
*
as
echarts
from
'
echarts
'
;
// 定义组件属性 - 与ChartComponent保持一致
interface
Props
{
chartData
:
any
;
title
?:
string
;
width
?:
number
|
string
;
height
?:
number
|
string
;
}
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
title
:
'
折线图
'
,
width
:
'
100%
'
,
height
:
'
auto
'
});
const
chartContainer
=
ref
<
HTMLElement
>
();
// 使用shallowRef避免Vue 3 Proxy代理与ECharts的兼容性问题
const
chartInstance
=
shallowRef
<
echarts
.
ECharts
|
null
>
(
null
);
const
error
=
ref
<
string
>
(
''
);
const
isEmpty
=
ref
<
boolean
>
(
false
);
// ========== 工具函数抽离 ==========
/**
* 检查数据是否为空
*/
const
checkDataEmpty
=
(
data
:
any
):
boolean
=>
{
if
(
!
data
)
return
true
;
if
(
!
data
.
rows
||
!
Array
.
isArray
(
data
.
rows
))
return
true
;
if
(
data
.
rows
.
length
===
0
)
return
true
;
// 检查所有行是否都是空数据
const
hasValidData
=
data
.
rows
.
some
((
row
:
any
)
=>
{
if
(
data
.
indexFields
&&
Array
.
isArray
(
data
.
indexFields
))
{
return
data
.
indexFields
.
some
((
field
:
string
)
=>
{
const
value
=
row
[
field
];
return
value
!==
null
&&
value
!==
undefined
&&
value
!==
''
&&
!
isNaN
(
Number
(
value
));
});
}
return
false
;
});
return
!
hasValidData
;
};
/**
* 数字格式化工具
*/
const
formatNumber
=
(
value
:
any
):
string
=>
{
if
(
value
===
null
||
value
===
undefined
||
isNaN
(
value
)
||
value
===
''
)
{
return
'
0
'
;
}
const
numValue
=
Number
(
value
);
if
(
isNaN
(
numValue
))
return
'
0
'
;
if
(
numValue
===
0
)
return
'
0
'
;
const
roundedValue
=
Math
.
ceil
(
numValue
*
100
)
/
100
;
if
(
Math
.
abs
(
roundedValue
)
>=
100000000
)
{
return
`
${(
roundedValue
/
100000000
).
toFixed
(
2
)}
亿`
;
}
else
if
(
Math
.
abs
(
roundedValue
)
>=
10000
)
{
return
`
${(
roundedValue
/
10000
).
toFixed
(
2
)}
万`
;
}
else
{
return
roundedValue
.
toFixed
(
2
);
}
};
/**
* 获取颜色方案 - 使用ECharts内置颜色方案
*/
const
getColors
=
()
=>
{
return
[
'
#5470c6
'
,
'
#91cc75
'
,
'
#fac858
'
,
'
#ee6666
'
,
'
#73c0de
'
,
'
#3ba272
'
,
'
#fc8452
'
,
'
#9a60b4
'
,
'
#ea7ccc
'
,
'
#c23531
'
,
'
#2f4554
'
,
'
#61a0a8
'
,
'
#d48265
'
,
'
#91c7ae
'
,
'
#749f83
'
,
'
#ca8622
'
,
'
#bda29a
'
,
'
#6e7074
'
,
'
#546570
'
,
'
#c4ccd3
'
,
'
#f4eccc
'
,
'
#76bca0
'
,
'
#e69d87
'
,
'
#8dc1a9
'
];
};
/**
* 数据格式化工具
*/
const
formatData
=
(
data
:
any
)
=>
{
if
(
data
&&
data
.
rows
&&
Array
.
isArray
(
data
.
rows
)
&&
data
.
dimFields
&&
data
.
indexFields
)
{
const
{
rows
,
dimFields
,
indexFields
}
=
data
;
if
(
rows
.
length
===
0
)
{
return
{
isEmpty
:
true
,
data
:
[],
chartType
:
'
line
'
};
}
const
chartConfig
:
any
=
{
data
:
rows
,
chartType
:
'
line
'
};
// 维度字段处理
if
(
dimFields
.
length
===
1
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
false
;
}
else
if
(
dimFields
.
length
>=
2
)
{
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
groupField
=
dimFields
[
1
];
chartConfig
.
isGroup
=
true
;
}
// 指标字段处理
if
(
indexFields
.
length
===
1
)
{
chartConfig
.
yField
=
indexFields
[
0
];
chartConfig
.
isMultiY
=
false
;
}
else
if
(
indexFields
.
length
>=
2
)
{
chartConfig
.
isMultiY
=
true
;
chartConfig
.
yFields
=
indexFields
;
chartConfig
.
xField
=
dimFields
[
0
];
chartConfig
.
isGroup
=
dimFields
.
length
>=
2
;
}
return
chartConfig
;
}
else
{
throw
new
Error
(
'
不支持的数据格式,请使用新的结构化数据格式
'
);
}
};
// ========== 图表配置生成器 ==========
/**
* 基础图表配置
*/
const
getBaseChartConfig
=
()
=>
({
responsive
:
true
,
animation
:
true
,
animationDuration
:
500
,
animationEasing
:
'
cubicOut
'
as
const
,
grid
:
{
left
:
'
3%
'
,
right
:
'
3%
'
,
bottom
:
'
3%
'
,
top
:
'
80px
'
,
// 增加顶部间距,为legend留出更多空间
containLabel
:
true
}
});
/**
* 基础legend配置 - 始终放在顶部
*/
const
getBaseLegendConfig
=
(
data
:
string
[])
=>
({
data
,
orient
:
'
horizontal
'
as
const
,
left
:
'
center
'
as
const
,
top
:
'
0
'
as
const
,
padding
:
[
10
,
20
,
0
,
20
],
// 调整padding,确保顶部有足够空间
itemGap
:
15
,
type
:
'
scroll
'
as
const
,
textStyle
:
{
fontSize
:
12
},
pageTextStyle
:
{
fontSize
:
10
},
pageIconSize
:
12
,
pageButtonItemGap
:
5
});
/**
* 基础tooltip配置 - 简化版本,确保兼容性
*/
const
getBaseTooltipConfig
=
()
=>
({
trigger
:
'
axis
'
,
axisPointer
:
{
type
:
'
line
'
,
lineStyle
:
{
color
:
'
#1890ff
'
,
width
:
2
},
label
:
{
show
:
true
,
backgroundColor
:
'
#1890ff
'
,
color
:
'
#fff
'
,
fontSize
:
12
,
padding
:
[
4
,
6
],
borderRadius
:
2
}
},
backgroundColor
:
'
rgba(255, 255, 255, 0.95)
'
,
borderColor
:
'
#e8e8e8
'
,
borderWidth
:
1
,
textStyle
:
{
color
:
'
#333
'
,
fontSize
:
12
},
confine
:
false
,
triggerOn
:
'
mousemove
'
,
show
:
true
,
alwaysShowContent
:
false
,
position
:
'
top
'
,
padding
:
8
,
extraCssText
:
'
box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 9999 !important;
'
,
formatter
:
function
(
params
:
any
)
{
if
(
!
params
||
params
.
length
===
0
)
{
return
''
;
}
const
firstParam
=
Array
.
isArray
(
params
)
?
params
[
0
]
:
params
;
const
axisValue
=
firstParam
.
axisValue
||
firstParam
.
name
||
''
;
let
result
=
`<div style="font-weight: bold; margin-bottom: 8px;">
${
axisValue
}
</div>`
;
const
paramsArray
=
Array
.
isArray
(
params
)
?
params
:
[
params
];
paramsArray
.
forEach
((
param
:
any
)
=>
{
const
value
=
param
.
value
!==
undefined
&&
param
.
value
!==
null
?
(
Array
.
isArray
(
param
.
value
)
?
formatNumber
(
param
.
value
[
param
.
componentIndex
||
0
])
:
formatNumber
(
param
.
value
))
:
'
0
'
;
const
color
=
param
.
color
||
'
#5470c6
'
;
const
seriesName
=
param
.
seriesName
||
'
数据
'
;
result
+=
`
<div style="display: flex; align-items: center; margin: 4px 0;">
<span style="display: inline-block; width: 12px; height: 12px; background-color:
${
color
}
; border-radius: 6px; margin-right: 8px;"></span>
<span style="flex: 1;">
${
seriesName
}
:</span>
<span style="font-weight: bold; margin-left: 8px;">
${
value
}
</span>
</div>
`
;
});
return
result
;
}
});
/**
* 基础x轴配置
*/
const
getXAxisConfig
=
(
xAxisData
:
string
[])
=>
({
type
:
'
category
'
as
const
,
data
:
xAxisData
,
axisLabel
:
{
interval
:
0
,
rotate
:
xAxisData
.
length
>
6
?
45
:
0
,
margin
:
8
,
fontSize
:
12
}
});
/**
* 基础y轴配置
*/
const
getYAxisConfig
=
(
name
?:
string
)
=>
({
type
:
'
value
'
as
const
,
...(
name
&&
{
name
}),
axisLabel
:
{
formatter
:
(
value
:
number
)
=>
formatNumber
(
value
)
},
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
},
axisTick
:
{
show
:
true
,
alignWithLabel
:
true
,
lineStyle
:
{
width
:
1
}
},
axisLine
:
{
show
:
true
,
lineStyle
:
{
width
:
1
}
}
});
/**
* 双y轴配置
*/
const
getDualYAxisConfig
=
(
yFields
:
string
[])
=>
[
{
...
getYAxisConfig
(
yFields
[
0
]),
position
:
'
left
'
as
const
,
nameTextStyle
:
{
align
:
'
right
'
},
splitLine
:
{
lineStyle
:
{
type
:
'
dashed
'
as
const
,
color
:
'
#ccc
'
,
width
:
1
}
}
},
{
...
getYAxisConfig
(
yFields
[
1
]),
position
:
'
right
'
as
const
,
nameTextStyle
:
{
align
:
'
left
'
},
splitLine
:
{
lineStyle
:
{
type
:
'
solid
'
as
const
,
color
:
'
#e8e8e8
'
,
width
:
1
}
}
}
];
/**
* 基础系列配置 - 折线图专用
*/
const
getBaseSeriesConfig
=
(
name
:
string
,
data
:
any
[],
color
:
string
,
yAxisIndex
=
0
)
=>
({
name
,
type
:
'
line
'
as
const
,
data
,
yAxisIndex
,
smooth
:
true
,
showSymbol
:
true
,
symbol
:
'
circle
'
,
symbolSize
:
6
,
lineStyle
:
{
color
,
width
:
3
},
itemStyle
:
{
color
,
borderColor
:
'
#fff
'
,
borderWidth
:
2
}
});
// ========== 图表类型配置生成器 ==========
/**
* 单指标折线图配置生成器
*/
const
createSingleLineOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
getBaseSeriesConfig
(
group
,
groupData
,
colors
[
index
%
colors
.
length
]);
});
return
{
...
getBaseChartConfig
(),
tooltip
:
getBaseTooltipConfig
(),
legend
:
getBaseLegendConfig
(
groups
),
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
};
}
else
{
const
seriesData
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yField
]);
return
{
...
getBaseChartConfig
(),
tooltip
:
getBaseTooltipConfig
(),
legend
:
{
show
:
false
},
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getYAxisConfig
(),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yField
,
seriesData
,
colors
[
0
])]
};
}
};
/**
* 双轴折线图配置生成器
*/
const
createDualLineOption
=
(
chartConfig
:
any
):
echarts
.
EChartsOption
=>
{
const
colors
=
getColors
();
const
xAxisData
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
xField
]))];
if
(
chartConfig
.
isGroup
&&
chartConfig
.
groupField
)
{
const
groups
=
[...
new
Set
(
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]))];
const
series1
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
0
]}
`
,
groupData
,
colors
[
index
%
colors
.
length
],
0
);
});
const
series2
=
groups
.
map
((
group
,
index
)
=>
{
const
groupData
=
chartConfig
.
data
.
filter
((
item
:
any
)
=>
item
[
chartConfig
.
groupField
]
===
group
)
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
getBaseSeriesConfig
(
`
${
group
}
-
${
chartConfig
.
yFields
[
1
]}
`
,
groupData
,
colors
[(
index
+
3
)
%
colors
.
length
],
1
);
});
const
legendData
=
[...
series1
,
...
series2
].
map
(
s
=>
s
.
name
);
return
{
...
getBaseChartConfig
(),
tooltip
:
getBaseTooltipConfig
(),
legend
:
getBaseLegendConfig
(
legendData
),
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[...
series1
,
...
series2
]
};
}
else
{
const
series1Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
0
]]);
const
series2Data
=
chartConfig
.
data
.
map
((
item
:
any
)
=>
item
[
chartConfig
.
yFields
[
1
]]);
return
{
...
getBaseChartConfig
(),
tooltip
:
getBaseTooltipConfig
(),
legend
:
getBaseLegendConfig
([
chartConfig
.
yFields
[
0
],
chartConfig
.
yFields
[
1
]]),
xAxis
:
getXAxisConfig
(
xAxisData
),
yAxis
:
getDualYAxisConfig
(
chartConfig
.
yFields
),
series
:
[
getBaseSeriesConfig
(
chartConfig
.
yFields
[
0
],
series1Data
,
colors
[
0
],
0
),
getBaseSeriesConfig
(
chartConfig
.
yFields
[
1
],
series2Data
,
colors
[
1
],
1
)
]
};
}
};
// ========== 响应式处理逻辑 ==========
/**
* 创建响应式处理函数
*/
const
createResizeHandler
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
let
resizeTimer
:
NodeJS
.
Timeout
;
return
()
=>
{
clearTimeout
(
resizeTimer
);
resizeTimer
=
setTimeout
(()
=>
{
if
(
chartInstance
.
value
&&
chartContainer
.
value
)
{
const
newOption
=
chartTypeForLogic
===
'
line
'
?
createSingleLineOption
(
chartConfig
)
:
createDualLineOption
(
chartConfig
);
chartInstance
.
value
.
setOption
({
...
newOption
,
animation
:
false
});
chartInstance
.
value
.
resize
();
}
},
100
);
};
};
/**
* 初始化图表大小监听
*/
const
initChartResizeListener
=
(
chartConfig
:
any
,
chartTypeForLogic
:
string
)
=>
{
const
handleResize
=
createResizeHandler
(
chartConfig
,
chartTypeForLogic
);
// 窗口大小变化监听
window
.
addEventListener
(
'
resize
'
,
handleResize
);
// ResizeObserver监听
if
(
typeof
ResizeObserver
!==
'
undefined
'
&&
chartContainer
.
value
)
{
const
resizeObserver
=
new
ResizeObserver
(
handleResize
);
resizeObserver
.
observe
(
chartContainer
.
value
);
onUnmounted
(()
=>
{
resizeObserver
.
disconnect
();
});
}
// 清理函数
onUnmounted
(()
=>
{
window
.
removeEventListener
(
'
resize
'
,
handleResize
);
});
};
// ========== 主图表创建函数 ==========
/**
* 创建ECharts图表
*/
const
createChart
=
()
=>
{
if
(
!
chartContainer
.
value
)
return
;
// 检查数据是否为空
isEmpty
.
value
=
checkDataEmpty
(
props
.
chartData
);
if
(
isEmpty
.
value
)
{
// 清理之前的图表实例
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
chartInstance
.
value
=
null
;
}
error
.
value
=
''
;
return
;
}
const
chartConfig
=
formatData
(
props
.
chartData
);
// 根据数据特征自动选择图表类型
const
chartTypeForLogic
=
chartConfig
.
isMultiY
&&
chartConfig
.
yFields
&&
chartConfig
.
yFields
.
length
>
1
?
'
dualLine
'
:
'
line
'
;
// 创建ECharts实例
chartInstance
.
value
=
echarts
.
init
(
chartContainer
.
value
);
// 生成图表配置
const
option
=
chartTypeForLogic
===
'
line
'
?
createSingleLineOption
(
chartConfig
)
:
createDualLineOption
(
chartConfig
);
// 设置图表选项
chartInstance
.
value
.
setOption
(
option
);
error
.
value
=
''
;
// 初始化响应式监听
initChartResizeListener
(
chartConfig
,
chartTypeForLogic
);
};
// ========== 生命周期和响应式 ==========
// 监听数据变化
watch
(()
=>
props
.
chartData
,
createChart
);
onMounted
(()
=>
{
nextTick
(
createChart
);
});
// 组件卸载时清理
onUnmounted
(()
=>
{
if
(
chartInstance
.
value
)
{
chartInstance
.
value
.
dispose
();
}
});
</
script
>
<
style
scoped
>
.message-chart
{
margin
:
16px
0
;
box-sizing
:
border-box
;
}
.chart-title
{
font-size
:
16px
;
font-weight
:
bold
;
margin-bottom
:
8px
;
color
:
#333
;
width
:
100%
;
box-sizing
:
border-box
;
text-align
:
center
;
/* 标题居中 */
}
.chart-container
{
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
aspect-ratio
:
3
/
2
;
min-height
:
200px
;
width
:
100%
;
box-sizing
:
border-box
;
overflow
:
visible
!important
;
/* 确保tooltip可以正常显示 */
position
:
relative
;
z-index
:
1
;
/* 确保tooltip可以正常显示 */
isolation
:
isolate
;
}
/* 空状态样式 */
.chart-empty
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
350px
;
border
:
1px
solid
#e8e8e8
;
border-radius
:
4px
;
background-color
:
#fafafa
;
color
:
#999
;
width
:
100%
;
box-sizing
:
border-box
;
overflow
:
hidden
;
}
.empty-icon
{
font-size
:
48px
;
margin-bottom
:
16px
;
width
:
80px
;
height
:
80px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
#e8e8e8
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.1
);
}
.empty-text
{
font-size
:
16px
;
font-weight
:
500
;
margin-bottom
:
8px
;
color
:
#666
;
text-align
:
center
;
max-width
:
100%
;
}
.empty-desc
{
font-size
:
14px
;
color
:
#999
;
text-align
:
center
;
max-width
:
100%
;
}
.chart-error
{
padding
:
10px
;
text-align
:
center
;
color
:
#f5222d
;
background-color
:
#fff2f0
;
border
:
1px
solid
#ffccc7
;
border-radius
:
4px
;
margin-top
:
10px
;
}
</
style
>
\ No newline at end of file
src/views/components/style.less
View file @
e1ea9e38
...
@@ -597,7 +597,7 @@ li {
...
@@ -597,7 +597,7 @@ li {
font-size: 14px;
font-size: 14px;
span {
span {
color:
#2eb0a1
;
color:
@primary-color
;
font-weight: bold;
font-weight: bold;
}
}
}
}
...
...
src/views/components/utils/contentTemplateService.ts
View file @
e1ea9e38
...
@@ -2,6 +2,12 @@ import dayjs from 'dayjs';
...
@@ -2,6 +2,12 @@ import dayjs from 'dayjs';
import
{
tableTemplate
}
from
'
./tableTemplate
'
;
import
{
tableTemplate
}
from
'
./tableTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./markdownTemplate
'
;
import
{
markdownTemplate
,
isLastBlockMarkdown
,
getLastMarkdownBlockIndex
,
mergeMarkdownContent
}
from
'
./markdownTemplate
'
;
// 图表类型常量定义
const
CHART_TYPES
=
{
COLUMN
:
'
column
'
,
LINE
:
'
line
'
}
as
const
;
// 内容模板类型定义
// 内容模板类型定义
export
interface
ContentTemplates
{
export
interface
ContentTemplates
{
text
:
(
content
:
string
)
=>
string
;
text
:
(
content
:
string
)
=>
string
;
...
@@ -300,7 +306,16 @@ export class ContentTemplateService {
...
@@ -300,7 +306,16 @@ export class ContentTemplateService {
thinkContent
:
''
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
thinkBoxExpanded
:
false
,
chartData
:
messageContent
,
chartData
:
messageContent
,
chartType
:
3
,
chartType
:
CHART_TYPES
.
COLUMN
,
});
// 图表数据处理
updatedResponse
.
contentBlocks
.
push
({
content
:
''
,
hasThinkBox
:
false
,
thinkContent
:
''
,
thinkBoxExpanded
:
false
,
chartData
:
messageContent
,
chartType
:
messageContent
.
chartType
||
CHART_TYPES
.
COLUMN
,
});
});
break
;
break
;
...
@@ -620,7 +635,7 @@ export class ContentTemplateService {
...
@@ -620,7 +635,7 @@ export class ContentTemplateService {
let
currentBlockIdx
=
-
1
;
let
currentBlockIdx
=
-
1
;
// 历史数据处理,isHistoryData设为true,思考框折叠
// 历史数据处理,isHistoryData设为true,思考框折叠
data
.
answerInfoList
.
forEach
((
answer
)
=>
{
data
.
answerInfoList
.
forEach
((
answer
:
any
)
=>
{
const
sseData
:
SSEData
=
{
const
sseData
:
SSEData
=
{
message
:
answer
.
message
||
''
,
message
:
answer
.
message
||
''
,
status
:
answer
.
status
||
0
,
status
:
answer
.
status
||
0
,
...
...
src/views/components/utils/sseService.ts
View file @
e1ea9e38
...
@@ -75,8 +75,7 @@ export class SSEService {
...
@@ -75,8 +75,7 @@ export class SSEService {
'
x-app-code
'
:
this
.
config
.
appCode
||
''
,
'
x-app-code
'
:
this
.
config
.
appCode
||
''
,
},
},
withCredentials
:
true
,
withCredentials
:
true
,
connectionTimeout
:
60000
,
connectionTimeout
:
60000
heartbeatTimeout
:
45000
,
// 心跳超时时间
});
});
this
.
eventSource
.
onopen
=
(
event
)
=>
{
this
.
eventSource
.
onopen
=
(
event
)
=>
{
...
@@ -183,6 +182,13 @@ export class SSEService {
...
@@ -183,6 +182,13 @@ export class SSEService {
console
.
log
(
'
⏳ 重连已启动,等待连接建立...
'
);
console
.
log
(
'
⏳ 重连已启动,等待连接建立...
'
);
}
}
// 标记重连成功(供外部调用)
public
markReconnectSuccess
():
void
{
this
.
isReconnecting
.
value
=
false
;
this
.
reconnectAttempts
=
0
;
console
.
log
(
'
✅ 重连成功,重置重连状态
'
);
}
// 关闭SSE连接
// 关闭SSE连接
public
closeSSE
():
void
{
public
closeSSE
():
void
{
if
(
this
.
eventSource
)
{
if
(
this
.
eventSource
)
{
...
@@ -385,6 +391,9 @@ export class SSEService {
...
@@ -385,6 +391,9 @@ export class SSEService {
// 不再自动触发重连,避免重复消息
// 不再自动触发重连,避免重复消息
// 网络恢复事件会由网络状态监听器自动处理
// 网络恢复事件会由网络状态监听器自动处理
}
}
}
else
{
// 正在重连中,避免重复触发
console
.
log
(
'
⏳ 心跳检测 - 正在重连中,跳过本次检测
'
);
}
}
}
}
}
}
...
...
src/views/components/utils/tableTemplate.ts
View file @
e1ea9e38
...
@@ -42,7 +42,7 @@ export const generateTableHTML = (tableData: TableData[], config: TableConfig =
...
@@ -42,7 +42,7 @@ export const generateTableHTML = (tableData: TableData[], config: TableConfig =
});
});
};
};
// 数字格式化函数 - 万/亿格式化,保留两位小数,
向上取整
// 数字格式化函数 - 万/亿格式化,保留两位小数,
四舍五入
const
formatNumber
=
(
value
:
any
)
=>
{
const
formatNumber
=
(
value
:
any
)
=>
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
)
return
''
;
if
(
value
===
null
||
value
===
undefined
||
value
===
''
)
return
''
;
...
@@ -51,15 +51,15 @@ export const generateTableHTML = (tableData: TableData[], config: TableConfig =
...
@@ -51,15 +51,15 @@ export const generateTableHTML = (tableData: TableData[], config: TableConfig =
if
(
num
>=
100000000
)
{
if
(
num
>=
100000000
)
{
// 亿级别
// 亿级别
const
result
=
Math
.
ceil
((
num
/
100000000
)
*
100
)
/
100
;
const
result
=
Math
.
round
((
num
/
100000000
)
*
100
)
/
100
;
return
result
.
toFixed
(
2
)
+
'
亿
'
;
return
result
.
toFixed
(
2
)
+
'
亿
'
;
}
else
if
(
num
>=
10000
)
{
}
else
if
(
num
>=
10000
)
{
// 万级别
// 万级别
const
result
=
Math
.
ceil
((
num
/
10000
)
*
100
)
/
100
;
const
result
=
Math
.
round
((
num
/
10000
)
*
100
)
/
100
;
return
result
.
toFixed
(
2
)
+
'
万
'
;
return
result
.
toFixed
(
2
)
+
'
万
'
;
}
else
{
}
else
{
// 小于万
// 小于万
return
Math
.
ceil
(
num
).
toString
();
return
Math
.
round
(
num
).
toString
();
}
}
};
};
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment