Files
gongxue-base/apps/admin/src/pages/Students/StudentColumns.tsx

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),
];
}