544 lines
11 KiB
Vue
544 lines
11 KiB
Vue
<template>
|
|
<div class="chart-card">
|
|
<!-- 图表头部 -->
|
|
<div class="chart-header">
|
|
<div class="header-left">
|
|
<h3 class="chart-title">{{ title }}</h3>
|
|
<p class="chart-subtitle" v-if="subtitle">{{ subtitle }}</p>
|
|
</div>
|
|
<div class="header-right">
|
|
<el-dropdown @command="handleCommand">
|
|
<el-button type="text" icon="el-icon-more">
|
|
<i class="el-icon-arrow-down el-icon--right"></i>
|
|
</el-button>
|
|
<el-dropdown-menu slot="dropdown">
|
|
<el-dropdown-item command="refresh">刷新</el-dropdown-item>
|
|
<el-dropdown-item command="export">导出</el-dropdown-item>
|
|
<el-dropdown-item command="fullscreen">全屏</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表内容 -->
|
|
<div class="chart-content" v-loading="loading">
|
|
<!-- 图表容器 -->
|
|
<div
|
|
ref="chartContainer"
|
|
class="chart-container"
|
|
:style="{ height: chartHeight + 'px' }"
|
|
></div>
|
|
|
|
<!-- 无数据状态 -->
|
|
<div class="no-data" v-if="!loading && (!data || data.length === 0)">
|
|
<i class="el-icon-data-line"></i>
|
|
<p>暂无数据</p>
|
|
</div>
|
|
|
|
<!-- 图表加载失败 -->
|
|
<div class="chart-error" v-if="error">
|
|
<i class="el-icon-warning"></i>
|
|
<p>{{ error }}</p>
|
|
<el-button type="text" @click="retry">重试</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表底部信息 -->
|
|
<div class="chart-footer" v-if="footerInfo">
|
|
<div class="footer-info">
|
|
<span v-for="(info, index) in footerInfo" :key="index" class="info-item">
|
|
<span class="info-label">{{ info.label }}:</span>
|
|
<span class="info-value">{{ info.value }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import * as echarts from 'echarts'
|
|
|
|
export default {
|
|
name: 'ChartCard',
|
|
props: {
|
|
title: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
subtitle: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
type: {
|
|
type: String,
|
|
default: 'line',
|
|
validator: value => ['line', 'bar', 'pie', 'scatter', 'gauge', 'funnel'].includes(value)
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
options: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
height: {
|
|
type: Number,
|
|
default: 300
|
|
},
|
|
footerInfo: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
chart: null,
|
|
error: null,
|
|
chartHeight: this.height
|
|
}
|
|
},
|
|
computed: {
|
|
// 合并默认配置和用户配置
|
|
chartOptions() {
|
|
const defaultOptions = this.getDefaultOptions()
|
|
return this.mergeOptions(defaultOptions, this.options)
|
|
}
|
|
},
|
|
watch: {
|
|
data: {
|
|
handler() {
|
|
this.updateChart()
|
|
},
|
|
deep: true
|
|
},
|
|
options: {
|
|
handler() {
|
|
this.updateChart()
|
|
},
|
|
deep: true
|
|
},
|
|
loading(newVal) {
|
|
if (!newVal && this.chart) {
|
|
this.updateChart()
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.initChart()
|
|
window.addEventListener('resize', this.handleResize)
|
|
},
|
|
beforeDestroy() {
|
|
if (this.chart) {
|
|
this.chart.dispose()
|
|
}
|
|
window.removeEventListener('resize', this.handleResize)
|
|
},
|
|
methods: {
|
|
// 初始化图表
|
|
initChart() {
|
|
if (!this.$refs.chartContainer) return
|
|
|
|
try {
|
|
this.chart = echarts.init(this.$refs.chartContainer)
|
|
this.updateChart()
|
|
this.error = null
|
|
} catch (error) {
|
|
console.error('图表初始化失败:', error)
|
|
this.error = '图表初始化失败'
|
|
}
|
|
},
|
|
|
|
// 更新图表
|
|
updateChart() {
|
|
if (!this.chart || this.loading) return
|
|
|
|
try {
|
|
const options = this.chartOptions
|
|
this.chart.setOption(options, true)
|
|
this.error = null
|
|
} catch (error) {
|
|
console.error('图表更新失败:', error)
|
|
this.error = '图表更新失败'
|
|
}
|
|
},
|
|
|
|
// 获取默认配置
|
|
getDefaultOptions() {
|
|
const baseOptions = {
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
borderColor: 'transparent',
|
|
textStyle: {
|
|
color: '#fff'
|
|
}
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
color: ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
|
|
}
|
|
|
|
switch (this.type) {
|
|
case 'line':
|
|
return {
|
|
...baseOptions,
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: this.data.map(item => item.time || item.name || item.x)
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [{
|
|
type: 'line',
|
|
data: this.data.map(item => item.value || item.y),
|
|
smooth: true,
|
|
areaStyle: {
|
|
opacity: 0.1
|
|
}
|
|
}]
|
|
}
|
|
|
|
case 'bar':
|
|
return {
|
|
...baseOptions,
|
|
xAxis: {
|
|
type: 'category',
|
|
data: this.data.map(item => item.name || item.x)
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [{
|
|
type: 'bar',
|
|
data: this.data.map(item => item.value || item.y),
|
|
barWidth: '60%'
|
|
}]
|
|
}
|
|
|
|
case 'pie':
|
|
return {
|
|
...baseOptions,
|
|
tooltip: {
|
|
trigger: 'item',
|
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
|
},
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['40%', '70%'],
|
|
data: this.data,
|
|
emphasis: {
|
|
itemStyle: {
|
|
shadowBlur: 10,
|
|
shadowOffsetX: 0,
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
|
|
case 'gauge':
|
|
return {
|
|
...baseOptions,
|
|
series: [{
|
|
type: 'gauge',
|
|
data: this.data,
|
|
radius: '80%',
|
|
startAngle: 200,
|
|
endAngle: -20,
|
|
min: 0,
|
|
max: 100,
|
|
splitNumber: 10,
|
|
axisLine: {
|
|
lineStyle: {
|
|
width: 6
|
|
}
|
|
},
|
|
axisTick: {
|
|
distance: -30,
|
|
splitNumber: 5,
|
|
lineStyle: {
|
|
width: 2,
|
|
color: '#999'
|
|
}
|
|
},
|
|
axisLabel: {
|
|
distance: -20,
|
|
color: '#999',
|
|
fontSize: 12
|
|
},
|
|
pointer: {
|
|
itemStyle: {
|
|
color: 'auto'
|
|
}
|
|
},
|
|
title: {
|
|
color: '#999'
|
|
},
|
|
detail: {
|
|
valueAnimation: true,
|
|
formatter: '{value}%',
|
|
color: 'auto'
|
|
}
|
|
}]
|
|
}
|
|
|
|
default:
|
|
return baseOptions
|
|
}
|
|
},
|
|
|
|
// 合并配置
|
|
mergeOptions(defaultOptions, userOptions) {
|
|
return {
|
|
...defaultOptions,
|
|
...userOptions,
|
|
series: userOptions.series || defaultOptions.series
|
|
}
|
|
},
|
|
|
|
// 处理窗口大小变化
|
|
handleResize() {
|
|
if (this.chart) {
|
|
this.chart.resize()
|
|
}
|
|
},
|
|
|
|
// 处理下拉菜单命令
|
|
handleCommand(command) {
|
|
switch (command) {
|
|
case 'refresh':
|
|
this.$emit('refresh')
|
|
break
|
|
case 'export':
|
|
this.exportChart()
|
|
break
|
|
case 'fullscreen':
|
|
this.toggleFullscreen()
|
|
break
|
|
}
|
|
},
|
|
|
|
// 导出图表
|
|
exportChart() {
|
|
if (!this.chart) return
|
|
|
|
try {
|
|
const url = this.chart.getDataURL({
|
|
type: 'png',
|
|
pixelRatio: 2,
|
|
backgroundColor: '#fff'
|
|
})
|
|
|
|
const link = document.createElement('a')
|
|
link.download = `${this.title}_${new Date().getTime()}.png`
|
|
link.href = url
|
|
link.click()
|
|
|
|
this.$message.success('图表导出成功')
|
|
} catch (error) {
|
|
console.error('图表导出失败:', error)
|
|
this.$message.error('图表导出失败')
|
|
}
|
|
},
|
|
|
|
// 切换全屏
|
|
toggleFullscreen() {
|
|
this.$emit('fullscreen', this.title)
|
|
},
|
|
|
|
// 重试
|
|
retry() {
|
|
this.error = null
|
|
this.initChart()
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.chart-card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.chart-card:hover {
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.chart-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.header-left {
|
|
flex: 1;
|
|
}
|
|
|
|
.chart-title {
|
|
margin: 0 0 4px 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
}
|
|
|
|
.chart-subtitle {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: #909399;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.chart-content {
|
|
position: relative;
|
|
padding: 20px;
|
|
}
|
|
|
|
.chart-container {
|
|
width: 100%;
|
|
min-height: 200px;
|
|
}
|
|
|
|
.no-data {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 200px;
|
|
color: #c0c4cc;
|
|
}
|
|
|
|
.no-data i {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.no-data p {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chart-error {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 200px;
|
|
color: #f56c6c;
|
|
}
|
|
|
|
.chart-error i {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.chart-error p {
|
|
margin: 0 0 12px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chart-footer {
|
|
padding: 12px 20px;
|
|
border-top: 1px solid #f0f0f0;
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.footer-info {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
.info-item {
|
|
font-size: 12px;
|
|
color: #606266;
|
|
}
|
|
|
|
.info-label {
|
|
font-weight: 500;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.info-value {
|
|
color: #909399;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 768px) {
|
|
.chart-header {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chart-content {
|
|
padding: 16px;
|
|
}
|
|
|
|
.chart-container {
|
|
min-height: 150px;
|
|
}
|
|
|
|
.chart-footer {
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.footer-info {
|
|
gap: 8px;
|
|
}
|
|
}
|
|
|
|
/* 动画效果 */
|
|
.chart-card {
|
|
animation: fadeInUp 0.6s ease-out;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* 加载状态样式 */
|
|
.chart-content .el-loading-mask {
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
.chart-content .el-loading-spinner {
|
|
margin-top: -25px;
|
|
}
|
|
|
|
.chart-content .el-loading-spinner .el-icon-loading {
|
|
font-size: 24px;
|
|
color: #409EFF;
|
|
}
|
|
</style>
|
|
|