作者:呱牛
发布日期:2026年4月6日
标签:FastAPI、绩效考核
🔥 项目亮点
2026年4月6日 - 系统优化与完结
- ✅ 注释掉所有卡片的"较上周期"比较功能:简化界面,减少数据加载
- ✅ 修改顶部导航栏色调:将背景色改为 #4080FF,提升视觉效果
- ✅ 更新导航标签样式:同步更新激活状态和悬停效果的颜色
- ✅ 定位登录页面:确认登录组件的具体位置和功能
- ✅ 修复头像上传问题:解决超级管理员头像上传不显示的问题
- ✅ 系统功能完善:完成所有核心功能的开发和优化
📋 文章目录
🎯 系统优化与完善
1.1 注释掉"较上周期"比较功能
功能需求
根据用户要求,注释掉所有卡片的"较上周期"比较功能,简化界面显示,减少不必要的数据加载。
实现步骤
步骤1:注释总览视图卡片的"较上周期"显示
<!-- 考核周期人数卡片 -->
<div class="stat-card blue">
<div class="stat-title">考核周期人数</div>
<div class="stat-value">{{ assessmentPeriodStaffCount }}</div>
<!-- <div :class="['stat-change', assessmentPeriodStaffCountDiff > 0 ? 'positive' : assessmentPeriodStaffCountDiff < 0 ? 'negative' : 'neutral']">
<span v-if="assessmentPeriodStaffCountDiff > 0"><i class="fas fa-arrow-up"></i> +{{ assessmentPeriodStaffCountDiff }}</span>
<span v-else-if="assessmentPeriodStaffCountDiff < 0"><i class="fas fa-arrow-down"></i> {{ assessmentPeriodStaffCountDiff }}</span>
<span v-else><i class="fas fa-minus"></i> 0</span>
<span>较上周期</span>
</div> -->
</div>
步骤2:注释个人绩效卡片的"较上周期"显示
<div class="stat-card blue" style="cursor: pointer;" @click="handleIndicatorCardClick('需求工作')">
<div class="stat-title">需求工作</div>
<div class="stat-value">{{ currentEmployee.demand_total_score || 0 }}</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">得分占比:{{ calculatePercentage(currentEmployee.demand_total_score, currentEmployee.total_score) }}</div>
<div style="font-size: 14px; color: #666; margin-top: 5px;">工作数量:{{ indicatorRecordCounts['需求工作'] }}个</div>
<!-- <div class="stat-change positive">
<span><i class="fas fa-arrow-up"></i> 15.2</span>
<span>较上周期</span>
</div> -->
</div>
步骤3:注释数据加载逻辑中的"较上周期"比较代码
// 加载考核周期人数
const loadAssessmentPeriodStaffCount = async () => {
if (!selectedAssessmentPeriod.value || !selectedDataTimepoint.value) {
console.warn('请先选择考核期次和数据时点');
return;
}
try {
// 如果选择的是"全部团队",则不传递team_name参数
const teamName = selectedTeam.value === 'all' ? undefined : selectedTeam.value;
// 获取当前期次人数
const currentResponse = await YurdmcPaPerformanceDashboardAPI.getAssessmentPeriodStaffCount(
selectedAssessmentPeriod.value,
selectedDataTimepoint.value,
teamName
);
if (currentResponse.data.code === 0 && currentResponse.data.data !== undefined) {
assessmentPeriodStaffCount.value = currentResponse.data.data;
console.log('考核周期人数:', assessmentPeriodStaffCount.value);
// 注释掉较上周期比较逻辑
// // 获取前一个考核期次(注意:考核期次是倒序排列的,所以前一个期次的索引是 currentPeriodIndex + 1)
// const currentPeriodIndex = assessmentPeriods.value.indexOf(selectedAssessmentPeriod.value);
// if (currentPeriodIndex < assessmentPeriods.value.length - 1) {
// const previousPeriod = assessmentPeriods.value[currentPeriodIndex + 1];
// // 获取前一期次人数
// const previousResponse = await YurdmcPaPerformanceDashboardAPI.getAssessmentPeriodStaffCount(
// previousPeriod,
// selectedDataTimepoint.value,
// teamName
// );
// if (previousResponse.data.code === 0 && previousResponse.data.data !== undefined) {
// const previousCount = previousResponse.data.data;
// assessmentPeriodStaffCountDiff.value = assessmentPeriodStaffCount.value - previousCount;
// console.log('考核周期人数差值:', assessmentPeriodStaffCountDiff.value);
// }
// } else {
// assessmentPeriodStaffCountDiff.value = 0;
// }
}
} catch (error) {
console.error('加载考核周期人数失败:', error);
}
};
同样的注释操作应用到其他卡片的数据加载函数中:
loadHighestScore:最高分loadAverageScore:平均分loadSelfDevWorkloadDays:自研工作量汇总loadSelfDevWorkloadAvg:自研工作量平均
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 YurdmcPaPerformanceDashboardAPI.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'
}));
});
🎨 UI美化与调整
2.1 顶部导航栏色调调整
功能需求
将顶部导航栏的背景色调改为 #4080FF,提升系统的视觉效果和一致性。
实现步骤
步骤1:修改顶部导航栏背景颜色
.header {
background: linear-gradient(135deg, #4080FF 0%, #4080FF 100%);
color: white;
padding: 15px 40px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
}
步骤2:更新导航标签样式
.nav-tab:hover {
color: #4080FF;
background: rgba(64, 128, 255, 0.05);
}
.nav-tab.active {
color: #4080FF;
border-bottom-color: #4080FF;
font-weight: 600;
}
2.2 视觉效果优化
优化1:保持颜色一致性
- 顶部导航栏背景:#4080FF
- 导航标签激活状态:#4080FF
- 导航标签悬停效果:#4080FF(淡色背景)
优化2:提升用户体验
- 颜色过渡平滑
- 视觉层次清晰
- 响应式设计保持一致
🔍 登录页面定位
3.1 登录页面位置确认
登录页面文件路径
d:\Users\workspace\trae\yu\YuAdmin\frontend\src\views\module_system\auth\components\Login.vue
登录页面主要功能
- 账号登录:支持用户名、密码和验证码登录
- 快速登录:提供免登录用户列表,支持下拉选择或网格展示
- 第三方登录:集成微信、QQ、GitHub、Gitee登录
- 记住我:支持记住登录状态
- 密码找回:提供忘记密码功能
- 注册入口:提供新用户注册功能
技术实现
- 使用 Vue 3 + TypeScript
- 集成 Element Plus 组件库
- 支持国际化(i18n)
- 响应式设计,适配不同设备
- 包含表单验证和错误处理
- 支持自动登录和验证码功能
🐛 问题排查与修复
4.1 超级管理员头像上传问题
问题1:超级管理员不能修改个人信息
问题描述: 超级管理员尝试修改个人信息时,系统报错"超级管理员不能修改个人信息"。
解决方案: 注释掉后端限制超级管理员修改个人信息的代码。
# 注释掉超级管理员限制,允许超级管理员修改个人信息
# if user.is_superuser:
# raise CustomException(msg="超级管理员不能修改个人信息")
问题2:头像上传成功但不显示
问题描述: 超级管理员上传头像后,系统提示修改成功,但头像没有显示。
原因分析:
- 模板中使用
infoFormState.avatar来显示头像 - 但
infoFormState只在页面初始化时从userStore.basicInfo加载一次 - 当上传头像后,虽然更新了
userStore,但infoFormState没有同步更新
解决方案:
- 修改模板,直接使用
userStore.basicInfo.avatar来显示头像 - 在头像上传成功后自动保存到数据库
- 使用
userStore.setAvatar方法更新头像信息
<el-avatar v-if="userStore.basicInfo.avatar" :src="userStore.basicInfo.avatar" :size="120" />
<el-avatar v-else icon="UserFilled" :size="120" />
// 自动保存头像到数据库
try {
const response = await UserAPI.updateCurrentUserInfo({ ...infoFormState });
// 使用 setAvatar 方法更新头像,而不是 setUserInfo
userStore.setAvatar(fileUrl);
ElMessage.success("头像已保存");
} catch (error) {
console.error("保存头像失败:", error);
ElMessage.error("头像保存失败,请手动点击保存更改");
}
4.2 代码注释与维护
问题1:注释代码的完整性
问题描述: 确保所有"较上周期"相关的代码都被正确注释,包括模板显示和数据加载逻辑。
解决方案:
- 逐个检查所有卡片的模板代码
- 逐个检查所有数据加载函数
- 确保注释完整,不影响其他功能
问题2:代码可读性
问题描述: 注释后的代码需要保持良好的可读性,便于后续维护。
解决方案:
- 使用标准的注释格式
- 保持代码缩进一致
- 添加必要的注释说明
4.3 UI调整问题
问题1:颜色一致性
问题描述: 确保顶部导航栏和导航标签的颜色保持一致,避免视觉冲突。
解决方案:
- 统一使用 #4080FF 作为主题色
- 调整悬停和激活状态的颜色
- 确保文本颜色与背景色的对比度合适
问题2:响应式设计
问题描述: 确保在不同屏幕尺寸下,顶部导航栏的显示效果一致。
解决方案:
- 保持现有的响应式设计
- 测试不同屏幕尺寸下的显示效果
- 确保颜色调整不影响响应式布局
🔧 代码优化
5.1 代码结构优化
优化1:注释代码的组织
// 加载考核周期人数
const loadAssessmentPeriodStaffCount = async () => {
if (!selectedAssessmentPeriod.value || !selectedDataTimepoint.value) {
console.warn('请先选择考核期次和数据时点');
return;
}
try {
// 如果选择的是"全部团队",则不传递team_name参数
const teamName = selectedTeam.value === 'all' ? undefined : selectedTeam.value;
// 获取当前期次人数
const currentResponse = await YurdmcPaPerformanceDashboardAPI.getAssessmentPeriodStaffCount(
selectedAssessmentPeriod.value,
selectedDataTimepoint.value,
teamName
);
if (currentResponse.data.code === 0 && currentResponse.data.data !== undefined) {
assessmentPeriodStaffCount.value = currentResponse.data.data;
console.log('考核周期人数:', assessmentPeriodStaffCount.value);
// 注释掉较上周期比较逻辑
// // 获取前一个考核期次(注意:考核期次是倒序排列的,所以前一个期次的索引是 currentPeriodIndex + 1)
// const currentPeriodIndex = assessmentPeriods.value.indexOf(selectedAssessmentPeriod.value);
// if (currentPeriodIndex < assessmentPeriods.value.length - 1) {
// const previousPeriod = assessmentPeriods.value[currentPeriodIndex + 1];
// // 获取前一期次人数
// const previousResponse = await YurdmcPaPerformanceDashboardAPI.getAssessmentPeriodStaffCount(
// previousPeriod,
// selectedDataTimepoint.value,
// teamName
// );
// if (previousResponse.data.code === 0 && previousResponse.data.data !== undefined) {
// const previousCount = previousResponse.data.data;
// assessmentPeriodStaffCountDiff.value = assessmentPeriodStaffCount.value - previousCount;
// console.log('考核周期人数差值:', assessmentPeriodStaffCountDiff.value);
// }
// } else {
// assessmentPeriodStaffCountDiff.value = 0;
// }
}
} catch (error) {
console.error('加载考核周期人数失败:', error);
}
};
优化2:样式代码的组织
.header {
background: linear-gradient(135deg, #4080FF 0%, #4080FF 100%);
color: white;
padding: 15px 40px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
overflow: hidden;
}
.nav-tab:hover {
color: #4080FF;
background: rgba(64, 128, 255, 0.05);
}
.nav-tab.active {
color: #4080FF;
border-bottom-color: #4080FF;
font-weight: 600;
}
5.2 性能优化
优化1:减少数据加载
- 注释掉"较上周期"比较逻辑后,减少了一半的API调用
- 提升了页面加载速度和响应性能
优化2:简化DOM结构
- 减少了页面中的DOM元素数量
- 提升了页面渲染性能
5.3 数据可视化图表开发
雷达图开发
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
}
}
}
});
}
};
柱状图开发
// 得分对比图
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
}
}
}
});
}
🚀 技术要点
6.1 代码注释最佳实践
注释格式
- 使用
<!-- -->进行HTML注释 - 使用
//进行单行注释 - 使用
/* */进行多行注释
注释内容
- 清晰说明被注释代码的功能
- 保留注释代码以便后续可能的恢复
- 保持注释的一致性和可读性
6.2 CSS颜色管理
主题色设置
- 统一使用变量或直接指定颜色值
- 确保颜色在整个系统中的一致性
- 考虑颜色的可访问性和对比度
渐变效果
background: linear-gradient(135deg, #4080FF 0%, #4080FF 100%);
6.3 前端项目结构
文件组织
- 按功能模块组织文件
- 组件化开发,提高代码复用性
- 保持目录结构清晰
登录页面位置
src/views/module_system/auth/components/Login.vue
6.4 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
}
}
}
}
6.5 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'
}));
});
6.6 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:选择变化事件处理
📈 项目总结
7.1 已完成功能
-
核心功能
- 总览视图:考核周期人数、最高分、平均分、自研工作量等统计卡片
- 个人绩效:员工选择、绩效数据展示、指标得分雷达图、得分对比柱状图
- 指标使用率TOP10:按使用次数排序,支持团队筛选
- 临时任务得分情况:颜色区分,支持明细展示
- 团队指标横向对比:各团队绩效数据对比
- 指标分类得分占比:饼图展示各类指标占比
-
技术实现
- 前端:Vue 3 + TypeScript + Element Plus + Chart.js
- 后端:FastAPI + SQLAlchemy
- 数据可视化:雷达图、柱状图、饼图、折线图
- 响应式设计:适配不同屏幕尺寸
-
优化改进
- 注释掉"较上周期"比较功能,简化界面
- 修改顶部导航栏色调为 #4080FF,提升视觉效果
- 优化数据加载逻辑,提升性能
- 完善错误处理和边界情况
- 修复超级管理员头像上传问题
7.2 系统架构
前端架构
- 组件化设计,模块化开发
- 响应式布局,适配多种设备
- 数据可视化,直观展示绩效数据
- 表单验证,提升用户体验
后端架构
- FastAPI框架,高性能API
- SQLAlchemy ORM,简化数据库操作
- 数据聚合与统计,提供丰富的分析数据
- 错误处理和日志记录,提高系统稳定性
🧪 测试验证
8.1 功能测试
测试1:"较上周期"功能注释验证
测试步骤:
- 打开总览视图页面
- 检查所有统计卡片是否不再显示"较上周期"数据
- 打开个人绩效页面
- 检查所有绩效卡片是否不再显示"较上周期"数据
预期结果:
- 所有卡片都不显示"较上周期"相关数据
- 页面加载速度有所提升
- 其他功能正常运行
测试2:顶部导航栏色调验证
测试步骤:
- 打开系统首页
- 检查顶部导航栏背景颜色是否为 #4080FF
- 检查导航标签的激活状态和悬停效果
- 测试不同屏幕尺寸下的显示效果
预期结果:
- 顶部导航栏背景为 #4080FF
- 导航标签激活状态为 #4080FF
- 导航标签悬停效果为淡蓝色
- 在不同屏幕尺寸下显示正常
测试3:超级管理员头像上传验证
测试步骤:
- 登录超级管理员账号
- 进入个人资料页面
- 上传新头像
- 查看头像是否正确显示
- 刷新页面后查看头像是否仍然显示
预期结果:
- 头像上传成功
- 页面立即显示新头像
- 刷新页面后头像仍然显示
- 没有错误提示
测试4:个人绩效功能测试
测试步骤:
- 打开个人绩效页面
- 测试员工选择功能
- 查看绩效数据和图表显示
- 测试团队筛选功能
预期结果:
- 员工选择功能正常
- 绩效数据显示正确
- 图表渲染正常
- 团队筛选功能正常
8.2 性能测试
测试1:页面加载速度
测试数据:
- 员工数量:100+
- 考核期次:5个
- 数据时点:每个考核期次3个
测试步骤:
- 打开总览视图页面
- 记录页面加载时间
- 切换到个人绩效页面
- 记录页面加载时间
- 选择不同员工,记录数据加载时间
预期结果:
- 页面加载时间 < 2秒
- 数据切换时间 < 1秒
- 无明显卡顿
测试2:响应式设计测试
测试设备:
- 桌面端:1920x1080
- 平板端:768x1024
- 移动端:375x667
测试步骤:
- 在不同设备上打开系统
- 检查页面布局是否正常
- 测试交互功能是否正常
- 检查图表显示是否正常
预期结果:
- 在所有设备上布局正常
- 交互功能正常
- 图表显示正常
⚠️ 注意事项
9.1 代码注释注意事项
注意1:保留注释代码
原因: 注释掉的代码可能在未来需要恢复使用,保留完整的代码便于后续维护。
解决方案:
- 完整注释掉相关代码
- 添加清晰的注释说明
- 保持代码格式一致
注意2:避免注释影响其他功能
原因: 注释代码时可能会影响其他相关功能的正常运行。
解决方案:
- 仔细检查注释范围
- 测试注释后的功能
- 确保其他功能不受影响
9.2 UI调整注意事项
注意1:颜色对比度
原因: 新的颜色可能影响文本的可读性。
解决方案:
- 确保文本颜色与背景色的对比度合适
- 测试不同设备上的显示效果
- 确保颜色符合 accessibility 标准
注意2:响应式设计
原因: 颜色调整可能影响响应式布局的显示效果。
解决方案:
- 测试不同屏幕尺寸下的显示效果
- 确保颜色调整不破坏响应式布局
- 保持各设备上的视觉一致性
9.3 头像上传注意事项
注意1:数据同步
原因: 头像上传后需要确保所有相关数据都同步更新。
解决方案:
- 同时更新本地状态和全局状态
- 确保数据库中存储的头像URL正确
- 测试页面刷新后头像是否仍然显示
注意2:文件大小限制
原因: 大文件上传可能导致性能问题。
解决方案:
- 限制上传文件大小
- 优化文件上传速度
- 提供清晰的错误提示
9.4 数据类型转换
问题: 后端返回的数值字段可能是字符串类型,需要转换为数值类型。
解决方案: 使用 parseFloat() 或 Number() 进行类型转换,并提供默认值。
const score = parseFloat(employee.total_score) || 0;
9.5 图表实例管理
问题: 图表实例未正确销毁会导致内存泄漏和显示异常。
解决方案: 在初始化新图表前,先销毁旧图表实例。
if (radarChart) {
radarChart.destroy();
radarChart = null;
}
9.6 响应式数据更新
问题: 直接修改响应式数据可能不会触发视图更新。
解决方案: 使用 ref 和 computed 管理响应式数据,确保数据变化时视图正确更新。
const employeeList = ref<any[]>([]);
const filteredEmployeeList = computed(() => {
return employeeList.value.filter(...);
});
📝 最终总结
10.1 项目完成情况
-
核心功能实现
- ✅ 总览视图:统计卡片、图表展示
- ✅ 个人绩效:员工选择、数据可视化
- ✅ 指标使用率TOP10:排序、筛选
- ✅ 临时任务得分情况:颜色区分、明细展示
- ✅ 团队指标横向对比:数据对比
- ✅ 指标分类得分占比:饼图展示
- ✅ 历史绩效趋势:趋势线展示
- ✅ 登录页面:完整的登录功能
- ✅ 头像上传功能:支持超级管理员修改头像
-
技术实现
- ✅ 前端:Vue 3 + TypeScript + Element Plus + Chart.js
- ✅ 后端:FastAPI + SQLAlchemy
- ✅ 数据可视化:多种图表类型
- ✅ 响应式设计:适配不同设备
- ✅ 国际化支持:多语言切换
-
优化改进
- ✅ 注释掉"较上周期"比较功能,简化界面
- ✅ 修改顶部导航栏色调为 #4080FF,提升视觉效果
- ✅ 优化数据加载逻辑,提升性能
- ✅ 完善错误处理和边界情况
- ✅ 修复超级管理员头像上传问题
10.2 技术收获
-
前端开发
- 熟练掌握 Vue 3 + TypeScript 开发
- 精通 Element Plus 组件库使用
- 掌握 Chart.js 数据可视化
- 理解响应式设计原理
-
后端开发
- 熟悉 FastAPI 框架
- 掌握 SQLAlchemy ORM
- 理解 RESTful API 设计
- 学会数据聚合与统计查询
-
项目管理
- 理解完整的项目开发流程
- 掌握代码版本控制
- 学会需求分析与功能设计
- 理解测试与部署流程
10.3 未来展望
-
功能扩展
- 实现历史数据对比分析
- 添加数据导出功能(PDF、Excel)
- 开发移动端应用
- 集成更多第三方认证方式
-
性能优化
- 实现数据缓存机制
- 优化数据库查询
- 前端代码分割与懒加载
- 服务器负载均衡
-
用户体验
- 实现个性化仪表盘
- 添加更多数据可视化图表
- 优化移动端体验
- 实现智能推荐功能
-
系统集成
- 与HR系统集成
- 与项目管理系统集成
- 与考勤系统集成
- 与财务系统集成
10.4 项目亮点
-
完整的绩效考核解决方案
- 涵盖了绩效考核的各个方面
- 提供了丰富的数据可视化图表
- 支持多维度的数据分析
-
技术栈先进性
- 使用最新的前端技术栈
- 采用高性能的后端框架
- 实现了现代化的用户界面
-
用户体验优秀
- 响应式设计,适配不同设备
- 直观的数据可视化
- 流畅的交互体验
-
代码质量高
- 模块化设计,易于维护
- 完善的错误处理
- 良好的代码注释
项目已成功完成,为企业提供了一个功能完整、性能优异、用户体验良好的绩效考核系统。
编辑