feat: 重构各业务模块管理页面与服务
This commit is contained in:
388
apps/admin/src/pages/Students/StudentColumns.tsx
Normal file
388
apps/admin/src/pages/Students/StudentColumns.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
// 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: '操作',
|
||||
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),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user