<template>
|
<div>
|
<PanelHeader title="产品大类" />
|
<div class="panel-item-customers">
|
<div class="pie-chart-wrapper" ref="pieWrapperRef">
|
<div class="pie-background" ref="pieBackgroundRef"></div>
|
<Echarts
|
ref="chart"
|
:chartStyle="chartStyle"
|
:legend="landLegend"
|
:series="landSeries"
|
:tooltip="landTooltip"
|
:color="landColors"
|
:options="pieOptions"
|
style="height: 100%"
|
class="land-chart"
|
/>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, onMounted, onBeforeUnmount, inject, watch } 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'
|
|
const pieWrapperRef = ref(null)
|
const pieBackgroundRef = ref(null)
|
const chart = ref(null)
|
|
// 数据列表(来自接口)
|
const dataList = ref([])
|
|
// 颜色列表
|
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
|
|
// 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 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,
|
},
|
},
|
},
|
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]
|
},
|
formatter: function (params) {
|
return `${params.name} (${params.value}类)`
|
},
|
}
|
|
// 双层环形饼图
|
const landSeries = ref([
|
{
|
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',
|
},
|
},
|
itemStyle: {
|
color: function (params) {
|
return landColors[params.dataIndex % landColors.length]
|
},
|
},
|
data: dataList.value,
|
},
|
{
|
// 内圈
|
type: 'pie',
|
radius: ['35%', '40%'],
|
center: ['50%', '50%'],
|
silent: true,
|
label: {
|
show: false,
|
},
|
labelLine: {
|
show: false,
|
},
|
itemStyle: {
|
color: 'rgba(0, 127, 255, 0.25)',
|
},
|
data: [1],
|
},
|
])
|
|
const chartStyle = {
|
width: '100%',
|
height: '126%',
|
}
|
|
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 loadData = async () => {
|
try {
|
const res = await productCategoryDistribution()
|
const items = res?.data?.items || []
|
dataList.value = items.map((it) => ({
|
name: it.name,
|
value: Number(it.value || 0),
|
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 = []
|
}
|
}
|
|
|
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
|
if (dataDashboardRefreshTick) {
|
watch(dataDashboardRefreshTick, () => {
|
loadData()
|
})
|
}
|
|
onMounted(() => {
|
loadData()
|
initBackground()
|
})
|
|
onBeforeUnmount(() => {
|
cleanupBackground()
|
})
|
</script>
|
|
<style scoped>
|
.panel-item-customers {
|
border: 1px solid #1a58b0;
|
padding: 18px;
|
width: 100%;
|
height: 420px;
|
position: relative;
|
overflow: hidden;
|
}
|
|
/* 面板角落装饰 */
|
.panel-item-customers::before,
|
.panel-item-customers::after {
|
content: '';
|
position: absolute;
|
width: 15px;
|
height: 15px;
|
border-color: rgba(0, 212, 255, 0.5);
|
border-style: solid;
|
pointer-events: none;
|
}
|
|
.panel-item-customers::before {
|
top: -1px;
|
left: -1px;
|
border-width: 2px 0 0 2px;
|
}
|
|
.panel-item-customers::after {
|
bottom: -1px;
|
right: -1px;
|
border-width: 0 2px 2px 0;
|
}
|
|
.pie-chart-wrapper {
|
position: relative;
|
width: 100%;
|
height: 320px;
|
background: transparent;
|
animation: pieFloat 4s ease-in-out infinite;
|
}
|
|
@keyframes pieFloat {
|
0%, 100% { transform: translateY(0); }
|
50% { transform: translateY(-3px); }
|
}
|
|
.pie-background {
|
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%;
|
top: 50%;
|
transform: translate(-51.5%, -39%);
|
animation: pieBgRotate 30s linear infinite;
|
}
|
|
@keyframes pieBgRotate {
|
from { transform: translate(-51.5%, -39%) rotate(0deg); }
|
to { transform: translate(-51.5%, -39%) rotate(360deg); }
|
}
|
</style>
|