企业级软件研发团队绩效考核系统开发(持续更新 Day 10 已完结)

0 阅读19分钟

作者:呱牛

发布日期: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

登录页面主要功能

  1. 账号登录:支持用户名、密码和验证码登录
  2. 快速登录:提供免登录用户列表,支持下拉选择或网格展示
  3. 第三方登录:集成微信、QQ、GitHub、Gitee登录
  4. 记住我:支持记住登录状态
  5. 密码找回:提供忘记密码功能
  6. 注册入口:提供新用户注册功能

技术实现

  • 使用 Vue 3 + TypeScript
  • 集成 Element Plus 组件库
  • 支持国际化(i18n)
  • 响应式设计,适配不同设备
  • 包含表单验证和错误处理
  • 支持自动登录和验证码功能

🐛 问题排查与修复

4.1 超级管理员头像上传问题

问题1:超级管理员不能修改个人信息

问题描述: 超级管理员尝试修改个人信息时,系统报错"超级管理员不能修改个人信息"。

解决方案: 注释掉后端限制超级管理员修改个人信息的代码。

# 注释掉超级管理员限制,允许超级管理员修改个人信息
# if user.is_superuser:
#     raise CustomException(msg="超级管理员不能修改个人信息")

问题2:头像上传成功但不显示

问题描述: 超级管理员上传头像后,系统提示修改成功,但头像没有显示。

原因分析:

  • 模板中使用 infoFormState.avatar 来显示头像
  • infoFormState 只在页面初始化时从 userStore.basicInfo 加载一次
  • 当上传头像后,虽然更新了 userStore,但 infoFormState 没有同步更新

解决方案:

  1. 修改模板,直接使用 userStore.basicInfo.avatar 来显示头像
  2. 在头像上传成功后自动保存到数据库
  3. 使用 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 已完成功能

  1. 核心功能

    • 总览视图:考核周期人数、最高分、平均分、自研工作量等统计卡片
    • 个人绩效:员工选择、绩效数据展示、指标得分雷达图、得分对比柱状图
    • 指标使用率TOP10:按使用次数排序,支持团队筛选
    • 临时任务得分情况:颜色区分,支持明细展示
    • 团队指标横向对比:各团队绩效数据对比
    • 指标分类得分占比:饼图展示各类指标占比
  2. 技术实现

    • 前端:Vue 3 + TypeScript + Element Plus + Chart.js
    • 后端:FastAPI + SQLAlchemy
    • 数据可视化:雷达图、柱状图、饼图、折线图
    • 响应式设计:适配不同屏幕尺寸
  3. 优化改进

    • 注释掉"较上周期"比较功能,简化界面
    • 修改顶部导航栏色调为 #4080FF,提升视觉效果
    • 优化数据加载逻辑,提升性能
    • 完善错误处理和边界情况
    • 修复超级管理员头像上传问题

7.2 系统架构

前端架构

  • 组件化设计,模块化开发
  • 响应式布局,适配多种设备
  • 数据可视化,直观展示绩效数据
  • 表单验证,提升用户体验

后端架构

  • FastAPI框架,高性能API
  • SQLAlchemy ORM,简化数据库操作
  • 数据聚合与统计,提供丰富的分析数据
  • 错误处理和日志记录,提高系统稳定性

🧪 测试验证

8.1 功能测试

测试1:"较上周期"功能注释验证

测试步骤:

  1. 打开总览视图页面
  2. 检查所有统计卡片是否不再显示"较上周期"数据
  3. 打开个人绩效页面
  4. 检查所有绩效卡片是否不再显示"较上周期"数据

预期结果:

  • 所有卡片都不显示"较上周期"相关数据
  • 页面加载速度有所提升
  • 其他功能正常运行

测试2:顶部导航栏色调验证

测试步骤:

  1. 打开系统首页
  2. 检查顶部导航栏背景颜色是否为 #4080FF
  3. 检查导航标签的激活状态和悬停效果
  4. 测试不同屏幕尺寸下的显示效果

预期结果:

  • 顶部导航栏背景为 #4080FF
  • 导航标签激活状态为 #4080FF
  • 导航标签悬停效果为淡蓝色
  • 在不同屏幕尺寸下显示正常

测试3:超级管理员头像上传验证

测试步骤:

  1. 登录超级管理员账号
  2. 进入个人资料页面
  3. 上传新头像
  4. 查看头像是否正确显示
  5. 刷新页面后查看头像是否仍然显示

预期结果:

  • 头像上传成功
  • 页面立即显示新头像
  • 刷新页面后头像仍然显示
  • 没有错误提示

测试4:个人绩效功能测试

