|
|
@@ -1,10 +1,39 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onUnmounted, nextTick, watch, reactive } from 'vue';
|
|
|
import { Search, Upload, Check, FolderAdd, Document } from '@element-plus/icons-vue';
|
|
|
-import * as echarts from 'echarts';
|
|
|
-import type { EChartsOption } from 'echarts';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
import request from '@/utils/request';
|
|
|
+import { getOverview } from '@/api/home/dashboard';
|
|
|
+import type { OverviewVO } from '@/api/home/dashboard/types';
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import type { EChartsOption } from 'echarts';
|
|
|
+
|
|
|
+// 总览数据
|
|
|
+const overviewData = ref<OverviewVO>({
|
|
|
+ total: 0,
|
|
|
+ submitted: 0,
|
|
|
+ toSubmit: 0,
|
|
|
+ lateToSubmit: 0,
|
|
|
+ lateSubmitted: 0
|
|
|
+});
|
|
|
+
|
|
|
+// 获取总览数据
|
|
|
+const getOverviewData = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getOverview();
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ overviewData.value = res.data;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取总览数据失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 计算占比
|
|
|
+const calculatePercentage = (value: number, total: number): string => {
|
|
|
+ if (total === 0) return '0.0%';
|
|
|
+ return ((value / total) * 100).toFixed(1) + '%';
|
|
|
+};
|
|
|
|
|
|
// 待办统计
|
|
|
const todoCount = ref({
|
|
|
@@ -126,7 +155,6 @@ const getProgressStatus = (percentage: number) => {
|
|
|
// 查看项目详情
|
|
|
const viewProjectDetail = (row: any) => {
|
|
|
// TODO: 实现跳转到项目详情页
|
|
|
- console.log('View project detail:', row);
|
|
|
// 这里可以添加路由跳转逻辑,例如:
|
|
|
// router.push({
|
|
|
// path: '/project/detail',
|
|
|
@@ -134,16 +162,337 @@ const viewProjectDetail = (row: any) => {
|
|
|
// });
|
|
|
};
|
|
|
|
|
|
+// Echarts 实例
|
|
|
+const pieChartRef = ref<HTMLElement>();
|
|
|
+const barChartRef = ref<HTMLElement>();
|
|
|
+const lineChartRef = ref<HTMLElement>();
|
|
|
+let pieChart: echarts.ECharts | null = null;
|
|
|
+let barChart: echarts.ECharts | null = null;
|
|
|
+let lineChart: echarts.ECharts | null = null;
|
|
|
+
|
|
|
+// 初始化饼图 - 项目状态分布
|
|
|
+const initPieChart = () => {
|
|
|
+ if (!pieChartRef.value) return;
|
|
|
+
|
|
|
+ pieChart = echarts.init(pieChartRef.value);
|
|
|
+
|
|
|
+ const option: EChartsOption = {
|
|
|
+ title: {
|
|
|
+ text: '项目状态分布',
|
|
|
+ left: 'center',
|
|
|
+ top: 10,
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 600
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ formatter: '{a} <br/>{b}: {c} ({d}%)'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ orient: 'vertical',
|
|
|
+ left: 'left',
|
|
|
+ top: 'middle'
|
|
|
+ },
|
|
|
+ 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: 20,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ { value: overviewData.value.submitted, name: '已递交', itemStyle: { color: '#67c23a' } },
|
|
|
+ { value: overviewData.value.toSubmit, name: '待递交', itemStyle: { color: '#409eff' } },
|
|
|
+ { value: overviewData.value.lateToSubmit, name: '逾期未递交', itemStyle: { color: '#f56c6c' } },
|
|
|
+ { value: overviewData.value.lateSubmitted, name: '逾期已提交', itemStyle: { color: '#e6a23c' } }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ pieChart.setOption(option);
|
|
|
+};
|
|
|
+
|
|
|
+// 初始化柱状图 - 待办任务统计
|
|
|
+const initBarChart = () => {
|
|
|
+ if (!barChartRef.value) return;
|
|
|
+
|
|
|
+ barChart = echarts.init(barChartRef.value);
|
|
|
+
|
|
|
+ const option: EChartsOption = {
|
|
|
+ title: {
|
|
|
+ text: '待办任务统计',
|
|
|
+ left: 'center',
|
|
|
+ top: 10,
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 600
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ top: '60px',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: ['待递交', '待审核', '待归档', '待质控'],
|
|
|
+ axisLabel: {
|
|
|
+ interval: 0,
|
|
|
+ rotate: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '数量'
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '待办数量',
|
|
|
+ type: 'bar',
|
|
|
+ barWidth: '50%',
|
|
|
+ data: [
|
|
|
+ { value: todoCount.value.toSubmit, itemStyle: { color: '#409eff' } },
|
|
|
+ { value: todoCount.value.toAudit, itemStyle: { color: '#e6a23c' } },
|
|
|
+ { value: todoCount.value.toFiling, itemStyle: { color: '#67c23a' } },
|
|
|
+ { value: todoCount.value.toQc, itemStyle: { color: '#909399' } }
|
|
|
+ ],
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'top'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ barChart.setOption(option);
|
|
|
+};
|
|
|
+
|
|
|
+// 初始化雷达图 - 项目完成度分析
|
|
|
+const initLineChart = () => {
|
|
|
+ if (!lineChartRef.value) return;
|
|
|
+
|
|
|
+ lineChart = echarts.init(lineChartRef.value);
|
|
|
+
|
|
|
+ // 计算各项指标的完成度(基于现有数据)
|
|
|
+ const total = overviewData.value.total || 1; // 避免除以0
|
|
|
+ const submittedRate = ((overviewData.value.submitted / total) * 100).toFixed(1);
|
|
|
+ const toSubmitRate = ((overviewData.value.toSubmit / total) * 100).toFixed(1);
|
|
|
+ const onTimeRate = (((overviewData.value.submitted - overviewData.value.lateSubmitted) / total) * 100).toFixed(1);
|
|
|
+
|
|
|
+ // 计算待办完成度(假设总待办为所有待办之和)
|
|
|
+ const totalTodo = todoCount.value.toSubmit + todoCount.value.toAudit + todoCount.value.toFiling + todoCount.value.toQc || 1;
|
|
|
+ const todoProgress = ((todoCount.value.toSubmit / totalTodo) * 100).toFixed(1);
|
|
|
+
|
|
|
+ const option: EChartsOption = {
|
|
|
+ title: {
|
|
|
+ text: '项目完成度分析',
|
|
|
+ left: 'center',
|
|
|
+ top: 10,
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 16,
|
|
|
+ fontWeight: 600
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis'
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['当前指标'],
|
|
|
+ top: 40
|
|
|
+ },
|
|
|
+ radar: {
|
|
|
+ indicator: [
|
|
|
+ { name: '递交完成率', max: 100 },
|
|
|
+ { name: '按时完成率', max: 100 },
|
|
|
+ { name: '待办处理率', max: 100 },
|
|
|
+ { name: '项目进度', max: 100 },
|
|
|
+ { name: '质量达标率', max: 100 }
|
|
|
+ ],
|
|
|
+ center: ['50%', '60%'],
|
|
|
+ radius: '60%',
|
|
|
+ splitNumber: 4,
|
|
|
+ axisName: {
|
|
|
+ color: '#606266',
|
|
|
+ fontSize: 12
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#dcdfe6'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ splitArea: {
|
|
|
+ areaStyle: {
|
|
|
+ color: ['rgba(64, 158, 255, 0.05)', 'rgba(64, 158, 255, 0.1)']
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '当前指标',
|
|
|
+ type: 'radar',
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ value: [
|
|
|
+ parseFloat(submittedRate),
|
|
|
+ parseFloat(onTimeRate),
|
|
|
+ 100 - parseFloat(todoProgress),
|
|
|
+ parseFloat(submittedRate),
|
|
|
+ parseFloat(onTimeRate)
|
|
|
+ ],
|
|
|
+ name: '当前指标',
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.RadialGradient(0.5, 0.5, 1, [
|
|
|
+ { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
|
|
+ { offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ color: '#409eff'
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ color: '#409eff',
|
|
|
+ width: 2
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ lineChart.setOption(option);
|
|
|
+};
|
|
|
+
|
|
|
+// 更新图表数据
|
|
|
+const updateCharts = () => {
|
|
|
+ if (pieChart) {
|
|
|
+ pieChart.setOption({
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ data: [
|
|
|
+ { value: overviewData.value.submitted, name: '已递交' },
|
|
|
+ { value: overviewData.value.toSubmit, name: '待递交' },
|
|
|
+ { value: overviewData.value.lateToSubmit, name: '逾期未递交' },
|
|
|
+ { value: overviewData.value.lateSubmitted, name: '逾期已提交' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (barChart) {
|
|
|
+ barChart.setOption({
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ data: [
|
|
|
+ { value: todoCount.value.toSubmit, itemStyle: { color: '#409eff' } },
|
|
|
+ { value: todoCount.value.toAudit, itemStyle: { color: '#e6a23c' } },
|
|
|
+ { value: todoCount.value.toFiling, itemStyle: { color: '#67c23a' } },
|
|
|
+ { value: todoCount.value.toQc, itemStyle: { color: '#909399' } }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (lineChart) {
|
|
|
+ const total = overviewData.value.total || 1;
|
|
|
+ const submittedRate = ((overviewData.value.submitted / total) * 100).toFixed(1);
|
|
|
+ const toSubmitRate = ((overviewData.value.toSubmit / total) * 100).toFixed(1);
|
|
|
+ const onTimeRate = (((overviewData.value.submitted - overviewData.value.lateSubmitted) / total) * 100).toFixed(1);
|
|
|
+
|
|
|
+ const totalTodo = todoCount.value.toSubmit + todoCount.value.toAudit + todoCount.value.toFiling + todoCount.value.toQc || 1;
|
|
|
+ const todoProgress = ((todoCount.value.toSubmit / totalTodo) * 100).toFixed(1);
|
|
|
+
|
|
|
+ lineChart.setOption({
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ value: [
|
|
|
+ parseFloat(submittedRate),
|
|
|
+ parseFloat(onTimeRate),
|
|
|
+ 100 - parseFloat(todoProgress),
|
|
|
+ parseFloat(submittedRate),
|
|
|
+ parseFloat(onTimeRate)
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 监听窗口大小变化
|
|
|
+const handleResize = () => {
|
|
|
+ pieChart?.resize();
|
|
|
+ barChart?.resize();
|
|
|
+ lineChart?.resize();
|
|
|
+};
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
+ await getOverviewData();
|
|
|
await getTodoCount();
|
|
|
await getProjectList();
|
|
|
+
|
|
|
+ // 等待 DOM 更新后初始化图表
|
|
|
+ await nextTick();
|
|
|
+ initPieChart();
|
|
|
+ initBarChart();
|
|
|
+ initLineChart();
|
|
|
+
|
|
|
+ // 监听窗口大小变化
|
|
|
+ window.addEventListener('resize', handleResize);
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ // 销毁图表实例
|
|
|
+ pieChart?.dispose();
|
|
|
+ barChart?.dispose();
|
|
|
+ lineChart?.dispose();
|
|
|
+
|
|
|
+ // 移除事件监听
|
|
|
+ window.removeEventListener('resize', handleResize);
|
|
|
});
|
|
|
|
|
|
-onUnmounted(() => {});
|
|
|
+// 监听数据变化,更新图表
|
|
|
+watch([overviewData, todoCount], () => {
|
|
|
+ updateCharts();
|
|
|
+}, { deep: true });
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="workbench-container">
|
|
|
+
|
|
|
<!-- 待办统计卡片 -->
|
|
|
<div class="todo-cards">
|
|
|
<el-row :gutter="20">
|
|
|
@@ -202,6 +551,60 @@ onUnmounted(() => {});
|
|
|
</el-row>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 总览数据 -->
|
|
|
+ <div class="overview-section" v-hasPermi="['home:dashboard:index']">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="header-title">总览</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="overview-content">
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="overview-value">{{ overviewData.submitted }}</div>
|
|
|
+ <div class="overview-percentage">{{ calculatePercentage(overviewData.submitted, overviewData.total) }}</div>
|
|
|
+ <div class="overview-label">已递交数量/占比</div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="overview-value">{{ overviewData.toSubmit }}</div>
|
|
|
+ <div class="overview-percentage">{{ calculatePercentage(overviewData.toSubmit, overviewData.total) }}</div>
|
|
|
+ <div class="overview-label">待递交数量/占比</div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="overview-value">{{ overviewData.lateToSubmit }}</div>
|
|
|
+ <div class="overview-percentage">{{ calculatePercentage(overviewData.lateToSubmit, overviewData.total) }}</div>
|
|
|
+ <div class="overview-label">逾期未递交数量/占比</div>
|
|
|
+ </div>
|
|
|
+ <div class="overview-item">
|
|
|
+ <div class="overview-value">{{ overviewData.lateSubmitted }}</div>
|
|
|
+ <div class="overview-percentage">{{ calculatePercentage(overviewData.lateSubmitted, overviewData.total) }}</div>
|
|
|
+ <div class="overview-label">逾期已提交数量/占比</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 数据图表 -->
|
|
|
+ <div class="charts-section">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div ref="pieChartRef" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div ref="barChartRef" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="hover" class="chart-card">
|
|
|
+ <div ref="lineChartRef" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 项目列表 -->
|
|
|
<div class="project-list">
|
|
|
<el-card shadow="hover">
|
|
|
@@ -305,6 +708,58 @@ onUnmounted(() => {});
|
|
|
.workbench-container {
|
|
|
padding: 20px;
|
|
|
|
|
|
+ .overview-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ .header-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-content {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px 0;
|
|
|
+
|
|
|
+ .overview-item {
|
|
|
+ text-align: center;
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .overview-value {
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.2;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-percentage {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #606266;
|
|
|
+ margin-top: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .overview-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:not(:last-child) {
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.todo-cards {
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
@@ -412,32 +867,19 @@ onUnmounted(() => {});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .chart-section {
|
|
|
+ .charts-section {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
.chart-card {
|
|
|
- :deep(.el-card__header) {
|
|
|
- padding: 16px 20px;
|
|
|
- border-bottom: 1px solid #ebeef5;
|
|
|
- }
|
|
|
+ height: 100%;
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
- padding: 20px;
|
|
|
- }
|
|
|
-
|
|
|
- .card-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
-
|
|
|
- .header-title {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
- }
|
|
|
+ padding: 10px;
|
|
|
}
|
|
|
|
|
|
- .pie-chart {
|
|
|
+ .chart-container {
|
|
|
width: 100%;
|
|
|
- height: 400px;
|
|
|
+ height: 350px;
|
|
|
}
|
|
|
}
|
|
|
}
|