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'
|
'bonus_score', 'deduction_score', 'total_score', 'created_at'
|
||||||
]
|
]
|
||||||
list_filter = ['department', 'evaluation_date', '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']
|
ordering = ['-evaluation_date', '-created_at']
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from finance.models import Department
|
from finance.models import Department
|
||||||
from personnel.models import Personnel
|
|
||||||
|
|
||||||
from .models import EvaluationRecord
|
from .models import EvaluationRecord
|
||||||
|
|
||||||
@@ -11,23 +10,18 @@ class EvaluationRecordFilter(django_filters.FilterSet):
|
|||||||
field_name='department',
|
field_name='department',
|
||||||
label='部门'
|
label='部门'
|
||||||
)
|
)
|
||||||
personnel = django_filters.ModelChoiceFilter(
|
personnel = django_filters.CharFilter(
|
||||||
queryset=Personnel.objects.all(),
|
|
||||||
field_name='personnel',
|
field_name='personnel',
|
||||||
|
lookup_expr='icontains',
|
||||||
label='人员'
|
label='人员'
|
||||||
)
|
)
|
||||||
date_from = django_filters.DateFilter(
|
grade = django_filters.CharFilter(
|
||||||
field_name='evaluation_date',
|
field_name='grade',
|
||||||
lookup_expr='gte',
|
lookup_expr='icontains',
|
||||||
label='开始日期'
|
label='年级'
|
||||||
)
|
|
||||||
date_to = django_filters.DateFilter(
|
|
||||||
field_name='evaluation_date',
|
|
||||||
lookup_expr='lte',
|
|
||||||
label='结束日期'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = EvaluationRecord
|
model = EvaluationRecord
|
||||||
fields = ['department', 'personnel', 'evaluation_date']
|
fields = ['department', 'personnel', 'grade']
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ class EvaluationRecord(models.Model):
|
|||||||
related_name='evaluation_records',
|
related_name='evaluation_records',
|
||||||
verbose_name='所属部门'
|
verbose_name='所属部门'
|
||||||
)
|
)
|
||||||
personnel = models.ForeignKey(
|
personnel = models.CharField(max_length=100, verbose_name='人员')
|
||||||
'personnel.Personnel',
|
grade = models.CharField(max_length=50, blank=True, verbose_name='年级')
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='evaluation_records',
|
|
||||||
verbose_name='人员'
|
|
||||||
)
|
|
||||||
item_description = models.CharField(max_length=255, 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='加分数值')
|
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='扣分数值')
|
deduction_score = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='扣分数值')
|
||||||
@@ -32,10 +28,11 @@ class EvaluationRecord(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['department', 'evaluation_date']),
|
models.Index(fields=['department', 'evaluation_date']),
|
||||||
models.Index(fields=['personnel', 'evaluation_date']),
|
models.Index(fields=['personnel', 'evaluation_date']),
|
||||||
|
models.Index(fields=['evaluation_date']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
def save(self, *args, **kwargs):
|
||||||
self.total_score = (self.bonus_score or 0) - (self.deduction_score or 0)
|
self.total_score = (self.bonus_score or 0) - (self.deduction_score or 0)
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.db.models import Sum, Count, Q
|
||||||
|
|
||||||
from .models import EvaluationRecord
|
from .models import EvaluationRecord
|
||||||
|
|
||||||
|
|
||||||
class EvaluationRecordSerializer(serializers.ModelSerializer):
|
class EvaluationRecordSerializer(serializers.ModelSerializer):
|
||||||
department_name = serializers.CharField(source='department.name', read_only=True)
|
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:
|
class Meta:
|
||||||
model = EvaluationRecord
|
model = EvaluationRecord
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'department', 'department_name', 'personnel', 'personnel_name',
|
'id', 'department', 'department_name', 'personnel', 'personnel_name', 'grade',
|
||||||
'item_description', 'bonus_score', 'deduction_score', 'remarks',
|
'item_description', 'bonus_score', 'deduction_score', 'remarks',
|
||||||
'total_score', 'evaluation_date', 'created_at', 'updated_at'
|
'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
|
import os
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Sum, Count, Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
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 rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill
|
||||||
|
|
||||||
from finance.models import Department
|
from finance.models import Department
|
||||||
from personnel.models import Personnel
|
|
||||||
|
|
||||||
from .filters import EvaluationRecordFilter
|
from .filters import EvaluationRecordFilter
|
||||||
from .models import EvaluationRecord
|
from .models import EvaluationRecord
|
||||||
from .serializers import EvaluationRecordSerializer
|
from .serializers import EvaluationRecordSerializer, PersonnelSummarySerializer
|
||||||
|
|
||||||
|
|
||||||
class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
||||||
"""考评记录视图集"""
|
"""考评记录视图集"""
|
||||||
authentication_classes = [JWTAuthentication]
|
authentication_classes = [JWTAuthentication]
|
||||||
queryset = EvaluationRecord.objects.select_related('department', 'personnel').all()
|
queryset = EvaluationRecord.objects.select_related('department').all()
|
||||||
serializer_class = EvaluationRecordSerializer
|
serializer_class = EvaluationRecordSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
filterset_class = EvaluationRecordFilter
|
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_fields = ['evaluation_date', 'created_at', 'total_score', 'bonus_score', 'deduction_score']
|
||||||
ordering = ['-evaluation_date', '-created_at']
|
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')
|
@action(detail=False, methods=['get'], url_path='export')
|
||||||
def export_records(self, request, *args, **kwargs):
|
def export_records(self, request, *args, **kwargs):
|
||||||
|
"""导出人员考评记录,每个人一个表格"""
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
if not queryset.exists():
|
if not queryset.exists():
|
||||||
return Response({'detail': '暂无数据可导出'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': '暂无数据可导出'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
filename = f'evaluation_records_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
# 创建Excel工作簿
|
||||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
wb = Workbook()
|
||||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
wb.remove(wb.active) # 删除默认工作表
|
||||||
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)
|
|
||||||
|
|
||||||
|
# 按人员分组
|
||||||
|
personnel_groups = {}
|
||||||
for record in queryset:
|
for record in queryset:
|
||||||
writer.writerow([
|
personnel_key = f"{record.personnel}_{record.department.name if record.department else ''}"
|
||||||
record.id,
|
if personnel_key not in personnel_groups:
|
||||||
record.department_id,
|
personnel_groups[personnel_key] = {
|
||||||
record.department.name if record.department else '',
|
'personnel': record.personnel,
|
||||||
record.personnel_id,
|
'department_name': record.department.name if record.department else '',
|
||||||
record.personnel.name if record.personnel 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.item_description,
|
||||||
f'{record.bonus_score:.2f}',
|
record.evaluation_date.strftime('%Y-%m-%d') if record.evaluation_date else '',
|
||||||
f'{record.deduction_score:.2f}',
|
score_value,
|
||||||
f'{record.total_score:.2f}',
|
|
||||||
record.evaluation_date.isoformat() if record.evaluation_date else '',
|
|
||||||
record.remarks or '',
|
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
|
return response
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='import')
|
@action(detail=False, methods=['post'], url_path='import')
|
||||||
@@ -112,62 +284,205 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
|||||||
if not records_data:
|
if not records_data:
|
||||||
return Response({'detail': '导入文件没有数据'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'detail': '导入文件没有数据'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
reader_fieldnames = records_data[0].keys() if isinstance(records_data[0], dict) else []
|
# 字段名映射:中文列名 -> 英文字段名
|
||||||
required_columns = {
|
field_mapping = {
|
||||||
'id',
|
'部门': 'department_name',
|
||||||
'department_id',
|
'所属部门': 'department_name',
|
||||||
'department_name',
|
'department_name': 'department_name',
|
||||||
'personnel_id',
|
'姓名': 'personnel_name',
|
||||||
'personnel_name',
|
'personnel_name': 'personnel_name',
|
||||||
'item_description',
|
'年级': 'grade',
|
||||||
'bonus_score',
|
'grade': 'grade',
|
||||||
'deduction_score',
|
'扣分/加分说明': 'item_description',
|
||||||
'evaluation_date',
|
'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(
|
return Response(
|
||||||
{'detail': f'缺少必要的列: {", ".join(sorted(missing_columns))}'},
|
{'detail': f'缺少必要的列: {", ".join(missing_display)}'},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
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 = []
|
records_to_create = []
|
||||||
seen_ids = set()
|
|
||||||
errors = []
|
errors = []
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
for idx, row in enumerate(records_data, start=2):
|
for idx, row in enumerate(records_data, start=2):
|
||||||
try:
|
try:
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
raise ValueError('数据格式不正确')
|
raise ValueError('数据格式不正确')
|
||||||
|
|
||||||
record_id = int(row['id']) if row.get('id') else None
|
# 根据 department_name 查找部门(支持中英文列名)
|
||||||
if record_id in seen_ids:
|
department_name = get_field_value(row, 'department_name')
|
||||||
raise ValueError('表中存在重复的记录 ID')
|
if department_name is None:
|
||||||
seen_ids.add(record_id)
|
# 显示实际存在的列名,帮助调试
|
||||||
|
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_name 直接作为字符串存储(支持中英文列名)
|
||||||
personnel = self._resolve_personnel(row.get('personnel_id'), row.get('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')
|
grade = str(get_field_value(row, 'grade') or '').strip()
|
||||||
deduction_score = Decimal(row.get('deduction_score') or '0')
|
|
||||||
remarks = row.get('remarks', '')
|
|
||||||
|
|
||||||
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(
|
record = EvaluationRecord(
|
||||||
id=record_id,
|
|
||||||
department=department,
|
department=department,
|
||||||
personnel=personnel,
|
personnel=personnel_name,
|
||||||
item_description=row.get('item_description', ''),
|
grade=grade,
|
||||||
|
item_description=item_description,
|
||||||
bonus_score=bonus_score,
|
bonus_score=bonus_score,
|
||||||
deduction_score=deduction_score,
|
deduction_score=deduction_score,
|
||||||
total_score=bonus_score - deduction_score,
|
|
||||||
evaluation_date=evaluation_date,
|
evaluation_date=evaluation_date,
|
||||||
remarks=remarks,
|
remarks=remarks,
|
||||||
created_at=created_at or timezone.now(),
|
|
||||||
updated_at=updated_at or timezone.now(),
|
|
||||||
)
|
)
|
||||||
records_to_create.append(record)
|
records_to_create.append(record)
|
||||||
except Exception as exc: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
@@ -176,44 +491,24 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
|||||||
if errors:
|
if errors:
|
||||||
return Response({'detail': '导入失败', 'errors': errors}, status=status.HTTP_400_BAD_REQUEST)
|
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():
|
with transaction.atomic():
|
||||||
|
if not is_template_format:
|
||||||
|
# 完整格式导入时,先删除所有记录(保持原有行为)
|
||||||
EvaluationRecord.objects.all().delete()
|
EvaluationRecord.objects.all().delete()
|
||||||
EvaluationRecord.objects.bulk_create(records_to_create)
|
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
|
@staticmethod
|
||||||
def _read_excel(file_bytes):
|
def _read_excel(file_bytes):
|
||||||
@@ -238,13 +533,16 @@ class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
|||||||
if not header:
|
if not header:
|
||||||
continue
|
continue
|
||||||
value = row[col_idx] if col_idx < len(row) else None
|
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):
|
if isinstance(value, datetime):
|
||||||
value = value.strftime('%Y-%m-%d %H:%M:%S')
|
value = value.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
elif isinstance(value, date):
|
elif isinstance(value, date):
|
||||||
value = value.strftime('%Y-%m-%d')
|
value = value.strftime('%Y-%m-%d')
|
||||||
elif isinstance(value, (int, float)) and header_key in {'evaluation_date', 'created_at', 'updated_at'}:
|
elif isinstance(value, (int, float)) and is_date_column:
|
||||||
value = EvaluationRecordViewSet._excel_date_to_iso(value, header_key != 'evaluation_date')
|
value = EvaluationRecordViewSet._excel_date_to_iso(value, False)
|
||||||
elif isinstance(value, Decimal):
|
elif isinstance(value, Decimal):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
elif isinstance(value, float):
|
elif isinstance(value, float):
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupMan
|
|||||||
import Settings from '../views/Settings.vue'
|
import Settings from '../views/Settings.vue'
|
||||||
import Memo from '../views/Memo.vue'
|
import Memo from '../views/Memo.vue'
|
||||||
import EvaluationRecordList from '../views/EvaluationRecordList.vue'
|
import EvaluationRecordList from '../views/EvaluationRecordList.vue'
|
||||||
|
import EvaluationRecordDetail from '../views/EvaluationRecordDetail.vue'
|
||||||
import { authService } from '@/services/api'
|
import { authService } from '@/services/api'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -87,6 +88,12 @@ const routes = [
|
|||||||
path: '/evaluations',
|
path: '/evaluations',
|
||||||
name: 'EvaluationRecordList',
|
name: 'EvaluationRecordList',
|
||||||
component: EvaluationRecordList
|
component: EvaluationRecordList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/evaluations/:personnel',
|
||||||
|
name: 'EvaluationRecordDetail',
|
||||||
|
component: EvaluationRecordDetail,
|
||||||
|
props: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -431,21 +431,28 @@ export const projectGroupService = {
|
|||||||
|
|
||||||
// 考评记录服务
|
// 考评记录服务
|
||||||
export const evaluationService = {
|
export const evaluationService = {
|
||||||
// 获取考评记录列表
|
// 获取人员汇总列表
|
||||||
getEvaluationRecords(params = {}) {
|
getPersonnelSummary(params = {}) {
|
||||||
return apiClient.get('/evaluation-records/', { 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/', {
|
return apiClient.get('/evaluation-records/export/', {
|
||||||
params,
|
params,
|
||||||
responseType: 'arraybuffer'
|
responseType: 'arraybuffer'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导入考评记录总表
|
// 导入人员考评记录
|
||||||
importEvaluationRecords(file) {
|
importPersonnelRecords(file) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
return apiClient.post('/evaluation-records/import/', formData, {
|
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) {
|
createEvaluationRecord(payload) {
|
||||||
return apiClient.post('/evaluation-records/', payload)
|
return apiClient.post('/evaluation-records/', payload)
|
||||||
@@ -468,6 +487,18 @@ export const evaluationService = {
|
|||||||
// 删除考评记录
|
// 删除考评记录
|
||||||
deleteEvaluationRecord(id) {
|
deleteEvaluationRecord(id) {
|
||||||
return apiClient.delete(`/evaluation-records/${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-icon><Refresh/></el-icon>
|
||||||
重置筛选
|
重置筛选
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" @click="openCreateDialog">
|
<el-button type="success" :loading="downloadingTemplate" @click="handleDownloadTemplate">
|
||||||
<el-icon><Plus/></el-icon>
|
<el-icon><Download/></el-icon>
|
||||||
新增记录
|
下载样表
|
||||||
</el-button>
|
|
||||||
<el-button type="success" @click="openSummaryDialog">
|
|
||||||
<el-icon><Search/></el-icon>
|
|
||||||
查看总表
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="warning" :loading="exporting" @click="handleExport">
|
<el-button type="warning" :loading="exporting" @click="handleExport">
|
||||||
<el-icon><Download/></el-icon>
|
<el-icon><Download/></el-icon>
|
||||||
导出总表
|
导出人员
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="info" :loading="importing" @click="triggerImport">
|
<el-button type="info" :loading="importing" @click="triggerImport">
|
||||||
<el-icon><UploadFilled/></el-icon>
|
<el-icon><UploadFilled/></el-icon>
|
||||||
导入总表
|
导入人员
|
||||||
</el-button>
|
</el-button>
|
||||||
<input
|
<input
|
||||||
ref="importInput"
|
ref="importInput"
|
||||||
@@ -53,76 +49,46 @@
|
|||||||
:value="String(dept.id)"/>
|
:value="String(dept.id)"/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="人员">
|
<el-form-item label="年级">
|
||||||
<el-select
|
<el-input
|
||||||
v-model="filters.personnel"
|
v-model="filters.grade"
|
||||||
placeholder="选择人员"
|
placeholder="输入年级"
|
||||||
style="min-width: 200px"
|
style="min-width: 200px"
|
||||||
clearable
|
clearable
|
||||||
filterable
|
|
||||||
@change="fetchRecords">
|
@change="fetchRecords">
|
||||||
<el-option
|
</el-input>
|
||||||
v-for="person in personnel"
|
|
||||||
:key="person.id"
|
|
||||||
:label="person.name"
|
|
||||||
:value="String(person.id)"/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="日期">
|
<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-input
|
<el-input
|
||||||
v-model="filters.keyword"
|
v-model="filters.personnel"
|
||||||
placeholder="事项说明 / 备注 / 人员"
|
placeholder="输入人员姓名"
|
||||||
|
style="min-width: 200px"
|
||||||
clearable
|
clearable
|
||||||
@change="fetchRecords">
|
@change="fetchRecords">
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search/></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
:data="records"
|
:data="personnelList"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
stripe>
|
stripe
|
||||||
<el-table-column prop="evaluation_date" label="考评日期" width="120"/>
|
table-layout="auto">
|
||||||
<el-table-column prop="department_name" label="部门" width="150"/>
|
<el-table-column prop="department_name" label="所属部门" min-width="150"/>
|
||||||
<el-table-column prop="personnel_name" label="姓名" width="120"/>
|
<el-table-column prop="grade" label="年级" min-width="120"/>
|
||||||
<el-table-column prop="item_description" label="加/扣分事项说明" min-width="220" show-overflow-tooltip/>
|
<el-table-column prop="personnel" label="姓名" min-width="120"/>
|
||||||
<el-table-column prop="bonus_score" label="加分" width="90">
|
<el-table-column label="目前总分" min-width="120">
|
||||||
<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">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.total_score >= 0 ? 'success' : 'danger'">
|
<el-tag :type="scope.row.total_score >= 0 ? 'success' : 'danger'">
|
||||||
{{ formatNumber(scope.row.total_score) }}
|
{{ formatNumber(scope.row.total_score) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remarks" label="备注" min-width="160" show-overflow-tooltip/>
|
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
<template #default="scope">
|
<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>
|
<el-button size="small" type="danger" @click="confirmDelete(scope.row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -130,250 +96,60 @@
|
|||||||
|
|
||||||
<div class="summary-bar">
|
<div class="summary-bar">
|
||||||
<div>
|
<div>
|
||||||
<span>记录数:{{ records.length }}</span>
|
<span>人员数:{{ personnelList.length }}</span>
|
||||||
<span>总加分:{{ formatNumber(totalBonus) }}</span>
|
<span>平均分:{{ averageScore }}</span>
|
||||||
<span>总扣分:{{ formatNumber(totalDeduction) }}</span>
|
|
||||||
<span>综合分:{{ formatNumber(totalScore) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { evaluationService, financeService, personnelService } from '@/services/api'
|
import { evaluationService, financeService } from '@/services/api'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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'
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EvaluationRecordList',
|
name: 'EvaluationRecordList',
|
||||||
components: {
|
components: {
|
||||||
AppHeader,
|
AppHeader,
|
||||||
Plus,
|
|
||||||
Refresh,
|
Refresh,
|
||||||
Search,
|
|
||||||
Download,
|
Download,
|
||||||
UploadFilled
|
UploadFilled
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
|
||||||
exporting: false,
|
exporting: false,
|
||||||
importing: false,
|
importing: false,
|
||||||
records: [],
|
downloadingTemplate: false,
|
||||||
|
personnelList: [],
|
||||||
departments: [],
|
departments: [],
|
||||||
personnel: [],
|
|
||||||
dialogVisible: false,
|
|
||||||
summaryDialogVisible: false,
|
|
||||||
isEditing: false,
|
|
||||||
currentRecordId: null,
|
|
||||||
filters: {
|
filters: {
|
||||||
department: '',
|
department: '',
|
||||||
personnel: '',
|
personnel: '',
|
||||||
keyword: '',
|
grade: ''
|
||||||
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' }]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalBonus() {
|
averageScore() {
|
||||||
return this.records.reduce((sum, item) => sum + Number(item.bonus_score || 0), 0)
|
if (!this.personnelList || this.personnelList.length === 0) {
|
||||||
},
|
return '0.00'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
existing.bonus += Number(record.bonus_score || 0)
|
const total = this.personnelList.reduce((sum, item) => sum + Number(item.total_score || 0), 0)
|
||||||
existing.deduction += Number(record.deduction_score || 0)
|
const average = total / this.personnelList.length
|
||||||
existing.total += Number(record.total_score || 0)
|
return average.toFixed(2)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await Promise.all([this.fetchDepartments(), this.fetchPersonnel()])
|
await this.fetchDepartments()
|
||||||
await this.fetchRecords()
|
await this.fetchRecords()
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
async fetchDepartments() {
|
||||||
try {
|
try {
|
||||||
const resp = await financeService.getAllDepartments()
|
const resp = await financeService.getAllDepartments()
|
||||||
@@ -383,36 +159,26 @@ export default {
|
|||||||
ElMessage.error('获取部门信息失败')
|
ElMessage.error('获取部门信息失败')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchPersonnel() {
|
|
||||||
try {
|
|
||||||
const resp = await personnelService.getAllPersonnel()
|
|
||||||
this.personnel = resp.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取人员失败:', error)
|
|
||||||
ElMessage.error('获取人员信息失败')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buildQueryParams() {
|
buildQueryParams() {
|
||||||
const params = {}
|
const params = {}
|
||||||
const department = this.parseIdForApi(this.filters.department)
|
const department = this.parseIdForApi(this.filters.department)
|
||||||
const personnel = this.parseIdForApi(this.filters.personnel)
|
|
||||||
if (department !== null) params.department = department
|
if (department !== null) params.department = department
|
||||||
if (personnel !== null) params.personnel = personnel
|
if (this.filters.personnel && this.filters.personnel.trim()) {
|
||||||
if (this.filters.keyword) params.search = this.filters.keyword
|
params.personnel = this.filters.personnel.trim()
|
||||||
if (this.filters.dateRange && this.filters.dateRange.length === 2) {
|
}
|
||||||
params.date_from = this.filters.dateRange[0]
|
if (this.filters.grade && this.filters.grade.trim()) {
|
||||||
params.date_to = this.filters.dateRange[1]
|
params.grade = this.filters.grade.trim()
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
},
|
},
|
||||||
async fetchRecords() {
|
async fetchRecords() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
const resp = await evaluationService.getEvaluationRecords(this.buildQueryParams())
|
const resp = await evaluationService.getPersonnelSummary(this.buildQueryParams())
|
||||||
this.records = resp.data
|
this.personnelList = resp.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取考评记录失败:', error)
|
console.error('获取人员列表失败:', error)
|
||||||
ElMessage.error('加载考评记录失败')
|
ElMessage.error('加载人员列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -421,19 +187,72 @@ export default {
|
|||||||
this.filters = {
|
this.filters = {
|
||||||
department: '',
|
department: '',
|
||||||
personnel: '',
|
personnel: '',
|
||||||
keyword: '',
|
grade: ''
|
||||||
dateRange: []
|
|
||||||
}
|
}
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
},
|
},
|
||||||
openCreateDialog() {
|
editPersonnel(row) {
|
||||||
this.isEditing = false
|
this.$router.push({
|
||||||
this.currentRecordId = null
|
name: 'EvaluationRecordDetail',
|
||||||
this.form = this.getEmptyForm()
|
params: {
|
||||||
this.dialogVisible = true
|
personnel: row.personnel
|
||||||
},
|
},
|
||||||
openSummaryDialog() {
|
query: {
|
||||||
this.summaryDialogVisible = true
|
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() {
|
triggerImport() {
|
||||||
this.$refs.importInput?.click()
|
this.$refs.importInput?.click()
|
||||||
@@ -444,11 +263,11 @@ export default {
|
|||||||
const file = files[0]
|
const file = files[0]
|
||||||
this.importing = true
|
this.importing = true
|
||||||
try {
|
try {
|
||||||
await evaluationService.importEvaluationRecords(file)
|
await evaluationService.importPersonnelRecords(file)
|
||||||
ElMessage.success('导入成功')
|
ElMessage.success('导入成功')
|
||||||
await this.fetchRecords()
|
await this.fetchRecords()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导入考评记录失败:', error)
|
console.error('导入失败:', error)
|
||||||
const detail = error.response?.data?.detail
|
const detail = error.response?.data?.detail
|
||||||
const errorList = error.response?.data?.errors
|
const errorList = error.response?.data?.errors
|
||||||
let message = detail || '导入失败'
|
let message = detail || '导入失败'
|
||||||
@@ -461,163 +280,16 @@ export default {
|
|||||||
if (event.target) event.target.value = ''
|
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() {
|
async handleExport() {
|
||||||
if (!this.personSummary.length) {
|
if (!this.personnelList.length) {
|
||||||
ElMessage.warning('暂无数据可导出')
|
ElMessage.warning('暂无数据可导出')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.exporting = true
|
this.exporting = true
|
||||||
try {
|
try {
|
||||||
const params = this.buildQueryParams()
|
const params = this.buildQueryParams()
|
||||||
const response = await evaluationService.exportEvaluationRecords(params)
|
const response = await evaluationService.exportPersonnelRecords(params)
|
||||||
const workbook = XLSX.read(response.data, { type: 'array' })
|
const blob = new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||||
const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
|
|
||||||
const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
|
||||||
const filename = this.extractFilename(response.headers?.['content-disposition']) || this.generateExportFilename()
|
const filename = this.extractFilename(response.headers?.['content-disposition']) || this.generateExportFilename()
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
@@ -629,7 +301,7 @@ export default {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
ElMessage.success('导出成功')
|
ElMessage.success('导出成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出考评记录失败:', error)
|
console.error('导出失败:', error)
|
||||||
ElMessage.error('导出失败')
|
ElMessage.error('导出失败')
|
||||||
} finally {
|
} finally {
|
||||||
this.exporting = false
|
this.exporting = false
|
||||||
@@ -644,7 +316,16 @@ export default {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const pad = value => value.toString().padStart(2, '0')
|
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())}`
|
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 {
|
.evaluation-container {
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
padding: 0 20px;
|
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 {
|
.card-header {
|
||||||
@@ -671,6 +367,7 @@ export default {
|
|||||||
|
|
||||||
.filter-section {
|
.filter-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-form {
|
.filter-form {
|
||||||
@@ -688,43 +385,14 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #606266;
|
color: #606266;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-bar span {
|
.summary-bar span {
|
||||||
margin-right: 20px;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.evaluation-container {
|
.evaluation-container {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
@@ -735,4 +403,3 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user