测试步骤:

  1. 打开个人绩效页面
  2. 测试员工选择功能
  3. 查看绩效数据和图表显示
  4. 测试团队筛选功能

预期结果:

  • 员工选择功能正常
  • 绩效数据显示正确
  • 图表渲染正常
  • 团队筛选功能正常

8.2 性能测试

测试1:页面加载速度

测试数据:

  • 员工数量:100+
  • 考核期次:5个
  • 数据时点:每个考核期次3个

测试步骤:

  1. 打开总览视图页面
  2. 记录页面加载时间
  3. 切换到个人绩效页面
  4. 记录页面加载时间
  5. 选择不同员工,记录数据加载时间

预期结果:

  • 页面加载时间 < 2秒
  • 数据切换时间 < 1秒
  • 无明显卡顿

测试2:响应式设计测试

测试设备:

  • 桌面端:1920x1080
  • 平板端:768x1024
  • 移动端:375x667

测试步骤:

  1. 在不同设备上打开系统
  2. 检查页面布局是否正常
  3. 测试交互功能是否正常
  4. 检查图表显示是否正常

预期结果:

  • 在所有设备上布局正常
  • 交互功能正常
  • 图表显示正常

⚠️ 注意事项

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 响应式数据更新

问题: 直接修改响应式数据可能不会触发视图更新。

解决方案: 使用 refcomputed 管理响应式数据,确保数据变化时视图正确更新。

const employeeList = ref<any[]>([]);
const filteredEmployeeList = computed(() => {
  return employeeList.value.filter(...);
});


📝 最终总结

10.1 项目完成情况

  1. 核心功能实现

    • ✅ 总览视图:统计卡片、图表展示
    • ✅ 个人绩效:员工选择、数据可视化
    • ✅ 指标使用率TOP10:排序、筛选
    • ✅ 临时任务得分情况:颜色区分、明细展示
    • ✅ 团队指标横向对比:数据对比
    • ✅ 指标分类得分占比:饼图展示
    • ✅ 历史绩效趋势:趋势线展示
    • ✅ 登录页面:完整的登录功能
    • ✅ 头像上传功能:支持超级管理员修改头像
  2. 技术实现

    • ✅ 前端:Vue 3 + TypeScript + Element Plus + Chart.js
    • ✅ 后端:FastAPI + SQLAlchemy
    • ✅ 数据可视化:多种图表类型
    • ✅ 响应式设计:适配不同设备
    • ✅ 国际化支持:多语言切换
  3. 优化改进

    • ✅ 注释掉"较上周期"比较功能,简化界面
    • ✅ 修改顶部导航栏色调为 #4080FF,提升视觉效果
    • ✅ 优化数据加载逻辑,提升性能
    • ✅ 完善错误处理和边界情况
    • ✅ 修复超级管理员头像上传问题

10.2 技术收获

  1. 前端开发

    • 熟练掌握 Vue 3 + TypeScript 开发
    • 精通 Element Plus 组件库使用
    • 掌握 Chart.js 数据可视化
    • 理解响应式设计原理
  2. 后端开发

    • 熟悉 FastAPI 框架
    • 掌握 SQLAlchemy ORM
    • 理解 RESTful API 设计
    • 学会数据聚合与统计查询
  3. 项目管理

    • 理解完整的项目开发流程
    • 掌握代码版本控制
    • 学会需求分析与功能设计
    • 理解测试与部署流程

10.3 未来展望

  1. 功能扩展

    • 实现历史数据对比分析
    • 添加数据导出功能(PDF、Excel)
    • 开发移动端应用
    • 集成更多第三方认证方式
  2. 性能优化

    • 实现数据缓存机制
    • 优化数据库查询
    • 前端代码分割与懒加载
    • 服务器负载均衡
  3. 用户体验

    • 实现个性化仪表盘
    • 添加更多数据可视化图表
    • 优化移动端体验
    • 实现智能推荐功能
  4. 系统集成

    • 与HR系统集成
    • 与项目管理系统集成
    • 与考勤系统集成
    • 与财务系统集成

10.4 项目亮点

  1. 完整的绩效考核解决方案

    • 涵盖了绩效考核的各个方面
    • 提供了丰富的数据可视化图表
    • 支持多维度的数据分析
  2. 技术栈先进性

    • 使用最新的前端技术栈
    • 采用高性能的后端框架
    • 实现了现代化的用户界面
  3. 用户体验优秀

    • 响应式设计,适配不同设备
    • 直观的数据可视化
    • 流畅的交互体验
  4. 代码质量高

    • 模块化设计,易于维护
    • 完善的错误处理
    • 良好的代码注释

项目已成功完成,为企业提供了一个功能完整、性能优异、用户体验良好的绩效考核系统。

​编辑