作者:呱牛
发布日期:2026年3月31日
标签:FastAPI、绩效考核、数据可视化
🔥 今日亮点
2026年3月31日 - 个人绩效页面与数据可视化完成
- ✅ 实现员工选择功能:支持模糊搜索,根据归属团队动态筛选员工列表
- ✅ 开发个人绩效数据展示:总分、团队排名、总工时及各项指标得分
- ✅ 实现指标得分雷达图:个人得分vs团队平均,支持动态计算团队平均分
- ✅ 开发得分对比柱状图:个人得分vs团队最高,显示具体人员信息
- ✅ 优化图表交互体验:鼠标悬停显示详细数据(得分、占比、人员姓名)
- ✅ 实现智能排序功能:默认按总分降序,选择团队后按团队内部总分排序
- ✅ 修复界面布局问题:标签换行、文本对齐、下拉框样式优化
- ✅ 完善数据初始化逻辑:默认加载总分最高的员工数据
📋 文章目录
🎯 个人绩效页面开发
1.1 功能需求
开发个人绩效页面,实现以下功能:
- 员工选择功能,支持模糊搜索,根据归属团队动态筛选
- 显示员工基本信息(姓名、工号、归属团队)
- 显示绩效数据(总分、团队排名、总工时)
- 显示各项指标得分及占比(需求工作、项目工作、自主研发、运维工作、临时任务)
- 实现数据可视化图表(雷达图、柱状图)
1.2 实现步骤
步骤1:前端页面布局设计
<template>
<el-tab-pane label="个人绩效" name="individual">
<!-- 筛选条件 -->
<div class="filter-container">
<div class="filter-group">
<label class="filter-label">选择员工:</label>
<el-select
v-model="selectedEmployee"
class="w-64 custom-select"
filterable
placeholder="搜索员工姓名..."
@change="handleEmployeeChange"
>
<el-option
v-for="employee in filteredEmployeeList"
:key="employee.staff_name"
:label="`${employee.staff_name} (${employee.staff_no})`"
:value="employee.staff_name"
/>
</el-select>
</div>
</div>
<!-- 员工基本信息 -->
<div class="employee-info-card" v-if="currentEmployee">
<div class="info-item">
<span class="info-label">员工姓名</span>
<span class="info-value">{{ currentEmployee.staff_name }}</span>
</div>
<div class="info-item">
<span class="info-label">员工工号</span>
<span class="info-value">{{ currentEmployee.staff_no }}</span>
</div>
<div class="info-item">
<span class="info-label">归属团队</span>
<span class="info-value">{{ currentEmployee.team_name }}</span>
</div>
</div>
<!-- 绩效数据卡片 -->
<div class="performance-cards">
<div class="performance-card">
<div class="card-title">本考核期次总分</div>
<div class="card-value">{{ currentEmployee.total_score || 0 }}</div>
</div>
<div class="performance-card">
<div class="card-title">团队排名</div>
<div class="card-value">{{ getTeamRank() }}</div>
</div>
<div class="performance-card">
<div class="card-title">总工时(人天)</div>
<div class="card-value">{{ currentEmployee.workload_days || 0 }}</div>
</div>
</div>
<!-- 指标得分卡片 -->
<div class="indicator-cards">
<div class="indicator-card" v-for="indicator in indicators" :key="indicator.name">
<div class="indicator-name">{{ indicator.name }}</div>
<div class="indicator-score">{{ indicator.score }}</div>
<div class="indicator-percentage">占比:{{ indicator.percentage }}%</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-container">
<div class="chart-wrapper">
<div class="chart-title">指标得分雷达图</div>
<canvas ref="categoryRadarChart"></canvas>
</div>
<div class="chart-wrapper">
<div class="chart-title">得分对比(vs 团队最高)</div>
<canvas ref="scoreComparisonChart"></canvas>
</div>
</div>
</el-tab-pane>
</template>
步骤2:实现员工选择功能
// 员工列表数据
const employeeList = ref<any[]>([]);
const selectedEmployee = ref<string>('');
const currentEmployee = ref<any>(null);
// 计算过滤后的员工列表(根据归属团队筛选)
const filteredEmployeeList = computed(() => {
if (selectedTeam.value === 'all') {
return employeeList.value;
}
return employeeList.value.filter((employee: any) =>
employee.team_name === selectedTeam.value
);
});
// 处理员工选择变化
const handleEmployeeChange = () => {
console.log('选择的员工:', selectedEmployee.value);
// 从员工列表中找到选中的员工
const employee = employeeList.value.find(
(emp: any) => emp.staff_name === selectedEmployee.value
);
if (employee) {
currentEmployee.value = employee;
console.log('当前员工信息:', currentEmployee.value);
// 重新初始化图表
initCharts();
}
};
步骤3:实现数据加载与排序
// 加载部门、团队和员工数据
const loadDeptAndTeamData = async () => {
try {
const params: any = {
assessment_period: selectedPeriod.value,
data_timepoint: selectedTimepoint.value
};
if (selectedDeptLevel2.value) {
params.dept_level_2 = selectedDeptLevel2.value;
}
const response = await *PaPerformanceDashboardAPI.getPerformanceList(params);
if (response.data && response.data.length > 0) {
const deptSet = new Set<string>();
const teamSet = new Set<string>();
const employeeSet = new Set<any>();
response.data.forEach((item: any) => {
if (item.dept_level_2) deptSet.add(item.dept_level_2);
if (item.team_name) teamSet.add(item.team_name);
employeeSet.add({
staff_name: item.staff_name,
staff_no: item.staff_no,
team_name: item.team_name,
total_score: item.total_score,
workload_days: item.workload_days,
demand_total_score: item.demand_total_score,
project_total_score: item.project_total_score,
self_dev_total_score: item.self_dev_total_score,
ops_total_score: item.ops_total_score,
task_total_score: item.task_total_score
});
});
deptLevel2List.value = Array.from(deptSet).sort();
teamList.value = Array.from(teamSet).sort();
// 按总分降序排序员工列表
employeeList.value = Array.from(employeeSet).sort((a, b) => {
const scoreA = parseFloat(a.total_score) || 0;
const scoreB = parseFloat(b.total_score) || 0;
return scoreB - scoreA;
});
// 默认选择总分最高的员工
if (employeeList.value.length > 0) {
selectedEmployee.value = employeeList.value[0].staff_name;
}
// 数据加载完成后重新初始化图表
initCharts();
}
} catch (error) {
console.error('加载数据失败:', error);
}
};
步骤4:实现团队排名计算
// 计算团队排名
const getTeamRank = () => {
if (!currentEmployee.value || !currentEmployee.value.team_name) {
return '-';
}
// 获取同团队的员工列表
const teamEmployees = employeeList.value.filter(
(emp: any) => emp.team_name === currentEmployee.value.team_name
);
// 按总分降序排序
const sortedEmployees = teamEmployees.sort((a, b) => {
const scoreA = parseFloat(a.total_score) || 0;
const scoreB = parseFloat(b.total_score) || 0;
return scoreB - scoreA;
});
// 找到当前员工的排名
const rank = sortedEmployees.findIndex(
(emp: any) => emp.staff_name === currentEmployee.value.staff_name
) + 1;
return rank > 0 ? rank : '-';
};
步骤5:实现指标得分及占比计算
// 计算指标数据
const indicators = computed(() => {
if (!currentEmployee.value) {
return [];
}
const totalScore = parseFloat(currentEmployee.value.total_score) || 0;
const indicatorList = [
{
name: '需求工作',
score: parseFloat(currentEmployee.value.demand_total_score) || 0,
field: 'demand_total_score'
},
{
name: '项目工作',
score: parseFloat(currentEmployee.value.project_total_score) || 0,
field: 'project_total_score'
},
{
name: '自主研发',
score: parseFloat(currentEmployee.value.self_dev_total_score) || 0,
field: 'self_dev_total_score'
},
{
name: '运维工作',
score: parseFloat(currentEmployee.value.ops_total_score) || 0,
field: 'ops_total_score'
},
{
name: '临时任务',
score: parseFloat(currentEmployee.value.task_total_score) || 0,
field: 'task_total_score'
}
];
// 计算占比
return indicatorList.map(indicator => ({
...indicator,
percentage: totalScore > 0 ? ((indicator.score / totalScore) * 100).toFixed(1) : '0.0'
}));
});
📊 数据可视化图表开发
2.1 雷达图开发
步骤1:计算团队平均分
// 计算团队平均分
const calculateTeamAverage = () => {
let filteredEmployees = employeeList.value;
// 根据归属团队筛选
if (selectedTeam.value !== 'all') {
filteredEmployees = employeeList.value.filter(
(employee: any) => employee.team_name === selectedTeam.value
);
}
if (filteredEmployees.length === 0) {
return {
demand: 0,
project: 0,
selfDev: 0,
ops: 0,
task: 0
};
}
// 计算各项指标的平均分
const totalDemand = filteredEmployees.reduce(
(sum: number, emp: any) => sum + (parseFloat(emp.demand_total_score) || 0), 0
);
const totalProject = filteredEmployees.reduce(
(sum: number, emp: any) => sum + (parseFloat(emp.project_total_score) || 0), 0
);
const totalSelfDev = filteredEmployees.reduce(
(sum: number, emp: any) => sum + (parseFloat(emp.self_dev_total_score) || 0), 0
);
const totalOps = filteredEmployees.reduce(
(sum: number, emp: any) => sum + (parseFloat(emp.ops_total_score) || 0), 0
);
const totalTask = filteredEmployees.reduce(
(sum: number, emp: any) => sum + (parseFloat(emp.task_total_score) || 0), 0
);
const count = filteredEmployees.length;
return {
demand: totalDemand / count,
project: totalProject / count,
selfDev: totalSelfDev / count,
ops: totalOps / count,
task: totalTask / count
};
};
步骤2:初始化雷达图
const initCharts = () => {
// 销毁旧图表实例
if (radarChart) {
radarChart.destroy();
radarChart = null;
}
if (comparisonChart) {
comparisonChart.destroy();
comparisonChart = null;
}
// 雷达图
if (categoryRadarChart.value) {
const teamAverage = calculateTeamAverage();
radarChart = new Chart(categoryRadarChart.value, {
type: 'radar',
data: {
labels: ['需求工作', '项目工作', '自主研发', '系统运维', '临时任务'],
datasets: [{
label: '个人得分',
data: [
parseFloat(currentEmployee.value.demand_total_score) || 0,
parseFloat(currentEmployee.value.project_total_score) || 0,
parseFloat(currentEmployee.value.self_dev_total_score) || 0,
parseFloat(currentEmployee.value.ops_total_score) || 0,
parseFloat(currentEmployee.value.task_total_score) || 0
],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.2)',
pointBackgroundColor: '#667eea',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#667eea'
}, {
label: '团队平均',
data: [
parseFloat(teamAverage.demand) || 0,
parseFloat(teamAverage.project) || 0,
parseFloat(teamAverage.selfDev) || 0,
parseFloat(teamAverage.ops) || 0,
parseFloat(teamAverage.task) || 0
],
borderColor: '#2ecc71',
backgroundColor: 'rgba(46, 204, 113, 0.1)',
pointBackgroundColor: '#2ecc71',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#2ecc71'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
enabled: true,
mode: 'point',
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.raw || 0;
if (label === '个人得分') {
const score = value.toFixed(1);
const totalScore = parseFloat(currentEmployee.value.total_score) || 0;
const percentage = totalScore > 0 ? ((value / totalScore) * 100).toFixed(1) : '0.0';
return [
`${label} (${currentEmployee.value.staff_name || '请选择员工'})`,
`得分: ${score}`,
`占比: ${percentage}%`
];
} else if (label === '团队平均') {
const score = value.toFixed(1);
let teamInfo = '';
if (selectedTeam.value === 'all') {
teamInfo = '整个二级部门';
} else {
teamInfo = selectedTeam.value;
}
return [
`${label} (${teamInfo})`,
`得分: ${score}`
];
}
return `${label}: ${value.toFixed(1)}`;
}
}
}
},
scales: {
r: {
beginAtZero: true,
min: 0,
max: 100
}
}
}
});
}
};
2.2 柱状图开发
步骤1:计算团队最高分及对应人员
// 得分对比图
if (scoreComparisonChart.value) {
// 获取团队最高分
let filteredEmployees = employeeList.value;
if (selectedTeam.value !== 'all') {
filteredEmployees = employeeList.value.filter((employee: any) =>
employee.team_name === selectedTeam.value
);
}
// 计算各指标的最高分和对应的人员
let demandMax = 0;
let projectMax = 0;
let selfDevMax = 0;
let opsMax = 0;
let taskMax = 0;
let demandMaxName = '';
let projectMaxName = '';
let selfDevMaxName = '';
let opsMaxName = '';
let taskMaxName = '';
filteredEmployees.forEach((employee: any) => {
const demandScore = parseFloat(employee.demand_total_score) || 0;
const projectScore = parseFloat(employee.project_total_score) || 0;
const selfDevScore = parseFloat(employee.self_dev_total_score) || 0;
const opsScore = parseFloat(employee.ops_total_score) || 0;
const taskScore = parseFloat(employee.task_total_score) || 0;
if (demandScore > demandMax) {
demandMax = demandScore;
demandMaxName = employee.staff_name;
}
if (projectScore > projectMax) {
projectMax = projectScore;
projectMaxName = employee.staff_name;
}
if (selfDevScore > selfDevMax) {
selfDevMax = selfDevScore;
selfDevMaxName = employee.staff_name;
}
if (opsScore > opsMax) {
opsMax = opsScore;
opsMaxName = employee.staff_name;
}
if (taskScore > taskMax) {
taskMax = taskScore;
taskMaxName = employee.staff_name;
}
});
// 存储最高分人员信息
const maxScoreNames = {
'需求工作': demandMaxName,
'项目工作': projectMaxName,
'自主研发': selfDevMaxName,
'系统运维': opsMaxName,
'临时任务': taskMaxName
};
comparisonChart = new Chart(scoreComparisonChart.value, {
type: 'bar',
data: {
labels: ['需求工作', '项目工作', '自主研发', '系统运维', '临时任务'],
datasets: [{
label: '个人得分',
data: [
parseFloat(currentEmployee.value.demand_total_score) || 0,
parseFloat(currentEmployee.value.project_total_score) || 0,
parseFloat(currentEmployee.value.self_dev_total_score) || 0,
parseFloat(currentEmployee.value.ops_total_score) || 0,
parseFloat(currentEmployee.value.task_total_score) || 0
],
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 1
}, {
label: '团队最高',
data: [
demandMax,
projectMax,
selfDevMax,
opsMax,
taskMax
],
backgroundColor: 'rgba(46, 204, 113, 0.6)',
borderColor: 'rgba(46, 204, 113, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
enabled: true,
mode: 'index',
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.raw || 0;
const index = context.dataIndex;
const category = context.chart.data.labels[index];
if (label === '个人得分') {
return `${label} (${currentEmployee.value.staff_name || '请选择员工'}): ${value.toFixed(1)}`;
} else if (label === '团队最高') {
const maxName = maxScoreNames[category] || '无数据';
return `${label} (${maxName}): ${value.toFixed(1)}`;
}
return `${label}: ${value.toFixed(1)}`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
min: 0,
max: Math.max(...[demandMax, projectMax, selfDevMax, opsMax, taskMax, parseFloat(currentEmployee.value.demand_total_score) || 0, parseFloat(currentEmployee.value.project_total_score) || 0, parseFloat(currentEmployee.value.self_dev_total_score) || 0, parseFloat(currentEmployee.value.ops_total_score) || 0, parseFloat(currentEmployee.value.task_total_score) || 0]) * 1.2 || 100
}
}
}
});
}
🐛 问题排查与修复
3.1 界面布局问题修复
问题1:标签换行问题
问题描述: "选择员工"标签在一行没有显示完,出现了换行。
解决方案: 为标签添加 white-space: nowrap 样式。
.filter-label {
font-weight: 500;
color: #606266;
margin-right: 8px;
white-space: nowrap;
}
问题2:员工姓名左对齐太靠边
问题描述: 下拉框加载出的姓名太靠左边,需要预留几个像素的位置。
解决方案: 为下拉选项添加左内边距。
.el-select-dropdown__item {
padding-left: 20px !important;
}
3.2 图表数据显示问题修复
问题1:雷达图无数据显示
问题描述: 选择员工后,雷达图没有显示数据。
原因分析:
- employeeList未包含所需字段(demand_total_score等)
- 数据加载后未重新初始化图表
- 图表实例未销毁导致冲突
解决方案:
- 在构建employeeSet时添加所有指标字段
- 在loadDeptAndTeamData函数中数据加载完成后调用initCharts()
- 在initCharts函数开头销毁所有旧图表实例
// 构建员工数据时包含所有字段
employeeSet.add({
staff_name: item.staff_name,
staff_no: item.staff_no,
team_name: item.team_name,
total_score: item.total_score,
workload_days: item.workload_days,
demand_total_score: item.demand_total_score,
project_total_score: item.project_total_score,
self_dev_total_score: item.self_dev_total_score,
ops_total_score: item.ops_total_score,
task_total_score: item.task_total_score
});
// 销毁旧图表实例
const initCharts = () => {
if (radarChart) {
radarChart.destroy();
radarChart = null;
}
if (comparisonChart) {
comparisonChart.destroy();
comparisonChart = null;
}
// ... 初始化新图表
};
问题2:柱状图只显示一个柱子
问题描述: 需求工作和临时任务只显示一个柱子,没有显示完整。
原因分析: 图表配置缺少必要的样式属性,导致柱子重叠或不可见。
解决方案: 添加 borderColor、borderWidth 和 scales 配置。
datasets: [{
label: '个人得分',
data: [...],
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 1
}, {
label: '团队最高',
data: [...],
backgroundColor: 'rgba(46, 204, 113, 0.6)',
borderColor: 'rgba(46, 204, 113, 1)',
borderWidth: 1
}],
options: {
scales: {
y: {
beginAtZero: true,
min: 0,
max: Math.max(...所有数据) * 1.2 || 100
}
}
}
3.3 数据排序问题修复
问题:初始加载数据默认是员工曹操
问题描述: 初始加载数据默认是按姓名首字母排序,应该按总分降序排序,默认加载总分最高的员工。
解决方案: 修改排序逻辑,按总分降序排序。
// 按总分降序排序员工列表
employeeList.value = Array.from(employeeSet).sort((a, b) => {
const scoreA = parseFloat(a.total_score) || 0;
const scoreB = parseFloat(b.total_score) || 0;
return scoreB - scoreA;
});
// 默认选择总分最高的员工
if (employeeList.value.length > 0) {
selectedEmployee.value = employeeList.value[0].staff_name;
}
问题:选择团队后排序逻辑
问题描述: 选择归属团队后,应该按团队内部总分从高到低排序。
解决方案: 在handleTeamChange函数中添加排序逻辑。
const handleTeamChange = () => {
selectedEmployee.value = '';
if (selectedTeam.value !== 'all') {
// 先过滤出该团队的员工,再按总分降序排序
const teamEmployees = employeeList.value
.filter((employee: any) => employee.team_name === selectedTeam.value)
.sort((a, b) => {
const scoreA = parseFloat(a.total_score) || 0;
const scoreB = parseFloat(b.total_score) || 0;
return scoreB - scoreA;
});
// 如果该团队有员工,默认选择总分最高的
if (teamEmployees.length > 0) {
selectedEmployee.value = teamEmployees[0].staff_name;
}
} else {
// 选择全部团队时,按全团队总分降序排序,默认选择最高的
const sortedEmployees = [...employeeList.value].sort((a, b) => {
const scoreA = parseFloat(a.total_score) || 0;
const scoreB = parseFloat(b.total_score) || 0;
return scoreB - scoreA;
});
if (sortedEmployees.length > 0) {
selectedEmployee.value = sortedEmployees[0].staff_name;
}
}
refreshData();
};
🔧 代码优化
4.1 图表交互优化
优化1:雷达图tooltip显示详细信息
tooltip: {
enabled: true,
mode: 'point',
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.raw || 0;
if (label === '个人得分') {
const score = value.toFixed(1);
const totalScore = parseFloat(currentEmployee.value.total_score) || 0;
const percentage = totalScore > 0 ? ((value / totalScore) * 100).toFixed(1) : '0.0';
return [
`${label} (${currentEmployee.value.staff_name || '请选择员工'})`,
`得分: ${score}`,
`占比: ${percentage}%`
];
} else if (label === '团队平均') {
const score = value.toFixed(1);
let teamInfo = '';
if (selectedTeam.value === 'all') {
teamInfo = '整个二级部门';
} else {
teamInfo = selectedTeam.value;
}
return [
`${label} (${teamInfo})`,
`得分: ${score}`
];
}
return `${label}: ${value.toFixed(1)}`;
}
}
}
优化2:柱状图tooltip显示人员信息
tooltip: {
enabled: true,
mode: 'index',
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.raw || 0;
const index = context.dataIndex;
const category = context.chart.data.labels[index];
if (label === '个人得分') {
return `${label} (${currentEmployee.value.staff_name || '请选择员工'}): ${value.toFixed(1)}`;
} else if (label === '团队最高') {
const maxName = maxScoreNames[category] || '无数据';
return `${label} (${maxName}): ${value.toFixed(1)}`;
}
return `${label}: ${value.toFixed(1)}`;
}
}
}
4.2 性能优化
优化1:使用computed缓存计算结果
// 使用computed缓存过滤后的员工列表
const filteredEmployeeList = computed(() => {
if (selectedTeam.value === 'all') {
return employeeList.value;
}
return employeeList.value.filter((employee: any) =>
employee.team_name === selectedTeam.value
);
});
// 使用computed缓存指标数据
const indicators = computed(() => {
if (!currentEmployee.value) {
return [];
}
const totalScore = parseFloat(currentEmployee.value.total_score) || 0;
return indicatorList.map(indicator => ({
...indicator,
percentage: totalScore > 0 ? ((indicator.score / totalScore) * 100).toFixed(1) : '0.0'
}));
});
优化2:图表实例销毁与重建
// 在初始化图表前销毁旧实例
const initCharts = () => {
if (radarChart) {
radarChart.destroy();
radarChart = null;
}
if (comparisonChart) {
comparisonChart.destroy();
comparisonChart = null;
}
// 初始化新图表...
};
🚀 技术要点
5.1 Chart.js图表配置
雷达图配置要点
{
type: 'radar',
data: {
labels: ['指标1', '指标2', '指标3', '指标4', '指标5'],
datasets: [{
label: '个人得分',
data: [85, 90, 78, 92, 88],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.2)',
pointBackgroundColor: '#667eea',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#667eea'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
enabled: true,
mode: 'point'
}
},
scales: {
r: {
beginAtZero: true,
min: 0,
max: 100
}
}
}
}
柱状图配置要点
{
type: 'bar',
data: {
labels: ['指标1', '指标2', '指标3', '指标4', '指标5'],
datasets: [{
label: '个人得分',
data: [85, 90, 78, 92, 88],
backgroundColor: 'rgba(102, 126, 234, 0.8)',
borderColor: 'rgba(102, 126, 234, 1)',
borderWidth: 1
}, {
label: '团队最高',
data: [95, 98, 88, 96, 92],
backgroundColor: 'rgba(46, 204, 113, 0.6)',
borderColor: 'rgba(46, 204, 113, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
},
tooltip: {
enabled: true,
mode: 'index'
}
},
scales: {
y: {
beginAtZero: true,
min: 0
}
}
}
}
5.2 Vue 3响应式数据管理
ref与computed的使用
// 使用ref管理响应式数据
const employeeList = ref<any[]>([]);
const selectedEmployee = ref<string>('');
const currentEmployee = ref<any>(null);
// 使用computed缓存计算结果
const filteredEmployeeList = computed(() => {
if (selectedTeam.value === 'all') {
return employeeList.value;
}
return employeeList.value.filter((employee: any) =>
employee.team_name === selectedTeam.value
);
});
const indicators = computed(() => {
if (!currentEmployee.value) {
return [];
}
const totalScore = parseFloat(currentEmployee.value.total_score) || 0;
return indicatorList.map(indicator => ({
...indicator,
percentage: totalScore > 0 ? ((indicator.score / totalScore) * 100).toFixed(1) : '0.0'
}));
});
5.3 Element Plus组件使用
el-select组件配置
<el-select
v-model="selectedEmployee"
class="w-64 custom-select"
filterable
placeholder="搜索员工姓名..."
@change="handleEmployeeChange"
>
<el-option
v-for="employee in filteredEmployeeList"
:key="employee.staff_name"
:label="`${employee.staff_name} (${employee.staff_no})`"
:value="employee.staff_name"
/>
</el-select>
关键属性:
v-model:双向绑定选中的值filterable:启用搜索功能placeholder:占位提示文本@change:选择变化事件处理
📈 待实现功能
6.1 详细指标展示
- 实现各指标的详细数据展示(C101-C403)
- 添加指标得分明细表格
- 实现指标数据导出功能
6.2 历史数据对比
- 实现历史考核期次数据对比
- 添加趋势图表展示
- 实现环比、同比分析
6.3 团队绩效分析
- 实现团队整体绩效分析
- 添加团队对比图表
- 实现团队排名趋势分析
6.4 数据导出功能
- 实现个人绩效报告导出
- 支持PDF格式导出
- 支持Excel格式导出
🧪 测试验证
7.1 功能测试
测试1:员工选择功能测试
测试步骤:
- 打开个人绩效页面
- 点击"选择员工"下拉框
- 输入员工姓名进行搜索
- 选择员工后查看数据是否正确加载
预期结果:
- 下拉框显示所有员工(根据归属团队筛选)
- 搜索功能正常工作
- 选择员工后,页面显示该员工的绩效数据
测试2:图表显示测试
测试步骤:
- 选择员工后,查看雷达图是否正确显示
- 查看柱状图是否正确显示
- 鼠标悬停在图表上,查看tooltip是否显示详细信息
预期结果:
- 雷达图显示个人得分和团队平均
- 柱状图显示个人得分和团队最高
- tooltip显示详细的得分、占比和人员信息
测试3:排序功能测试
测试步骤:
- 打开个人绩效页面,查看默认选中的员工
- 选择不同的归属团队
- 查看员工列表是否按团队内部总分排序
预期结果:
- 默认选中总分最高的员工
- 选择团队后,默认选中该团队总分最高的员工
- 员工列表按总分降序排列
7.2 性能测试
测试1:大数据量测试
测试数据:
- 员工数量:100+
- 考核期次:5个
- 数据时点:每个考核期次3个
测试步骤:
- 加载所有数据
- 查看页面响应速度
- 查看图表渲染速度
预期结果:
- 页面加载时间 < 2秒
- 图表渲染时间 < 1秒
- 无明显卡顿
测试2:并发测试
测试场景:
- 10个用户同时访问个人绩效页面
预期结果:
- 所有用户都能正常访问
- 数据加载正确
- 无明显性能下降
⚠️ 注意事项
8.1 数据类型转换
问题: 后端返回的数值字段可能是字符串类型,需要转换为数值类型。
解决方案: 使用 parseFloat() 或 Number() 进行类型转换,并提供默认值。
const score = parseFloat(employee.total_score) || 0;
8.2 图表实例管理
问题: 图表实例未正确销毁会导致内存泄漏和显示异常。
解决方案: 在初始化新图表前,先销毁旧图表实例。
if (radarChart) {
radarChart.destroy();
radarChart = null;
}
8.3 响应式数据更新
问题: 直接修改响应式数据可能不会触发视图更新。
解决方案: 使用 ref 和 computed 管理响应式数据,确保数据变化时视图正确更新。
const employeeList = ref<any[]>([]);
const filteredEmployeeList = computed(() => {
return employeeList.value.filter(...);
});
8.4 样式作用域
问题: Vue组件的样式可能影响全局样式,或被全局样式覆盖。
解决方案: 使用 scoped 属性限制样式作用域,或使用更具体的选择器。
<style scoped>
.filter-label {
white-space: nowrap;
}
</style>
8.5 异步数据加载
问题: 异步数据加载完成前,图表可能已经初始化,导致数据为空。
解决方案: 在数据加载完成后再初始化图表。
const loadData = async () => {
await fetchEmployeeData();
initCharts();
};
📝 今日总结
9.1 完成的工作
-
个人绩效页面开发
- 实现员工选择功能,支持模糊搜索和团队筛选
- 显示员工基本信息和绩效数据
- 实现指标得分展示及占比计算
-
数据可视化图表开发
- 开发指标得分雷达图(vs 团队平均)
- 开发得分对比柱状图(vs 团队最高)
- 实现图表交互优化(tooltip显示详细数据)
-
数据排序优化
- 默认按总分从高到低排序
- 选择团队后按团队内部总分排序
- 默认选择总分最高的员工
-
问题修复
- 修复界面布局问题(标签换行、文本对齐)
- 修复图表数据显示问题
- 修复数据排序问题
9.2 技术收获
-
Chart.js图表库使用
- 掌握雷达图和柱状图的配置方法
- 学会使用tooltip回调函数显示自定义信息
- 理解图表实例的销毁与重建机制
-
Vue 3响应式数据管理
- 熟练使用ref和computed管理响应式数据
- 理解computed的缓存机制
- 掌握异步数据加载的最佳实践
-
Element Plus组件使用
- 掌握el-select组件的配置和使用
- 学会使用filterable属性实现搜索功能
- 理解组件事件处理机制
9.3 下一步计划
-
详细指标展示
- 实现各指标的详细数据展示
- 添加指标得分明细表格
- 实现指标数据导出功能
-
历史数据对比
- 实现历史考核期次数据对比
- 添加趋势图表展示
- 实现环比、同比分析
-
团队绩效分析
- 实现团队整体绩效分析
- 添加团队对比图表
- 实现团队排名趋势分析
-
性能优化
- 优化大数据量加载速度
- 实现数据分页和懒加载
- 优化图表渲染性能