655 lines
16 KiB
Vue
655 lines
16 KiB
Vue
<template>
|
||
<div class="dashboard">
|
||
<el-row :gutter="20">
|
||
<!-- 统计卡片 -->
|
||
<el-col :span="6">
|
||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon stat-icon-pulse" style="background-color: #409EFF;">
|
||
<el-icon :size="30"><Box /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value stat-value-animate">{{ overviewData.total_materials || 0 }}</div>
|
||
<div class="stat-label">材料总数</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<el-col :span="6">
|
||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon stat-icon-pulse" style="background-color: #67C23A;">
|
||
<el-icon :size="30"><Grid /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value stat-value-animate">{{ overviewData.total_material_categories || 0 }}</div>
|
||
<div class="stat-label">材料种类</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<el-col :span="6">
|
||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon stat-icon-pulse" style="background-color: #E6A23C;">
|
||
<el-icon :size="30"><OfficeBuilding /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value stat-value-animate">{{ overviewData.total_brands || 0 }}</div>
|
||
<div class="stat-label">品牌数</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<el-col :span="6">
|
||
<el-card class="stat-card stat-card-hover" shadow="hover">
|
||
<div class="stat-content">
|
||
<div class="stat-icon stat-icon-pulse" style="background-color: #F56C6C;">
|
||
<el-icon :size="30"><Document /></el-icon>
|
||
</div>
|
||
<div class="stat-info">
|
||
<div class="stat-value stat-value-animate">{{ overviewData.cases_list?.length || 0 }}</div>
|
||
<div class="stat-label">应用案例</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" class="charts-row">
|
||
<!-- 按专业类别的材料数量分布 -->
|
||
<el-col :span="12">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>按专业类别的材料数量分布</span>
|
||
</div>
|
||
</template>
|
||
<div ref="majorCategoryChart" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 按材料子类的材料数量分布 -->
|
||
<el-col :span="12">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>按材料子类的材料数量分布(TOP10)</span>
|
||
</div>
|
||
</template>
|
||
<div ref="materialSubcategoryChart" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" class="charts-row">
|
||
<!-- 按所属品牌的材料数量分布 -->
|
||
<el-col :span="12">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>按所属品牌的材料数量分布</span>
|
||
</div>
|
||
</template>
|
||
<div ref="brandChart" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 按地区的工厂数量分布 -->
|
||
<el-col :span="12">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>按地区的工厂数量分布</span>
|
||
</div>
|
||
</template>
|
||
<div ref="regionChart" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" class="charts-row">
|
||
<!-- 应用案例列表 -->
|
||
<el-col :span="24">
|
||
<el-card class="case-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>应用案例列表</span>
|
||
<el-tag type="info" size="small">共 {{ overviewData.cases_list?.length || 0 }} 条</el-tag>
|
||
</div>
|
||
</template>
|
||
<el-table
|
||
:data="overviewData.cases_list || []"
|
||
stripe
|
||
class="case-table"
|
||
:header-cell-style="{background:'#f5f7fa', color:'#606266'}"
|
||
:row-style="{height: '60px'}"
|
||
:cell-style="{padding: '12px 0'}"
|
||
>
|
||
<el-table-column prop="name" label="材料名称" width="200">
|
||
<template #default="scope">
|
||
<div class="material-name">{{ scope.row.name }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="factory__factory_name" label="所属品牌" width="200">
|
||
<template #default="scope">
|
||
<el-tag size="small" type="primary">{{ scope.row.factory__factory_name }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="cases" label="案例说明" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<div class="case-description">{{ scope.row.cases }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||
import * as echarts from 'echarts'
|
||
import { getOverviewStatistics } from '@/api/statistics'
|
||
|
||
// 图表实例
|
||
let majorCategoryChartInstance = null
|
||
let materialSubcategoryChartInstance = null
|
||
let brandChartInstance = null
|
||
let regionChartInstance = null
|
||
|
||
// 图表容器引用
|
||
const majorCategoryChart = ref(null)
|
||
const materialSubcategoryChart = ref(null)
|
||
const brandChart = ref(null)
|
||
const regionChart = ref(null)
|
||
|
||
// 数据总览数据
|
||
const overviewData = ref({
|
||
total_materials: 0,
|
||
total_material_categories: 0,
|
||
total_brands: 0,
|
||
major_category_stats: [],
|
||
material_subcategory_stats: [],
|
||
brand_stats: [],
|
||
region_stats: [],
|
||
cases_list: []
|
||
})
|
||
|
||
// 自动刷新定时器
|
||
let refreshTimer = null
|
||
|
||
// 加载数据
|
||
const loadData = async () => {
|
||
try {
|
||
const data = await getOverviewStatistics()
|
||
overviewData.value = data
|
||
|
||
// 更新图表
|
||
updateCharts(data)
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 初始化图表
|
||
const initCharts = () => {
|
||
majorCategoryChartInstance = echarts.init(majorCategoryChart.value)
|
||
materialSubcategoryChartInstance = echarts.init(materialSubcategoryChart.value)
|
||
brandChartInstance = echarts.init(brandChart.value)
|
||
regionChartInstance = echarts.init(regionChart.value)
|
||
}
|
||
|
||
// 更新图表
|
||
const updateCharts = (data) => {
|
||
// 按专业类别的材料数量分布 - 饼图
|
||
majorCategoryChartInstance.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor: '#eee',
|
||
borderWidth: 1,
|
||
textStyle: {
|
||
color: '#333'
|
||
}
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
left: 'left',
|
||
textStyle: {
|
||
color: '#666'
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '专业类别',
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
avoidLabelOverlap: false,
|
||
itemStyle: {
|
||
borderRadius: 10,
|
||
borderColor: '#fff',
|
||
borderWidth: 2
|
||
},
|
||
label: {
|
||
show: false,
|
||
position: 'center'
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: 16,
|
||
fontWeight: 'bold'
|
||
},
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowOffsetX: 0,
|
||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
data: data.major_category_stats.map(item => ({
|
||
value: item.count,
|
||
name: item.major_category
|
||
}))
|
||
}
|
||
]
|
||
})
|
||
|
||
// 按材料子类的材料数量分布 - 柱状图
|
||
materialSubcategoryChartInstance.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor: '#eee',
|
||
borderWidth: 1,
|
||
textStyle: {
|
||
color: '#333'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'value',
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#ddd'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f0f0f0'
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
data: data.material_subcategory_stats.map(item => item.material_subcategory),
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
},
|
||
axisLabel: {
|
||
color: '#666'
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '材料数量',
|
||
type: 'bar',
|
||
data: data.material_subcategory_stats.map(item => item.count),
|
||
itemStyle: {
|
||
borderRadius: [0, 4, 4, 0],
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: '#83bff6' },
|
||
{ offset: 0.5, color: '#188df0' },
|
||
{ offset: 1, color: '#188df0' }
|
||
])
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: '#2378f7' },
|
||
{ offset: 0.7, color: '#2378f7' },
|
||
{ offset: 1, color: '#83bff6' }
|
||
])
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
// 按所属品牌的材料数量分布 - 柱状图
|
||
brandChartInstance.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor: '#eee',
|
||
borderWidth: 1,
|
||
textStyle: {
|
||
color: '#333'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: data.brand_stats.map(item => item.factory__factory_name),
|
||
axisLabel: {
|
||
interval: 0,
|
||
rotate: 30,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#ddd'
|
||
}
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f0f0f0'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '材料数量',
|
||
type: 'bar',
|
||
data: data.brand_stats.map(item => item.count),
|
||
itemStyle: {
|
||
borderRadius: [4, 4, 0, 0],
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: '#67C23A' },
|
||
{ offset: 1, color: '#85CE61' }
|
||
])
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: '#529B2E' },
|
||
{ offset: 1, color: '#67C23A' }
|
||
])
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
|
||
// 按地区的工厂数量分布 - 柱状图
|
||
regionChartInstance.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor: '#eee',
|
||
borderWidth: 1,
|
||
textStyle: {
|
||
color: '#333'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: data.region_stats.map(item => `${item.province} ${item.city}`),
|
||
axisLabel: {
|
||
interval: 0,
|
||
rotate: 30,
|
||
color: '#666'
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#ddd'
|
||
}
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#f0f0f0'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '工厂数量',
|
||
type: 'bar',
|
||
data: data.region_stats.map(item => item.count),
|
||
itemStyle: {
|
||
borderRadius: [4, 4, 0, 0],
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: '#E6A23C' },
|
||
{ offset: 1, color: '#F3D19E' }
|
||
])
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: '#CF8E2F' },
|
||
{ offset: 1, color: '#E6A23C' }
|
||
])
|
||
}
|
||
}
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
// 窗口大小变化时重新调整图表大小
|
||
const handleResize = () => {
|
||
majorCategoryChartInstance && majorCategoryChartInstance.resize()
|
||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.resize()
|
||
brandChartInstance && brandChartInstance.resize()
|
||
regionChartInstance && regionChartInstance.resize()
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 初始化图表
|
||
initCharts()
|
||
|
||
// 加载数据
|
||
loadData()
|
||
|
||
// 设置自动刷新,每10秒刷新一次
|
||
refreshTimer = setInterval(loadData, 10000)
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
// 清除定时器
|
||
if (refreshTimer) {
|
||
clearInterval(refreshTimer)
|
||
}
|
||
|
||
// 销毁图表实例
|
||
majorCategoryChartInstance && majorCategoryChartInstance.dispose()
|
||
materialSubcategoryChartInstance && materialSubcategoryChartInstance.dispose()
|
||
brandChartInstance && brandChartInstance.dispose()
|
||
regionChartInstance && regionChartInstance.dispose()
|
||
|
||
// 移除窗口大小变化监听
|
||
window.removeEventListener('resize', handleResize)
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.dashboard {
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.stat-card {
|
||
margin-bottom: 20px;
|
||
transition: all 0.3s ease;
|
||
border: 1px solid #EBEEF5;
|
||
|
||
&.stat-card-hover:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.stat-content {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.stat-icon {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
color: #fff;
|
||
margin-right: 20px;
|
||
transition: all 0.3s ease;
|
||
|
||
&.stat-icon-pulse:hover {
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
}
|
||
|
||
.stat-info {
|
||
flex: 1;
|
||
|
||
.stat-value {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #303133;
|
||
margin-bottom: 5px;
|
||
transition: all 0.3s ease;
|
||
|
||
&.stat-value-animate {
|
||
animation: fadeInUp 0.5s ease;
|
||
}
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.charts-row {
|
||
margin-top: 20px;
|
||
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 400px;
|
||
}
|
||
|
||
.case-card {
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.case-table {
|
||
.material-name {
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.case-description {
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.el-table__row) {
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
background-color: #f5f7fa;
|
||
transform: translateX(5px);
|
||
}
|
||
}
|
||
|
||
:deep(.el-table__body tr:hover > td) {
|
||
background-color: #f5f7fa !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
|
||
}
|
||
70% {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
</style>
|