390 lines
11 KiB
TypeScript
390 lines
11 KiB
TypeScript
// aislop-ignore-file: duplicate-block -- 单元格渲染结构相似且字段不同,逻辑已通过 EditableStudentCell 共享
|
|
import React from 'react';
|
|
import { Button, Popconfirm, Space, Tag } from 'antd';
|
|
import { EyeOutlined, InboxOutlined, UndoOutlined } from '@ant-design/icons';
|
|
import PermissionButton from '../../components/PermissionButton';
|
|
import EditableCell from '../../components/EditableCell';
|
|
import { maskIdNumber, maskPhone } from '../../utils/sensitive';
|
|
|
|
export const statusMap: Record<string, { text: string; color: string }> = {
|
|
active: { text: '在读', color: 'green' },
|
|
graduated: { text: '已毕业', color: 'blue' },
|
|
withdrawn: { text: '已退训', color: 'red' },
|
|
archived: { text: '已归档', color: '#999' },
|
|
};
|
|
|
|
export const STUDENT_FIELDS = {
|
|
name: 'name',
|
|
studentNo: 'studentNo',
|
|
ethnicity: 'ethnicity',
|
|
emergencyContact: 'emergencyContact',
|
|
supervisor: 'supervisor',
|
|
status: 'status',
|
|
organizationId: 'organizationId',
|
|
} as const;
|
|
|
|
export const SENSITIVE_LABELS = {
|
|
phone: '电话',
|
|
idNumber: '身份证号',
|
|
emergencyPhone: '紧急联系人电话',
|
|
} as const;
|
|
|
|
export const STUDENT_STATUS_OPTIONS = [
|
|
{ value: 'active', label: '在读' },
|
|
{ value: 'graduated', label: '已毕业' },
|
|
{ value: 'withdrawn', label: '已退训' },
|
|
];
|
|
|
|
export interface StudentColumnContext {
|
|
pageInfo: { current: number; pageSize: number };
|
|
organizations: Array<{ id: number; name: string; isHost?: boolean }>;
|
|
canChooseOrganization: boolean;
|
|
canEditStudent: boolean;
|
|
canDeleteStudent: boolean;
|
|
canPurgeStudent: boolean;
|
|
canViewSensitive: boolean;
|
|
onSaveCell: (record: any, field: string, value: unknown) => Promise<void> | void;
|
|
onViewSensitive: (recordId: number, field: string, value: string) => void;
|
|
onOpenDrawer: (recordId: number) => void;
|
|
onEdit: (record: any) => void;
|
|
onRestore: (id: number) => Promise<unknown> | unknown;
|
|
onPurge: (id: number, name: string) => void;
|
|
onArchive: (id: number) => Promise<unknown> | unknown;
|
|
}
|
|
|
|
export const EditableStudentCell = <R extends { id: number; status?: string }>({
|
|
value,
|
|
field,
|
|
record,
|
|
editor,
|
|
min,
|
|
max,
|
|
required,
|
|
options,
|
|
onSave,
|
|
children,
|
|
}: {
|
|
value: unknown;
|
|
field: string;
|
|
record: R;
|
|
editor?: React.ComponentProps<typeof EditableCell>['editor'];
|
|
min?: number;
|
|
max?: number;
|
|
required?: boolean;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
onSave: (record: R, field: string, value: unknown) => Promise<void> | void;
|
|
children?: React.ReactNode;
|
|
}) => (
|
|
<EditableCell
|
|
value={value}
|
|
editor={editor}
|
|
min={min}
|
|
max={max}
|
|
required={required}
|
|
options={options}
|
|
permission="student:edit"
|
|
disabled={record.status === 'archived'}
|
|
onSave={async (next) => {
|
|
await onSave(record, field, next);
|
|
}}
|
|
>
|
|
{children ?? String(value ?? '-')}
|
|
</EditableCell>
|
|
);
|
|
|
|
export const SensitiveValue: React.FC<{
|
|
value: string;
|
|
masked: string;
|
|
label: string;
|
|
recordId: number;
|
|
canViewSensitive: boolean;
|
|
onViewSensitive: (recordId: number, field: string, value: string) => void;
|
|
}> = ({ value, masked, label, recordId, canViewSensitive, onViewSensitive }) => {
|
|
if (!value) return <>-</>;
|
|
return (
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
|
<span style={{ marginRight: 4 }}>{masked}</span>
|
|
{canViewSensitive ? (
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
style={{ padding: '8px 4px', flex: 'none' }}
|
|
onClick={() => onViewSensitive(recordId, label, value)}
|
|
title="点击查看完整号码"
|
|
>
|
|
<EyeOutlined style={{ fontSize: 12, color: '#999' }} />
|
|
</Button>
|
|
) : null}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
function buildIdentityColumns(ctx: StudentColumnContext) {
|
|
const {
|
|
pageInfo,
|
|
canViewSensitive,
|
|
onSaveCell,
|
|
onViewSensitive,
|
|
} = ctx;
|
|
|
|
return [
|
|
{
|
|
title: '序号',
|
|
key: 'index',
|
|
width: 70,
|
|
render: (_: unknown, __: unknown, index: number) =>
|
|
(pageInfo.current - 1) * pageInfo.pageSize + index + 1,
|
|
},
|
|
{
|
|
title: '姓名',
|
|
dataIndex: 'name',
|
|
width: 120,
|
|
render: (v: string, record: any) => (
|
|
<EditableStudentCell value={v} field={STUDENT_FIELDS.name} record={record} required onSave={onSaveCell}>
|
|
{v}
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
{
|
|
title: '电话',
|
|
dataIndex: 'phone',
|
|
width: 140,
|
|
render: (v: string, record: any) => (
|
|
<SensitiveValue
|
|
value={v}
|
|
masked={maskPhone(v)}
|
|
label={SENSITIVE_LABELS.phone}
|
|
recordId={record.id}
|
|
canViewSensitive={canViewSensitive}
|
|
onViewSensitive={onViewSensitive}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '学号',
|
|
dataIndex: 'studentNo',
|
|
width: 120,
|
|
render: (v: string, record: any) => (
|
|
<EditableStudentCell value={v} field={STUDENT_FIELDS.studentNo} record={record} onSave={onSaveCell}>
|
|
{v || '-'}
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
{
|
|
title: '身份证',
|
|
dataIndex: 'idNumber',
|
|
width: 180,
|
|
render: (v: string, record: any) => (
|
|
<SensitiveValue
|
|
value={v}
|
|
masked={maskIdNumber(v)}
|
|
label={SENSITIVE_LABELS.idNumber}
|
|
recordId={record.id}
|
|
canViewSensitive={canViewSensitive}
|
|
onViewSensitive={onViewSensitive}
|
|
/>
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildContactColumns(ctx: StudentColumnContext) {
|
|
const { organizations, canChooseOrganization, canViewSensitive, onSaveCell, onViewSensitive } =
|
|
ctx;
|
|
return [
|
|
{
|
|
title: '民族',
|
|
dataIndex: 'ethnicity',
|
|
width: 90,
|
|
render: (v: string, record: any) => (
|
|
<EditableStudentCell value={v} field={STUDENT_FIELDS.ethnicity} record={record} onSave={onSaveCell}>
|
|
{v || '-'}
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
{
|
|
title: '紧急联系人',
|
|
dataIndex: 'emergencyContact',
|
|
width: 100,
|
|
render: (v: string, record: any) => (
|
|
<EditableStudentCell
|
|
value={v}
|
|
field={STUDENT_FIELDS.emergencyContact}
|
|
record={record}
|
|
onSave={onSaveCell}
|
|
>
|
|
{v || '-'}
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
{
|
|
title: '紧急联系人电话',
|
|
dataIndex: 'emergencyPhone',
|
|
width: 150,
|
|
render: (v: string, record: any) => (
|
|
<SensitiveValue
|
|
value={v}
|
|
masked={maskPhone(v)}
|
|
label={SENSITIVE_LABELS.emergencyPhone}
|
|
recordId={record.id}
|
|
canViewSensitive={canViewSensitive}
|
|
onViewSensitive={onViewSensitive}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '所属机构',
|
|
dataIndex: 'organization',
|
|
width: 100,
|
|
render: (organization: { name?: string } | null, record: any) =>
|
|
canChooseOrganization ? (
|
|
<EditableStudentCell
|
|
value={record.organizationId}
|
|
field={STUDENT_FIELDS.organizationId}
|
|
record={record}
|
|
editor="select"
|
|
options={organizations.map((item) => ({ value: item.id, label: item.name }))}
|
|
required
|
|
onSave={onSaveCell}
|
|
>
|
|
{organization?.name ? (
|
|
<Tag
|
|
color="purple"
|
|
style={{ maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
>
|
|
{organization.name}
|
|
</Tag>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</EditableStudentCell>
|
|
) : organization?.name ? (
|
|
<Tag color="purple">{organization.name}</Tag>
|
|
) : (
|
|
'-'
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildProfileColumns(ctx: StudentColumnContext) {
|
|
const { onSaveCell } = ctx;
|
|
return [
|
|
{
|
|
title: '负责人',
|
|
dataIndex: 'supervisor',
|
|
width: 100,
|
|
render: (v: string, record: any) => (
|
|
<EditableStudentCell value={v} field={STUDENT_FIELDS.supervisor} record={record} onSave={onSaveCell}>
|
|
{v || '-'}
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 80,
|
|
render: (s: string, record: any) => (
|
|
<EditableStudentCell
|
|
value={s}
|
|
field={STUDENT_FIELDS.status}
|
|
record={record}
|
|
editor="select"
|
|
options={STUDENT_STATUS_OPTIONS}
|
|
onSave={onSaveCell}
|
|
>
|
|
<Tag
|
|
color={statusMap[s]?.color}
|
|
style={{ maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
>
|
|
{statusMap[s]?.text || s}
|
|
</Tag>
|
|
</EditableStudentCell>
|
|
),
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildActionColumn(ctx: StudentColumnContext) {
|
|
const {
|
|
canEditStudent,
|
|
canDeleteStudent,
|
|
canPurgeStudent,
|
|
onOpenDrawer,
|
|
onEdit,
|
|
onRestore,
|
|
onPurge,
|
|
onArchive,
|
|
} = ctx;
|
|
return {
|
|
title: '操作',
|
|
fixed: 'right' as const,
|
|
width: 180,
|
|
render: (_: any, record: any) => (
|
|
<Space>
|
|
{record.status === 'archived' ? (
|
|
<>
|
|
{canEditStudent ? (
|
|
<Popconfirm
|
|
title="确定恢复此学生?恢复后将重新出现在学生列表中。"
|
|
onConfirm={() => onRestore(record.id)}
|
|
okText="恢复"
|
|
cancelText="取消"
|
|
>
|
|
<Button size="small" icon={<UndoOutlined />} type="link">
|
|
恢复
|
|
</Button>
|
|
</Popconfirm>
|
|
) : null}
|
|
{canPurgeStudent ? (
|
|
<Button
|
|
size="small"
|
|
danger
|
|
type="link"
|
|
onClick={() => onPurge(record.id, record.name)}
|
|
>
|
|
删除
|
|
</Button>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<>
|
|
<PermissionButton
|
|
permission="student:view"
|
|
size="small"
|
|
type="link"
|
|
onClick={() => onOpenDrawer(record.id)}
|
|
>
|
|
档案
|
|
</PermissionButton>
|
|
<PermissionButton permission="student:edit" size="small" onClick={() => onEdit(record)}>
|
|
编辑
|
|
</PermissionButton>
|
|
{canDeleteStudent ? (
|
|
<Popconfirm
|
|
title="归档后不会删除数据,可随时恢复。确定归档?"
|
|
onConfirm={() => onArchive(record.id)}
|
|
okText="归档"
|
|
cancelText="取消"
|
|
>
|
|
<Button size="small" icon={<InboxOutlined />}>
|
|
归档
|
|
</Button>
|
|
</Popconfirm>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</Space>
|
|
),
|
|
};
|
|
}
|
|
|
|
export function buildStudentColumns(ctx: StudentColumnContext) {
|
|
return [
|
|
...buildIdentityColumns(ctx),
|
|
...buildContactColumns(ctx),
|
|
...buildProfileColumns(ctx),
|
|
buildActionColumn(ctx),
|
|
];
|
|
}
|