<script lang="ts" setup>
|
import type { BiDashboardApi } from '#/api/bi/dashboard';
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
import { Empty } from 'ant-design-vue';
|
|
import {
|
getAreaLineChartOptions,
|
getBarChartOptions,
|
getDonutPieChartOptions,
|
getGaugeChartOptions,
|
getScatterChartOptions,
|
} from '../chart-options';
|
|
defineOptions({ name: 'BiChartCard' });
|
|
interface Props {
|
chart: BiDashboardApi.ChartItem;
|
accentColor?: string;
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
accentColor: '#00E5FF',
|
});
|
|
const chartRef = ref<EchartsUIType>();
|
const { renderEcharts } = useEcharts(chartRef);
|
|
let refreshTimer: ReturnType<typeof setInterval> | null = null;
|
let resizeObserver: ResizeObserver | null = null;
|
|
function getEchartsOptions(chart: BiDashboardApi.ChartItem) {
|
if (!chart.data || chart.data.length === 0) return {};
|
|
const keys = Object.keys(chart.data[0] || {});
|
const labelKey = keys[0] || 'name';
|
const valueKey = keys[1] || 'value';
|
const labels = chart.data.map((d: Record<string, unknown>) => String(d[labelKey] || ''));
|
const values = chart.data.map((d: Record<string, unknown>) => Number(d[valueKey]) || 0);
|
|
switch (chart.chartType) {
|
case 'bar':
|
return getBarChartOptions(labels, [{ name: chart.name, data: values }]);
|
case 'line':
|
return getAreaLineChartOptions(labels, [{ name: chart.name, data: values }]);
|
case 'pie':
|
return getDonutPieChartOptions(labels.map((l, i) => ({ name: l, value: values[i] || 0 })));
|
case 'gauge': {
|
const gVal = values[0] || values.find((v) => v > 0) || 0;
|
return getGaugeChartOptions(Math.min(gVal, 100), chart.name, 100);
|
}
|
case 'scatter': {
|
const pts: Array<[number, number]> = chart.data.map((d: Record<string, unknown>) => [
|
Number(d[keys[0]]) || 0,
|
Number(d[keys[1]]) || 0,
|
]);
|
return getScatterChartOptions(pts, keys[0], keys[1]);
|
}
|
default:
|
return {};
|
}
|
}
|
|
async function render() {
|
const type = props.chart.chartType;
|
if (!['bar', 'line', 'pie', 'gauge', 'scatter'].includes(type)) return;
|
const options = getEchartsOptions(props.chart);
|
if (props.chart.chartOptions) {
|
Object.assign(options, props.chart.chartOptions);
|
}
|
await renderEcharts(options);
|
}
|
|
function setupTimer() {
|
clearTimer();
|
if (props.chart.refreshInterval > 0) {
|
refreshTimer = setInterval(render, props.chart.refreshInterval * 1000);
|
}
|
}
|
|
function clearTimer() {
|
if (refreshTimer !== null) {
|
clearInterval(refreshTimer);
|
refreshTimer = null;
|
}
|
}
|
|
onMounted(() => {
|
render();
|
setupTimer();
|
const el = (chartRef.value as any)?.$el || (chartRef.value as any)?.root;
|
if (el) {
|
resizeObserver = new ResizeObserver(() => {
|
setTimeout(() => window.dispatchEvent(new Event('resize')), 50);
|
});
|
resizeObserver.observe(el);
|
}
|
});
|
|
watch(() => props.chart, () => {
|
render();
|
setupTimer();
|
});
|
|
onBeforeUnmount(() => {
|
clearTimer();
|
resizeObserver?.disconnect();
|
});
|
</script>
|
|
<template>
|
<div class="chart-card">
|
<div class="chart-header">
|
<span class="chart-dot" :style="{ background: accentColor, boxShadow: `0 0 6px ${accentColor}` }" />
|
<span class="chart-title">{{ chart.name }}</span>
|
<span
|
v-if="chart.refreshInterval > 0"
|
class="chart-badge"
|
>每 {{ chart.refreshInterval }}s</span>
|
</div>
|
<div v-if="chart.error" class="flex h-[240px] items-center justify-center">
|
<Empty :description="chart.error" />
|
</div>
|
<EchartsUI
|
v-else
|
ref="chartRef"
|
class="!h-full"
|
:style="{ minHeight: '220px' }"
|
/>
|
</div>
|
</template>
|
|
<style scoped>
|
.chart-card {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
background: rgba(10, 24, 52, 0.55);
|
backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
border: 1px solid rgba(0, 229, 255, 0.2);
|
border-radius: 10px;
|
padding: 14px;
|
transition: all 0.35s;
|
animation: chart-fade-up 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
|
position: relative;
|
}
|
.chart-card::before {
|
content: '';
|
position: absolute; inset: 0; border-radius: 10px; pointer-events: none;
|
background: linear-gradient(135deg, rgba(0, 229, 255, 0.04) 0%, transparent 50%, rgba(0, 255, 136, 0.03) 100%);
|
}
|
|
.chart-card:hover {
|
border-color: rgba(0, 229, 255, 0.35);
|
box-shadow:
|
0 0 24px rgba(0, 229, 255, 0.08),
|
0 0 48px rgba(0, 229, 255, 0.03),
|
0 4px 20px rgba(0, 0, 0, 0.3);
|
}
|
|
.chart-card > :last-child {
|
flex: 1;
|
min-height: 0;
|
}
|
|
.chart-header {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 8px;
|
}
|
|
.chart-dot {
|
width: 6px;
|
height: 6px;
|
border-radius: 50%;
|
flex-shrink: 0;
|
}
|
|
.chart-title {
|
font-size: 13px;
|
font-weight: 600;
|
color: rgba(230, 236, 250, 0.95);
|
letter-spacing: 0.5px;
|
}
|
|
.chart-badge {
|
margin-left: auto;
|
font-size: 10px;
|
font-weight: 400;
|
padding: 2px 6px;
|
border-radius: 10px;
|
background: rgba(0, 229, 255, 0.06);
|
color: rgba(180, 190, 215, 0.6);
|
border: 1px solid rgba(0, 229, 255, 0.1);
|
}
|
|
@keyframes chart-fade-up {
|
from { opacity: 0; transform: translateY(16px); }
|
to { opacity: 1; transform: translateY(0); }
|
}
|
</style>
|