+62
@@ -0,0 +1,62 @@
|
||||
# OS artifacts
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE / editor folders
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Python build artifacts
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info/
|
||||
.Python
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Django / local data
|
||||
*.sqlite3
|
||||
media/
|
||||
staticfiles/
|
||||
*.log
|
||||
*/migrations/*.py
|
||||
!*/migrations/__init__.py
|
||||
|
||||
# Virtual environments
|
||||
env/
|
||||
venv/
|
||||
.venv/
|
||||
src/backend/venv/
|
||||
|
||||
# Environment / secret files
|
||||
*.env
|
||||
.env.*
|
||||
src/backend/item_manager/secure.json
|
||||
|
||||
# Node / frontend artifacts
|
||||
node_modules/
|
||||
src/fronted/node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
.pnpm-store/
|
||||
dist/
|
||||
src/fronted/dist/
|
||||
|
||||
# Coverage / test reports
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
# Local tooling
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.sass-cache/
|
||||
|
||||
.gitignore
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import EvaluationRecord
|
||||
|
||||
|
||||
@admin.register(EvaluationRecord)
|
||||
class EvaluationRecordAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'evaluation_date', 'department', 'personnel', 'item_description',
|
||||
'bonus_score', 'deduction_score', 'total_score', 'created_at'
|
||||
]
|
||||
list_filter = ['department', 'evaluation_date', 'created_at']
|
||||
search_fields = ['personnel__name', 'item_description', 'remarks']
|
||||
ordering = ['-evaluation_date', '-created_at']
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EvaluationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'evaluation'
|
||||
@@ -0,0 +1,33 @@
|
||||
import django_filters
|
||||
from finance.models import Department
|
||||
from personnel.models import Personnel
|
||||
|
||||
from .models import EvaluationRecord
|
||||
|
||||
|
||||
class EvaluationRecordFilter(django_filters.FilterSet):
|
||||
department = django_filters.ModelChoiceFilter(
|
||||
queryset=Department.objects.all(),
|
||||
field_name='department',
|
||||
label='部门'
|
||||
)
|
||||
personnel = django_filters.ModelChoiceFilter(
|
||||
queryset=Personnel.objects.all(),
|
||||
field_name='personnel',
|
||||
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='结束日期'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = EvaluationRecord
|
||||
fields = ['department', 'personnel', 'evaluation_date']
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class EvaluationRecord(models.Model):
|
||||
"""考评记录"""
|
||||
department = models.ForeignKey(
|
||||
'finance.Department',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='evaluation_records',
|
||||
verbose_name='所属部门'
|
||||
)
|
||||
personnel = models.ForeignKey(
|
||||
'personnel.Personnel',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='evaluation_records',
|
||||
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='扣分数值')
|
||||
remarks = models.CharField(max_length=255, blank=True, verbose_name='备注')
|
||||
total_score = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name='总计分数')
|
||||
evaluation_date = models.DateField(default=timezone.now, verbose_name='考评日期')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '考评记录'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-evaluation_date', '-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['department', 'evaluation_date']),
|
||||
models.Index(fields=['personnel', 'evaluation_date']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.evaluation_date} {self.personnel.name} ({self.total_score})'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.total_score = (self.bonus_score or 0) - (self.deduction_score or 0)
|
||||
super().save(*args, **kwargs)
|
||||
@@ -0,0 +1,18 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = EvaluationRecord
|
||||
fields = [
|
||||
'id', 'department', 'department_name', 'personnel', 'personnel_name',
|
||||
'item_description', 'bonus_score', 'deduction_score', 'remarks',
|
||||
'total_score', 'evaluation_date', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['total_score', 'created_at', 'updated_at']
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .views import EvaluationRecordViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'evaluation-records', EvaluationRecordViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import csv
|
||||
import io
|
||||
from datetime import datetime, date, timedelta
|
||||
from decimal import Decimal
|
||||
import os
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.utils import timezone
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from finance.models import Department
|
||||
from personnel.models import Personnel
|
||||
|
||||
from .filters import EvaluationRecordFilter
|
||||
from .models import EvaluationRecord
|
||||
from .serializers import EvaluationRecordSerializer
|
||||
|
||||
|
||||
class EvaluationRecordViewSet(viewsets.ModelViewSet):
|
||||
"""考评记录视图集"""
|
||||
authentication_classes = [JWTAuthentication]
|
||||
queryset = EvaluationRecord.objects.select_related('department', 'personnel').all()
|
||||
serializer_class = EvaluationRecordSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_class = EvaluationRecordFilter
|
||||
search_fields = ['item_description', 'remarks', 'personnel__name', 'department__name']
|
||||
ordering_fields = ['evaluation_date', 'created_at', 'total_score', 'bonus_score', 'deduction_score']
|
||||
ordering = ['-evaluation_date', '-created_at']
|
||||
|
||||
@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)
|
||||
|
||||
for record in queryset:
|
||||
writer.writerow([
|
||||
record.id,
|
||||
record.department_id,
|
||||
record.department.name if record.department else '',
|
||||
record.personnel_id,
|
||||
record.personnel.name if record.personnel else '',
|
||||
record.item_description,
|
||||
f'{record.bonus_score:.2f}',
|
||||
f'{record.deduction_score:.2f}',
|
||||
f'{record.total_score:.2f}',
|
||||
record.evaluation_date.isoformat() if record.evaluation_date else '',
|
||||
record.remarks or '',
|
||||
record.created_at.isoformat(sep=' ') if record.created_at else '',
|
||||
record.updated_at.isoformat(sep=' ') if record.updated_at else '',
|
||||
])
|
||||
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='import')
|
||||
def import_records(self, request, *args, **kwargs):
|
||||
upload = request.FILES.get('file')
|
||||
if not upload:
|
||||
return Response({'detail': '请上传文件'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
file_bytes = upload.read()
|
||||
records_data = []
|
||||
ext = os.path.splitext(upload.name or '')[1].lower()
|
||||
|
||||
try:
|
||||
if ext in ('.xlsx', '.xlsm', '.xltx', '.xltm'):
|
||||
records_data = self._read_excel(file_bytes)
|
||||
elif ext == '.csv' or not ext:
|
||||
decoded = file_bytes.decode('utf-8-sig')
|
||||
reader = csv.DictReader(io.StringIO(decoded))
|
||||
records_data = list(reader)
|
||||
else:
|
||||
decoded = file_bytes.decode('utf-8-sig')
|
||||
reader = csv.DictReader(io.StringIO(decoded))
|
||||
records_data = list(reader)
|
||||
except UnicodeDecodeError:
|
||||
return Response({'detail': '文件编码必须为UTF-8'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except ValueError as exc:
|
||||
return Response({'detail': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
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',
|
||||
}
|
||||
missing_columns = required_columns - set(reader_fieldnames)
|
||||
if missing_columns:
|
||||
return Response(
|
||||
{'detail': f'缺少必要的列: {", ".join(sorted(missing_columns))}'},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
records_to_create = []
|
||||
seen_ids = set()
|
||||
errors = []
|
||||
|
||||
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 = self._resolve_department(row.get('department_id'), row.get('department_name'))
|
||||
personnel = self._resolve_personnel(row.get('personnel_id'), row.get('personnel_name'))
|
||||
|
||||
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', '')
|
||||
|
||||
created_at = self._parse_datetime(row.get('created_at'))
|
||||
updated_at = self._parse_datetime(row.get('updated_at'))
|
||||
|
||||
record = EvaluationRecord(
|
||||
id=record_id,
|
||||
department=department,
|
||||
personnel=personnel,
|
||||
item_description=row.get('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
|
||||
errors.append(f'第 {idx} 行: {exc}')
|
||||
|
||||
if errors:
|
||||
return Response({'detail': '导入失败', 'errors': errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
with transaction.atomic():
|
||||
EvaluationRecord.objects.all().delete()
|
||||
EvaluationRecord.objects.bulk_create(records_to_create)
|
||||
|
||||
return Response({'detail': f'成功导入 {len(records_to_create)} 条考评记录'}, 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):
|
||||
workbook = load_workbook(filename=io.BytesIO(file_bytes), data_only=True)
|
||||
sheet = workbook.active
|
||||
rows = list(sheet.iter_rows(values_only=True))
|
||||
if not rows:
|
||||
return []
|
||||
headers = [
|
||||
(str(cell).strip() if cell is not None else '').strip()
|
||||
for cell in rows[0]
|
||||
]
|
||||
if not any(headers):
|
||||
raise ValueError('Excel表头为空')
|
||||
|
||||
records = []
|
||||
for row_idx, row in enumerate(rows[1:], start=2):
|
||||
if row is None:
|
||||
continue
|
||||
row_dict = {}
|
||||
for col_idx, header in enumerate(headers):
|
||||
if not header:
|
||||
continue
|
||||
value = row[col_idx] if col_idx < len(row) else None
|
||||
header_key = header.strip().lower()
|
||||
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, Decimal):
|
||||
value = str(value)
|
||||
elif isinstance(value, float):
|
||||
formatted = format(value, 'f')
|
||||
if '.' in formatted:
|
||||
formatted = formatted.rstrip('0').rstrip('.')
|
||||
value = formatted
|
||||
row_dict[header] = '' if value is None else value
|
||||
if any(value not in (None, '') for value in row_dict.values()):
|
||||
records.append(row_dict)
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def _excel_date_to_iso(excel_value, include_time):
|
||||
try:
|
||||
float_value = float(excel_value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('无法解析Excel日期') from exc
|
||||
base_date = datetime(1899, 12, 30)
|
||||
delta = timedelta(days=float_value)
|
||||
result = base_date + delta
|
||||
if include_time:
|
||||
return result.strftime('%Y-%m-%d %H:%M:%S')
|
||||
return result.strftime('%Y-%m-%d')
|
||||
@@ -55,6 +55,7 @@ INSTALLED_APPS = [
|
||||
"personnel",
|
||||
"scheduler",
|
||||
"memo",
|
||||
"evaluation",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -28,6 +28,7 @@ urlpatterns = [
|
||||
path("", include("email_notice.urls")),
|
||||
path("", include("personnel.urls")),
|
||||
path("", include("memo.urls")),
|
||||
path("", include("evaluation.urls")),
|
||||
path("api-auth/", include("rest_framework.urls")),
|
||||
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
||||
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
||||
|
||||
Binary file not shown.
@@ -18,7 +18,8 @@
|
||||
"marked": "^16.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.5.1"
|
||||
"vue-router": "^4.5.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
|
||||
Generated
+8298
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,10 @@
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
部门管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/evaluations">
|
||||
<el-icon><Trophy /></el-icon>
|
||||
考评记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/finance">
|
||||
<el-icon><Money /></el-icon>
|
||||
财务管理
|
||||
@@ -82,6 +86,10 @@
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
部门管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/evaluations">
|
||||
<el-icon><Trophy /></el-icon>
|
||||
考评记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/finance">
|
||||
<el-icon><Money /></el-icon>
|
||||
财务管理
|
||||
@@ -100,7 +108,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Box, Document, House, Money, OfficeBuilding, Setting, Tickets, User, Menu, EditPen} from '@element-plus/icons-vue'
|
||||
import {Box, Document, House, Money, OfficeBuilding, Setting, Tickets, User, Menu, EditPen, Trophy} from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'AppHeader',
|
||||
@@ -114,7 +122,8 @@ export default {
|
||||
User,
|
||||
OfficeBuilding,
|
||||
Menu,
|
||||
EditPen
|
||||
EditPen,
|
||||
Trophy
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import PersonnelList from '../views/PersonnelList.vue'
|
||||
import DepartmentProjectGroupManagement from '../views/DepartmentProjectGroupManagement.vue'
|
||||
import Settings from '../views/Settings.vue'
|
||||
import Memo from '../views/Memo.vue'
|
||||
import EvaluationRecordList from '../views/EvaluationRecordList.vue'
|
||||
import { authService } from '@/services/api'
|
||||
|
||||
const routes = [
|
||||
@@ -81,6 +82,11 @@ const routes = [
|
||||
path: '/memo',
|
||||
name: 'Memo',
|
||||
component: Memo
|
||||
},
|
||||
{
|
||||
path: '/evaluations',
|
||||
name: 'EvaluationRecordList',
|
||||
component: EvaluationRecordList
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -429,6 +429,48 @@ export const projectGroupService = {
|
||||
}
|
||||
}
|
||||
|
||||
// 考评记录服务
|
||||
export const evaluationService = {
|
||||
// 获取考评记录列表
|
||||
getEvaluationRecords(params = {}) {
|
||||
return apiClient.get('/evaluation-records/', { params })
|
||||
},
|
||||
|
||||
// 导出考评记录总表
|
||||
exportEvaluationRecords(params = {}) {
|
||||
return apiClient.get('/evaluation-records/export/', {
|
||||
params,
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
},
|
||||
|
||||
// 导入考评记录总表
|
||||
importEvaluationRecords(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return apiClient.post('/evaluation-records/import/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 创建考评记录
|
||||
createEvaluationRecord(payload) {
|
||||
return apiClient.post('/evaluation-records/', payload)
|
||||
},
|
||||
|
||||
// 更新考评记录
|
||||
updateEvaluationRecord(id, payload) {
|
||||
return apiClient.put(`/evaluation-records/${id}/`, payload)
|
||||
},
|
||||
|
||||
// 删除考评记录
|
||||
deleteEvaluationRecord(id) {
|
||||
return apiClient.delete(`/evaluation-records/${id}/`)
|
||||
}
|
||||
}
|
||||
|
||||
// 备忘录管理服务
|
||||
export const memoService = {
|
||||
// 获取所有备忘录
|
||||
|
||||
@@ -0,0 +1,739 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader/>
|
||||
<div class="evaluation-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>考评记录</span>
|
||||
<div class="header-actions">
|
||||
<el-button @click="resetFilters">
|
||||
<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>
|
||||
<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"
|
||||
type="file"
|
||||
style="display: none"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
@change="handleImportChange">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" label-width="80px" class="filter-form">
|
||||
<el-form-item label="部门">
|
||||
<el-select
|
||||
v-model="filters.department"
|
||||
placeholder="选择部门"
|
||||
style="min-width: 200px"
|
||||
clearable
|
||||
@change="fetchRecords">
|
||||
<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="人员">
|
||||
<el-select
|
||||
v-model="filters.personnel"
|
||||
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-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-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="事项说明 / 备注 / 人员"
|
||||
clearable
|
||||
@change="fetchRecords">
|
||||
<template #prefix>
|
||||
<el-icon><Search/></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="records"
|
||||
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">
|
||||
<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="danger" @click="confirmDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="summary-bar">
|
||||
<div>
|
||||
<span>记录数:{{ records.length }}</span>
|
||||
<span>总加分:{{ formatNumber(totalBonus) }}</span>
|
||||
<span>总扣分:{{ formatNumber(totalDeduction) }}</span>
|
||||
<span>综合分:{{ formatNumber(totalScore) }}</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 AppHeader from '@/components/AppHeader.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, 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: [],
|
||||
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' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await Promise.all([this.fetchDepartments(), this.fetchPersonnel()])
|
||||
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()
|
||||
this.departments = resp.data
|
||||
} catch (error) {
|
||||
console.error('获取部门失败:', error)
|
||||
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]
|
||||
}
|
||||
return params
|
||||
},
|
||||
async fetchRecords() {
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await evaluationService.getEvaluationRecords(this.buildQueryParams())
|
||||
this.records = resp.data
|
||||
} catch (error) {
|
||||
console.error('获取考评记录失败:', error)
|
||||
ElMessage.error('加载考评记录失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
department: '',
|
||||
personnel: '',
|
||||
keyword: '',
|
||||
dateRange: []
|
||||
}
|
||||
this.fetchRecords()
|
||||
},
|
||||
openCreateDialog() {
|
||||
this.isEditing = false
|
||||
this.currentRecordId = null
|
||||
this.form = this.getEmptyForm()
|
||||
this.dialogVisible = true
|
||||
},
|
||||
openSummaryDialog() {
|
||||
this.summaryDialogVisible = true
|
||||
},
|
||||
triggerImport() {
|
||||
this.$refs.importInput?.click()
|
||||
},
|
||||
async handleImportChange(event) {
|
||||
const files = event.target.files
|
||||
if (!files || !files.length) return
|
||||
const file = files[0]
|
||||
this.importing = true
|
||||
try {
|
||||
await evaluationService.importEvaluationRecords(file)
|
||||
ElMessage.success('导入成功')
|
||||
await this.fetchRecords()
|
||||
} catch (error) {
|
||||
console.error('导入考评记录失败:', error)
|
||||
const detail = error.response?.data?.detail
|
||||
const errorList = error.response?.data?.errors
|
||||
let message = detail || '导入失败'
|
||||
if (Array.isArray(errorList) && errorList.length) {
|
||||
message = `${message}: ${errorList[0]}`
|
||||
}
|
||||
ElMessage.error(message)
|
||||
} finally {
|
||||
this.importing = false
|
||||
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) {
|
||||
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 filename = this.extractFilename(response.headers?.['content-disposition']) || this.generateExportFilename()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出考评记录失败:', error)
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
this.exporting = false
|
||||
}
|
||||
},
|
||||
extractFilename(disposition) {
|
||||
if (!disposition) return ''
|
||||
const match = /filename="?([^"]+)"?/i.exec(disposition)
|
||||
return match ? decodeURIComponent(match[1]) : ''
|
||||
},
|
||||
generateExportFilename() {
|
||||
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`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.evaluation-container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.summary-bar {
|
||||
margin-top: 16px;
|
||||
padding: 14px 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user