| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- <script setup lang="ts">
- import * as echarts from 'echarts'
- type ClassifyItem = {
- category?: string
- name?: string
- value?: number | string
- }
- const props = defineProps<{
- data: ClassifyItem[]
- loading?: boolean
- }>()
- const chartRef = ref<HTMLDivElement>()
- let chart: echarts.ECharts | null = null
- let resizeObserver: ResizeObserver | null = null
- const barPalette = [
- ['#47b5ff', '#147ff3'],
- ['#5f8dff', '#6a5cff'],
- ['#34d3b3', '#12a99d'],
- ['#ffbd59', '#f58b2d'],
- ['#8c7cff', '#6854ee']
- ]
- const categories = computed(() => props.data.map((item) => item.category || item.name || '未分类'))
- const values = computed(() => props.data.map((item) => Number(item.value ?? 0)))
- const escapeHtml = (value: string) =>
- value.replace(/[&<>"']/g, (char) => {
- const entities: Record<string, string> = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- }
- return entities[char]
- })
- const formatTooltip = (params: {
- color?: string
- marker?: string
- name?: string
- value?: number | string
- }) => {
- const marker =
- params.marker ||
- '<span style="display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:50%;background:#1688f5;"></span>'
- const name = escapeHtml(params.name || '未分类')
- const value = params.value ?? 0
- return `<div style="line-height:1.8;font-size:14px;">${marker}<span>${name}</span><br/><span style="padding-left:15px;">设备数量 <strong>${value}</strong></span></div>`
- }
- const seriesData = computed(() =>
- values.value.map((value, index) => {
- const colors = barPalette[index % barPalette.length]
- return {
- value,
- itemStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: colors[0] },
- { offset: 1, color: colors[1] }
- ])
- }
- }
- })
- )
- const chartOption = computed(() => ({
- grid: {
- top: 16,
- right: 18,
- bottom: 40,
- left: 38
- },
- tooltip: {
- trigger: 'item',
- backgroundColor: '#fff',
- borderColor: '#e8edf5',
- borderWidth: 1,
- padding: [10, 13],
- textStyle: {
- color: '#172033',
- fontSize: 14
- },
- extraCssText: 'box-shadow: 0 12px 28px rgba(15, 23, 42, 0.14); border-radius: 12px;',
- formatter: formatTooltip
- },
- xAxis: {
- type: 'category',
- data: categories.value,
- axisTick: { show: false },
- axisLine: { lineStyle: { color: '#e8edf5' } },
- axisLabel: {
- color: '#738199',
- rotate: 30,
- interval: 0,
- fontSize: 11,
- margin: 10
- }
- },
- yAxis: {
- type: 'value',
- splitLine: {
- lineStyle: {
- color: '#edf1f6',
- type: 'dashed'
- }
- },
- axisLabel: {
- color: '#8b98ac',
- fontSize: 11
- }
- },
- series: [
- {
- name: '设备数量',
- type: 'bar',
- data: seriesData.value,
- barWidth: 16,
- barMinHeight: 6,
- showBackground: true,
- backgroundStyle: {
- color: 'rgba(22, 136, 245, 0.055)'
- },
- itemStyle: {},
- emphasis: {
- scale: true,
- itemStyle: {
- shadowBlur: 12,
- shadowColor: 'rgba(22, 136, 245, 0.22)'
- }
- }
- }
- ]
- }))
- const renderChart = () => {
- if (!chartRef.value || props.loading || props.data.length === 0) return
- if (!chart) {
- chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
- }
- chart.setOption(chartOption.value, true)
- }
- watch(
- () => [props.loading, props.data],
- () => nextTick(renderChart),
- { deep: true, immediate: true }
- )
- onMounted(() => {
- nextTick(renderChart)
- if (chartRef.value) {
- resizeObserver = new ResizeObserver(() => chart?.resize())
- resizeObserver.observe(chartRef.value)
- }
- })
- onBeforeUnmount(() => {
- resizeObserver?.disconnect()
- chart?.dispose()
- chart = null
- })
- </script>
- <template>
- <section class="device-classify-card">
- <div class="card-header">
- <div>
- <h3>分类 Top</h3>
- </div>
- <span class="soft-badge">Top {{ data.length }}</span>
- </div>
- <div v-show="data.length > 0 && !loading" ref="chartRef" class="chart-el"></div>
- <div v-if="data.length === 0 || loading" class="empty-state">
- {{ loading ? '加载中...' : '暂无分类数据' }}
- </div>
- </section>
- </template>
- <style scoped lang="scss">
- .device-classify-card {
- position: relative;
- min-width: 0;
- min-height: 164px;
- padding: 12px 14px 6px;
- overflow: hidden;
- background: radial-gradient(circle at 92% 18%, rgb(22 136 245 / 9%) 0, transparent 28%),
- linear-gradient(180deg, #fff 0%, #fbfdff 100%);
- border: 1px solid #e8edf5;
- border-radius: 16px;
- box-shadow: 0 10px 26px rgb(15 23 42 / 8%);
- }
- .device-classify-card::before,
- .device-classify-card::after {
- position: absolute;
- pointer-events: none;
- border-radius: 999px;
- content: '';
- }
- .device-classify-card::before {
- top: -52px;
- right: 120px;
- width: 112px;
- height: 112px;
- background: rgb(53 205 178 / 8%);
- }
- .device-classify-card::after {
- right: -42px;
- bottom: -48px;
- width: 132px;
- height: 132px;
- background: rgb(140 124 255 / 7%);
- border: 1px solid rgb(140 124 255 / 10%);
- }
- .card-header {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 700;
- color: #25324a;
- }
- .soft-badge {
- padding: 5px 11px;
- font-size: 12px;
- color: #1688f5;
- background: #edf7ff;
- border: 1px solid #d8ecff;
- border-radius: 999px;
- }
- .chart-el,
- .empty-state {
- position: relative;
- z-index: 1;
- height: 168px;
- }
- .empty-state {
- display: flex;
- font-size: 13px;
- color: #9aa6b8;
- align-items: center;
- justify-content: center;
- }
- </style>
|