init
This commit is contained in:
838
ruoyi-ui/src/views/index.vue
Normal file
838
ruoyi-ui/src/views/index.vue
Normal file
@@ -0,0 +1,838 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>数据分析仪表盘</h1>
|
||||
<div class="header-actions">
|
||||
|
||||
<el-button type="primary" @click="refreshData">
|
||||
<i class="el-icon-refresh"></i>
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据概览卡片 -->
|
||||
<div class="overview-cards">
|
||||
<div class="card-item">
|
||||
<div class="card-icon card-icon-blue">
|
||||
<i class="el-icon-user"></i>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-title">总成员数</div>
|
||||
<div class="card-value">{{ totalMembers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<div class="card-icon card-icon-green">
|
||||
<i class="el-icon-folder"></i>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-title">总项目数</div>
|
||||
<div class="card-value">{{ totalProjects }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<div class="card-icon card-icon-orange">
|
||||
<i class="el-icon-warning"></i>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-title">逾期项目模块</div>
|
||||
<div class="card-value">{{ overdueProjects }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item">
|
||||
<div class="card-icon card-icon-purple">
|
||||
<i class="el-icon-checked"></i>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-title">完成率</div>
|
||||
<div class="card-value">{{ completionRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- 成员表格 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>成员任务完成情况</h2>
|
||||
<el-input v-model="searchMember" placeholder="搜索成员姓名" clearable @input="filterMembers" style="width: 200px;" />
|
||||
<el-button type="text" @click="viewAllMembers">查看全部</el-button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<el-table
|
||||
:data="filteredMembersData"
|
||||
height="280"
|
||||
border
|
||||
style="width: 100%"
|
||||
@row-click="userClick"
|
||||
>
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="date" label="已完成" width="100" />
|
||||
<el-table-column prop="address" label="未完成" />
|
||||
<el-table-column label="完成率" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-progress
|
||||
:percentage="calculateRate(scope.row.date, scope.row.address)"
|
||||
:stroke-width="6"
|
||||
:show-text="true"
|
||||
:color="getRateColor(calculateRate(scope.row.date, scope.row.address))"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目表格 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>项目状态</h2>
|
||||
<el-input v-model="searchProject" placeholder="搜索项目名称" clearable style="width: 200px; margin-right: 10px;" />
|
||||
<el-tabs v-model="activeTab" type="card" size="small">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
<el-tab-pane label="已完成" name="completed" />
|
||||
<el-tab-pane label="未完成" name="pending" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<el-table
|
||||
:data="getProjectData()"
|
||||
height="280"
|
||||
border
|
||||
style="width: 100%"
|
||||
@row-click="projectClick"
|
||||
>
|
||||
<el-table-column prop="name" label="项目" width="160" />
|
||||
<el-table-column prop="ownerNick" label="负责人" width="120" />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column prop="date" label="逾期(量)" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
:type="scope.row.status === 'completed' ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.row.statusText || (scope.row.status === 'completed' ? '已完成' : '未完成') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间区域 -->
|
||||
<div class="center-section">
|
||||
<!-- 流量来源柱状图 -->
|
||||
<!-- <div class="panel"> -->
|
||||
<!-- <div class="panel-header">
|
||||
<h2>流量来源占比分析</h2>
|
||||
</div> -->
|
||||
<!-- <div class="panel-body">
|
||||
<div id="trafficChart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 任务完成趋势 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>任务完成趋势</h2>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="trendChart" style="width: 100%; height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域(已移除任务完成率分布与项目类型分布图表) -->
|
||||
<div class="right-section"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
import { listProject } from '@/api/project/project'
|
||||
import { listModule } from '@/api/project/module'
|
||||
import { listUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: 'DataAnalysisDashboard',
|
||||
data() {
|
||||
return {
|
||||
// 日期范围
|
||||
dateRange: [],
|
||||
// 标签页
|
||||
activeTab: 'all',
|
||||
// 成员完成情况表数据(name: 成员名, date: 已完成数量, address: 未完成数量)
|
||||
membersData: [],
|
||||
// 项目状态表数据(name: 项目名, date: 逾期量, status: 'completed'|'pending', projectId, ownerNick, createTime, statusText)
|
||||
projectsData: [],
|
||||
// 模块与项目原始数据(可选持有)
|
||||
_allModules: [],
|
||||
_allProjects: [],
|
||||
// 统计数据
|
||||
totalMembers: 0,
|
||||
totalProjects: 0,
|
||||
overdueProjects: 0,
|
||||
completionRate: 0,
|
||||
searchMember: '',
|
||||
searchProject: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredMembersData() {
|
||||
const keyword = this.searchMember.toLowerCase();
|
||||
return this.membersData.filter(m => m.name.toLowerCase().includes(keyword));
|
||||
},
|
||||
filteredProjectsData() {
|
||||
const keyword = this.searchProject.toLowerCase();
|
||||
return this.projectsData.filter(p => p.name.toLowerCase().includes(keyword));
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化图表
|
||||
// this.initTrafficChart();
|
||||
this.initTrendChart();
|
||||
|
||||
// 加载真实数据
|
||||
this.loadDashboardData();
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 清理监听器
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
// 兼容后端日期字符串(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)
|
||||
parseDate(val) {
|
||||
if (!val) return null;
|
||||
if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
|
||||
if (typeof val === 'string') {
|
||||
const s = val.replace(' ', 'T');
|
||||
const d = new Date(s);
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
const d2 = new Date(val.split(' ')[0]);
|
||||
return isNaN(d2.getTime()) ? null : d2;
|
||||
}
|
||||
const d = new Date(val);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
},
|
||||
toYMD(d) {
|
||||
if (!d) return '';
|
||||
const y = d.getFullYear();
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${d.getDate()}`.padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
},
|
||||
mapProjectStatus(code) {
|
||||
// 后端 project.status: 0未完成 1已完成 2隐藏(见表结构注释)
|
||||
if (code === '1' || code === 1) return '已完成';
|
||||
if (code === '2' || code === 2) return '隐藏';
|
||||
return '未完成';
|
||||
},
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
const [projRes, modRes] = await Promise.all([
|
||||
listProject({ pageNum: 1, pageSize: 9999 }),
|
||||
listModule({ pageNum: 1, pageSize: 9999 })
|
||||
]);
|
||||
const projects = (projRes && projRes.rows) ? projRes.rows : [];
|
||||
const modules = (modRes && modRes.rows) ? modRes.rows : [];
|
||||
this._allProjects = projects;
|
||||
this._allModules = modules;
|
||||
|
||||
// 统计总数
|
||||
this.totalProjects = projects.length;
|
||||
// 读取用户映射:userId -> 昵称/账号
|
||||
let userMap = {};
|
||||
let users = [];
|
||||
try {
|
||||
const usersRes = await listUser({ pageNum: 1, pageSize: 9999 });
|
||||
users = (usersRes && usersRes.rows) ? usersRes.rows : [];
|
||||
userMap = users.reduce((acc, u) => {
|
||||
acc[String(u.userId)] = u.nickName || u.userName || String(u.userId);
|
||||
return acc;
|
||||
}, {});
|
||||
} catch (ignore) {
|
||||
userMap = {};
|
||||
}
|
||||
// 成员聚合(按 assignee=用户ID 字符串)
|
||||
const memberMap = {};
|
||||
modules.forEach(m => {
|
||||
const assigneeId = m.assignee || '未指派';
|
||||
const displayName = assigneeId === '未指派' ? '未指派' : (userMap[assigneeId] || assigneeId);
|
||||
if (!memberMap[assigneeId]) memberMap[assigneeId] = { name: displayName, assigneeId, date: 0, address: 0 };
|
||||
if (m.status === '2') memberMap[assigneeId].date += 1; else memberMap[assigneeId].address += 1;
|
||||
});
|
||||
this.membersData = Object.values(memberMap);
|
||||
this.totalMembers = users.length + (users.some(u => u.userName === 'admin') ? 0 : 1);
|
||||
|
||||
// 项目聚合
|
||||
const now = new Date();
|
||||
const modulesByProject = modules.reduce((acc, m) => {
|
||||
const pid = m.projectId;
|
||||
if (!acc[pid]) acc[pid] = [];
|
||||
acc[pid].push(m);
|
||||
return acc;
|
||||
}, {});
|
||||
// 项目截止日期映射 + 负责人昵称映射
|
||||
const projectDeadlineMap = {};
|
||||
const ownerNickMap = {};
|
||||
projects.forEach(p => {
|
||||
projectDeadlineMap[p.projectId] = this.parseDate(p.deadline);
|
||||
if (p.ownerId != null) ownerNickMap[p.ownerId] = userMap[String(p.ownerId)] || p.ownerId;
|
||||
});
|
||||
|
||||
const projectsData = projects.map(p => {
|
||||
const list = modulesByProject[p.projectId] || [];
|
||||
const total = list.length;
|
||||
const completed = list.filter(x => x.status === '2').length;
|
||||
const pending = total - completed;
|
||||
const deadline = projectDeadlineMap[p.projectId];
|
||||
const overdue = deadline && now > deadline ? list.filter(x => x.status !== '2').length : 0;
|
||||
return {
|
||||
name: p.projectName,
|
||||
date: overdue,
|
||||
status: total > 0 && completed === total ? 'completed' : 'pending',
|
||||
statusText: this.mapProjectStatus(p.status),
|
||||
projectId: p.projectId,
|
||||
ownerNick: ownerNickMap[p.ownerId] || (p.ownerId || ''),
|
||||
createTime: p.createTime
|
||||
};
|
||||
});
|
||||
this.projectsData = projectsData;
|
||||
// 逾期模块数(而非逾期项目数)
|
||||
const overdueModulesCount = modules.filter(m => {
|
||||
const ddl = projectDeadlineMap[m.projectId];
|
||||
return ddl && now > ddl && m.status !== '2';
|
||||
}).length;
|
||||
this.overdueProjects = overdueModulesCount;
|
||||
|
||||
// 完成率(按模块)
|
||||
const totalModules = modules.length;
|
||||
const completedModules = modules.filter(m => m.status === '2').length;
|
||||
this.completionRate = totalModules > 0 ? Math.round((completedModules / totalModules) * 100) : 0;
|
||||
|
||||
// 更新饼图
|
||||
this.refreshCompletionPieChart( this.completionRate );
|
||||
|
||||
// 用真实数据更新趋势图
|
||||
this.updateTrendChart(modules);
|
||||
} catch (e) {
|
||||
this.$message.error('仪表盘数据加载失败');
|
||||
}
|
||||
},
|
||||
// 计算完成率
|
||||
calculateRate(completed, pending) {
|
||||
const completedNum = parseInt(completed) || 0;
|
||||
const pendingNum = parseInt(pending) || 0;
|
||||
const total = completedNum + pendingNum;
|
||||
return total > 0 ? Math.round((completedNum / total) * 100) : 0;
|
||||
},
|
||||
|
||||
// 根据完成率获取颜色
|
||||
getRateColor(rate) {
|
||||
if (rate >= 80) return '#67c23a';
|
||||
if (rate >= 50) return '#e6a23c';
|
||||
return '#f56c6c';
|
||||
},
|
||||
|
||||
// 根据标签页获取项目数据
|
||||
filterByTab(data) {
|
||||
if (this.activeTab === 'all') return data;
|
||||
if (this.activeTab === 'completed') return data.filter(item => item.status === 'completed');
|
||||
if (this.activeTab === 'pending') return data.filter(item => item.status === 'pending');
|
||||
return data;
|
||||
},
|
||||
getProjectData() {
|
||||
return this.filterByTab(this.filteredProjectsData);
|
||||
},
|
||||
|
||||
// 刷新数据
|
||||
async refreshData() {
|
||||
await this.loadDashboardData();
|
||||
this.$message.success('数据已刷新');
|
||||
},
|
||||
|
||||
// 查看所有成员
|
||||
viewAllMembers() {
|
||||
this.$router.push({ path: '/unView/userview' });
|
||||
},
|
||||
|
||||
// 用户详情页面跳转方法
|
||||
userClick(row) {
|
||||
this.$router.push({ path: '/UnView/UserProject', query: { assignee: row.assigneeId, name: row.name } });
|
||||
},
|
||||
|
||||
// 项目详情页面跳转方法
|
||||
projectClick(row) {
|
||||
this.$router.push({ path: '/UnView/Project', query: { table: row.name, projectId: row.projectId } });
|
||||
},
|
||||
|
||||
// 过滤成员数据
|
||||
filterMembers() {
|
||||
// 当搜索框有值时,使用 filteredMembersData,否则使用原始 membersData
|
||||
this.$nextTick(() => {
|
||||
if (this.searchMember) {
|
||||
this.$emit('update:membersData', this.filteredMembersData);
|
||||
} else {
|
||||
this.$emit('update:membersData', this.membersData);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 移除 filterProjects 方法,因为 computed 会自动响应 v-model 变化
|
||||
// 如果需要即时过滤,可以保留一个空方法或移除 @input
|
||||
// filterProjects() {
|
||||
// // 当搜索框有值时,使用 filteredProjectsData,否则使用原始 projectsData
|
||||
// this.$nextTick(() => {
|
||||
// if (this.searchProject) {
|
||||
// this.$emit('update:projectsData', this.filteredProjectsData);
|
||||
// } else {
|
||||
// this.$emit('update:projectsData', this.projectsData);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
|
||||
// 初始化流量来源柱状图
|
||||
// initTrafficChart() {
|
||||
// const chart = echarts.init(document.getElementById('trafficChart'));
|
||||
|
||||
// // 准备图表数据
|
||||
// const rawData = [
|
||||
// [100, 302, 301, 334, 390, 330, 320],
|
||||
// [320, 132, 101, 134, 90, 230, 210],
|
||||
// [220, 182, 191, 234, 290, 330, 310],
|
||||
// [150, 212, 201, 154, 190, 330, 410],
|
||||
// [820, 832, 901, 934, 1290, 1330, 1320]
|
||||
// ];
|
||||
|
||||
// // 计算总数据
|
||||
// const totalData = [];
|
||||
// for (let i = 0; i < rawData[0].length; ++i) {
|
||||
// let sum = 0;
|
||||
// for (let j = 0; j < rawData.length; ++j) {
|
||||
// sum += rawData[j][i];
|
||||
// }
|
||||
// totalData.push(sum);
|
||||
// }
|
||||
|
||||
// // 配置系列数据
|
||||
// const series = [
|
||||
// 'Direct',
|
||||
// 'Mail Ad',
|
||||
// 'Affiliate Ad',
|
||||
// 'Video Ad',
|
||||
// 'Search Engine'
|
||||
// ].map((name, sid) => {
|
||||
// return {
|
||||
// name,
|
||||
// type: 'bar',
|
||||
// stack: 'total',
|
||||
// barWidth: '60%',
|
||||
// label: {
|
||||
// show: true,
|
||||
// formatter: (params) => Math.round(params.value * 1000) / 10 + '%'
|
||||
// },
|
||||
// data: rawData[sid].map((d, did) =>
|
||||
// totalData[did] <= 0 ? 0 : d / totalData[did]
|
||||
// )
|
||||
// };
|
||||
// });
|
||||
|
||||
// const option = {
|
||||
// tooltip: {
|
||||
// trigger: 'axis',
|
||||
// axisPointer: { type: 'shadow' }
|
||||
// },
|
||||
// legend: {
|
||||
// data: ['Direct', 'Mail Ad', 'Affiliate Ad', 'Video Ad', 'Search Engine'],
|
||||
// bottom: 10
|
||||
// },
|
||||
// grid: {
|
||||
// left: '3%',
|
||||
// right: '4%',
|
||||
// bottom: '15%',
|
||||
// top: '3%',
|
||||
// containLabel: true
|
||||
// },
|
||||
// yAxis: {
|
||||
// type: 'value',
|
||||
// axisLabel: { formatter: '{value} %' }
|
||||
// },
|
||||
// xAxis: {
|
||||
// type: 'category',
|
||||
// data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
// },
|
||||
// series
|
||||
// };
|
||||
|
||||
// chart.setOption(option);
|
||||
|
||||
// // 保存图表实例以便调整大小
|
||||
// this.trafficChart = chart;
|
||||
// },
|
||||
|
||||
// 初始化任务完成趋势图
|
||||
initTrendChart() {
|
||||
const chart = echarts.init(document.getElementById('trendChart'));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['已完成', '未完成'],
|
||||
bottom: 10
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '已完成',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [],
|
||||
smooth: true,
|
||||
lineStyle: { color: '#67c23a' }
|
||||
},
|
||||
{
|
||||
name: '未完成',
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
data: [],
|
||||
smooth: true,
|
||||
lineStyle: { color: '#f56c6c' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
this.trendChart = chart;
|
||||
},
|
||||
updateTrendChart(modules) {
|
||||
if (!this.trendChart) return;
|
||||
// 最近7天
|
||||
const days = [];
|
||||
const labels = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - i);
|
||||
days.push(new Date(d));
|
||||
labels.push(`${d.getMonth() + 1}-${String(d.getDate()).padStart(2, '0')}`);
|
||||
}
|
||||
const endOf = (d) => {
|
||||
const x = new Date(d);
|
||||
x.setHours(23, 59, 59, 999);
|
||||
return x;
|
||||
};
|
||||
|
||||
const completedDaily = days.map(d => 0);
|
||||
const totalCumu = days.map(() => 0);
|
||||
const completedCumu = days.map(() => 0);
|
||||
|
||||
modules.forEach(m => {
|
||||
const ct = this.parseDate(m.createTime);
|
||||
const ft = this.parseDate(m.finishTime);
|
||||
days.forEach((d, idx) => {
|
||||
if (ct && ct <= endOf(d)) totalCumu[idx] += 1;
|
||||
if (m.status === '2' && ft && ft <= endOf(d)) completedCumu[idx] += 1;
|
||||
});
|
||||
// 当日完成
|
||||
if (m.status === '2' && ft) {
|
||||
const ymd = this.toYMD(ft);
|
||||
days.forEach((d, idx) => {
|
||||
if (this.toYMD(d) === ymd) completedDaily[idx] += 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
const unfinished = totalCumu.map((t, i) => Math.max(0, t - completedCumu[i]));
|
||||
|
||||
const option = this.trendChart.getOption();
|
||||
option.xAxis[0].data = labels;
|
||||
option.series[0].data = completedDaily;
|
||||
option.series[1].data = unfinished;
|
||||
this.trendChart.setOption(option, true);
|
||||
},
|
||||
|
||||
// 初始化任务完成率饼图
|
||||
initCompletionPieChart() {
|
||||
const chart = echarts.init(document.getElementById('completionPieChart'));
|
||||
|
||||
// 定义颜色方案
|
||||
const colors = [
|
||||
new echarts.graphic.LinearGradient(0, 0, 1, 1, [
|
||||
{ offset: 0, color: '#67c23a' },
|
||||
{ offset: 1, color: '#85ce61' }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(0, 0, 1, 1, [
|
||||
{ offset: 0, color: '#f56c6c' },
|
||||
{ offset: 1, color: '#f78989' }
|
||||
])
|
||||
];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '任务状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
data: [
|
||||
{ value: this.completionRate, name: '已完成' },
|
||||
{ value: 100 - this.completionRate, name: '未完成' },
|
||||
],
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '20',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
labelLine: { show: false },
|
||||
itemStyle: {
|
||||
color: (params) => colors[params.dataIndex],
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
this.completionPieChart = chart;
|
||||
},
|
||||
refreshCompletionPieChart(rate) {
|
||||
if (!this.completionPieChart) return;
|
||||
const option = this.completionPieChart.getOption();
|
||||
if (option && option.series && option.series[0]) {
|
||||
option.series[0].data = [
|
||||
{ value: rate, name: '已完成' },
|
||||
{ value: 100 - rate, name: '未完成' },
|
||||
];
|
||||
this.completionPieChart.setOption(option, true);
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化项目类型分布图表
|
||||
initProjectTypeChart() {
|
||||
const chart = echarts.init(document.getElementById('projectTypeChart'));
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
roseType: 'area',
|
||||
data: [
|
||||
{ value: 10, name: '开发' },
|
||||
{ value: 5, name: '测试' },
|
||||
{ value: 3, name: '设计' },
|
||||
{ value: 7, name: '文档' },
|
||||
{ value: 2, name: '其他' }
|
||||
],
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{d}%'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
this.projectTypeChart = chart;
|
||||
},
|
||||
|
||||
// 处理窗口大小变化
|
||||
handleResize() {
|
||||
// if (this.trafficChart) this.trafficChart.resize();
|
||||
if (this.trendChart) this.trendChart.resize();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 页面标题 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 数据概览卡片 */
|
||||
.overview-cards {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-item:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card-icon-blue { background-color: #409eff; }
|
||||
.card-icon-green { background-color: #67c23a; }
|
||||
.card-icon-orange { background-color: #e6a23c; }
|
||||
.card-icon-purple { background-color: #909399; }
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 面板样式 */
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.center-section {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.overview-cards {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user