Files
gongxue-base/apps/admin/src/components/StudentProfileContent/AttachmentsTab.tsx

155 lines
5.3 KiB
TypeScript

import React, { useState } from 'react';
import { App, Button, Popconfirm, Space, Table, Upload } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { EyeOutlined, InboxOutlined, UploadOutlined } from '@ant-design/icons';
import api from '../../api';
import { message } from '../../ui/app-message';
import { useApiMutation } from '../../hooks/useApiMutation';
import { usePermission } from '../../hooks/usePermission';
import { getErrorMessage } from '../../utils/error';
import { ATTACHMENT_CATEGORY_OPTIONS, formatFileSize } from './shared';
import type { AttachmentRecord, TabProps } from './shared';
export const AttachmentsTab: React.FC<TabProps & { data: AttachmentRecord[] }> = ({
data,
studentId,
}) => {
const { modal } = App.useApp();
const { hasPermission } = usePermission();
const canPurgeArchive = hasPermission('archive:purge');
const [uploading, setUploading] = useState(false);
const deleteAttachmentMutation = useApiMutation(
async (attachmentId: number) => api.delete(`/archive/attachments/${attachmentId}`),
{ invalidate: [['archive', studentId]] },
);
const purgeAttachmentMutation = useApiMutation(
async (id: number) => api.delete(`/archive/attachments/${id}/permanent`),
{ invalidate: [['archive', studentId]] },
);
const uploadAttachmentMutation = useApiMutation(
async (formData: FormData) =>
api.post(`/archive/${studentId}/attachments`, formData),
{ invalidate: [['archive', studentId]] },
);
const handleDelete = async (attachmentId: number) => {
try {
await deleteAttachmentMutation.mutateAsync(attachmentId);
message.success('已归档');
} catch {
// 错误提示由 useApiMutation 统一处理
}
};
const handlePurge = (record: AttachmentRecord) => {
modal.confirm({
title: `永久删除附件「${record.fileName}」?`,
content: '删除后不可恢复,磁盘上的附件文件将被清除。确定继续?',
okText: '永久删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
try {
await purgeAttachmentMutation.mutateAsync(record.id);
message.success('已永久删除(不可恢复)');
} catch {
// 错误提示由 useApiMutation 统一处理
}
},
});
};
const columns: ColumnsType<AttachmentRecord> = [
{
title: '类别',
dataIndex: 'category',
render: (v: string) => ATTACHMENT_CATEGORY_OPTIONS.find((o) => o.value === v)?.label || v,
},
{ title: '文件名', dataIndex: 'fileName' },
{ title: '大小', dataIndex: 'fileSize', render: formatFileSize },
{
title: '操作',
render: (_: unknown, record: AttachmentRecord) => (
<Space>
<Button
size="small"
icon={<EyeOutlined />}
onClick={async () => {
try {
const blob = await api.get<Blob>(`/archive/${studentId}/attachments/${record.id}`, {
responseType: 'blob',
});
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
setTimeout(() => URL.revokeObjectURL(url), 60_000);
} catch (e: unknown) {
message.error(getErrorMessage(e, '查看失败'));
}
}}
>
</Button>
{hasPermission('student:edit') && record.status !== 'archived' ? (
<Popconfirm title="确定归档该附件?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" danger icon={<InboxOutlined />}>
</Button>
</Popconfirm>
) : null}
{record.status === 'archived' && canPurgeArchive ? (
<Button size="small" danger type="link" onClick={() => handlePurge(record)}>
</Button>
) : null}
</Space>
),
},
];
return (
<div>
{hasPermission('student:edit') ? (
<Upload
showUploadList={false}
customRequest={async (options) => {
const formData = new FormData();
formData.append(
'file',
options.file instanceof File
? options.file
: new File([options.file as Blob], 'attachment'),
);
setUploading(true);
try {
await uploadAttachmentMutation.mutateAsync(formData);
message.success('上传成功');
options.onSuccess?.({});
} catch (e) {
options.onError?.(e instanceof Error ? e : new Error('上传失败'));
} finally {
setUploading(false);
}
}}
>
<Button icon={<UploadOutlined />} loading={uploading}>
</Button>
</Upload>
) : null}
<Table<AttachmentRecord>
columns={columns}
dataSource={data}
rowKey="id"
rowClassName={(record) => (record.status === 'archived' ? 'archived-row' : '')}
pagination={{
defaultPageSize: 15,
showSizeChanger: true,
pageSizeOptions: [15, 30, 50],
}}
style={{ marginTop: 16 }}
/>
</div>
);
};