304 lines
9.7 KiB
TypeScript
304 lines
9.7 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
App,
|
||
Button,
|
||
Input,
|
||
Popconfirm,
|
||
Select,
|
||
Space,
|
||
Upload,
|
||
} from 'antd';
|
||
import type { UploadProps } from 'antd';
|
||
import {
|
||
CloudUploadOutlined,
|
||
DeleteOutlined,
|
||
DownloadOutlined,
|
||
ExportOutlined,
|
||
InboxOutlined,
|
||
PlusOutlined,
|
||
SwapOutlined,
|
||
SyncOutlined,
|
||
UndoOutlined,
|
||
UploadOutlined,
|
||
} from '@ant-design/icons';
|
||
import PermissionButton from '../../components/PermissionButton';
|
||
import { RefreshButton } from '../../components/RefreshButton';
|
||
import { statusMap } from './StudentColumns';
|
||
|
||
export interface StudentsToolbarProps {
|
||
onSearchName: (value: string) => void;
|
||
filterStatus?: string;
|
||
onFilterStatus: (value?: string) => void;
|
||
effectiveFilterOrganizationId?: number;
|
||
onFilterOrganization: (value?: number) => void;
|
||
canViewOrganizations: boolean;
|
||
organizations: Array<{ id: number; name: string }>;
|
||
filterClassId?: number;
|
||
onFilterClass: (value?: number) => void;
|
||
classOptions: Array<{ id: number; name: string; code?: string }>;
|
||
filterTeacherId?: number;
|
||
onFilterTeacher: (value?: number) => void;
|
||
teacherOptions: Array<{ id: number; name: string; username: string }>;
|
||
showArchived: boolean;
|
||
onToggleArchived: () => void;
|
||
selectedRowKeys: number[];
|
||
batchLoading: boolean;
|
||
canEditStudent: boolean;
|
||
canPurgeStudent: boolean;
|
||
canDeleteStudent: boolean;
|
||
canSyncJinshuju: boolean;
|
||
canSyncDingTalk: boolean;
|
||
dingSyncLoading: boolean;
|
||
onBatchRestore: () => void;
|
||
onBatchPurge: () => void;
|
||
onBatchDelete: () => void;
|
||
onAddStudent: () => void;
|
||
onOpenJinshuju: () => void;
|
||
onDingTalkSync: () => void;
|
||
onCreateImport: UploadProps['customRequest'];
|
||
onUpdateImport: UploadProps['customRequest'];
|
||
onDownloadTemplate: () => void;
|
||
onExport: () => void;
|
||
templateLoading?: boolean;
|
||
exportLoading?: boolean;
|
||
refreshLoading?: boolean;
|
||
onRefresh?: () => void;
|
||
}
|
||
|
||
export const StudentsToolbar: React.FC<StudentsToolbarProps> = ({
|
||
onSearchName,
|
||
filterStatus,
|
||
onFilterStatus,
|
||
effectiveFilterOrganizationId,
|
||
onFilterOrganization,
|
||
canViewOrganizations,
|
||
organizations,
|
||
filterClassId,
|
||
onFilterClass,
|
||
classOptions,
|
||
filterTeacherId,
|
||
onFilterTeacher,
|
||
teacherOptions,
|
||
showArchived,
|
||
onToggleArchived,
|
||
selectedRowKeys,
|
||
batchLoading,
|
||
canEditStudent,
|
||
canPurgeStudent,
|
||
canDeleteStudent,
|
||
canSyncJinshuju,
|
||
canSyncDingTalk,
|
||
dingSyncLoading,
|
||
onBatchRestore,
|
||
onBatchPurge,
|
||
onBatchDelete,
|
||
onAddStudent,
|
||
onOpenJinshuju,
|
||
onDingTalkSync,
|
||
onCreateImport,
|
||
onUpdateImport,
|
||
onDownloadTemplate,
|
||
onExport,
|
||
templateLoading,
|
||
exportLoading,
|
||
refreshLoading,
|
||
onRefresh,
|
||
}) => {
|
||
const { modal } = App.useApp();
|
||
return (
|
||
<>
|
||
<div className="responsive-toolbar">
|
||
<Space wrap className="responsive-toolbar__group">
|
||
<Input.Search
|
||
placeholder="搜索学生姓名"
|
||
onSearch={onSearchName}
|
||
allowClear
|
||
style={{ width: 250 }}
|
||
/>
|
||
<Select
|
||
placeholder="状态筛选"
|
||
allowClear
|
||
style={{ width: 120 }}
|
||
value={filterStatus}
|
||
onChange={onFilterStatus}
|
||
>
|
||
{Object.entries(statusMap)
|
||
.filter(([k]) => k !== 'archived')
|
||
.map(([k, v]) => (
|
||
<Select.Option key={k} value={k}>
|
||
{v.text}
|
||
</Select.Option>
|
||
))}
|
||
</Select>
|
||
{canViewOrganizations ? (
|
||
<Select
|
||
placeholder="所属机构"
|
||
allowClear
|
||
style={{ width: 140 }}
|
||
value={effectiveFilterOrganizationId}
|
||
onChange={onFilterOrganization}
|
||
>
|
||
{organizations.map((t: { id: number; name: string }) => (
|
||
<Select.Option key={t.id} value={t.id}>
|
||
{t.name}
|
||
</Select.Option>
|
||
))}
|
||
</Select>
|
||
) : null}
|
||
<Select
|
||
placeholder="所属班级"
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
style={{ width: 160 }}
|
||
value={filterClassId}
|
||
onChange={onFilterClass}
|
||
options={classOptions.map((item) => ({
|
||
value: item.id,
|
||
label: item.code ? `${item.name}(${item.code})` : item.name,
|
||
}))}
|
||
/>
|
||
<Select
|
||
placeholder="所属老师"
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
style={{ width: 160 }}
|
||
value={filterTeacherId}
|
||
onChange={onFilterTeacher}
|
||
options={teacherOptions.map((item) => ({
|
||
value: item.id,
|
||
label: item.name === item.username ? item.name : `${item.name}(${item.username})`,
|
||
}))}
|
||
/>
|
||
<Button type={showArchived ? 'primary' : 'default'} onClick={onToggleArchived}>
|
||
{showArchived ? '返回正常数据' : '查看已归档'}
|
||
</Button>
|
||
</Space>
|
||
<Space wrap className="responsive-toolbar__group">
|
||
{onRefresh ? <RefreshButton loading={refreshLoading} onRefresh={onRefresh} /> : null}
|
||
{showArchived && canEditStudent ? (
|
||
<>
|
||
<Popconfirm
|
||
title={`确定批量恢复选中的 ${selectedRowKeys.length} 名学生?`}
|
||
onConfirm={onBatchRestore}
|
||
okText="恢复"
|
||
cancelText="取消"
|
||
disabled={selectedRowKeys.length === 0}
|
||
>
|
||
<Button
|
||
type="primary"
|
||
icon={<UndoOutlined />}
|
||
disabled={selectedRowKeys.length === 0}
|
||
loading={batchLoading}
|
||
>
|
||
批量恢复
|
||
</Button>
|
||
</Popconfirm>
|
||
{canPurgeStudent ? (
|
||
<Popconfirm
|
||
title={`确定永久删除选中的 ${selectedRowKeys.length} 名学生?删除后不可恢复!`}
|
||
onConfirm={onBatchPurge}
|
||
okText="永久删除"
|
||
okButtonProps={{ danger: true }}
|
||
cancelText="取消"
|
||
disabled={selectedRowKeys.length === 0}
|
||
>
|
||
<Button
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
disabled={selectedRowKeys.length === 0}
|
||
loading={batchLoading}
|
||
>
|
||
批量删除
|
||
</Button>
|
||
</Popconfirm>
|
||
) : null}
|
||
</>
|
||
) : !showArchived && canDeleteStudent ? (
|
||
<Popconfirm
|
||
title={`确定批量归档选中的 ${selectedRowKeys.length} 名学生?(数据保留,可恢复)`}
|
||
onConfirm={onBatchDelete}
|
||
okText="归档"
|
||
cancelText="取消"
|
||
disabled={selectedRowKeys.length === 0}
|
||
>
|
||
<Button
|
||
danger
|
||
icon={<InboxOutlined />}
|
||
disabled={selectedRowKeys.length === 0}
|
||
loading={batchLoading}
|
||
>
|
||
批量归档
|
||
</Button>
|
||
</Popconfirm>
|
||
) : null}
|
||
{!showArchived ? (
|
||
<PermissionButton
|
||
permission="student:create"
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={onAddStudent}
|
||
>
|
||
添加学生
|
||
</PermissionButton>
|
||
) : null}
|
||
{!showArchived && (
|
||
<>
|
||
<Upload accept=".xlsx,.xls" showUploadList={false} customRequest={onCreateImport}>
|
||
<Button icon={<UploadOutlined />}>导入Excel</Button>
|
||
</Upload>
|
||
<Upload
|
||
accept=".xlsx,.xls"
|
||
showUploadList={false}
|
||
customRequest={(options) => {
|
||
const file = options.file as File;
|
||
modal.confirm({
|
||
title: '确认覆盖更新学生资料?',
|
||
content: `将按手机号/身份证匹配并更新「${file.name}」中的学生资料,匹配到的学生记录会被直接覆盖且无法撤销。建议先导出名单核对后再操作。`,
|
||
okText: '确认更新',
|
||
okButtonProps: { danger: true },
|
||
cancelText: '取消',
|
||
onOk: () => {
|
||
// 实现方只使用 { file, onSuccess, onError };info 参数仅用于满足类型签名
|
||
onUpdateImport?.(options, { defaultRequest: () => undefined });
|
||
},
|
||
});
|
||
}}
|
||
>
|
||
<Button icon={<SwapOutlined />}>更新已有学生资料</Button>
|
||
</Upload>
|
||
</>
|
||
)}
|
||
{!showArchived && canSyncJinshuju ? (
|
||
<Button icon={<CloudUploadOutlined />} onClick={onOpenJinshuju}>
|
||
同步金数据
|
||
</Button>
|
||
) : null}
|
||
{!showArchived && canSyncDingTalk ? (
|
||
<Button icon={<SyncOutlined />} loading={dingSyncLoading} onClick={onDingTalkSync}>
|
||
同步钉钉
|
||
</Button>
|
||
) : null}
|
||
<PermissionButton
|
||
permission="student:view"
|
||
icon={<DownloadOutlined />}
|
||
loading={templateLoading}
|
||
onClick={onDownloadTemplate}
|
||
>
|
||
下载模板
|
||
</PermissionButton>
|
||
<PermissionButton
|
||
permission="student:export"
|
||
icon={<ExportOutlined />}
|
||
loading={exportLoading}
|
||
onClick={onExport}
|
||
>
|
||
导出名单
|
||
</PermissionButton>
|
||
</Space>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|