duidui_admin_web/src/pages/Document/DocumentList.tsx
2026-03-27 10:38:12 +08:00

237 lines
9.0 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 { useState, useEffect, useCallback } from 'react'
import { Breadcrumb, Button, Card, Table, Modal, Form, Input, InputNumber, message, Space, Popconfirm, Upload, Typography } from 'antd'
import { FolderOutlined, FileOutlined, PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, ArrowLeftOutlined } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import { listFolders, createFolder, updateFolder, deleteFolder, listFiles, createFile, deleteFile, type DocFolder, type DocFile } from '@/api/document'
import { getUploadSignature } from '@/api/oss'
const ACCEPT_MIME = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt'
/** 可上传文件类型说明(展示用) */
const UPLOAD_FILE_TIP = '支持上传PDF、Word.doc/.docx、Excel.xls/.xlsx、PPT.ppt/.pptx、TXT 等格式。'
const DocumentList = () => {
const [folders, setFolders] = useState<DocFolder[]>([])
const [files, setFiles] = useState<DocFile[]>([])
const [currentFolder, setCurrentFolder] = useState<DocFolder | null>(null)
const [loading, setLoading] = useState(false)
const [folderModalVisible, setFolderModalVisible] = useState(false)
const [editingFolder, setEditingFolder] = useState<DocFolder | null>(null)
const [uploading, setUploading] = useState(false)
const [form] = Form.useForm()
const fetchFolders = useCallback(async () => {
setLoading(true)
try {
const res = await listFolders()
if (res.success && res.list) setFolders(res.list)
} catch (e) {
message.error('获取文件夹列表失败')
} finally {
setLoading(false)
}
}, [])
const fetchFiles = useCallback(async (folderId: string) => {
setLoading(true)
try {
const res = await listFiles(folderId)
if (res.success && res.list) setFiles(res.list)
} catch (e) {
message.error('获取文件列表失败')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchFolders()
}, [fetchFolders])
useEffect(() => {
if (currentFolder) fetchFiles(currentFolder.id)
else setFiles([])
}, [currentFolder, fetchFiles])
const handleCreateFolder = () => {
setEditingFolder(null)
form.resetFields()
setFolderModalVisible(true)
}
const handleEditFolder = (record: DocFolder) => {
setEditingFolder(record)
form.setFieldsValue({ name: record.name, sort_order: record.sort_order })
setFolderModalVisible(true)
}
const handleFolderSubmit = async () => {
try {
const values = await form.validateFields()
if (editingFolder) {
await updateFolder({ id: editingFolder.id, name: values.name, sort_order: values.sort_order ?? 0 })
message.success('更新成功')
} else {
await createFolder({ name: values.name, sort_order: values.sort_order ?? 0 })
message.success('创建成功')
}
setFolderModalVisible(false)
fetchFolders()
} catch (e: any) {
if (e.errorFields) return
message.error(e.message || '操作失败')
}
}
const handleDeleteFolder = async (id: string) => {
try {
await deleteFolder(id)
message.success('删除成功')
if (currentFolder?.id === id) setCurrentFolder(null)
fetchFolders()
} catch (e: any) {
message.error(e.response?.data?.message || e.message || '删除失败')
}
}
const handleUploadFile = async (file: File) => {
if (!currentFolder) return
setUploading(true)
try {
const credentials = await getUploadSignature('doc')
const key = `${credentials.dir}${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${(file.name.split('.').pop() || '')}`
const formData = new FormData()
formData.append('success_action_status', '200')
formData.append('policy', credentials.policy)
formData.append('x-oss-signature', credentials.signature)
formData.append('x-oss-signature-version', credentials.x_oss_signature_version)
formData.append('x-oss-credential', credentials.x_oss_credential)
formData.append('x-oss-date', credentials.x_oss_date)
formData.append('key', key)
formData.append('x-oss-security-token', credentials.security_token)
formData.append('file', file)
const resp = await fetch(credentials.host, { method: 'POST', body: formData })
if (!resp.ok) throw new Error(`上传失败: ${resp.status}`)
const fileUrl = `${credentials.host}/${key}`
await createFile({
folder_id: currentFolder.id,
file_name: file.name,
file_url: fileUrl,
file_size: file.size,
mime_type: file.type || '',
})
message.success('上传成功')
fetchFiles(currentFolder.id)
} catch (e: any) {
message.error(e.message || '上传失败')
} finally {
setUploading(false)
}
return false // 阻止 Upload 默认上传
}
const handleDeleteFile = async (id: string) => {
try {
await deleteFile(id)
message.success('删除成功')
if (currentFolder) fetchFiles(currentFolder.id)
} catch (e: any) {
message.error(e.response?.data?.message || e.message || '删除失败')
}
}
const formatSize = (n: number) => {
if (n < 1024) return `${n} B`
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
return `${(n / (1024 * 1024)).toFixed(1)} MB`
}
const folderColumns: ColumnsType<DocFolder> = [
{ title: '名称', dataIndex: 'name', key: 'name', render: (name, record) => <a onClick={() => setCurrentFolder(record)}><FolderOutlined /> {name}</a> },
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80 },
{
title: '操作',
key: 'action',
width: 140,
render: (_, record) => (
<Space>
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditFolder(record)}></Button>
<Popconfirm title="确定删除该文件夹?其下文件记录将一并删除。" onConfirm={() => handleDeleteFolder(record.id)}>
<Button type="link" danger size="small" icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space>
),
},
]
const fileColumns: ColumnsType<DocFile> = [
{ title: '名称', dataIndex: 'name', key: 'name', render: (name, row) => <a href={row.file_url} target="_blank" rel="noopener noreferrer"><FileOutlined /> {name}</a> },
{ title: '原始文件名', dataIndex: 'file_name', key: 'file_name' },
{ title: '大小', dataIndex: 'file_size', key: 'file_size', width: 100, render: (n: number) => formatSize(n) },
{ title: '类型', dataIndex: 'mime_type', key: 'mime_type', width: 120 },
{
title: '操作',
key: 'action',
width: 80,
render: (_, record) => (
<Popconfirm title="确定删除该文档记录?" onConfirm={() => handleDeleteFile(record.id)}>
<Button type="link" danger size="small" icon={<DeleteOutlined />}></Button>
</Popconfirm>
),
},
]
return (
<div style={{ padding: 24 }}>
<Breadcrumb style={{ marginBottom: 16 }} items={[
{ title: <a onClick={() => setCurrentFolder(null)}></a> },
...(currentFolder ? [{ title: currentFolder.name }] : []),
]} />
<Card
title={currentFolder ? `文件夹:${currentFolder.name}` : '文件夹列表'}
extra={
<Space>
{currentFolder ? (
<>
<Button icon={<ArrowLeftOutlined />} onClick={() => setCurrentFolder(null)}></Button>
<Upload accept={ACCEPT_MIME} showUploadList={false} beforeUpload={handleUploadFile} disabled={uploading}>
<Button type="primary" icon={<UploadOutlined />} loading={uploading}></Button>
</Upload>
</>
) : (
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreateFolder}></Button>
)}
</Space>
}
>
{currentFolder ? (
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>{UPLOAD_FILE_TIP}</Typography.Text>
) : null}
{!currentFolder ? (
<Table rowKey="id" loading={loading} columns={folderColumns} dataSource={folders} pagination={false} />
) : (
<Table rowKey="id" loading={loading} columns={fileColumns} dataSource={files} pagination={false} />
)}
</Card>
<Modal
title={editingFolder ? '编辑文件夹' : '新建文件夹'}
open={folderModalVisible}
onOk={handleFolderSubmit}
onCancel={() => setFolderModalVisible(false)}
destroyOnClose
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="文件夹名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="请输入文件夹名称" />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default DocumentList