feat: change the main logic of evaluation
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+405
-107
@@ -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 '',
|
||||
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,
|
||||
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.evaluation_date.strftime('%Y-%m-%d') if record.evaluation_date else '',
|
||||
score_value,
|
||||
record.remarks or '',
|
||||
record.created_at.isoformat(sep=' ') if record.created_at else '',
|
||||
record.updated_at.isoformat(sep=' ') if record.updated_at else '',
|
||||
])
|
||||
|
||||
# 设置列宽
|
||||
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:
|
||||
|
||||
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(sorted(missing_columns))}'},
|
||||
{'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():
|
||||
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):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader/>
|
||||
<div class="detail-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-button @click="$router.go(-1)">
|
||||
<el-icon><ArrowLeft/></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<span>考评记录详情</span>
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="info-section">
|
||||
<h3>基本信息</h3>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="部门">{{ personnelInfo.department_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年级">{{ personnelInfo.grade || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ personnelInfo.personnel }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="statistics-section">
|
||||
<h3>统计信息</h3>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="总分" :value="personnelInfo.total_score">
|
||||
<template #suffix>
|
||||
<span class="bonus-text">分</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-statistic title="总加分数" :value="personnelInfo.total_bonus - 39">
|
||||
<template #suffix>
|
||||
<span class="bonus-text">分</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-statistic title="总扣分数" :value="personnelInfo.total_deduction">
|
||||
<template #suffix>
|
||||
<span class="deduction-text">分</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-statistic title="加分次数" :value="personnelInfo.bonus_count">
|
||||
<template #suffix>
|
||||
<span>次</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-statistic title="扣分次数" :value="personnelInfo.deduction_count">
|
||||
<template #suffix>
|
||||
<span>次</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<div class="records-section">
|
||||
<div class="section-header">
|
||||
<h3>考评记录</h3>
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus/></el-icon>
|
||||
新增记录
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="records"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe>
|
||||
<el-table-column prop="item_description" label="扣分/加分说明" min-width="200" show-overflow-tooltip/>
|
||||
<el-table-column prop="evaluation_date" label="考评时间" width="120"/>
|
||||
<el-table-column label="分值" width="120">
|
||||
<template #default="scope">
|
||||
<span :class="getScoreClass(scope.row)">
|
||||
{{ formatScore(scope.row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remarks" label="备注" min-width="200" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openEditDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="confirmDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑记录' : '新增记录'"
|
||||
v-model="dialogVisible"
|
||||
width="600px"
|
||||
destroy-on-close>
|
||||
<el-form
|
||||
ref="recordFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="120px">
|
||||
<el-form-item label="扣分/加分说明" prop="item_description">
|
||||
<el-input
|
||||
v-model="form.item_description"
|
||||
placeholder="请输入扣分/加分说明"
|
||||
maxlength="255"
|
||||
show-word-limit/>
|
||||
</el-form-item>
|
||||
<el-form-item label="考评时间" prop="evaluation_date">
|
||||
<el-date-picker
|
||||
v-model="form.evaluation_date"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" prop="score">
|
||||
<el-input-number
|
||||
v-model="form.score"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
controls-position="right"
|
||||
placeholder="正数为加分,负数为扣分"
|
||||
style="width: 100%"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="form.remarks"
|
||||
placeholder="可填写附加说明"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="255"
|
||||
show-word-limit/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">
|
||||
{{ isEditing ? '保存修改' : '创建记录' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { evaluationService, financeService } from '@/services/api'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'EvaluationRecordDetail',
|
||||
components: {
|
||||
AppHeader,
|
||||
ArrowLeft,
|
||||
Plus
|
||||
},
|
||||
props: {
|
||||
personnel: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
personnelInfo: {
|
||||
personnel: '',
|
||||
department_name: '',
|
||||
grade: '',
|
||||
total_bonus: 0,
|
||||
total_deduction: 0,
|
||||
bonus_count: 0,
|
||||
deduction_count: 0
|
||||
},
|
||||
records: [],
|
||||
dialogVisible: false,
|
||||
isEditing: false,
|
||||
currentRecordId: null,
|
||||
form: this.getEmptyForm(),
|
||||
formRules: {
|
||||
item_description: [
|
||||
{ required: true, message: '请输入扣分/加分说明', trigger: 'blur' },
|
||||
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
|
||||
],
|
||||
evaluation_date: [{ required: true, message: '请选择考评时间', trigger: 'change' }],
|
||||
score: [{ required: true, message: '请输入分值', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
try {
|
||||
// 从路由参数或props获取人员姓名,确保正确解码
|
||||
let personnelName = this.personnel
|
||||
if (!personnelName && this.$route.params.personnel) {
|
||||
const paramValue = this.$route.params.personnel
|
||||
try {
|
||||
// 先尝试解码(处理URL编码的情况)
|
||||
personnelName = decodeURIComponent(paramValue)
|
||||
} catch (e) {
|
||||
// 如果解码失败,可能是已经被解码过了或者是其他格式,直接使用
|
||||
personnelName = paramValue
|
||||
}
|
||||
// 如果解码后还是编码格式(包含%),再次尝试解码
|
||||
if (personnelName.includes('%')) {
|
||||
try {
|
||||
personnelName = decodeURIComponent(personnelName)
|
||||
} catch (e2) {
|
||||
// 忽略解码错误
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!personnelName) {
|
||||
personnelName = ''
|
||||
}
|
||||
const department = this.$route.query.department || ''
|
||||
const grade = this.$route.query.grade || ''
|
||||
|
||||
// 获取人员信息和记录
|
||||
const [summaryResp, recordsResp] = await Promise.all([
|
||||
evaluationService.getPersonnelSummary({ personnel: personnelName }),
|
||||
evaluationService.getPersonnelRecords(personnelName)
|
||||
])
|
||||
|
||||
if (summaryResp.data && summaryResp.data.length > 0) {
|
||||
this.personnelInfo = summaryResp.data[0]
|
||||
// 确保personnel字段是正确的(后端返回的应该是正确的)
|
||||
if (this.personnelInfo.personnel && this.personnelInfo.personnel.includes('%')) {
|
||||
try {
|
||||
this.personnelInfo.personnel = decodeURIComponent(this.personnelInfo.personnel)
|
||||
} catch (e) {
|
||||
// 忽略解码错误
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.personnelInfo = {
|
||||
personnel: personnelName, // 此时personnelName应该已经是解码后的
|
||||
department_name: department,
|
||||
grade: grade,
|
||||
total_bonus: 0,
|
||||
total_deduction: 0,
|
||||
bonus_count: 0,
|
||||
deduction_count: 0
|
||||
}
|
||||
}
|
||||
|
||||
this.records = recordsResp.data || []
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getEmptyForm() {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return {
|
||||
item_description: '',
|
||||
evaluation_date: today,
|
||||
score: 0,
|
||||
remarks: ''
|
||||
}
|
||||
},
|
||||
openCreateDialog() {
|
||||
this.isEditing = false
|
||||
this.currentRecordId = null
|
||||
this.form = this.getEmptyForm()
|
||||
this.dialogVisible = true
|
||||
},
|
||||
openEditDialog(record) {
|
||||
this.isEditing = true
|
||||
this.currentRecordId = record.id
|
||||
const score = Number(record.bonus_score || 0) > 0
|
||||
? Number(record.bonus_score)
|
||||
: -Number(record.deduction_score || 0)
|
||||
this.form = {
|
||||
item_description: record.item_description,
|
||||
evaluation_date: record.evaluation_date,
|
||||
score: score,
|
||||
remarks: record.remarks || ''
|
||||
}
|
||||
this.dialogVisible = true
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.$refs.recordFormRef.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
const personnelName = this.personnelInfo.personnel
|
||||
const score = Number(this.form.score) || 0
|
||||
|
||||
// 获取部门ID
|
||||
const departmentId = await this.getDepartmentId()
|
||||
if (!departmentId) {
|
||||
ElMessage.error('无法找到部门,请确保人员信息中包含有效的部门名称')
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
department: departmentId,
|
||||
personnel: personnelName,
|
||||
grade: this.personnelInfo.grade || '',
|
||||
item_description: this.form.item_description,
|
||||
bonus_score: score > 0 ? score : 0,
|
||||
deduction_score: score < 0 ? Math.abs(score) : 0,
|
||||
evaluation_date: this.form.evaluation_date,
|
||||
remarks: this.form.remarks || ''
|
||||
}
|
||||
|
||||
if (this.isEditing && this.currentRecordId) {
|
||||
await evaluationService.updateEvaluationRecord(this.currentRecordId, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await evaluationService.createEvaluationRecord(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
this.dialogVisible = false
|
||||
await this.fetchData()
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
const errorMsg = error.response?.data?.detail || error.response?.data?.department?.[0] || '保存失败'
|
||||
ElMessage.error(errorMsg)
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
},
|
||||
async confirmDelete(record) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除这条记录吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await evaluationService.deleteEvaluationRecord(record.id)
|
||||
ElMessage.success('删除成功')
|
||||
await this.fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
async getDepartmentId() {
|
||||
// 根据部门名称查找部门ID
|
||||
try {
|
||||
const resp = await financeService.getAllDepartments()
|
||||
const dept = resp.data.find(d => d.name === this.personnelInfo.department_name)
|
||||
return dept ? dept.id : null
|
||||
} catch (error) {
|
||||
console.error('获取部门ID失败:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
formatScore(record) {
|
||||
if (record.bonus_score > 0) {
|
||||
return `+${Number(record.bonus_score).toFixed(2)}`
|
||||
} else if (record.deduction_score > 0) {
|
||||
return `-${Number(record.deduction_score).toFixed(2)}`
|
||||
}
|
||||
return '0.00'
|
||||
},
|
||||
getScoreClass(record) {
|
||||
if (record.bonus_score > 0) {
|
||||
return 'bonus-text'
|
||||
} else if (record.deduction_score > 0) {
|
||||
return 'deduction-text'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
formatNumber(value) {
|
||||
const num = Number(value || 0)
|
||||
return num.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-container {
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-section,
|
||||
.statistics-section,
|
||||
.records-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-section h3,
|
||||
.statistics-section h3,
|
||||
.records-section h3 {
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bonus-text {
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deduction-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,21 +11,17 @@
|
||||
<el-icon><Refresh/></el-icon>
|
||||
重置筛选
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus/></el-icon>
|
||||
新增记录
|
||||
</el-button>
|
||||
<el-button type="success" @click="openSummaryDialog">
|
||||
<el-icon><Search/></el-icon>
|
||||
查看总表
|
||||
<el-button type="success" :loading="downloadingTemplate" @click="handleDownloadTemplate">
|
||||
<el-icon><Download/></el-icon>
|
||||
下载样表
|
||||
</el-button>
|
||||
<el-button type="warning" :loading="exporting" @click="handleExport">
|
||||
<el-icon><Download/></el-icon>
|
||||
导出总表
|
||||
导出人员
|
||||
</el-button>
|
||||
<el-button type="info" :loading="importing" @click="triggerImport">
|
||||
<el-icon><UploadFilled/></el-icon>
|
||||
导入总表
|
||||
导入人员
|
||||
</el-button>
|
||||
<input
|
||||
ref="importInput"
|
||||
@@ -53,76 +49,46 @@
|
||||
:value="String(dept.id)"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="人员">
|
||||
<el-select
|
||||
v-model="filters.personnel"
|
||||
placeholder="选择人员"
|
||||
<el-form-item label="年级">
|
||||
<el-input
|
||||
v-model="filters.grade"
|
||||
placeholder="输入年级"
|
||||
style="min-width: 200px"
|
||||
clearable
|
||||
filterable
|
||||
@change="fetchRecords">
|
||||
<el-option
|
||||
v-for="person in personnel"
|
||||
:key="person.id"
|
||||
:label="person.name"
|
||||
:value="String(person.id)"/>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="fetchRecords"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="搜索">
|
||||
<el-form-item label="姓名">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="事项说明 / 备注 / 人员"
|
||||
v-model="filters.personnel"
|
||||
placeholder="输入人员姓名"
|
||||
style="min-width: 200px"
|
||||
clearable
|
||||
@change="fetchRecords">
|
||||
<template #prefix>
|
||||
<el-icon><Search/></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="records"
|
||||
:data="personnelList"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
stripe>
|
||||
<el-table-column prop="evaluation_date" label="考评日期" width="120"/>
|
||||
<el-table-column prop="department_name" label="部门" width="150"/>
|
||||
<el-table-column prop="personnel_name" label="姓名" width="120"/>
|
||||
<el-table-column prop="item_description" label="加/扣分事项说明" min-width="220" show-overflow-tooltip/>
|
||||
<el-table-column prop="bonus_score" label="加分" width="90">
|
||||
<template #default="scope">
|
||||
<span class="bonus-text">+{{ formatNumber(scope.row.bonus_score) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deduction_score" label="扣分" width="90">
|
||||
<template #default="scope">
|
||||
<span class="deduction-text">-{{ formatNumber(scope.row.deduction_score) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_score" label="总计分数" width="110">
|
||||
stripe
|
||||
table-layout="auto">
|
||||
<el-table-column prop="department_name" label="所属部门" min-width="150"/>
|
||||
<el-table-column prop="grade" label="年级" min-width="120"/>
|
||||
<el-table-column prop="personnel" label="姓名" min-width="120"/>
|
||||
<el-table-column label="目前总分" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.total_score >= 0 ? 'success' : 'danger'">
|
||||
{{ formatNumber(scope.row.total_score) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remarks" label="备注" min-width="160" show-overflow-tooltip/>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="openEditDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" @click="editPersonnel(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="confirmDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -130,250 +96,60 @@
|
||||
|
||||
<div class="summary-bar">
|
||||
<div>
|
||||
<span>记录数:{{ records.length }}</span>
|
||||
<span>总加分:{{ formatNumber(totalBonus) }}</span>
|
||||
<span>总扣分:{{ formatNumber(totalDeduction) }}</span>
|
||||
<span>综合分:{{ formatNumber(totalScore) }}</span>
|
||||
<span>人员数:{{ personnelList.length }}</span>
|
||||
<span>平均分:{{ averageScore }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
title="个人分数总表"
|
||||
v-model="summaryDialogVisible"
|
||||
width="640px"
|
||||
destroy-on-close>
|
||||
<div v-if="personSummary.length">
|
||||
<el-table
|
||||
:data="personSummary"
|
||||
border
|
||||
size="small"
|
||||
style="width: 100%">
|
||||
<el-table-column prop="name" label="姓名" min-width="140"/>
|
||||
<el-table-column label="总加分" width="120">
|
||||
<template #default="scope">
|
||||
<span class="bonus-text">+{{ formatNumber(scope.row.bonus) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总扣分" width="120">
|
||||
<template #default="scope">
|
||||
<span class="deduction-text">-{{ formatNumber(scope.row.deduction) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="综合分" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.total >= 0 ? 'success' : 'danger'">
|
||||
{{ formatNumber(scope.row.total) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="summary-empty">
|
||||
暂无数据
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="summary-footer">
|
||||
<div>
|
||||
<span>总加分:{{ formatNumber(summaryTotals.bonus) }}</span>
|
||||
<span>总扣分:{{ formatNumber(summaryTotals.deduction) }}</span>
|
||||
<span>综合分:{{ formatNumber(summaryTotals.total) }}</span>
|
||||
</div>
|
||||
<el-button @click="summaryDialogVisible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑考评记录' : '新增考评记录'"
|
||||
v-model="dialogVisible"
|
||||
width="520px"
|
||||
destroy-on-close>
|
||||
<el-form
|
||||
ref="recordFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
label-width="110px">
|
||||
<el-form-item label="所属部门" prop="department">
|
||||
<el-select v-model="form.department" placeholder="请选择部门" style="width: 260px">
|
||||
<el-option
|
||||
v-for="dept in departments"
|
||||
:key="dept.id"
|
||||
:label="dept.name"
|
||||
:value="String(dept.id)"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="人员" prop="personnel">
|
||||
<el-select v-model="form.personnel" placeholder="请选择人员" filterable style="width: 260px">
|
||||
<el-option
|
||||
v-for="person in personnel"
|
||||
:key="person.id"
|
||||
:label="person.name"
|
||||
:value="String(person.id)"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="考评日期" prop="evaluation_date">
|
||||
<el-date-picker
|
||||
v-model="form.evaluation_date"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
value-format="YYYY-MM-DD"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="事项说明" prop="item_description">
|
||||
<el-input
|
||||
v-model="form.item_description"
|
||||
placeholder="请输入加/扣分的事项说明"
|
||||
maxlength="255"
|
||||
show-word-limit/>
|
||||
</el-form-item>
|
||||
<el-form-item label="加分数值" prop="bonus_score">
|
||||
<el-input-number
|
||||
v-model="form.bonus_score"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="扣分数值" prop="deduction_score">
|
||||
<el-input-number
|
||||
v-model="form.deduction_score"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="form.remarks"
|
||||
placeholder="可填写附加说明"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="255"
|
||||
show-word-limit/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">
|
||||
{{ isEditing ? '保存修改' : '创建记录' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { evaluationService, financeService, personnelService } from '@/services/api'
|
||||
import { evaluationService, financeService } from '@/services/api'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Download, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { Refresh, Download, UploadFilled } from '@element-plus/icons-vue'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
export default {
|
||||
name: 'EvaluationRecordList',
|
||||
components: {
|
||||
AppHeader,
|
||||
Plus,
|
||||
Refresh,
|
||||
Search,
|
||||
Download,
|
||||
UploadFilled
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
saving: false,
|
||||
exporting: false,
|
||||
importing: false,
|
||||
records: [],
|
||||
downloadingTemplate: false,
|
||||
personnelList: [],
|
||||
departments: [],
|
||||
personnel: [],
|
||||
dialogVisible: false,
|
||||
summaryDialogVisible: false,
|
||||
isEditing: false,
|
||||
currentRecordId: null,
|
||||
filters: {
|
||||
department: '',
|
||||
personnel: '',
|
||||
keyword: '',
|
||||
dateRange: []
|
||||
},
|
||||
form: this.getEmptyForm(),
|
||||
formRules: {
|
||||
department: [{ required: true, message: '请选择部门', trigger: 'change' }],
|
||||
personnel: [{ required: true, message: '请选择人员', trigger: 'change' }],
|
||||
evaluation_date: [{ required: true, message: '请选择考评日期', trigger: 'change' }],
|
||||
item_description: [
|
||||
{ required: true, message: '请填写事项说明', trigger: 'blur' },
|
||||
{ min: 1, max: 255, message: '长度在 1 到 255 个字符', trigger: 'blur' }
|
||||
],
|
||||
bonus_score: [{ type: 'number', min: 0, message: '加分数值不能为负', trigger: 'change' }],
|
||||
deduction_score: [{ type: 'number', min: 0, message: '扣分数值不能为负', trigger: 'change' }]
|
||||
grade: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalBonus() {
|
||||
return this.records.reduce((sum, item) => sum + Number(item.bonus_score || 0), 0)
|
||||
},
|
||||
personSummary() {
|
||||
const summaryMap = new Map()
|
||||
this.records.forEach(record => {
|
||||
const id = this.extractPersonKey(record)
|
||||
const existing = summaryMap.get(id) || {
|
||||
id,
|
||||
name: this.resolvePersonName(record),
|
||||
bonus: 0,
|
||||
deduction: 0,
|
||||
total: 0
|
||||
averageScore() {
|
||||
if (!this.personnelList || this.personnelList.length === 0) {
|
||||
return '0.00'
|
||||
}
|
||||
existing.bonus += Number(record.bonus_score || 0)
|
||||
existing.deduction += Number(record.deduction_score || 0)
|
||||
existing.total += Number(record.total_score || 0)
|
||||
summaryMap.set(id, existing)
|
||||
})
|
||||
return Array.from(summaryMap.values()).sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'))
|
||||
},
|
||||
summaryTotals() {
|
||||
return this.personSummary.reduce(
|
||||
(acc, item) => {
|
||||
acc.bonus += item.bonus
|
||||
acc.deduction += item.deduction
|
||||
acc.total += item.total
|
||||
return acc
|
||||
},
|
||||
{ bonus: 0, deduction: 0, total: 0 }
|
||||
)
|
||||
},
|
||||
totalDeduction() {
|
||||
return this.records.reduce((sum, item) => sum + Number(item.deduction_score || 0), 0)
|
||||
},
|
||||
totalScore() {
|
||||
return this.records.reduce((sum, item) => sum + Number(item.total_score || 0), 0)
|
||||
const total = this.personnelList.reduce((sum, item) => sum + Number(item.total_score || 0), 0)
|
||||
const average = total / this.personnelList.length
|
||||
return average.toFixed(2)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await Promise.all([this.fetchDepartments(), this.fetchPersonnel()])
|
||||
await this.fetchDepartments()
|
||||
await this.fetchRecords()
|
||||
},
|
||||
methods: {
|
||||
getEmptyForm() {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return {
|
||||
department: '',
|
||||
personnel: '',
|
||||
evaluation_date: today,
|
||||
item_description: '',
|
||||
bonus_score: 0,
|
||||
deduction_score: 0,
|
||||
remarks: ''
|
||||
}
|
||||
},
|
||||
async fetchDepartments() {
|
||||
try {
|
||||
const resp = await financeService.getAllDepartments()
|
||||
@@ -383,36 +159,26 @@ export default {
|
||||
ElMessage.error('获取部门信息失败')
|
||||
}
|
||||
},
|
||||
async fetchPersonnel() {
|
||||
try {
|
||||
const resp = await personnelService.getAllPersonnel()
|
||||
this.personnel = resp.data
|
||||
} catch (error) {
|
||||
console.error('获取人员失败:', error)
|
||||
ElMessage.error('获取人员信息失败')
|
||||
}
|
||||
},
|
||||
buildQueryParams() {
|
||||
const params = {}
|
||||
const department = this.parseIdForApi(this.filters.department)
|
||||
const personnel = this.parseIdForApi(this.filters.personnel)
|
||||
if (department !== null) params.department = department
|
||||
if (personnel !== null) params.personnel = personnel
|
||||
if (this.filters.keyword) params.search = this.filters.keyword
|
||||
if (this.filters.dateRange && this.filters.dateRange.length === 2) {
|
||||
params.date_from = this.filters.dateRange[0]
|
||||
params.date_to = this.filters.dateRange[1]
|
||||
if (this.filters.personnel && this.filters.personnel.trim()) {
|
||||
params.personnel = this.filters.personnel.trim()
|
||||
}
|
||||
if (this.filters.grade && this.filters.grade.trim()) {
|
||||
params.grade = this.filters.grade.trim()
|
||||
}
|
||||
return params
|
||||
},
|
||||
async fetchRecords() {
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await evaluationService.getEvaluationRecords(this.buildQueryParams())
|
||||
this.records = resp.data
|
||||
const resp = await evaluationService.getPersonnelSummary(this.buildQueryParams())
|
||||
this.personnelList = resp.data
|
||||
} catch (error) {
|
||||
console.error('获取考评记录失败:', error)
|
||||
ElMessage.error('加载考评记录失败')
|
||||
console.error('获取人员列表失败:', error)
|
||||
ElMessage.error('加载人员列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -421,19 +187,72 @@ export default {
|
||||
this.filters = {
|
||||
department: '',
|
||||
personnel: '',
|
||||
keyword: '',
|
||||
dateRange: []
|
||||
grade: ''
|
||||
}
|
||||
this.fetchRecords()
|
||||
},
|
||||
openCreateDialog() {
|
||||
this.isEditing = false
|
||||
this.currentRecordId = null
|
||||
this.form = this.getEmptyForm()
|
||||
this.dialogVisible = true
|
||||
editPersonnel(row) {
|
||||
this.$router.push({
|
||||
name: 'EvaluationRecordDetail',
|
||||
params: {
|
||||
personnel: row.personnel
|
||||
},
|
||||
openSummaryDialog() {
|
||||
this.summaryDialogVisible = true
|
||||
query: {
|
||||
department: row.department_name,
|
||||
grade: row.grade || ''
|
||||
}
|
||||
})
|
||||
},
|
||||
async confirmDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除 ${row.personnel} 的所有考评记录吗?此操作不可恢复!`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 调用API删除该人员的所有记录
|
||||
await evaluationService.deletePersonnelRecords(
|
||||
row.personnel,
|
||||
row.department_name,
|
||||
row.grade || ''
|
||||
)
|
||||
|
||||
ElMessage.success('删除成功')
|
||||
// 刷新列表
|
||||
await this.fetchRecords()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
console.error('删除失败:', error)
|
||||
const errorMsg = error.response?.data?.detail || '删除失败'
|
||||
ElMessage.error(errorMsg)
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleDownloadTemplate() {
|
||||
this.downloadingTemplate = true
|
||||
try {
|
||||
const response = await evaluationService.downloadTemplate()
|
||||
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '人员导入样表.xlsx'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载样表失败:', error)
|
||||
ElMessage.error('下载样表失败')
|
||||
} finally {
|
||||
this.downloadingTemplate = false
|
||||
}
|
||||
},
|
||||
triggerImport() {
|
||||
this.$refs.importInput?.click()
|
||||
@@ -444,11 +263,11 @@ export default {
|
||||
const file = files[0]
|
||||
this.importing = true
|
||||
try {
|
||||
await evaluationService.importEvaluationRecords(file)
|
||||
await evaluationService.importPersonnelRecords(file)
|
||||
ElMessage.success('导入成功')
|
||||
await this.fetchRecords()
|
||||
} catch (error) {
|
||||
console.error('导入考评记录失败:', error)
|
||||
console.error('导入失败:', error)
|
||||
const detail = error.response?.data?.detail
|
||||
const errorList = error.response?.data?.errors
|
||||
let message = detail || '导入失败'
|
||||
@@ -461,163 +280,16 @@ export default {
|
||||
if (event.target) event.target.value = ''
|
||||
}
|
||||
},
|
||||
openEditDialog(record) {
|
||||
this.isEditing = true
|
||||
this.currentRecordId = record.id
|
||||
this.form = {
|
||||
department: this.toStringId(this.findDepartmentId(record)),
|
||||
personnel: this.toStringId(this.findPersonnelId(record)),
|
||||
evaluation_date: record.evaluation_date,
|
||||
item_description: record.item_description,
|
||||
bonus_score: Number(record.bonus_score),
|
||||
deduction_score: Number(record.deduction_score),
|
||||
remarks: record.remarks || ''
|
||||
}
|
||||
this.dialogVisible = true
|
||||
},
|
||||
handleSubmit() {
|
||||
this.$refs.recordFormRef.validate(async valid => {
|
||||
if (!valid) return
|
||||
|
||||
this.saving = true
|
||||
const payload = {
|
||||
...this.form,
|
||||
department: this.parseIdForApi(this.form.department),
|
||||
personnel: this.parseIdForApi(this.form.personnel),
|
||||
bonus_score: Number(this.form.bonus_score) || 0,
|
||||
deduction_score: Number(this.form.deduction_score) || 0
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.isEditing && this.currentRecordId) {
|
||||
await evaluationService.updateEvaluationRecord(this.currentRecordId, payload)
|
||||
ElMessage.success('更新考评记录成功')
|
||||
} else {
|
||||
await evaluationService.createEvaluationRecord(payload)
|
||||
ElMessage.success('新增考评记录成功')
|
||||
}
|
||||
this.dialogVisible = false
|
||||
await this.fetchRecords()
|
||||
} catch (error) {
|
||||
console.error('保存考评记录失败:', error)
|
||||
ElMessage.error('保存考评记录失败')
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
},
|
||||
async confirmDelete(record) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除 ${record.personnel_name} 在 ${record.evaluation_date} 的考评记录吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
await evaluationService.deleteEvaluationRecord(record.id)
|
||||
ElMessage.success('删除成功')
|
||||
await this.fetchRecords()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
console.error('删除考评记录失败:', error)
|
||||
ElMessage.error('删除考评记录失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
formatNumber(value) {
|
||||
const num = Number(value || 0)
|
||||
return num.toFixed(2)
|
||||
},
|
||||
toStringId(value) {
|
||||
if (value === null || value === undefined || value === '') return ''
|
||||
if (typeof value === 'object') {
|
||||
return value.id !== undefined && value.id !== null ? String(value.id) : ''
|
||||
}
|
||||
return String(value)
|
||||
},
|
||||
parseIdForApi(value) {
|
||||
if (value === '' || value === null || value === undefined) return null
|
||||
const numeric = Number(value)
|
||||
return Number.isNaN(numeric) ? value : numeric
|
||||
},
|
||||
findDepartmentId(record) {
|
||||
if (record?.department_id !== undefined && record.department_id !== null) {
|
||||
return record.department_id
|
||||
}
|
||||
if (record?.department) {
|
||||
if (typeof record.department === 'object') {
|
||||
const deptObj = record.department
|
||||
if (deptObj.id !== undefined && deptObj.id !== null) return deptObj.id
|
||||
} else {
|
||||
const matchById = this.departments.find(dept => String(dept.id) === String(record.department))
|
||||
if (matchById) return matchById.id
|
||||
const matchByName = this.departments.find(dept => dept.name === record.department || dept.name === record.department_name)
|
||||
if (matchByName) return matchByName.id
|
||||
}
|
||||
}
|
||||
if (record?.department_name) {
|
||||
const matchByName = this.departments.find(dept => dept.name === record.department_name)
|
||||
if (matchByName) return matchByName.id
|
||||
}
|
||||
return ''
|
||||
},
|
||||
findPersonnelId(record) {
|
||||
if (record?.personnel_id !== undefined && record.personnel_id !== null) {
|
||||
return record.personnel_id
|
||||
}
|
||||
if (record?.personnel) {
|
||||
if (typeof record.personnel === 'object') {
|
||||
const personObj = record.personnel
|
||||
if (personObj.id !== undefined && personObj.id !== null) return personObj.id
|
||||
} else {
|
||||
const matchById = this.personnel.find(person => String(person.id) === String(record.personnel))
|
||||
if (matchById) return matchById.id
|
||||
const matchByName = this.personnel.find(person => person.name === record.personnel || person.name === record.personnel_name)
|
||||
if (matchByName) return matchByName.id
|
||||
}
|
||||
}
|
||||
if (record?.personnel_name) {
|
||||
const matchByName = this.personnel.find(person => person.name === record.personnel_name)
|
||||
if (matchByName) return matchByName.id
|
||||
}
|
||||
return ''
|
||||
},
|
||||
extractPersonKey(record) {
|
||||
if (record?.personnel_id !== undefined && record.personnel_id !== null) {
|
||||
return `id:${record.personnel_id}`
|
||||
}
|
||||
if (record?.personnel && typeof record.personnel === 'object' && record.personnel.id !== undefined && record.personnel.id !== null) {
|
||||
return `id:${record.personnel.id}`
|
||||
}
|
||||
if (record?.personnel) {
|
||||
return `person:${record.personnel}`
|
||||
}
|
||||
if (record?.personnel_name) {
|
||||
return `name:${record.personnel_name}`
|
||||
}
|
||||
return `row:${record.id ?? Math.random()}`
|
||||
},
|
||||
resolvePersonName(record) {
|
||||
if (record?.personnel_name) return record.personnel_name
|
||||
if (record?.personnel && typeof record.personnel === 'object' && record.personnel.name) return record.personnel.name
|
||||
if (record?.personnel) return record.personnel
|
||||
return '未命名人员'
|
||||
},
|
||||
async handleExport() {
|
||||
if (!this.personSummary.length) {
|
||||
if (!this.personnelList.length) {
|
||||
ElMessage.warning('暂无数据可导出')
|
||||
return
|
||||
}
|
||||
this.exporting = true
|
||||
try {
|
||||
const params = this.buildQueryParams()
|
||||
const response = await evaluationService.exportEvaluationRecords(params)
|
||||
const workbook = XLSX.read(response.data, { type: 'array' })
|
||||
const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
|
||||
const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const response = await evaluationService.exportPersonnelRecords(params)
|
||||
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
const filename = this.extractFilename(response.headers?.['content-disposition']) || this.generateExportFilename()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
@@ -629,7 +301,7 @@ export default {
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出考评记录失败:', error)
|
||||
console.error('导出失败:', error)
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
this.exporting = false
|
||||
@@ -644,7 +316,16 @@ export default {
|
||||
const now = new Date()
|
||||
const pad = value => value.toString().padStart(2, '0')
|
||||
const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
||||
return `考评总表_${dateStr}.xlsx`
|
||||
return `人员考评记录_${dateStr}.xlsx`
|
||||
},
|
||||
formatNumber(value) {
|
||||
const num = Number(value || 0)
|
||||
return num.toFixed(2)
|
||||
},
|
||||
parseIdForApi(value) {
|
||||
if (value === '' || value === null || value === undefined) return null
|
||||
const numeric = Number(value)
|
||||
return Number.isNaN(numeric) ? value : numeric
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,6 +335,21 @@ export default {
|
||||
.evaluation-container {
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.evaluation-container :deep(.el-card) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.evaluation-container :deep(.el-card__body) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.evaluation-container :deep(.el-table) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -671,6 +367,7 @@ export default {
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
@@ -688,43 +385,14 @@ export default {
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.summary-bar span {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.summary-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-footer span {
|
||||
margin-right: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.summary-empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bonus-text {
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deduction-text {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.evaluation-container {
|
||||
padding: 0 10px;
|
||||
@@ -735,4 +403,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user