From a2f48b6bf441b3d0b06d7ed7cfc06f32f49543bc Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Sun, 16 Nov 2025 02:49:23 +0800 Subject: [PATCH] feat: change the main logic of evaluation --- src/backend/evaluation/admin.py | 2 +- src/backend/evaluation/filters.py | 20 +- src/backend/evaluation/models.py | 11 +- src/backend/evaluation/serializers.py | 27 +- src/backend/evaluation/views.py | 526 +++++++++++---- src/fronted/src/router/index.js | 7 + src/fronted/src/services/api.js | 45 +- .../src/views/EvaluationRecordDetail.vue | 468 +++++++++++++ .../src/views/EvaluationRecordList.vue | 617 ++++-------------- 9 files changed, 1103 insertions(+), 620 deletions(-) create mode 100644 src/fronted/src/views/EvaluationRecordDetail.vue diff --git a/src/backend/evaluation/admin.py b/src/backend/evaluation/admin.py index c41195e..229db37 100644 --- a/src/backend/evaluation/admin.py +++ b/src/backend/evaluation/admin.py @@ -10,5 +10,5 @@ class EvaluationRecordAdmin(admin.ModelAdmin): 'bonus_score', 'deduction_score', 'total_score', 'created_at' ] list_filter = ['department', 'evaluation_date', 'created_at'] - search_fields = ['personnel__name', 'item_description', 'remarks'] + search_fields = ['personnel', 'item_description', 'remarks'] ordering = ['-evaluation_date', '-created_at'] diff --git a/src/backend/evaluation/filters.py b/src/backend/evaluation/filters.py index 2df3e91..a953a00 100644 --- a/src/backend/evaluation/filters.py +++ b/src/backend/evaluation/filters.py @@ -1,6 +1,5 @@ import django_filters from finance.models import Department -from personnel.models import Personnel from .models import EvaluationRecord @@ -11,23 +10,18 @@ class EvaluationRecordFilter(django_filters.FilterSet): field_name='department', label='部门' ) - personnel = django_filters.ModelChoiceFilter( - queryset=Personnel.objects.all(), + personnel = django_filters.CharFilter( field_name='personnel', + lookup_expr='icontains', label='人员' ) - date_from = django_filters.DateFilter( - field_name='evaluation_date', - lookup_expr='gte', - label='开始日期' - ) - date_to = django_filters.DateFilter( - field_name='evaluation_date', - lookup_expr='lte', - label='结束日期' + grade = django_filters.CharFilter( + field_name='grade', + lookup_expr='icontains', + label='年级' ) class Meta: model = EvaluationRecord - fields = ['department', 'personnel', 'evaluation_date'] + fields = ['department', 'personnel', 'grade'] diff --git a/src/backend/evaluation/models.py b/src/backend/evaluation/models.py index a13fd88..52834f4 100644 --- a/src/backend/evaluation/models.py +++ b/src/backend/evaluation/models.py @@ -10,12 +10,8 @@ class EvaluationRecord(models.Model): related_name='evaluation_records', verbose_name='所属部门' ) - personnel = models.ForeignKey( - 'personnel.Personnel', - on_delete=models.CASCADE, - related_name='evaluation_records', - verbose_name='人员' - ) + personnel = models.CharField(max_length=100, verbose_name='人员') + grade = models.CharField(max_length=50, blank=True, verbose_name='年级') item_description = models.CharField(max_length=255, verbose_name='加/扣分事项说明') bonus_score = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='加分数值') deduction_score = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='扣分数值') @@ -32,10 +28,11 @@ class EvaluationRecord(models.Model): indexes = [ models.Index(fields=['department', 'evaluation_date']), models.Index(fields=['personnel', 'evaluation_date']), + models.Index(fields=['evaluation_date']), ] def __str__(self): - return f'{self.evaluation_date} {self.personnel.name} ({self.total_score})' + return f'{self.evaluation_date} {self.personnel} ({self.total_score})' def save(self, *args, **kwargs): self.total_score = (self.bonus_score or 0) - (self.deduction_score or 0) diff --git a/src/backend/evaluation/serializers.py b/src/backend/evaluation/serializers.py index 9f4db7a..676e1f5 100644 --- a/src/backend/evaluation/serializers.py +++ b/src/backend/evaluation/serializers.py @@ -1,18 +1,39 @@ from rest_framework import serializers +from django.db.models import Sum, Count, Q from .models import EvaluationRecord class EvaluationRecordSerializer(serializers.ModelSerializer): department_name = serializers.CharField(source='department.name', read_only=True) - personnel_name = serializers.CharField(source='personnel.name', read_only=True) + personnel_name = serializers.CharField(source='personnel', read_only=True) + + def validate_department(self, value): + """验证部门字段""" + if value is None: + raise serializers.ValidationError('部门不能为空') + if isinstance(value, dict): + raise serializers.ValidationError('部门必须是一个有效的ID,不能是对象') + return value class Meta: model = EvaluationRecord fields = [ - 'id', 'department', 'department_name', 'personnel', 'personnel_name', + 'id', 'department', 'department_name', 'personnel', 'personnel_name', 'grade', 'item_description', 'bonus_score', 'deduction_score', 'remarks', 'total_score', 'evaluation_date', 'created_at', 'updated_at' ] - read_only_fields = ['total_score', 'created_at', 'updated_at'] + read_only_fields = ['id', 'total_score', 'created_at', 'updated_at'] + + +class PersonnelSummarySerializer(serializers.Serializer): + """人员汇总序列化器""" + personnel = serializers.CharField() + department_name = serializers.CharField() + grade = serializers.CharField() + total_bonus = serializers.DecimalField(max_digits=10, decimal_places=2) + total_deduction = serializers.DecimalField(max_digits=10, decimal_places=2) + total_score = serializers.DecimalField(max_digits=10, decimal_places=2) + bonus_count = serializers.IntegerField() + deduction_count = serializers.IntegerField() diff --git a/src/backend/evaluation/views.py b/src/backend/evaluation/views.py index ab55a25..25a8471 100644 --- a/src/backend/evaluation/views.py +++ b/src/backend/evaluation/views.py @@ -5,6 +5,7 @@ from decimal import Decimal import os from django.db import transaction +from django.db.models import Sum, Count, Q from django.http import HttpResponse from django.utils import timezone from django_filters.rest_framework import DjangoFilterBackend @@ -15,72 +16,243 @@ from rest_framework.response import Response from rest_framework_simplejwt.authentication import JWTAuthentication from openpyxl import load_workbook +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill from finance.models import Department -from personnel.models import Personnel from .filters import EvaluationRecordFilter from .models import EvaluationRecord -from .serializers import EvaluationRecordSerializer +from .serializers import EvaluationRecordSerializer, PersonnelSummarySerializer class EvaluationRecordViewSet(viewsets.ModelViewSet): """考评记录视图集""" authentication_classes = [JWTAuthentication] - queryset = EvaluationRecord.objects.select_related('department', 'personnel').all() + queryset = EvaluationRecord.objects.select_related('department').all() serializer_class = EvaluationRecordSerializer filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = EvaluationRecordFilter - search_fields = ['item_description', 'remarks', 'personnel__name', 'department__name'] + search_fields = ['item_description', 'remarks', 'personnel', 'department__name', 'grade'] ordering_fields = ['evaluation_date', 'created_at', 'total_score', 'bonus_score', 'deduction_score'] ordering = ['-evaluation_date', '-created_at'] + @action(detail=False, methods=['get'], url_path='personnel-summary') + def personnel_summary(self, request, *args, **kwargs): + """获取人员汇总列表""" + queryset = self.filter_queryset(self.get_queryset()) + + # 按人员、部门、年级分组汇总 + summary_data = queryset.values('personnel', 'department__name', 'grade').annotate( + total_bonus=Sum('bonus_score'), + total_deduction=Sum('deduction_score'), + bonus_count=Count('id', filter=Q(bonus_score__gt=0)), + deduction_count=Count('id', filter=Q(deduction_score__gt=0)), + ).order_by('department__name', 'personnel') + + # 计算总分 + result = [] + for item in summary_data: + total_score = (item['total_bonus'] or Decimal('0')) - (item['total_deduction'] or Decimal('0')) + result.append({ + 'personnel': item['personnel'], + 'department_name': item['department__name'] or '', + 'grade': item['grade'] or '', + 'total_bonus': item['total_bonus'] or Decimal('0'), + 'total_deduction': item['total_deduction'] or Decimal('0'), + 'total_score': total_score, + 'bonus_count': item['bonus_count'], + 'deduction_count': item['deduction_count'], + }) + + serializer = PersonnelSummarySerializer(result, many=True) + return Response(serializer.data) + + @action(detail=False, methods=['get'], url_path='personnel-records') + def personnel_records(self, request, *args, **kwargs): + """获取某个人员的所有记录""" + personnel_name = request.query_params.get('personnel') + if not personnel_name: + return Response({'detail': '缺少personnel参数'}, status=status.HTTP_400_BAD_REQUEST) + queryset = self.get_queryset().filter(personnel=personnel_name).order_by('-evaluation_date') + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + @action(detail=False, methods=['delete'], url_path='delete-personnel') + def delete_personnel(self, request, *args, **kwargs): + """删除某个人员的所有记录""" + personnel_name = request.query_params.get('personnel') + department_name = request.query_params.get('department') + grade = request.query_params.get('grade', '') + + if not personnel_name: + return Response({'detail': '缺少personnel参数'}, status=status.HTTP_400_BAD_REQUEST) + + # 构建查询条件 + filter_params = {'personnel': personnel_name} + if department_name: + filter_params['department__name'] = department_name + if grade: + filter_params['grade'] = grade + + # 查找要删除的记录 + queryset = self.get_queryset().filter(**filter_params) + deleted_count = queryset.count() + + if deleted_count == 0: + return Response({'detail': '未找到要删除的记录'}, status=status.HTTP_404_NOT_FOUND) + + # 删除记录 + queryset.delete() + + return Response({ + 'detail': f'成功删除 {deleted_count} 条记录', + 'deleted_count': deleted_count + }, status=status.HTTP_200_OK) + @action(detail=False, methods=['get'], url_path='export') def export_records(self, request, *args, **kwargs): + """导出人员考评记录,每个人一个表格""" queryset = self.filter_queryset(self.get_queryset()) if not queryset.exists(): return Response({'detail': '暂无数据可导出'}, status=status.HTTP_400_BAD_REQUEST) - filename = f'evaluation_records_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv' - response = HttpResponse(content_type='text/csv; charset=utf-8') - response['Content-Disposition'] = f'attachment; filename="{filename}"' - response.write('\ufeff') - - writer = csv.writer(response) - headers = [ - 'id', - 'department_id', - 'department_name', - 'personnel_id', - 'personnel_name', - 'item_description', - 'bonus_score', - 'deduction_score', - 'total_score', - 'evaluation_date', - 'remarks', - 'created_at', - 'updated_at', - ] - writer.writerow(headers) + # 创建Excel工作簿 + wb = Workbook() + wb.remove(wb.active) # 删除默认工作表 + # 按人员分组 + personnel_groups = {} for record in queryset: - writer.writerow([ - record.id, - record.department_id, - record.department.name if record.department else '', - record.personnel_id, - record.personnel.name if record.personnel else '', - record.item_description, - f'{record.bonus_score:.2f}', - f'{record.deduction_score:.2f}', - f'{record.total_score:.2f}', - record.evaluation_date.isoformat() if record.evaluation_date else '', - record.remarks or '', - record.created_at.isoformat(sep=' ') if record.created_at else '', - record.updated_at.isoformat(sep=' ') if record.updated_at else '', - ]) + personnel_key = f"{record.personnel}_{record.department.name if record.department else ''}" + if personnel_key not in personnel_groups: + personnel_groups[personnel_key] = { + 'personnel': record.personnel, + 'department_name': record.department.name if record.department else '', + 'grade': record.grade or '', + 'records': [] + } + personnel_groups[personnel_key]['records'].append(record) + # 样式定义 + header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") + header_font = Font(bold=True, color="FFFFFF") + summary_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid") + summary_font = Font(bold=True) + + # 为每个人创建表格 + for idx, (key, group) in enumerate(personnel_groups.items(), 1): + ws = wb.create_sheet(title=f"{group['personnel']}_{idx}") + + # 计算统计信息 + total_bonus = sum(record.bonus_score for record in group['records']) + total_deduction = sum(record.deduction_score for record in group['records']) + total_score = total_bonus - total_deduction + bonus_count = sum(1 for record in group['records'] if record.bonus_score > 0) + deduction_count = sum(1 for record in group['records'] if record.deduction_score > 0) + + # 基本信息行 + ws.append(['基本信息']) + ws.append(['部门', group['department_name']]) + ws.append(['姓名', group['personnel']]) + ws.append(['年级', group['grade']]) + ws.append([]) + + # 统计信息行 + ws.append(['统计信息']) + ws.append(['总加分', f'{total_bonus:.2f}']) + ws.append(['总扣分', f'{total_deduction:.2f}']) + ws.append(['加分次数', bonus_count]) + ws.append(['扣分次数', deduction_count]) + ws.append([]) + + # 记录表头 + headers = ['扣分/加分说明', '考评时间', '分值', '备注'] + ws.append(headers) + + # 设置表头样式 + for col in range(1, len(headers) + 1): + cell = ws.cell(row=ws.max_row, column=col) + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal='center', vertical='center') + + # 添加记录数据 + for record in sorted(group['records'], key=lambda x: x.evaluation_date, reverse=True): + score_value = '' + if record.bonus_score > 0: + score_value = f"+{record.bonus_score:.2f}" + elif record.deduction_score > 0: + score_value = f"-{record.deduction_score:.2f}" + + ws.append([ + record.item_description, + record.evaluation_date.strftime('%Y-%m-%d') if record.evaluation_date else '', + score_value, + record.remarks or '', + ]) + + # 设置列宽 + ws.column_dimensions['A'].width = 30 + ws.column_dimensions['B'].width = 15 + ws.column_dimensions['C'].width = 12 + ws.column_dimensions['D'].width = 30 + + # 保存到内存 + output = io.BytesIO() + wb.save(output) + output.seek(0) + + filename = f'人员考评记录_{timezone.now().strftime("%Y%m%d_%H%M%S")}.xlsx' + response = HttpResponse( + output.getvalue(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = f'attachment; filename="{filename}"' + return response + + @action(detail=False, methods=['get'], url_path='download-template') + def download_template(self, request, *args, **kwargs): + """下载导入样表""" + # 创建Excel工作簿 + wb = Workbook() + ws = wb.active + ws.title = '人员导入样表' + + # 表头 + headers = ['部门', '年级', '姓名'] + ws.append(headers) + + # 设置表头样式 + header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") + header_font = Font(bold=True, color="FFFFFF") + for col in range(1, len(headers) + 1): + cell = ws.cell(row=1, column=col) + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal='center', vertical='center') + + # 添加示例数据行 + ws.append(['程序部', '24', '张三']) + ws.append(['Web部', '23', '李四']) + + + # 设置列宽 + ws.column_dimensions['A'].width = 20 + ws.column_dimensions['B'].width = 15 + ws.column_dimensions['C'].width = 15 + + # 保存到内存 + output = io.BytesIO() + wb.save(output) + output.seek(0) + + filename = f'人员导入样表_{timezone.now().strftime("%Y%m%d")}.xlsx' + response = HttpResponse( + output.getvalue(), + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = f'attachment; filename="{filename}"' return response @action(detail=False, methods=['post'], url_path='import') @@ -112,62 +284,205 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet): if not records_data: return Response({'detail': '导入文件没有数据'}, status=status.HTTP_400_BAD_REQUEST) - reader_fieldnames = records_data[0].keys() if isinstance(records_data[0], dict) else [] - required_columns = { - 'id', - 'department_id', - 'department_name', - 'personnel_id', - 'personnel_name', - 'item_description', - 'bonus_score', - 'deduction_score', - 'evaluation_date', + # 字段名映射:中文列名 -> 英文字段名 + field_mapping = { + '部门': 'department_name', + '所属部门': 'department_name', + 'department_name': 'department_name', + '姓名': 'personnel_name', + 'personnel_name': 'personnel_name', + '年级': 'grade', + 'grade': 'grade', + '扣分/加分说明': 'item_description', + 'item_description': 'item_description', + '加分': 'bonus_score', + 'bonus_score': 'bonus_score', + '扣分': 'deduction_score', + 'deduction_score': 'deduction_score', + '考评日期': 'evaluation_date', + 'evaluation_date': 'evaluation_date', + '考评时间': 'evaluation_date', + '备注': 'remarks', + 'remarks': 'remarks', + '分值': 'score', } - missing_columns = required_columns - set(reader_fieldnames) - if missing_columns: - return Response( - {'detail': f'缺少必要的列: {", ".join(sorted(missing_columns))}'}, - status=status.HTTP_400_BAD_REQUEST, - ) + + reader_fieldnames = records_data[0].keys() if isinstance(records_data[0], dict) else [] + # 获取实际存在的字段名(可能是中文或英文) + available_field_keys = set() + for field_name in reader_fieldnames: + # 去除首尾空格后查找映射 + field_name_clean = field_name.strip() if field_name else '' + mapped_key = field_mapping.get(field_name_clean) + if mapped_key: + available_field_keys.add(mapped_key) + + # 检查是样表格式(只有部门、年级、姓名)还是完整格式(有考评记录) + # 样表格式:必须包含部门名和姓名,且不包含扣分/加分说明和考评日期 + is_template_format = ( + 'department_name' in available_field_keys and + 'personnel_name' in available_field_keys and + 'item_description' not in available_field_keys and + 'evaluation_date' not in available_field_keys + ) + + if not is_template_format: + # 完整格式需要检查必要的列 + required_fields = {'department_name', 'personnel_name', 'item_description', + 'evaluation_date'} + missing_fields = required_fields - available_field_keys + if missing_fields: + # 将英文字段名转换为中文显示 + field_name_map = { + 'department_name': '部门', + 'personnel_name': '姓名', + 'item_description': '扣分/加分说明', + 'evaluation_date': '考评日期', + } + missing_display = [field_name_map.get(f, f) for f in sorted(missing_fields)] + return Response( + {'detail': f'缺少必要的列: {", ".join(missing_display)}'}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get_field_value(row_dict, field_key): + """从行数据中获取字段值,支持中英文列名""" + # 收集所有可能的中文列名(去除首尾空格匹配) + possible_keys = [field_key] # 先尝试英文字段名 + + # 找到所有映射到该字段的中文列名 + for chinese, english in field_mapping.items(): + if english == field_key: + possible_keys.append(chinese) + + # 尝试所有可能的键(包括去除空格后的键) + for key in possible_keys: + # 直接匹配 + if key in row_dict: + value = row_dict[key] + if value is not None: + return value + # 去除空格后匹配(忽略键名和值的前后空格) + for row_key in row_dict.keys(): + if row_key and str(row_key).strip() == key.strip(): + value = row_dict[row_key] + if value is not None: + return value + + return None records_to_create = [] - seen_ids = set() errors = [] + skipped_count = 0 for idx, row in enumerate(records_data, start=2): try: if not isinstance(row, dict): raise ValueError('数据格式不正确') - record_id = int(row['id']) if row.get('id') else None - if record_id in seen_ids: - raise ValueError('表中存在重复的记录 ID') - seen_ids.add(record_id) + # 根据 department_name 查找部门(支持中英文列名) + department_name = get_field_value(row, 'department_name') + if department_name is None: + # 显示实际存在的列名,帮助调试 + available_keys = list(row.keys()) + raise ValueError(f'部门名称不能为空。当前行的列名: {available_keys}') + department_name = str(department_name).strip() + if not department_name: + available_keys = list(row.keys()) + raise ValueError(f'部门名称不能为空。当前行的列名: {available_keys}') + department = Department.objects.filter(name=department_name).first() + if not department: + raise ValueError(f'找不到部门: {department_name}') - department = self._resolve_department(row.get('department_id'), row.get('department_name')) - personnel = self._resolve_personnel(row.get('personnel_id'), row.get('personnel_name')) + # personnel_name 直接作为字符串存储(支持中英文列名) + personnel_name = str(get_field_value(row, 'personnel_name') or '').strip() + if not personnel_name: + raise ValueError('人员名称不能为空') - evaluation_date = datetime.strptime(row['evaluation_date'], '%Y-%m-%d').date() - bonus_score = Decimal(row.get('bonus_score') or '0') - deduction_score = Decimal(row.get('deduction_score') or '0') - remarks = row.get('remarks', '') + # 年级(可选) + grade = str(get_field_value(row, 'grade') or '').strip() - created_at = self._parse_datetime(row.get('created_at')) - updated_at = self._parse_datetime(row.get('updated_at')) + # 检查是否已存在相同姓名、年级、部门的人员记录 + existing_records = EvaluationRecord.objects.filter( + personnel=personnel_name, + grade=grade, + department=department + ).exists() + + if existing_records: + skipped_count += 1 + continue # 跳过已存在的人员 + # 如果是样表格式(只有部门、年级、姓名),创建初始记录(总分为39) + if is_template_format: + evaluation_date = timezone.now().date() + item_description = '初始分数' + bonus_score = Decimal('39') + deduction_score = Decimal('0') + remarks = '' + else: + # 完整格式,从文件中读取 + # 解析日期,支持多种格式(支持中英文列名) + evaluation_date_value = get_field_value(row, 'evaluation_date') + if evaluation_date_value is None: + raise ValueError('考评日期不能为空') + + # 如果已经是date类型,直接使用 + if isinstance(evaluation_date_value, date): + evaluation_date = evaluation_date_value + else: + evaluation_date_str = str(evaluation_date_value).strip() + evaluation_date = None + for date_format in ('%Y-%m-%d', '%Y/%m/%d', '%Y-%m-%d %H:%M:%S', '%Y/%m/%d %H:%M:%S'): + try: + evaluation_date = datetime.strptime(evaluation_date_str, date_format).date() + break + except ValueError: + continue + if evaluation_date is None: + raise ValueError(f'无法解析日期格式: {evaluation_date_str}') + + # 获取加分和扣分(支持中英文列名) + # 支持两种格式:1. 分别的加分/扣分列 2. 统一的分值列(+为加分,-为扣分) + score_value = get_field_value(row, 'score') + bonus_score = Decimal('0') + deduction_score = Decimal('0') + + if score_value: + # 从分值列解析 + score_str = str(score_value).strip() + if score_str.startswith('+'): + bonus_score = Decimal(str(score_str[1:]) or '0') + elif score_str.startswith('-'): + deduction_score = Decimal(str(score_str[1:]) or '0') + else: + # 尝试解析为数字 + try: + score_decimal = Decimal(score_str) + if score_decimal >= 0: + bonus_score = score_decimal + else: + deduction_score = abs(score_decimal) + except (ValueError, TypeError): + pass + else: + # 从分别的列读取 + bonus_score = Decimal(str(get_field_value(row, 'bonus_score') or '0')) + deduction_score = Decimal(str(get_field_value(row, 'deduction_score') or '0')) + + item_description = str(get_field_value(row, 'item_description') or '').strip() + remarks = str(get_field_value(row, 'remarks') or '').strip() + + # total_score 会在 save() 方法中自动计算,不需要手动设置 record = EvaluationRecord( - id=record_id, department=department, - personnel=personnel, - item_description=row.get('item_description', ''), + personnel=personnel_name, + grade=grade, + item_description=item_description, bonus_score=bonus_score, deduction_score=deduction_score, - total_score=bonus_score - deduction_score, evaluation_date=evaluation_date, remarks=remarks, - created_at=created_at or timezone.now(), - updated_at=updated_at or timezone.now(), ) records_to_create.append(record) except Exception as exc: # pylint: disable=broad-except @@ -176,44 +491,24 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet): if errors: return Response({'detail': '导入失败', 'errors': errors}, status=status.HTTP_400_BAD_REQUEST) + if not records_to_create: + skip_msg = f'跳过 {skipped_count} 条已存在的记录' if skipped_count > 0 else '' + return Response( + {'detail': f'没有新数据需要导入。{skip_msg}'}, + status=status.HTTP_200_OK + ) + with transaction.atomic(): - EvaluationRecord.objects.all().delete() + if not is_template_format: + # 完整格式导入时,先删除所有记录(保持原有行为) + EvaluationRecord.objects.all().delete() EvaluationRecord.objects.bulk_create(records_to_create) - return Response({'detail': f'成功导入 {len(records_to_create)} 条考评记录'}, status=status.HTTP_200_OK) + skip_msg = f',跳过 {skipped_count} 条已存在的记录' if skipped_count > 0 else '' + return Response({ + 'detail': f'成功导入 {len(records_to_create)} 条记录{skip_msg}' + }, status=status.HTTP_200_OK) - @staticmethod - def _parse_datetime(value): - if not value: - return None - for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S.%f'): - try: - return datetime.strptime(value, fmt) - except ValueError: - continue - raise ValueError('无法解析日期时间字段') - - @staticmethod - def _resolve_department(department_id, department_name): - department = None - if department_id: - department = Department.objects.filter(id=department_id).first() - if not department and department_name: - department = Department.objects.filter(name=department_name).first() - if not department: - raise ValueError('无法匹配部门') - return department - - @staticmethod - def _resolve_personnel(personnel_id, personnel_name): - personnel = None - if personnel_id: - personnel = Personnel.objects.filter(id=personnel_id).first() - if not personnel and personnel_name: - personnel = Personnel.objects.filter(name=personnel_name).first() - if not personnel: - raise ValueError('无法匹配人员') - return personnel @staticmethod def _read_excel(file_bytes): @@ -238,13 +533,16 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet): if not header: continue value = row[col_idx] if col_idx < len(row) else None - header_key = header.strip().lower() + header_stripped = header.strip() + header_key = header_stripped.lower() + # 检查是否是日期列(支持中英文列名) + is_date_column = (header_key == 'evaluation_date' or header_stripped == '考评日期') if isinstance(value, datetime): value = value.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(value, date): value = value.strftime('%Y-%m-%d') - elif isinstance(value, (int, float)) and header_key in {'evaluation_date', 'created_at', 'updated_at'}: - value = EvaluationRecordViewSet._excel_date_to_iso(value, header_key != 'evaluation_date') + elif isinstance(value, (int, float)) and is_date_column: + value = EvaluationRecordViewSet._excel_date_to_iso(value, False) elif isinstance(value, Decimal): value = str(value) elif isinstance(value, float): diff --git a/src/fronted/src/router/index.js b/src/fronted/src/router/index.js index 6190962..147e33b 100644 --- a/src/fronted/src/router/index.js +++ b/src/fronted/src/router/index.js @@ -13,6 +13,7 @@ import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupMan import Settings from '../views/Settings.vue' import Memo from '../views/Memo.vue' import EvaluationRecordList from '../views/EvaluationRecordList.vue' +import EvaluationRecordDetail from '../views/EvaluationRecordDetail.vue' import { authService } from '@/services/api' const routes = [ @@ -87,6 +88,12 @@ const routes = [ path: '/evaluations', name: 'EvaluationRecordList', component: EvaluationRecordList + }, + { + path: '/evaluations/:personnel', + name: 'EvaluationRecordDetail', + component: EvaluationRecordDetail, + props: true } ] diff --git a/src/fronted/src/services/api.js b/src/fronted/src/services/api.js index 6b75c8f..38a9764 100644 --- a/src/fronted/src/services/api.js +++ b/src/fronted/src/services/api.js @@ -431,21 +431,28 @@ export const projectGroupService = { // 考评记录服务 export const evaluationService = { - // 获取考评记录列表 - getEvaluationRecords(params = {}) { - return apiClient.get('/evaluation-records/', { params }) + // 获取人员汇总列表 + getPersonnelSummary(params = {}) { + return apiClient.get('/evaluation-records/personnel-summary/', { params }) }, - // 导出考评记录总表 - exportEvaluationRecords(params = {}) { + // 获取某个人员的所有记录 + getPersonnelRecords(personnelName, params = {}) { + return apiClient.get('/evaluation-records/personnel-records/', { + params: { ...params, personnel: personnelName } + }) + }, + + // 导出人员考评记录 + exportPersonnelRecords(params = {}) { return apiClient.get('/evaluation-records/export/', { params, responseType: 'arraybuffer' }) }, - // 导入考评记录总表 - importEvaluationRecords(file) { + // 导入人员考评记录 + importPersonnelRecords(file) { const formData = new FormData() formData.append('file', file) return apiClient.post('/evaluation-records/import/', formData, { @@ -455,6 +462,18 @@ export const evaluationService = { }) }, + // 下载导入样表 + downloadTemplate() { + return apiClient.get('/evaluation-records/download-template/', { + responseType: 'arraybuffer' + }) + }, + + // 获取考评记录列表(用于详情页) + getEvaluationRecords(params = {}) { + return apiClient.get('/evaluation-records/', { params }) + }, + // 创建考评记录 createEvaluationRecord(payload) { return apiClient.post('/evaluation-records/', payload) @@ -468,6 +487,18 @@ export const evaluationService = { // 删除考评记录 deleteEvaluationRecord(id) { return apiClient.delete(`/evaluation-records/${id}/`) + }, + + // 删除某个人员的所有记录 + deletePersonnelRecords(personnelName, departmentName = '', grade = '') { + const params = { personnel: personnelName } + if (departmentName) { + params.department = departmentName + } + if (grade) { + params.grade = grade + } + return apiClient.delete('/evaluation-records/delete-personnel/', { params }) } } diff --git a/src/fronted/src/views/EvaluationRecordDetail.vue b/src/fronted/src/views/EvaluationRecordDetail.vue new file mode 100644 index 0000000..8ace897 --- /dev/null +++ b/src/fronted/src/views/EvaluationRecordDetail.vue @@ -0,0 +1,468 @@ + + + + + + diff --git a/src/fronted/src/views/EvaluationRecordList.vue b/src/fronted/src/views/EvaluationRecordList.vue index 4f27f06..ad55a95 100644 --- a/src/fronted/src/views/EvaluationRecordList.vue +++ b/src/fronted/src/views/EvaluationRecordList.vue @@ -11,21 +11,17 @@ 重置筛选 - - - 新增记录 - - - - 查看总表 + + + 下载样表 - 导出总表 + 导出人员 - 导入总表 + 导入人员 - - + - - + - - - - + - - - - - - - - - - - - + stripe + table-layout="auto"> + + + + - @@ -130,250 +96,60 @@
- 记录数:{{ records.length }} - 总加分:{{ formatNumber(totalBonus) }} - 总扣分:{{ formatNumber(totalDeduction) }} - 综合分:{{ formatNumber(totalScore) }} + 人员数:{{ personnelList.length }} + 平均分:{{ averageScore }}
- - -
- - - - - - - - - - - - -
-
- 暂无数据 -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -