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

304 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>
);
};