2026-03-17 07:18:07 +00:00
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
|
|
|
import {
|
2026-03-17 09:47:31 +00:00
|
|
|
|
App,
|
2026-03-17 07:18:07 +00:00
|
|
|
|
Alert,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Spin,
|
|
|
|
|
|
Table,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
|
import { UserOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
|
|
|
import { getTaskScoreDetail, saveTaskUserScore } from '@/api/appraisal';
|
|
|
|
|
|
import PageBackButton from '@/components/PageBackButton';
|
2026-03-17 07:18:56 +00:00
|
|
|
|
import { usePermission } from '@/contexts/PermissionContext';
|
|
|
|
|
|
import '@/styles/permission-link.css';
|
2026-03-17 07:18:07 +00:00
|
|
|
|
import './appraisal-detail.css';
|
|
|
|
|
|
|
|
|
|
|
|
interface DetailItem {
|
|
|
|
|
|
_rowKey?: string;
|
|
|
|
|
|
id?: string | number;
|
|
|
|
|
|
reviewCategory?: string;
|
|
|
|
|
|
reviewItem?: string;
|
|
|
|
|
|
remarks?: string;
|
|
|
|
|
|
score?: number;
|
|
|
|
|
|
weight?: number;
|
|
|
|
|
|
remark?: string;
|
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface GroupRow {
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
items: DetailItem[];
|
|
|
|
|
|
remarkCate: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ExamineTask {
|
|
|
|
|
|
taskName?: string;
|
|
|
|
|
|
templateId?: string | number;
|
|
|
|
|
|
templateType?: string | number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ExamineUser {
|
|
|
|
|
|
id?: string | number;
|
|
|
|
|
|
userName?: string;
|
|
|
|
|
|
manageScore?: string | number;
|
|
|
|
|
|
judgeContent?: string;
|
|
|
|
|
|
selfJudgeContent?: string;
|
|
|
|
|
|
examineStatus?: string | number;
|
|
|
|
|
|
examineStatusSelf?: string | number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface RemarkRow {
|
|
|
|
|
|
remark?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
|
|
|
|
typeof value === 'object' && value !== null;
|
|
|
|
|
|
|
|
|
|
|
|
const toNumber = (value: unknown, fallback = 0) => {
|
|
|
|
|
|
const num = Number(value);
|
|
|
|
|
|
return Number.isFinite(num) ? num : fallback;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clampScore = (value: number) => Math.min(10, Math.max(0, Math.round(value)));
|
|
|
|
|
|
|
|
|
|
|
|
const toBoolean = (value: unknown) => {
|
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof value === 'number') {
|
|
|
|
|
|
return value === 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
const raw = String(value ?? '').toLowerCase().trim();
|
|
|
|
|
|
return raw === '1' || raw === 'true' || raw === 'yes';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const normalizePayload = (response: unknown) =>
|
|
|
|
|
|
isObject(response) && response.data !== undefined ? response.data : response;
|
|
|
|
|
|
|
|
|
|
|
|
const groupDetailRows = (items: DetailItem[], remarks: RemarkRow[] = []) => {
|
|
|
|
|
|
const map = new Map<string, DetailItem[]>();
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
|
const key = String(item.reviewCategory ?? '未分类');
|
|
|
|
|
|
const list = map.get(key) ?? [];
|
|
|
|
|
|
const fallbackKey = `${key}_${list.length}`;
|
|
|
|
|
|
list.push({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
_rowKey: String(item.id ?? fallbackKey),
|
|
|
|
|
|
score: toNumber(item.score, 0),
|
|
|
|
|
|
weight: toNumber(item.weight, 0),
|
|
|
|
|
|
remark: String(item.remark ?? ''),
|
|
|
|
|
|
});
|
|
|
|
|
|
map.set(key, list);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const groups: GroupRow[] = [];
|
|
|
|
|
|
Array.from(map.entries()).forEach(([category, list], index) => {
|
|
|
|
|
|
groups.push({
|
|
|
|
|
|
category,
|
|
|
|
|
|
items: list,
|
|
|
|
|
|
remarkCate: String(remarks[index]?.remark ?? ''),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
return groups;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
interface ScoreBarProps {
|
|
|
|
|
|
value: number;
|
|
|
|
|
|
editable: boolean;
|
|
|
|
|
|
onChange: (value: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ScoreBar = ({ value, editable, onChange }: ScoreBarProps) => {
|
|
|
|
|
|
const normalized = clampScore(value);
|
|
|
|
|
|
|
|
|
|
|
|
const updateByPercent = (percent: number) => {
|
|
|
|
|
|
onChange(clampScore(percent * 10));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTrackClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
|
|
if (!editable) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const rect = event.currentTarget.getBoundingClientRect();
|
|
|
|
|
|
if (rect.width <= 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const percent = (event.clientX - rect.left) / rect.width;
|
|
|
|
|
|
updateByPercent(percent);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
|
|
|
|
if (!editable) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
onChange(clampScore(normalized - 1));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
onChange(clampScore(normalized + 1));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const bubblePosition = normalized * 10;
|
|
|
|
|
|
const bubbleClass =
|
|
|
|
|
|
normalized === 0 ? 'detail-score-bubble is-start' : normalized === 10 ? 'detail-score-bubble is-end' : 'detail-score-bubble';
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="detail-score-wrap">
|
|
|
|
|
|
<div className="detail-score-top">
|
|
|
|
|
|
<span>0</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`detail-score-track ${editable ? 'is-editable' : 'is-readonly'}`}
|
|
|
|
|
|
onClick={handleTrackClick}
|
|
|
|
|
|
role={editable ? 'slider' : undefined}
|
|
|
|
|
|
aria-valuemin={0}
|
|
|
|
|
|
aria-valuemax={10}
|
|
|
|
|
|
aria-valuenow={normalized}
|
|
|
|
|
|
tabIndex={editable ? 0 : -1}
|
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="detail-score-fill" style={{ width: `${bubblePosition}%` }} />
|
|
|
|
|
|
<div className={bubbleClass} style={{ left: `${bubblePosition}%` }}>
|
|
|
|
|
|
{normalized}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{normalized === 0 && <div className="statusText">暂未打分</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const AppraisalDetailPage = () => {
|
2026-03-17 09:47:31 +00:00
|
|
|
|
const { message, modal } = App.useApp();
|
2026-03-17 07:18:07 +00:00
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const [searchParams] = useSearchParams();
|
2026-03-17 07:18:56 +00:00
|
|
|
|
const { canAccessPath } = usePermission();
|
2026-03-17 07:18:07 +00:00
|
|
|
|
|
|
|
|
|
|
const isNormal = toBoolean(searchParams.get('isNormal'));
|
|
|
|
|
|
const [isEdit, setIsEdit] = useState(toBoolean(searchParams.get('edit')));
|
|
|
|
|
|
const examineTaskId = searchParams.get('examineTaskId') ?? searchParams.get('taskId') ?? '';
|
|
|
|
|
|
const reviewType = searchParams.get('reviewType') ?? (isNormal ? '1' : '0');
|
|
|
|
|
|
const routeExamineId = searchParams.get('examineId') ?? '';
|
|
|
|
|
|
const routeUserId = searchParams.get('userId') ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [examineTask, setExamineTask] = useState<ExamineTask>({});
|
|
|
|
|
|
const [examineUser, setExamineUser] = useState<ExamineUser>({});
|
|
|
|
|
|
const [groups, setGroups] = useState<GroupRow[]>([]);
|
|
|
|
|
|
const [manageScore, setManageScore] = useState<number>(0);
|
|
|
|
|
|
const [judgeContent, setJudgeContent] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
const [remarkModalOpen, setRemarkModalOpen] = useState(false);
|
|
|
|
|
|
const [editingCell, setEditingCell] = useState<{ groupIndex: number; rowIndex: number } | null>(null);
|
|
|
|
|
|
const [remarkDraft, setRemarkDraft] = useState('');
|
2026-03-17 07:18:56 +00:00
|
|
|
|
const canEditScore = canAccessPath('/workAppraisal/detail');
|
|
|
|
|
|
const effectiveIsEdit = isEdit && canEditScore;
|
2026-03-17 07:18:07 +00:00
|
|
|
|
|
|
|
|
|
|
const templateType = useMemo(() => String(examineTask.templateType ?? ''), [examineTask.templateType]);
|
|
|
|
|
|
const isTemplateZero = templateType === '0';
|
|
|
|
|
|
|
|
|
|
|
|
const recalcManageScore = useCallback((nextGroups: GroupRow[]) => {
|
|
|
|
|
|
const score = nextGroups
|
|
|
|
|
|
.flatMap((group) => group.items)
|
|
|
|
|
|
.reduce((sum, row) => sum + toNumber(row.score) * toNumber(row.weight), 0) / 10;
|
|
|
|
|
|
setManageScore(Number(score.toFixed(2)));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const loadDetail = useCallback(async () => {
|
|
|
|
|
|
if (!examineTaskId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params: Record<string, unknown> = {
|
|
|
|
|
|
examineTaskId,
|
|
|
|
|
|
reviewType,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (routeExamineId) {
|
|
|
|
|
|
params.examineId = routeExamineId;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (routeUserId) {
|
|
|
|
|
|
params.userId = routeUserId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await getTaskScoreDetail(params);
|
|
|
|
|
|
const payload = normalizePayload(response);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isObject(payload)) {
|
|
|
|
|
|
message.error('评分详情返回格式异常');
|
|
|
|
|
|
setGroups([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const detailRows = Array.isArray(payload.examineConfigDetailVoList)
|
|
|
|
|
|
? (payload.examineConfigDetailVoList as DetailItem[])
|
|
|
|
|
|
: [];
|
|
|
|
|
|
const remarks = Array.isArray(payload.remark) ? (payload.remark as RemarkRow[]) : [];
|
|
|
|
|
|
const nextGroups = groupDetailRows(detailRows, remarks);
|
|
|
|
|
|
setGroups(nextGroups);
|
|
|
|
|
|
|
|
|
|
|
|
const task = isObject(payload.examineTask) ? (payload.examineTask as ExamineTask) : {};
|
|
|
|
|
|
const user = isObject(payload.examineUser) ? (payload.examineUser as ExamineUser) : {};
|
|
|
|
|
|
setExamineTask(task);
|
|
|
|
|
|
setExamineUser(user);
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && String(task.templateType ?? '') === '0') {
|
|
|
|
|
|
setJudgeContent(String(user.selfJudgeContent ?? user.judgeContent ?? ''));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setJudgeContent(String(user.judgeContent ?? ''));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentManageScore = toNumber(user.manageScore);
|
|
|
|
|
|
if (currentManageScore > 0) {
|
|
|
|
|
|
setManageScore(currentManageScore);
|
|
|
|
|
|
} else if (!isNormal) {
|
|
|
|
|
|
recalcManageScore(nextGroups);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (String(user.examineStatusSelf ?? '') === '1' && String(user.examineStatus ?? '') === '1') {
|
|
|
|
|
|
setIsEdit(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch score detail:', error);
|
|
|
|
|
|
message.error('获取评分详情失败');
|
|
|
|
|
|
setGroups([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [examineTaskId, isNormal, recalcManageScore, reviewType, routeExamineId, routeUserId]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void loadDetail();
|
|
|
|
|
|
}, [loadDetail]);
|
|
|
|
|
|
|
|
|
|
|
|
const updateItemScore = (groupIndex: number, rowIndex: number, value: number) => {
|
|
|
|
|
|
setGroups((prev) => {
|
|
|
|
|
|
const next = prev.map((group, gIndex) => {
|
|
|
|
|
|
if (gIndex !== groupIndex) {
|
|
|
|
|
|
return group;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
...group,
|
|
|
|
|
|
items: group.items.map((row, rIndex) => (rIndex === rowIndex ? { ...row, score: value } : row)),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!isNormal) {
|
|
|
|
|
|
recalcManageScore(next);
|
|
|
|
|
|
}
|
|
|
|
|
|
return next;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateGroupRemark = (groupIndex: number, value: string) => {
|
|
|
|
|
|
setGroups((prev) =>
|
|
|
|
|
|
prev.map((group, idx) =>
|
|
|
|
|
|
idx === groupIndex
|
|
|
|
|
|
? {
|
|
|
|
|
|
...group,
|
|
|
|
|
|
remarkCate: value,
|
|
|
|
|
|
}
|
|
|
|
|
|
: group,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openRemarkModal = (groupIndex: number, rowIndex: number) => {
|
|
|
|
|
|
const row = groups[groupIndex]?.items[rowIndex];
|
|
|
|
|
|
setEditingCell({ groupIndex, rowIndex });
|
|
|
|
|
|
setRemarkDraft(String(row?.remark ?? ''));
|
|
|
|
|
|
setRemarkModalOpen(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const saveRemarkModal = () => {
|
|
|
|
|
|
if (!editingCell) {
|
|
|
|
|
|
setRemarkModalOpen(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (remarkDraft.length > 200) {
|
|
|
|
|
|
message.warning('自评总结限制200个字符');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setGroups((prev) =>
|
|
|
|
|
|
prev.map((group, gIndex) => {
|
|
|
|
|
|
if (gIndex !== editingCell.groupIndex) {
|
|
|
|
|
|
return group;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
...group,
|
|
|
|
|
|
items: group.items.map((row, rIndex) =>
|
|
|
|
|
|
rIndex === editingCell.rowIndex
|
|
|
|
|
|
? {
|
|
|
|
|
|
...row,
|
|
|
|
|
|
remark: remarkDraft,
|
|
|
|
|
|
}
|
|
|
|
|
|
: row,
|
|
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
setRemarkModalOpen(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const buildPayload = (submitStatus: 0 | 1) => {
|
|
|
|
|
|
const allItems = groups.flatMap((group) => group.items);
|
|
|
|
|
|
const detailList = allItems.map((item) => ({
|
|
|
|
|
|
score: toNumber(item.score, 0),
|
|
|
|
|
|
configId: item.id,
|
|
|
|
|
|
remark: String(item.remark ?? ''),
|
|
|
|
|
|
reviewCategory: item.reviewCategory,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const payload: Record<string, unknown> = {
|
|
|
|
|
|
examineId: examineUser.id ?? routeExamineId ?? '',
|
|
|
|
|
|
taskId: examineTaskId,
|
|
|
|
|
|
reviewType,
|
|
|
|
|
|
examineDetailList: detailList,
|
|
|
|
|
|
examineRemarkList: [],
|
|
|
|
|
|
manageScore: manageScore,
|
|
|
|
|
|
judgeContent,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal) {
|
|
|
|
|
|
payload.examineStatusSelf = submitStatus;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
payload.examineStatus = submitStatus;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && !isTemplateZero) {
|
|
|
|
|
|
payload.examineRemarkList = groups
|
|
|
|
|
|
.filter((group) => group.category !== '发展与协作')
|
|
|
|
|
|
.map((group) => ({
|
|
|
|
|
|
reviewCategory: group.category,
|
|
|
|
|
|
remark: group.remarkCate,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && isTemplateZero) {
|
|
|
|
|
|
payload.selfJudgeContent = judgeContent;
|
|
|
|
|
|
payload.judgeContent = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const validateSubmit = (submitStatus: 0 | 1) => {
|
|
|
|
|
|
if (submitStatus === 0) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allItems = groups.flatMap((group) => group.items);
|
|
|
|
|
|
const hasUnscored = allItems.some(
|
|
|
|
|
|
(item) => !toNumber(item.score, 0) && (!isNormal || item.reviewCategory !== '发展与协作'),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (hasUnscored) {
|
|
|
|
|
|
message.warning('存在未评分绩效项,请完善后再试');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal) {
|
|
|
|
|
|
const devRows = allItems.filter((item) => item.reviewCategory === '发展与协作');
|
|
|
|
|
|
const hasEmptyDevRemark = devRows.some((item) => !String(item.remark ?? '').trim());
|
|
|
|
|
|
if (hasEmptyDevRemark) {
|
|
|
|
|
|
message.warning('发展与协作下的自评总结为必填,请完善后再试');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
const hasShortDevRemark = devRows.some(
|
|
|
|
|
|
(item) => String(item.remark ?? '').trim().length > 0 && String(item.remark ?? '').trim().length < 100,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (hasShortDevRemark) {
|
|
|
|
|
|
message.warning('发展与协作下的自评总结最少100个字符,请完善后再试');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isNormal && isTemplateZero && judgeContent.length > 300) {
|
|
|
|
|
|
message.warning('总体评价限制300个字符');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isNormal && !judgeContent.trim()) {
|
|
|
|
|
|
message.warning('总体评价为必填');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && isTemplateZero && !judgeContent.trim()) {
|
|
|
|
|
|
message.warning('个人总体评价为必填');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && !isTemplateZero) {
|
|
|
|
|
|
const cateRemarks = groups.filter((group) => group.category !== '发展与协作').map((group) => group.remarkCate);
|
|
|
|
|
|
if (cateRemarks.some((remark) => !String(remark).trim())) {
|
|
|
|
|
|
message.warning('存在未填写大类评价,请完善后再试');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cateRemarks.some((remark) => String(remark).trim().length < 100)) {
|
|
|
|
|
|
message.warning('大类评价最少100个字符,请完善后再试');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const submitScore = async (submitStatus: 0 | 1) => {
|
2026-03-17 07:18:56 +00:00
|
|
|
|
if (!effectiveIsEdit) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
if (!validateSubmit(submitStatus)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const doSubmit = async () => {
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = buildPayload(submitStatus);
|
|
|
|
|
|
await saveTaskUserScore(payload);
|
|
|
|
|
|
message.success('操作成功');
|
|
|
|
|
|
navigate(-1);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to save score:', error);
|
|
|
|
|
|
message.error('保存失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (submitStatus === 1) {
|
2026-03-17 09:47:31 +00:00
|
|
|
|
modal.confirm({
|
2026-03-17 07:18:07 +00:00
|
|
|
|
title: '确认提交绩效评分',
|
|
|
|
|
|
content: '提交后将无法修改,该操作不可逆,请确认后再试',
|
|
|
|
|
|
okText: '确定',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: doSubmit,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await doSubmit();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const buildColumns = (group: GroupRow, groupIndex: number): ColumnsType<DetailItem> => {
|
|
|
|
|
|
const cols: ColumnsType<DetailItem> = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '考核项',
|
|
|
|
|
|
dataIndex: 'reviewItem',
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '评分标准',
|
|
|
|
|
|
dataIndex: 'remarks',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (!isNormal && examineTask.templateId && group.category === '发展与协作' && !isTemplateZero) {
|
|
|
|
|
|
cols.push({
|
|
|
|
|
|
title: '员工自评',
|
|
|
|
|
|
dataIndex: 'remark',
|
|
|
|
|
|
render: (_, row) => <Input.TextArea value={String(row.remark ?? '')} autoSize={{ minRows: 3 }} readOnly />,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ((isNormal && group.category !== '发展与协作') || !isNormal) {
|
|
|
|
|
|
cols.push({
|
|
|
|
|
|
title: '评分',
|
|
|
|
|
|
dataIndex: 'score',
|
|
|
|
|
|
width: 380,
|
|
|
|
|
|
render: (_, row, rowIndex) => (
|
|
|
|
|
|
<ScoreBar
|
|
|
|
|
|
value={toNumber(row.score, 0)}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
editable={effectiveIsEdit}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
onChange={(nextScore) => updateItemScore(groupIndex, rowIndex, nextScore)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isNormal && ((examineTask.templateId && group.category === '发展与协作') || !examineTask.templateId)) {
|
|
|
|
|
|
cols.push({
|
|
|
|
|
|
title: '自评总结',
|
|
|
|
|
|
dataIndex: 'remark',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (_, row, rowIndex) => (
|
2026-03-17 07:18:56 +00:00
|
|
|
|
effectiveIsEdit ? (
|
|
|
|
|
|
<Button type="link" onClick={() => openRemarkModal(groupIndex, rowIndex)}>
|
|
|
|
|
|
{String(row.remark ?? '').trim() ? '查看' : '暂未评价'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="permission-link-disabled">
|
|
|
|
|
|
{String(row.remark ?? '').trim() ? '查看' : '暂未评价'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
2026-03-17 07:18:07 +00:00
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cols;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="app-container appraisal-detail-page">
|
|
|
|
|
|
{!examineTaskId && <Alert type="warning" showIcon message="缺少 examineTaskId 参数,无法加载评分详情" />}
|
|
|
|
|
|
|
|
|
|
|
|
<Spin spinning={loading}>
|
|
|
|
|
|
<div className="conetentBox">
|
|
|
|
|
|
<div style={{ marginBottom: 12 }}>
|
|
|
|
|
|
<PageBackButton
|
|
|
|
|
|
fallbackPath={isNormal ? '/workAppraisal/normalWorker' : '/workAppraisal/manager'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="titleBox">
|
|
|
|
|
|
<div className="titleMain">
|
|
|
|
|
|
<span className="block" />
|
|
|
|
|
|
<span>{examineTask.taskName ?? '绩效考核详情'}</span>
|
|
|
|
|
|
</div>
|
2026-03-17 07:18:56 +00:00
|
|
|
|
{effectiveIsEdit && (
|
2026-03-17 07:18:07 +00:00
|
|
|
|
<Space size={20}>
|
|
|
|
|
|
<Button style={{ width: 90 }} onClick={() => submitScore(0)} loading={submitting}>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button style={{ width: 90 }} type="primary" onClick={() => submitScore(1)} loading={submitting}>
|
|
|
|
|
|
提交
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="headerBox">
|
|
|
|
|
|
<div className="userInfo">
|
|
|
|
|
|
<UserOutlined />
|
|
|
|
|
|
<span>{examineUser.userName ?? '-'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{!isNormal && (
|
|
|
|
|
|
<div className="totalBox">
|
|
|
|
|
|
<span>考核评分:</span>
|
|
|
|
|
|
<span className="scoreTotal">{manageScore}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="tableBox">
|
|
|
|
|
|
{groups.length === 0 ? (
|
|
|
|
|
|
<Empty description="暂无评分项" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
groups.map((group, groupIndex) => (
|
|
|
|
|
|
<div className="tableRow detail-group" key={`${group.category}_${groupIndex}`}>
|
|
|
|
|
|
<div className="userBox detail-group-title">{group.category}</div>
|
|
|
|
|
|
<Table<DetailItem>
|
|
|
|
|
|
rowKey="_rowKey"
|
|
|
|
|
|
columns={buildColumns(group, groupIndex)}
|
|
|
|
|
|
dataSource={group.items}
|
|
|
|
|
|
pagination={false}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{isNormal && !isTemplateZero && group.category !== '发展与协作' && (
|
|
|
|
|
|
<div className="detail-group-remark">
|
|
|
|
|
|
<div className="detail-subtitle">评价</div>
|
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
autoSize={{ minRows: 4 }}
|
|
|
|
|
|
maxLength={300}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
value={group.remarkCate}
|
|
|
|
|
|
onChange={(event) => updateGroupRemark(groupIndex, event.target.value)}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
readOnly={!effectiveIsEdit}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
placeholder="0/300"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{((isNormal && isTemplateZero) || !isNormal) && (
|
|
|
|
|
|
<div className="detail-overall">
|
|
|
|
|
|
<div className="userBox detail-group-title">总体评价</div>
|
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
autoSize={{ minRows: 4 }}
|
|
|
|
|
|
maxLength={300}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
value={judgeContent}
|
|
|
|
|
|
onChange={(event) => setJudgeContent(event.target.value)}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
readOnly={!effectiveIsEdit}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
placeholder="0/300"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="自评总结"
|
|
|
|
|
|
open={remarkModalOpen}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
onOk={effectiveIsEdit ? saveRemarkModal : () => setRemarkModalOpen(false)}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
onCancel={() => setRemarkModalOpen(false)}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
okButtonProps={{ style: { display: effectiveIsEdit ? 'inline-flex' : 'none' } }}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
okText="确定"
|
2026-03-17 07:18:56 +00:00
|
|
|
|
cancelText={effectiveIsEdit ? '取消' : '关闭'}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
>
|
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
|
autoSize={{ minRows: 4 }}
|
|
|
|
|
|
maxLength={200}
|
|
|
|
|
|
showCount
|
|
|
|
|
|
value={remarkDraft}
|
|
|
|
|
|
onChange={(event) => setRemarkDraft(event.target.value)}
|
2026-03-17 07:18:56 +00:00
|
|
|
|
readOnly={!effectiveIsEdit}
|
2026-03-17 07:18:07 +00:00
|
|
|
|
placeholder="0/200"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default AppraisalDetailPage;
|