mat/frontend/src/views/Dashboard.vue

655 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>