feat: 支持表格单元格快捷编辑
Some checks failed
CI 检查 / lint (pull_request) Has been cancelled
CI 检查 / typecheck (pull_request) Has been cancelled
CI 检查 / test (pull_request) Has been cancelled

This commit is contained in:
2026-07-21 14:17:08 +08:00
parent fe0b71a0a9
commit a585fd42d5
17 changed files with 2197 additions and 303 deletions

View File

@@ -36,6 +36,7 @@ import api from '../../api';
import { maskPhone, maskIdNumber } from '../../utils/sensitive';
import { useViewSensitive } from '../../hooks/useViewSensitive';
import { message } from '../../ui/app-message';
import EditableCell from '../EditableCell';
// ---- Types ----
@@ -409,20 +410,131 @@ const EnrollmentsTab: React.FC<TabProps & { data: EnrollmentRecord[] }> = ({
}
};
const saveCell = async (record: EnrollmentRecord, field: string, value: unknown) => {
await api.put(`/archive/enrollments/${record.id}`, { [field]: value });
message.success('报读记录已保存');
onRefresh();
};
const columns: ColumnsType<EnrollmentRecord> = [
{ title: '课程类别', dataIndex: 'courseCategory', render: getCourseCategoryLabel },
{ title: '班型', dataIndex: 'classType', render: getClassTypeLabel },
{ title: '班级名称', dataIndex: 'className', render: (v: string) => v || '-' },
{ title: '班主任', dataIndex: 'headTeacher', render: (v: string) => v || '-' },
{ title: '任课教师', dataIndex: 'subjectTeacher', render: (v: string) => v || '-' },
{ title: '开始日期', dataIndex: 'startDate', render: (v: string) => v || '-' },
{ title: '结束日期', dataIndex: 'endDate', render: (v: string) => v || '-' },
{
title: '课程类别',
dataIndex: 'courseCategory',
render: (v: string, r) => (
<EditableCell
value={v}
editor="select"
options={COURSE_CATEGORY_OPTIONS}
permission="student:edit"
required
onSave={(next) => saveCell(r, 'courseCategory', next)}
>
{getCourseCategoryLabel(v)}
</EditableCell>
),
},
{
title: '班型',
dataIndex: 'classType',
render: (v: string, r) => (
<EditableCell
value={v}
editor="select"
options={CLASS_TYPE_OPTIONS}
permission="student:edit"
required
onSave={(next) => saveCell(r, 'classType', next)}
>
{getClassTypeLabel(v)}
</EditableCell>
),
},
{
title: '班级名称',
dataIndex: 'className',
render: (v: string, r) => (
<EditableCell
value={v}
permission="student:edit"
onSave={(next) => saveCell(r, 'className', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '班主任',
dataIndex: 'headTeacher',
render: (v: string, r) => (
<EditableCell
value={v}
permission="student:edit"
onSave={(next) => saveCell(r, 'headTeacher', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '任课教师',
dataIndex: 'subjectTeacher',
render: (v: string, r) => (
<EditableCell
value={v}
permission="student:edit"
onSave={(next) => saveCell(r, 'subjectTeacher', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '开始日期',
dataIndex: 'startDate',
render: (v: string, r) => (
<EditableCell
value={v}
editor="date"
permission="student:edit"
onSave={(next) => saveCell(r, 'startDate', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '结束日期',
dataIndex: 'endDate',
render: (v: string, r) => (
<EditableCell
value={v}
editor="date"
permission="student:edit"
onSave={(next) => saveCell(r, 'endDate', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '状态',
dataIndex: 'status',
render: (v: string) => {
render: (v: string, r) => {
const status = getEnrollmentStatus(v);
return <Tag color={status.color}>{status.text}</Tag>;
return (
<EditableCell
value={v}
editor="select"
options={Object.entries(ENROLLMENT_STATUS_MAP).map(([value, item]) => ({
value,
label: item.text,
}))}
permission="student:edit"
onSave={(next) => saveCell(r, 'status', next)}
>
<Tag color={status.color}>{status.text}</Tag>
</EditableCell>
);
},
},
];
@@ -520,34 +632,137 @@ const ExamScoresTab: React.FC<
}
};
const saveCell = async (record: ExamScoreRecord, field: string, value: unknown) => {
await api.put(`/archive/exam-scores/${record.id}`, { [field]: value });
message.success('考试成绩已保存');
onRefresh();
};
const columns: ColumnsType<ExamScoreRecord> = [
{
title: '考试类型',
dataIndex: 'examType',
render: (v: string) => EXAM_TYPE_OPTIONS.find((o) => o.value === v)?.label || v,
render: (v: string, r) => (
<EditableCell
value={v}
editor="select"
options={EXAM_TYPE_OPTIONS}
permission="student:edit"
required
onSave={(next) => saveCell(r, 'examType', next)}
>
{EXAM_TYPE_OPTIONS.find((o) => o.value === v)?.label || v}
</EditableCell>
),
},
{
title: '考试名称',
dataIndex: 'examName',
render: (v: string, r) => (
<EditableCell
value={v}
permission="student:edit"
onSave={(next) => saveCell(r, 'examName', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '科目',
dataIndex: 'subject',
render: (v: string, r) => (
<EditableCell
value={v}
required
permission="student:edit"
onSave={(next) => saveCell(r, 'subject', next)}
>
{v}
</EditableCell>
),
},
{
title: '成绩',
dataIndex: 'score',
render: (v: number, r) => (
<EditableCell
value={v}
editor="number"
min={0}
required
permission="student:edit"
onSave={(next) => saveCell(r, 'score', next)}
>
{v}
</EditableCell>
),
},
{ title: '考试名称', dataIndex: 'examName', render: (v: string) => v || '-' },
{ title: '科目', dataIndex: 'subject' },
{ title: '成绩', dataIndex: 'score' },
{
title: '班级均分',
dataIndex: 'classAvg',
render: (v: number | undefined) => (v !== undefined ? v : '-'),
render: (v: number | undefined, r) => (
<EditableCell
value={v}
editor="number"
min={0}
permission="student:edit"
onSave={(next) => saveCell(r, 'classAvg', next)}
>
{v !== undefined ? v : '-'}
</EditableCell>
),
},
{
title: '排名',
dataIndex: 'rank',
render: (v: number | undefined) => (v !== undefined ? v : '-'),
render: (v: number | undefined, r) => (
<EditableCell
value={v}
editor="number"
min={1}
permission="student:edit"
onSave={(next) => saveCell(r, 'rank', next)}
>
{v !== undefined ? v : '-'}
</EditableCell>
),
},
{
title: '考试日期',
dataIndex: 'examDate',
render: (v: string, r) => (
<EditableCell
value={v}
editor="date"
permission="student:edit"
onSave={(next) => saveCell(r, 'examDate', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '考试日期', dataIndex: 'examDate', render: (v: string) => v || '-' },
{
title: '关联报读',
dataIndex: 'enrollmentId',
render: (v: number | undefined) => {
if (v === undefined) return '-';
const enr = enrollments.find((e) => e.id === v);
return enr ? formatEnrollmentDisplayName(enr) : String(v);
},
render: (v: number | undefined, r) => (
<EditableCell
value={v}
editor="select"
options={enrollments.map((item) => ({
value: item.id,
label: formatEnrollmentDisplayName(item),
}))}
permission="student:edit"
onSave={(next) => saveCell(r, 'enrollmentId', next)}
>
{(() => {
if (v === undefined) return '-';
const enr = enrollments.find((e) => e.id === v);
return enr ? formatEnrollmentDisplayName(enr) : String(v);
})()}
</EditableCell>
),
},
];
@@ -656,16 +871,87 @@ const LearningTab: React.FC<TabProps & { data: LearningRecord[] }> = ({
}
};
const saveCell = async (record: LearningRecord, field: string, value: unknown) => {
await api.put(`/archive/learning-records/${record.id}`, { [field]: value });
message.success('学情记录已保存');
onRefresh();
};
const columns: ColumnsType<LearningRecord> = [
{ title: '记录日期', dataIndex: 'recordDate' },
{
title: '记录日期',
dataIndex: 'recordDate',
render: (v: string, r) => (
<EditableCell
value={v}
editor="date"
permission="student:edit"
required
onSave={(next) => saveCell(r, 'recordDate', next)}
>
{v}
</EditableCell>
),
},
{
title: '记录类型',
dataIndex: 'recordType',
render: (v: string) => RECORD_TYPE_OPTIONS.find((o) => o.value === v)?.label || v,
render: (v: string, r) => (
<EditableCell
value={v}
editor="select"
options={RECORD_TYPE_OPTIONS}
permission="student:edit"
required
onSave={(next) => saveCell(r, 'recordType', next)}
>
{RECORD_TYPE_OPTIONS.find((o) => o.value === v)?.label || v}
</EditableCell>
),
},
{
title: '内容',
dataIndex: 'content',
ellipsis: true,
render: (v: string, r) => (
<EditableCell
value={v}
editor="textarea"
permission="student:edit"
required
onSave={(next) => saveCell(r, 'content', next)}
>
{v}
</EditableCell>
),
},
{
title: '跟进方式',
dataIndex: 'followUpMethod',
render: (v: string, r) => (
<EditableCell
value={v}
permission="student:edit"
onSave={(next) => saveCell(r, 'followUpMethod', next)}
>
{v || '-'}
</EditableCell>
),
},
{
title: '下一步计划',
dataIndex: 'nextStep',
render: (v: string, r) => (
<EditableCell
value={v}
editor="textarea"
permission="student:edit"
onSave={(next) => saveCell(r, 'nextStep', next)}
>
{v || '-'}
</EditableCell>
),
},
{ title: '内容', dataIndex: 'content', ellipsis: true },
{ title: '跟进方式', dataIndex: 'followUpMethod', render: (v: string) => v || '-' },
{ title: '下一步计划', dataIndex: 'nextStep', render: (v: string) => v || '-' },
];
return (