667 lines
20 KiB
TypeScript
667 lines
20 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useIntl } from 'umi';
|
||
import { Button, Table, Modal, message, Input, Select, Form, Tooltip, Switch, Tag, Tabs } from 'antd';
|
||
import {
|
||
PlusOutlined,
|
||
DeleteOutlined,
|
||
ExclamationCircleOutlined,
|
||
SearchOutlined,
|
||
VerticalAlignTopOutlined,
|
||
} from '@ant-design/icons';
|
||
import { getNoticeList, addNotice, updateNotice, deleteNotice, batchDeleteNotice, updateNoticeStatus, updateNoticeTopStatus } from '@/servers/api/notice';
|
||
import './noticeManage.less';
|
||
// 引入封装的WangEditor组件
|
||
import WangEditor from '@/components/WangEidtor/WangEidtor';
|
||
|
||
const { Option } = Select;
|
||
const { TextArea } = Input;
|
||
const { TabPane } = Tabs;
|
||
|
||
type NoticeType = API.NoticeRecord & {
|
||
key: string;
|
||
};
|
||
|
||
const NoticeManage: React.FC = () => {
|
||
const intl = useIntl();
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||
const [loading, setLoading] = useState<boolean>(false);
|
||
const [form] = Form.useForm();
|
||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||
const [currentId, setCurrentId] = useState<string>('');
|
||
const [modalForm] = Form.useForm();
|
||
|
||
// 富文本内容状态
|
||
const [htmlZh, setHtmlZh] = useState<string>('');
|
||
const [htmlEn, setHtmlEn] = useState<string>('');
|
||
|
||
const [noticeData, setNoticeData] = useState<NoticeType[]>([
|
||
{
|
||
key: '1',
|
||
id: '1',
|
||
title: '系统维护通知',
|
||
titleEn: 'System Maintenance Notice',
|
||
content: '系统将于2023年7月1日进行例行维护,请提前做好准备。',
|
||
contentEn: 'The system will undergo routine maintenance on July 1, 2023. Please prepare in advance.',
|
||
createTime: '2023-06-25 10:30:00',
|
||
createBy: 'admin',
|
||
status: '1', // 已发布
|
||
isTop: '1', // 已置顶
|
||
settingEn: 1, // 设置英文
|
||
},
|
||
{
|
||
key: '2',
|
||
id: '2',
|
||
title: '新功能上线通知',
|
||
titleEn: 'New Feature Launch',
|
||
content: '系统新增了XXXX功能,欢迎使用。',
|
||
contentEn: 'The system has added XXXX function, welcome to use.',
|
||
createTime: '2023-06-20 14:45:00',
|
||
createBy: 'admin',
|
||
status: '0', // 草稿
|
||
isTop: '0', // 未置顶
|
||
settingEn: 1,
|
||
},
|
||
{
|
||
key: '3',
|
||
id: '3',
|
||
title: '用户反馈调查',
|
||
titleEn: '',
|
||
content: '为了提升用户体验,我们正在收集用户反馈...',
|
||
contentEn: '',
|
||
createTime: '2023-06-18 09:15:00',
|
||
createBy: 'admin',
|
||
status: '1', // 已发布
|
||
isTop: '0', // 未置顶
|
||
settingEn: 0,
|
||
},
|
||
]);
|
||
const [pagination, setPagination] = useState({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 3,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||
});
|
||
const [searchParams, setSearchParams] = useState<API.NoticeSearchParams>({});
|
||
const [activeTabKey, setActiveTabKey] = useState<string>('zh');
|
||
|
||
// 获取通知列表(模拟数据)
|
||
const fetchNoticeList = (current: number = 1, pageSize: number = 10) => {
|
||
setLoading(true);
|
||
// 实际项目中应调用API
|
||
getNoticeList({
|
||
...searchParams,
|
||
})
|
||
.then(res => {
|
||
// 此处为模拟数据,实际项目中应使用API返回的数据
|
||
setNoticeData([
|
||
{
|
||
key: '1',
|
||
id: '1',
|
||
title: '系统维护通知',
|
||
titleEn: 'System Maintenance Notice',
|
||
content: '系统将于2023年7月1日进行例行维护,请提前做好准备。',
|
||
contentEn: 'The system will undergo routine maintenance on July 1, 2023. Please prepare in advance.',
|
||
createTime: '2023-06-25 10:30:00',
|
||
createBy: 'admin',
|
||
status: '1', // 已发布
|
||
isTop: '1', // 已置顶
|
||
settingEn: 1,
|
||
},
|
||
{
|
||
key: '2',
|
||
id: '2',
|
||
title: '新功能上线通知',
|
||
titleEn: 'New Feature Launch',
|
||
content: '系统新增了XXXX功能,欢迎使用。',
|
||
contentEn: 'The system has added XXXX function, welcome to use.',
|
||
createTime: '2023-06-20 14:45:00',
|
||
createBy: 'admin',
|
||
status: '0', // 草稿
|
||
isTop: '0', // 未置顶
|
||
settingEn: 1,
|
||
},
|
||
{
|
||
key: '3',
|
||
id: '3',
|
||
title: '用户反馈调查',
|
||
titleEn: '',
|
||
content: '为了提升用户体验,我们正在收集用户反馈...',
|
||
contentEn: '',
|
||
createTime: '2023-06-18 09:15:00',
|
||
createBy: 'admin',
|
||
status: '1', // 已发布
|
||
isTop: '0', // 未置顶
|
||
settingEn: 0,
|
||
},
|
||
]);
|
||
setPagination({
|
||
...pagination,
|
||
current,
|
||
pageSize,
|
||
total: 3, // 应该是res.total
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('获取通知列表失败:', error);
|
||
message.error('获取通知列表失败');
|
||
})
|
||
.finally(() => {
|
||
setLoading(false);
|
||
});
|
||
};
|
||
|
||
// 首次加载时获取数据
|
||
useEffect(() => {
|
||
fetchNoticeList();
|
||
}, []);
|
||
|
||
// 处理编辑
|
||
const handleEdit = (record: NoticeType) => {
|
||
// 检查是否为已发布状态
|
||
if (record.status === '1') {
|
||
message.warning('已发布的通知不能编辑');
|
||
return;
|
||
}
|
||
|
||
setIsEdit(true);
|
||
setCurrentId(record.id);
|
||
setModalVisible(true);
|
||
setActiveTabKey('zh');
|
||
|
||
// 填充表单数据
|
||
modalForm.setFieldsValue({
|
||
isTop: record.isTop === '1',
|
||
titleZh: record.title,
|
||
titleEn: record.titleEn,
|
||
contentZh: record.content,
|
||
contentEn: record.contentEn || '',
|
||
});
|
||
};
|
||
|
||
// 处理删除
|
||
const showDeleteConfirm = (record: NoticeType) => {
|
||
// 检查是否为已发布状态
|
||
if (record.status === '1') {
|
||
message.warning('已发布的通知不能删除');
|
||
return;
|
||
}
|
||
|
||
Modal.confirm({
|
||
title: '确定要删除该通知吗?',
|
||
icon: <ExclamationCircleOutlined />,
|
||
content: `标题: ${record.title}`,
|
||
okText: '确定',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
maskClosable: false,
|
||
onOk: async () => {
|
||
try {
|
||
// 调用删除API
|
||
await deleteNotice(record.id);
|
||
// 更新本地数据
|
||
const newData = noticeData.filter(item => item.id !== record.id);
|
||
setNoticeData(newData);
|
||
message.success('删除成功');
|
||
} catch (error) {
|
||
console.error('删除通知失败:', error);
|
||
message.error('删除失败');
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
// 处理发布/下架
|
||
const handlePublishStatus = async (record: NoticeType) => {
|
||
// 状态: 0-草稿,1-已发布
|
||
const isPublished = record.status === '1';
|
||
const actionText = isPublished ? '下架' : '发布';
|
||
const newStatus = isPublished ? '0' : '1';
|
||
|
||
try {
|
||
// 调用API更新状态
|
||
await updateNoticeStatus(record.id, newStatus);
|
||
// 更新本地数据
|
||
const newData = noticeData.map(item =>
|
||
item.id === record.id ? { ...item, status: newStatus } : item
|
||
);
|
||
setNoticeData(newData);
|
||
message.success(`${actionText}成功`);
|
||
} catch (error) {
|
||
console.error(`${actionText}失败:`, error);
|
||
message.error(`${actionText}失败`);
|
||
}
|
||
};
|
||
|
||
// 处理置顶/取消置顶
|
||
const handleToggleTop = async (record: NoticeType) => {
|
||
const isTop = record.isTop === '1';
|
||
const actionText = isTop ? '取消置顶' : '置顶';
|
||
const newIsTop = isTop ? '0' : '1';
|
||
|
||
try {
|
||
// 调用API更新置顶状态
|
||
await updateNoticeTopStatus(record.id, newIsTop);
|
||
// 更新本地数据
|
||
const newData = noticeData.map(item =>
|
||
item.id === record.id ? { ...item, isTop: newIsTop } : item
|
||
);
|
||
setNoticeData(newData);
|
||
message.success(`${actionText}成功`);
|
||
} catch (error) {
|
||
console.error(`${actionText}失败:`, error);
|
||
message.error(`${actionText}失败`);
|
||
}
|
||
};
|
||
|
||
// 获取状态标签
|
||
const getStatusTag = (status: string) => {
|
||
switch (status) {
|
||
case '0':
|
||
return <Tag color="default">草稿</Tag>;
|
||
case '1':
|
||
return <Tag color="green">已发布</Tag>;
|
||
case '2':
|
||
return <Tag color="orange">已下架</Tag>;
|
||
default:
|
||
return <Tag color="default">未知</Tag>;
|
||
}
|
||
};
|
||
|
||
// 处理表格分页变化
|
||
const handleTableChange = (newPagination: any) => {
|
||
fetchNoticeList(newPagination.current, newPagination.pageSize);
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '序号',
|
||
dataIndex: 'id',
|
||
key: 'id',
|
||
align: 'center' as const,
|
||
width: 80,
|
||
},
|
||
{
|
||
title: '标题',
|
||
dataIndex: 'title',
|
||
key: 'title',
|
||
ellipsis: {
|
||
showTitle: false,
|
||
},
|
||
render: (title: string, record: NoticeType) => (
|
||
<Tooltip placement="topLeft" title={title}>
|
||
<span>
|
||
{record.isTop === '1' && <Tag color="red" style={{ marginRight: 8 }}>置顶</Tag>}
|
||
{title}
|
||
</span>
|
||
</Tooltip>
|
||
),
|
||
},
|
||
{
|
||
title: '日期',
|
||
dataIndex: 'createTime',
|
||
key: 'createTime',
|
||
align: 'center' as const,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
align: 'center' as const,
|
||
render: (status: string) => getStatusTag(status),
|
||
},
|
||
{
|
||
title: '发布人',
|
||
dataIndex: 'createBy',
|
||
key: 'createBy',
|
||
align: 'center' as const,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 300,
|
||
align: 'center' as const,
|
||
render: (_: unknown, record: NoticeType) => (
|
||
<>
|
||
{record.status === '1' ? (
|
||
<>
|
||
<Button type="link" onClick={() => handlePublishStatus(record)}>
|
||
下架
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
onClick={() => handleToggleTop(record)}
|
||
>
|
||
{record.isTop === '1' ? '取消置顶' : '置顶'}
|
||
</Button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Button type="link" onClick={() => handleEdit(record)}>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
onClick={() => handlePublishStatus(record)}
|
||
>
|
||
发布
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
onClick={() => showDeleteConfirm(record)}
|
||
>
|
||
删除
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
onClick={() => handleToggleTop(record)}
|
||
>
|
||
{record.isTop === '1' ? '取消置顶' : '置顶'}
|
||
</Button>
|
||
</>
|
||
)}
|
||
</>
|
||
),
|
||
},
|
||
];
|
||
|
||
// 行选择限制
|
||
const checkSelectable = (record: NoticeType) => {
|
||
return record.status !== '1'; // 已发布的不能选择
|
||
};
|
||
|
||
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
|
||
setSelectedRowKeys(newSelectedRowKeys);
|
||
};
|
||
|
||
const rowSelection = {
|
||
selectedRowKeys,
|
||
onChange: onSelectChange,
|
||
getCheckboxProps: (record: NoticeType) => ({
|
||
disabled: record.status === '1', // 已发布的不能选择
|
||
}),
|
||
};
|
||
|
||
const hasSelected = selectedRowKeys.length > 0;
|
||
|
||
// 处理添加
|
||
const handleAdd = () => {
|
||
setIsEdit(false);
|
||
setCurrentId('');
|
||
modalForm.resetFields();
|
||
setModalVisible(true);
|
||
setActiveTabKey('zh');
|
||
};
|
||
|
||
// 处理批量删除
|
||
const handleBatchDelete = () => {
|
||
Modal.confirm({
|
||
title: '确定要删除选中的通知吗?',
|
||
icon: <ExclamationCircleOutlined />,
|
||
content: '删除后无法恢复',
|
||
okText: '确定',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
maskClosable: false,
|
||
onOk: async () => {
|
||
setLoading(true);
|
||
try {
|
||
// 获取可删除的ID(非已发布状态)
|
||
const deleteIds = selectedRowKeys.filter(key => {
|
||
const record = noticeData.find(item => item.key === key);
|
||
return record && record.status !== '1';
|
||
}) as string[];
|
||
|
||
if (deleteIds.length > 0) {
|
||
// 调用批量删除API
|
||
await batchDeleteNotice(deleteIds);
|
||
// 更新本地数据
|
||
const newData = noticeData.filter(item => !deleteIds.includes(item.key));
|
||
setNoticeData(newData);
|
||
setSelectedRowKeys([]);
|
||
message.success('删除成功');
|
||
} else {
|
||
message.warning('没有可删除的通知');
|
||
}
|
||
} catch (error) {
|
||
console.error('批量删除失败:', error);
|
||
message.error('批量删除失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
},
|
||
});
|
||
};
|
||
|
||
// 处理搜索
|
||
const handleSearch = (values: API.NoticeSearchParams) => {
|
||
setSearchParams(values);
|
||
fetchNoticeList(1, pagination.pageSize);
|
||
};
|
||
|
||
// 处理模态框提交
|
||
const handleModalSubmit = () => {
|
||
// 先触发所有字段的验证
|
||
modalForm.validateFields()
|
||
.then(async values => {
|
||
try {
|
||
const noticeParams: API.NoticeRequest = {
|
||
title: values.titleZh,
|
||
titleEn: values.titleEn || '',
|
||
content: values.contentZh,
|
||
contentEn: values.contentEn || '',
|
||
isTop: values.isTop ? '1' : '0',
|
||
settingEn: values.titleEn && values.contentEn ? 1 : 0,
|
||
};
|
||
|
||
if (isEdit) {
|
||
// 编辑模式,调用更新API
|
||
await updateNotice(currentId, noticeParams);
|
||
message.success('更新成功');
|
||
} else {
|
||
// 新增模式,调用添加API
|
||
await addNotice(noticeParams);
|
||
message.success('添加成功');
|
||
}
|
||
|
||
setModalVisible(false);
|
||
modalForm.resetFields();
|
||
// 刷新列表
|
||
fetchNoticeList(pagination.current, pagination.pageSize);
|
||
} catch (error) {
|
||
console.error(isEdit ? '更新通知失败:' : '添加通知失败:', error);
|
||
message.error(isEdit ? '更新失败' : '添加失败');
|
||
}
|
||
})
|
||
.catch(errorInfo => {
|
||
// 获取所有字段的错误信息
|
||
const errorFields = errorInfo.errorFields || [];
|
||
|
||
// 检查是否有中文标题或内容的错误
|
||
const hasZhError = errorFields.some((field: any) => {
|
||
if (!field.name) return false;
|
||
const fieldName = Array.isArray(field.name) ? field.name.join('.') : String(field.name);
|
||
return fieldName.includes('titleZh') || fieldName.includes('contentZh');
|
||
});
|
||
|
||
// 检查是否有英文标题或内容的错误
|
||
const hasEnError = errorFields.some((field: any) => {
|
||
if (!field.name) return false;
|
||
const fieldName = Array.isArray(field.name) ? field.name.join('.') : String(field.name);
|
||
return fieldName.includes('titleEn') || fieldName.includes('contentEn');
|
||
});
|
||
|
||
// 如果有中文字段错误,切换到中文Tab
|
||
if (hasZhError) {
|
||
setActiveTabKey('zh');
|
||
}
|
||
// 如果只有英文字段错误,切换到英文Tab
|
||
else if (hasEnError && !hasZhError) {
|
||
setActiveTabKey('en');
|
||
}
|
||
|
||
console.log('表单验证失败:', errorInfo);
|
||
});
|
||
};
|
||
|
||
// 处理Tab切换
|
||
const handleTabChange = (key: string) => {
|
||
setActiveTabKey(key);
|
||
};
|
||
|
||
// 处理模态框取消
|
||
const handleModalCancel = () => {
|
||
setModalVisible(false);
|
||
modalForm.resetFields();
|
||
};
|
||
|
||
return (
|
||
<div className="common-container">
|
||
<div className="filter-action-row">
|
||
<Form
|
||
form={form}
|
||
name="search"
|
||
onFinish={handleSearch}
|
||
layout="inline"
|
||
className="filter-form"
|
||
>
|
||
<Form.Item name="title" label="标题">
|
||
<Input placeholder="请输入标题关键词" allowClear />
|
||
</Form.Item>
|
||
<Form.Item name="status" label="状态">
|
||
<Select placeholder="请选择状态" allowClear>
|
||
<Option value="0">草稿</Option>
|
||
<Option value="1">已发布</Option>
|
||
<Option value="2">已下架</Option>
|
||
</Select>
|
||
</Form.Item>
|
||
<Form.Item className="filter-btns">
|
||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
||
搜索
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
onClick={() => {
|
||
form.resetFields();
|
||
setSearchParams({});
|
||
handleSearch({});
|
||
}}
|
||
>
|
||
重置
|
||
</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
|
||
<div className="right-buttons">
|
||
<Button
|
||
type="primary"
|
||
ghost
|
||
icon={<PlusOutlined />}
|
||
onClick={handleAdd}
|
||
>
|
||
新增
|
||
</Button>
|
||
<Button
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
onClick={handleBatchDelete}
|
||
disabled={!hasSelected}
|
||
loading={loading}
|
||
>
|
||
删除
|
||
</Button>
|
||
{hasSelected && (
|
||
<span className="selected-count">
|
||
已选择 {selectedRowKeys.length} 项
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="content-area">
|
||
<Table
|
||
rowSelection={rowSelection}
|
||
columns={columns}
|
||
dataSource={noticeData}
|
||
pagination={pagination}
|
||
loading={loading}
|
||
onChange={handleTableChange}
|
||
/>
|
||
</div>
|
||
|
||
{/* 新增/编辑模态框 */}
|
||
<Modal
|
||
title={isEdit ? "编辑通知" : "新增通知"}
|
||
visible={modalVisible}
|
||
onOk={handleModalSubmit}
|
||
onCancel={handleModalCancel}
|
||
width={900}
|
||
maskClosable={false}
|
||
destroyOnClose
|
||
>
|
||
<Form
|
||
form={modalForm}
|
||
layout="vertical"
|
||
name="noticeForm"
|
||
preserve={false}
|
||
>
|
||
<Form.Item name="isTop" label="是否置顶" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
|
||
<Tabs activeKey={activeTabKey} onChange={handleTabChange}>
|
||
<TabPane tab="中文版" key="zh">
|
||
<Form.Item
|
||
name="titleZh"
|
||
label="标题(中文)"
|
||
rules={[{ required: true, message: '请输入中文标题' }]}
|
||
>
|
||
<Input placeholder="请输入中文标题" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="contentZh"
|
||
label="内容(中文)"
|
||
rules={[{ required: true, message: '请输入中文内容' }]}
|
||
>
|
||
<WangEditor
|
||
language="zh-CN"
|
||
height="300px"
|
||
placeholder="请输入通知内容..."
|
||
/>
|
||
</Form.Item>
|
||
</TabPane>
|
||
<TabPane tab="英文版" key="en">
|
||
<Form.Item
|
||
name="titleEn"
|
||
label="标题(英文)"
|
||
rules={[{ required: true, message: 'Please enter title in English' }]}
|
||
>
|
||
<Input placeholder="Please enter title in English" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="contentEn"
|
||
label="内容(英文)"
|
||
rules={[{ required: true, message: 'Please enter content in English' }]}
|
||
>
|
||
<WangEditor
|
||
language="en"
|
||
height="300px"
|
||
placeholder="Please enter notice content..."
|
||
/>
|
||
</Form.Item>
|
||
</TabPane>
|
||
</Tabs>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default NoticeManage;
|