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

365 lines
9.9 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 } 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