| | |
| | | <div> |
| | | <PanelHeader title="产品大类" /> |
| | | <div class="panel-item-customers"> |
| | | <div class="pie-chart-wrapper" ref="pieWrapperRef"> |
| | | <div class="pie-background" ref="pieBackgroundRef"></div> |
| | | <div class="pie-chart-wrapper"> |
| | | <div class="pie-center"> |
| | | <span class="pie-center-value">{{ totalCategories }}</span> |
| | | <span class="pie-center-label">合计(类)</span> |
| | | </div> |
| | | <Echarts |
| | | ref="chart" |
| | | :chartStyle="chartStyle" |
| | |
| | | :options="pieOptions" |
| | | style="height: 100%" |
| | | class="land-chart" |
| | | /> |
| | | > |
| | | <div v-if="loaded && !dataList.length" class="chart-empty">暂无数据</div> |
| | | </Echarts> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount } from 'vue' |
| | | import { ref, onMounted, computed } from 'vue' |
| | | import Echarts from '@/components/Echarts/echarts.vue' |
| | | import PanelHeader from '../PanelHeader.vue' |
| | | import { productCategoryDistribution } from '@/api/viewIndex.js' |
| | | import { useChartBackground } from '@/hooks/useChartBackground.js' |
| | | import { BI, baseChartOptions, withTooltip } from '../../theme.js' |
| | | |
| | | const pieWrapperRef = ref(null) |
| | | const pieBackgroundRef = ref(null) |
| | | const chart = ref(null) |
| | | |
| | | // 数据列表(来自接口) |
| | | const dataList = ref([]) |
| | | const loaded = ref(false) |
| | | |
| | | // 颜色列表 |
| | | const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF'] |
| | | const landColors = BI.series |
| | | |
| | | // label 富文本:为每个颜色生成一个小圆点样式(确保在 label 中可见) |
| | | const dotRich = landColors.reduce((acc, color, idx) => { |
| | | acc[`dot${idx}`] = { |
| | | width: 8, |
| | | height: 8, |
| | | borderRadius: 8, |
| | | backgroundColor: color, |
| | | align: 'center', |
| | | } |
| | | return acc |
| | | }, {}) |
| | | // 大类总数(各分类下的类目数之和) |
| | | const totalCategories = computed(() => |
| | | dataList.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0) |
| | | ) |
| | | |
| | | const landObjData = computed(() => { |
| | | const obj = {} |
| | | dataList.value.forEach((d) => { |
| | | obj[d.name] = d |
| | | }) |
| | | return obj |
| | | }) |
| | | |
| | | // 图例配置(右侧竖排) |
| | | const landLegend = { |
| | | show: false, |
| | | icon: 'circle', |
| | | data: [], |
| | | right: '8%', |
| | | top: '40%', |
| | | orient: 'vertical', |
| | | itemGap: 14, |
| | | itemWidth: 6, |
| | | itemHeight: 6, |
| | | textStyle: { |
| | | fontSize: 12, |
| | | rich: { |
| | | unit: { |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | padding: [0, 10, 0, 0], |
| | | }, |
| | | text: { |
| | | width: 60, |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | const landLegend = computed(() => { |
| | | const data = dataList.value.map((d, idx) => ({ |
| | | name: d.name, |
| | | icon: 'circle', |
| | | textStyle: { |
| | | fontSize: 14, |
| | | color: landColors[idx % landColors.length], |
| | | }, |
| | | })) |
| | | |
| | | return { |
| | | show: data.length > 0, |
| | | orient: 'vertical', |
| | | top: 'center', |
| | | left: '50%', |
| | | itemGap: 20, |
| | | itemWidth: 8, |
| | | itemHeight: 8, |
| | | data, |
| | | formatter(name) { |
| | | const item = landObjData.value[name] |
| | | if (!item) return name |
| | | const short = name.length > 8 ? `${name.slice(0, 8)}…` : name |
| | | return `{title|${short}}{value|${item.value}}{unit|类}` |
| | | }, |
| | | textStyle: { |
| | | rich: { |
| | | title: { |
| | | fontSize: 13, |
| | | color: BI.text, |
| | | width: 108, |
| | | overflow: 'truncate', |
| | | }, |
| | | value: { |
| | | color: BI.textStrong, |
| | | fontSize: 14, |
| | | fontWeight: 600, |
| | | padding: [0, 0, 0, 10], |
| | | }, |
| | | unit: { |
| | | color: BI.text, |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | }, |
| | | }, |
| | | formatter: function (name) { |
| | | const list = dataList.value || [] |
| | | const item = list.find((d) => d.name === name) |
| | | if (!item) return name |
| | | const val = Number(item.value || 0) |
| | | const totalValue = list.reduce((sum, it) => sum + Number(it.value || 0), 0) |
| | | const percent = totalValue ? ((val / totalValue) * 100).toFixed(2) : '0.00' |
| | | return `{text|${name}}${val}{unit| 公顷}${percent}{unit|%}` |
| | | }, |
| | | } |
| | | } |
| | | }) |
| | | |
| | | // 提示框 |
| | | const landTooltip = { |
| | | // triggerOn: 'hover', |
| | | alwaysShowContent: true, |
| | | position: function (pt) { |
| | | return [pt[0], 130] |
| | | // 提示框:占比 + 子分类明细 |
| | | const landTooltip = withTooltip({ |
| | | trigger: 'item', |
| | | confine: true, |
| | | formatter(params) { |
| | | const d = params.data || {} |
| | | const val = Number(d.value || 0) |
| | | let html = `${params.marker}<b>${d.name || ''}</b> ${val} 类 · ${params.percent}%` |
| | | const children = Array.isArray(d.children) ? d.children : [] |
| | | if (children.length) { |
| | | const shown = children.slice(0, 8).map((c) => c.name).join('、') |
| | | html += `<br/><span style="color:${BI.text}">子类:</span>${shown}${children.length > 8 ? '…' : ''}` |
| | | } |
| | | return html |
| | | }, |
| | | formatter: function (params) { |
| | | return `${params.name} (${params.value}类)` |
| | | }, |
| | | } |
| | | }) |
| | | |
| | | // 双层环形饼图 |
| | | const landSeries = ref([ |
| | | const landSeries = computed(() => [ |
| | | { |
| | | name: '产品大类', |
| | | type: 'pie', |
| | | radius: ['35%', '55%'], |
| | | center: ['50%', '50%'], |
| | | label: { |
| | | show: true, |
| | | position: 'outside', |
| | | color: '#fff', |
| | | fontSize: 12, |
| | | lineHeight: 18, |
| | | rich: { |
| | | ...dotRich, |
| | | parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20, overflow: 'break' }, |
| | | child: { fontSize: 12, color: '#fff', lineHeight: 18 }, |
| | | }, |
| | | formatter: function (params) { |
| | | const children = params?.data?.children || [] |
| | | const parentName = params?.data?.name || '' |
| | | const rawVal = params?.data?.value |
| | | const parentValue = typeof rawVal === 'number' && !Number.isNaN(rawVal) ? rawVal : (Number(rawVal) || 0) |
| | | const dotKey = `dot${(params?.dataIndex || 0) % landColors.length}` |
| | | const dot = `{${dotKey}|} ` |
| | | const parentLine = `${dot}{parent|${parentName} (${parentValue}类)}` |
| | | if (!children.length) return parentLine |
| | | // 父级全部显示;子级最多 5 个,超出显示省略号 |
| | | const displayed = children.slice(0, 5).map((c) => `{child|${c.name}}`) |
| | | if (children.length > 5) displayed.push('{child|…}') |
| | | return [parentLine, ...displayed].join('\n') |
| | | }, |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | length: 20, |
| | | length2: 20, |
| | | lineStyle: { |
| | | color: '#B8C8E0', |
| | | }, |
| | | }, |
| | | radius: ['42%', '58%'], |
| | | center: ['25%', '50%'], |
| | | itemStyle: { |
| | | color: function (params) { |
| | | return landColors[params.dataIndex % landColors.length] |
| | | }, |
| | | borderColor: BI.sliceBorder, |
| | | borderWidth: 2, |
| | | borderRadius: 4, |
| | | }, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | minAngle: 8, |
| | | data: dataList.value, |
| | | animationType: 'scale', |
| | | animationEasing: 'elasticOut', |
| | | }, |
| | | { |
| | | // 内圈 |
| | | // 内圈轨道环 |
| | | type: 'pie', |
| | | radius: ['35%', '40%'], |
| | | center: ['50%', '50%'], |
| | | radius: ['33%', '36%'], |
| | | center: ['25%', '50%'], |
| | | silent: true, |
| | | label: { |
| | | show: false, |
| | |
| | | show: false, |
| | | }, |
| | | itemStyle: { |
| | | color: 'rgba(0, 127, 255, 0.25)', |
| | | color: 'rgba(74, 108, 240, 0.22)', |
| | | }, |
| | | data: [1], |
| | | }, |
| | |
| | | |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '126%', |
| | | height: '100%', |
| | | } |
| | | |
| | | const pieOptions = { |
| | | backgroundColor: 'transparent', |
| | | textStyle: { color: '#B8C8E0' }, |
| | | } |
| | | |
| | | // 使用封装的背景位置调整方法,可自定义偏移值 |
| | | const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({ |
| | | wrapperRef: pieWrapperRef, |
| | | backgroundRef: pieBackgroundRef, |
| | | offsetX: '-51.5%', // X 轴偏移,可动态调整 |
| | | offsetY: '-39%', // Y 轴偏移,可动态调整 |
| | | watchData: dataList // 监听数据变化,自动调整位置 |
| | | }) |
| | | const pieOptions = baseChartOptions |
| | | |
| | | const loadData = async () => { |
| | | try { |
| | |
| | | rate: it.rate, |
| | | children: Array.isArray(it.children) ? it.children : [], |
| | | })) |
| | | landLegend.data = dataList.value.map((d) => d.name) |
| | | landSeries.value[0].data = dataList.value |
| | | // 数据加载完成后调整背景位置 |
| | | adjustBackgroundPosition() |
| | | } catch (e) { |
| | | console.error('获取产品大类分布失败:', e) |
| | | dataList.value = [] |
| | | landLegend.data = [] |
| | | landSeries.value[0].data = [] |
| | | } finally { |
| | | loaded.value = true |
| | | } |
| | | } |
| | | |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | initBackground() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | cleanupBackground() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .panel-item-customers { |
| | | border: 1px solid #1a58b0; |
| | | border: 1px solid var(--bi-panel-border); |
| | | background: var(--bi-panel-bg); |
| | | border-radius: 8px; |
| | | padding: 18px; |
| | | width: 100%; |
| | | height: 420px; |
| | |
| | | .pie-chart-wrapper { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 320px; |
| | | height: 384px; |
| | | background: transparent; |
| | | } |
| | | |
| | | .pie-background { |
| | | .pie-center { |
| | | position: absolute; |
| | | width: 360px; |
| | | height: 360px; |
| | | background-image: url('@/assets/BI/玫瑰图边框.png'); |
| | | background-size: contain; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | z-index: 1; |
| | | pointer-events: none; |
| | | /* 默认居中,会在 JS 中动态调整 */ |
| | | left: 50%; |
| | | left: 25%; |
| | | top: 50%; |
| | | transform: translate(-51.5%, -39%); |
| | | transform: translate(-50%, -50%); |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 4px; |
| | | pointer-events: none; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .pie-center-value { |
| | | font-weight: 600; |
| | | font-size: 22px; |
| | | color: var(--bi-text-strong); |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .pie-center-label { |
| | | font-size: 12px; |
| | | color: var(--bi-text); |
| | | } |
| | | |
| | | .chart-empty { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | height: 384px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: var(--bi-text); |
| | | font-size: 14px; |
| | | letter-spacing: 1px; |
| | | pointer-events: none; |
| | | } |
| | | </style> |