365 lines
9.9 KiB
TypeScript
365 lines
9.9 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import {
|
||
Table,
|
||
Button,
|
||
Form,
|
||
Input,
|
||
Modal,
|
||
message,
|
||
Space,
|
||
Popconfirm,
|
||
} from 'antd'
|
||
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, EyeOutlined } from '@ant-design/icons'
|
||
import type { ColumnsType } from 'antd/es/table'
|
||
import type { Material } from '@/types/question'
|
||
import { MaterialType } from '@/types/question'
|
||
import { searchMaterials, createMaterial, updateMaterial, deleteMaterial } from '@/api/question'
|
||
import { DEFAULT_PAGE_SIZE } from '@/constants'
|
||
import dayjs from 'dayjs'
|
||
import RichTextEditor from '@/components/RichTextEditor/RichTextEditor'
|
||
|
||
const MaterialList = () => {
|
||
const [loading, setLoading] = useState(false)
|
||
const [dataSource, setDataSource] = useState<Material[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE)
|
||
const [query, setQuery] = useState('')
|
||
const [modalVisible, setModalVisible] = useState(false)
|
||
const [previewVisible, setPreviewVisible] = useState(false)
|
||
const [editingRecord, setEditingRecord] = useState<Material | null>(null)
|
||
const [previewContent, setPreviewContent] = useState('')
|
||
|
||
const [form] = Form.useForm()
|
||
|
||
// 获取列表数据
|
||
const fetchData = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await searchMaterials({
|
||
query,
|
||
type: MaterialType.OBJECTIVE,
|
||
page,
|
||
pageSize,
|
||
})
|
||
if (res.data.code === 200) {
|
||
const list = res.data.data.list || []
|
||
const total = res.data.data.total || 0
|
||
setDataSource(list)
|
||
setTotal(total)
|
||
}
|
||
} catch (error: any) {
|
||
console.error('获取材料列表错误:', error)
|
||
setDataSource([])
|
||
setTotal(0)
|
||
if (error.response?.status >= 500 || !error.response) {
|
||
message.error('服务器错误,请稍后重试')
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchData()
|
||
}, [page, pageSize, query])
|
||
|
||
// 监听 Modal 打开,设置表单初始值
|
||
useEffect(() => {
|
||
if (modalVisible && editingRecord) {
|
||
console.log('编辑材料数据:', editingRecord)
|
||
form.setFieldsValue({
|
||
name: editingRecord.name || '',
|
||
content: editingRecord.content || '',
|
||
})
|
||
}
|
||
}, [modalVisible, editingRecord, form])
|
||
|
||
// 打开新增/编辑弹窗
|
||
const handleOpenModal = (record?: Material) => {
|
||
if (record) {
|
||
setEditingRecord(record)
|
||
} else {
|
||
setEditingRecord(null)
|
||
form.resetFields()
|
||
form.setFieldsValue({
|
||
name: '',
|
||
content: '',
|
||
})
|
||
}
|
||
setModalVisible(true)
|
||
}
|
||
|
||
// 预览材料内容
|
||
const handlePreview = (record: Material) => {
|
||
setPreviewContent(record.content)
|
||
setPreviewVisible(true)
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
try {
|
||
const values = await form.validateFields()
|
||
|
||
const formData = {
|
||
type: MaterialType.OBJECTIVE,
|
||
name: values.name || '',
|
||
content: values.content || '',
|
||
}
|
||
|
||
if (editingRecord) {
|
||
// 编辑
|
||
await updateMaterial({ ...editingRecord, ...formData })
|
||
message.success('编辑材料成功')
|
||
} else {
|
||
// 新增
|
||
await createMaterial(formData)
|
||
message.success('新增材料成功')
|
||
}
|
||
|
||
setModalVisible(false)
|
||
setEditingRecord(null)
|
||
form.resetFields()
|
||
fetchData()
|
||
} catch (error: any) {
|
||
const errorMsg = error?.message || '操作失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 删除材料
|
||
const handleDelete = async (id: string) => {
|
||
try {
|
||
await deleteMaterial(id)
|
||
message.success('删除成功')
|
||
fetchData()
|
||
} catch (error: any) {
|
||
const errorMsg = error?.message || '删除失败'
|
||
message.error(errorMsg)
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = (value: string) => {
|
||
setQuery(value)
|
||
setPage(1)
|
||
}
|
||
|
||
const columns: ColumnsType<Material> = [
|
||
{
|
||
title: '材料名称',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
width: 200,
|
||
ellipsis: true,
|
||
render: (name: string) => name || <span style={{ color: '#999' }}>未命名</span>,
|
||
},
|
||
{
|
||
title: '材料内容',
|
||
dataIndex: 'content',
|
||
key: 'content',
|
||
width: 400,
|
||
ellipsis: true,
|
||
render: (content: string) => {
|
||
// 移除 HTML 标签,只显示纯文本
|
||
const textContent = content
|
||
.replace(/<img[^>]*>/gi, '[图片]')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/\n/g, ' ')
|
||
.trim()
|
||
return textContent || <span style={{ color: '#999' }}>无内容</span>
|
||
},
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
dataIndex: 'createdAt',
|
||
key: 'createdAt',
|
||
width: 180,
|
||
render: (timestamp: number) => {
|
||
if (!timestamp) return '-'
|
||
return dayjs(timestamp * 1000).format('YYYY-MM-DD HH:mm')
|
||
},
|
||
},
|
||
{
|
||
title: '更新时间',
|
||
dataIndex: 'updatedAt',
|
||
key: 'updatedAt',
|
||
width: 180,
|
||
render: (timestamp: number) => {
|
||
if (!timestamp) return '-'
|
||
return dayjs(timestamp * 1000).format('YYYY-MM-DD HH:mm')
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 200,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Space>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EyeOutlined />}
|
||
onClick={() => handlePreview(record)}
|
||
>
|
||
预览
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleOpenModal(record)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Popconfirm
|
||
title="确定要删除这个材料吗?"
|
||
onConfirm={() => handleDelete(record.id)}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
>
|
||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
{/* 页面标题 */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}>客观题管理 - 材料管理</h2>
|
||
<div style={{ color: '#666', fontSize: 14, marginTop: 8 }}>
|
||
管理客观题材料,材料可用于题目中
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: 16 }}>
|
||
<Space wrap>
|
||
<Input.Search
|
||
placeholder="搜索材料名称或内容"
|
||
allowClear
|
||
style={{ width: 300 }}
|
||
onSearch={handleSearch}
|
||
/>
|
||
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
||
刷新
|
||
</Button>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||
新增材料
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<Table
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
rowKey="id"
|
||
scroll={{ x: 1200 }}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total,
|
||
showSizeChanger: true,
|
||
showTotal: (total) => `共 ${total} 个材料`,
|
||
onChange: (page, pageSize) => {
|
||
setPage(page)
|
||
setPageSize(pageSize)
|
||
},
|
||
}}
|
||
/>
|
||
|
||
{/* 新增/编辑弹窗 */}
|
||
<Modal
|
||
title={editingRecord ? '编辑材料' : '新增材料'}
|
||
open={modalVisible}
|
||
onOk={handleSubmit}
|
||
onCancel={() => {
|
||
setModalVisible(false)
|
||
setEditingRecord(null)
|
||
form.resetFields()
|
||
}}
|
||
width={900}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item
|
||
label="材料名称"
|
||
name="name"
|
||
rules={[{ required: true, message: '请输入材料名称' }]}
|
||
>
|
||
<Input placeholder="请输入材料名称" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
label="材料内容"
|
||
name="content"
|
||
rules={[{ required: true, message: '请输入材料内容' }]}
|
||
>
|
||
<RichTextEditor placeholder="请输入材料内容,支持插入图片和文字格式" rows={10} />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* 预览弹窗 */}
|
||
<Modal
|
||
title="材料预览"
|
||
open={previewVisible}
|
||
onCancel={() => setPreviewVisible(false)}
|
||
footer={[
|
||
<Button key="close" onClick={() => setPreviewVisible(false)}>
|
||
关闭
|
||
</Button>,
|
||
]}
|
||
width={900}
|
||
>
|
||
<div
|
||
style={{
|
||
maxHeight: '70vh',
|
||
overflow: 'auto',
|
||
padding: '20px',
|
||
backgroundColor: '#fff',
|
||
borderRadius: 4,
|
||
border: '1px solid #e8e8e8',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
wordBreak: 'break-word',
|
||
lineHeight: '1.8',
|
||
fontSize: '14px',
|
||
color: '#333',
|
||
}}
|
||
dangerouslySetInnerHTML={{
|
||
__html: (() => {
|
||
let html = previewContent || ''
|
||
if (!html) return ''
|
||
|
||
// react-quill 生成的 HTML 已经是完整的格式,直接使用
|
||
// 只需要确保图片标签有正确的样式
|
||
return html.replace(
|
||
/<img([^>]*)>/gi,
|
||
(match, attrs) => {
|
||
if (!attrs.includes('style=')) {
|
||
return `<img${attrs} style="max-width: 100%; height: auto; margin: 12px 0; border-radius: 4px; display: block;">`
|
||
}
|
||
return match
|
||
}
|
||
)
|
||
})()
|
||
}}
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default MaterialList
|