Merge branch 'announcement'

This commit is contained in:
lix
2025-07-25 13:37:58 +08:00
8 changed files with 776 additions and 2 deletions

View File

@ -203,6 +203,17 @@ export default [
{ name: 'noticeManage', path: '/notice/noticeManage', component: './notice/noticeManage/components/NoticeManage' },//通知公告管理-系统管理员
]
},
{//公告中心
name: 'announcementCenter',
icon: 'notification',
path: '/AnnouncementCenter',
routes: [
{ name: 'announcementList', path: '/AnnouncementCenter', component: './AnnouncementCenter' },//公告中心列表
{ name: 'announcementAdd', path: '/AnnouncementCenter/add', component: './AnnouncementCenter/components/AnnouncementAdd' },//新增公告
{ name: 'announcementEdit', path: '/AnnouncementCenter/edit', component: './AnnouncementCenter/components/AnnouncementAdd' },//编辑公告
{ name: 'announcementView', path: '/AnnouncementCenter/view', component: './AnnouncementCenter/components/AnnouncementAdd' },//查看公告
]
},
{//委托
name: 'entrust',
icon: 'form',

View File

@ -55,7 +55,7 @@ const BraftText: React.FC<WangType> = (props) => {
'list', //列表
'undo', //撤销
'redo', //恢复
'fullscreen', //全屏
// 'fullscreen', //全屏
// 'image',//图片
// 'emoticon',//表情
@ -75,7 +75,7 @@ const BraftText: React.FC<WangType> = (props) => {
//工具栏
editor.config.menus = tools;
//提示
editor.config.placeholder = '为了能顺利发布,建议您内容去掉下划线等格式,尽量以纯文本形式发布。';
editor.config.placeholder = '';
// 配置 onchange 回调函数
editor.config.onchange = editorOnChange;
// 注册菜单

View File

@ -0,0 +1,284 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Button,
Card,
Form,
Input,
Radio,
DatePicker,
Checkbox,
message,
Row,
Col,
Spin,
Space
} from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import type { CheckboxValueType } from 'antd/es/checkbox/Group';
import { history, useLocation, useSearchParams } from '@umijs/max';
import moment from 'moment';
import BraftText from '@/components/richText/wang';
import {
getAnnouncementTypeDict,
createAnnouncement,
updateAnnouncement,
getAnnouncementDetail
} from '../service';
const { RangePicker } = DatePicker;
// 表单布局配置
const formItemLayout = {
labelCol: { span: 4 },
wrapperCol: { span: 16 },
};
// 公告发布网站
const PUBLISH_WEBSITE_OPTIONS = [
{
label: '中远海运集团采购信息网',
value: '1',
},
{
label: '中国招标投标公共服务平台',
value: '2',
},
];
const titleMap = {
add: '新增公告',
edit: '编辑公告',
view: '查看公告',
}
const AnnouncementAdd: React.FC<{}> = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [submitting, setSubmitting] = useState<boolean>(false);
const [publishWebsites, setPublishWebsites] = useState<CheckboxValueType[]>([]);
const [publishWebsitesIndeterminate, setPublishWebsitesIndeterminate] = useState<boolean>(false);
const [publishWebsitesCheckAll, setPublishWebsitesCheckAll] = useState<boolean>(false);
const [announcementTypeDict, setAnnouncementTypeDict] = useState<any[]>([]);
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
// 对 pathname 进行分割 取最后一个 判断页面状态
const pathnameArr = pathname.split('/');
const pageStatus = pathnameArr[pathnameArr.length - 1] as 'add' | 'edit' | 'view';
const announcementId = searchParams.get('id');
const richTextRef = useRef<any>(null);
// 初始化表单数据
useEffect(() => {
if (pageStatus !== 'add' && announcementId) {
loadAnnouncementDetail(announcementId);
}
}, [pageStatus, announcementId]);
// 富文本编辑器回显内容
const [richTextContent, setRichTextContent] = useState('');
useEffect(() => {
getAnnouncementTypeDict().then(res => {
setAnnouncementTypeDict(res.data);
if (pageStatus === 'add') {
form.setFieldsValue({
announcementType: res.data?.[0]?.value,
});
}
});
}, [pageStatus]);
// 加载公告详情
const loadAnnouncementDetail = async (id: string) => {
setLoading(true);
try {
const res = await getAnnouncementDetail(id);
if (res.code === 200) {
const data = res.data;
form.setFieldsValue({
announcementType: data.announcementType,
announcementName: data.announcementName,
publishTime: [
moment(data.publishStartTime),
moment(data.publishEndTime)
],
});
setPublishWebsites(data.publishWebsites || []);
setRichTextContent(data.content || '');
}
} catch (error) {
message.error('加载公告详情失败');
}
setLoading(false);
};
// 保存功能
const handleSave = async () => {
setSubmitting(true);
form.validateFields().then(async values => {
const content = richTextRef.current?.getHtml1() || '';
const formData = {
announcementType: values.announcementType,
announcementName: values.announcementName,
publishStartTime: values.publishTime[0].format('YYYY-MM-DD HH:mm:ss'),
publishEndTime: values.publishTime[1].format('YYYY-MM-DD HH:mm:ss'),
publishWebsites,
content,
};
let res;
if (pageStatus === 'edit' && announcementId) {
res = await updateAnnouncement({
...formData,
id: announcementId,
});
} else {
res = await createAnnouncement(formData);
}
if (res.code === 200) {
message.success(pageStatus === 'edit' ? '更新成功!' : '保存成功!');
// history.push('/AnnouncementCenter');
}
}).finally(() => {
setSubmitting(false);
})
};
// 取消功能
const handleCancel = () => {
history.replace('/AnnouncementCenter');
};
const onPublishWebsitesCheckAll = (e: CheckboxChangeEvent) => {
setPublishWebsites(e.target.checked ? PUBLISH_WEBSITE_OPTIONS.map(item => item.value) : []);
setPublishWebsitesIndeterminate(false);
setPublishWebsitesCheckAll(e.target.checked);
}
const onPublishWebsitesChange = (list: CheckboxValueType[]) => {
setPublishWebsites(list);
setPublishWebsitesIndeterminate(!!list.length && list.length < PUBLISH_WEBSITE_OPTIONS.length);
setPublishWebsitesCheckAll(list.length === PUBLISH_WEBSITE_OPTIONS.length);
};
return (
<Spin spinning={loading}>
<Card
title={(
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{titleMap[pageStatus]}</span>
<Space>
<Button type="primary" onClick={handleSave} loading={submitting}></Button>
<Button onClick={handleCancel}></Button>
</Space>
</div>
)}>
<Form
{...formItemLayout}
form={form}
disabled={pageStatus === 'view'}
>
<Row>
<Col span={24}>
<h3 style={{ marginBottom: 24, fontWeight: 'bold' }}></h3>
</Col>
</Row>
<Form.Item
label="公告类型"
name="announcementType"
rules={[{ required: true, message: '请选择公告类型' }]}
>
<Radio.Group style={{ width: '100%' }}>
<Row style={{ paddingTop: 5 }}>
{announcementTypeDict.map((option) => (
<Col span={6} key={option.value}>
<Radio value={option.value} style={{ marginBottom: 8 }}>
{option.label}
</Radio>
</Col>
))}
</Row>
</Radio.Group>
</Form.Item>
<Form.Item
label="公告标题"
name="announcementName"
rules={[
{ required: true, message: '请输入公告标题' },
{ max: 100, message: '公告标题不能超过100个字符' }
]}
>
<Input placeholder="请输入公告标题" />
</Form.Item>
<Form.Item
label="公告开始时间"
name="publishTime"
rules={[
{ required: true, message: '请选择公告时间' },
]}
>
<RangePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
placeholder={['公告开始时间', '公告截止时间']}
style={{ width: '100%' }}
getPopupContainer={trigger => trigger.parentElement || document.body}
popupStyle={{ zIndex: 9999 }}
/>
</Form.Item>
<Form.Item
label="公告发布网站"
name="publishWebsites"
>
<Row style={{ marginTop: 5 }}>
<Col span={24}>
<Checkbox indeterminate={publishWebsitesIndeterminate} checked={publishWebsitesCheckAll} onChange={onPublishWebsitesCheckAll}>
</Checkbox>
</Col>
<Col span={24}>
<Checkbox.Group value={publishWebsites} onChange={onPublishWebsitesChange}>
<Row>
{PUBLISH_WEBSITE_OPTIONS.map((option) => (
<Col span={24} key={option.value}>
<Checkbox value={option.value}>
{option.label}
</Checkbox>
</Col>
))}
</Row>
</Checkbox.Group>
</Col>
</Row>
</Form.Item>
<Row>
<Col span={24}>
<h3 style={{ marginBottom: 24, fontWeight: 'bold' }}></h3>
</Col>
</Row>
<Form.Item
label={'\u200B'}
name="content"
>
<div style={{ position: 'relative', zIndex: 1 }}>
<BraftText
braftRef={richTextRef}
height={400}
echo={richTextContent}
disabled={pageStatus === 'view'}
/>
</div>
</Form.Item>
</Form>
</Card>
</Spin>
);
};
export default AnnouncementAdd;

View File

@ -0,0 +1,260 @@
import React, { useRef, useState, useEffect } from 'react';
import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table';
import tableProps from '@/utils/tableProps';
import { Button, Card, message, Popconfirm, Space } from 'antd';
import { history } from '@umijs/max';
import styles from './style.module.less';
import type { DataType, AnnouncementSearchParams } from '../types';
import {
getAnnouncementList,
getAnnouncementTypeDict,
deleteAnnouncement,
publishAnnouncement
} from '../service';
const AnnouncementList: React.FC<{}> = () => {
const actionRef = useRef<ActionType>();
const [loading, setLoading] = useState<boolean>(false);
const [announcementTypeDict, setAnnouncementTypeDict] = useState<any[]>([]);
// 表格列定义
const columns: ProColumns<DataType>[] = [
{
title: '序号',
valueType: 'index',
width: 80,
search: false
},
{
title: '公告编号',
dataIndex: 'announcementNo',
search: false
},
{
title: '公告名称',
dataIndex: 'announcementName',
},
{
title: '公告类型',
dataIndex: 'announcementType',
valueType: 'select',
valueEnum: announcementTypeDict.reduce((acc, item) => {
acc[item.value] = item.label;
return acc;
}, {} as Record<string, string>),
},
{
title: '发布时间',
dataIndex: 'publishTime',
valueType: 'dateRange',
search: {
transform: (value) => {
return {
publishTimeStart: value[0],
publishTimeEnd: value[1],
};
},
},
render: (_, record) => `${record.publishTimeStart} - ${record.publishTimeEnd}`,
},
{
title: '状态',
dataIndex: 'status',
search: false,
valueEnum: {
'1': '编辑中',
'2': '已保存',
'3': '已发布',
},
},
{
title: '操作',
search: false,
width: 80,
render: (_, record) => {
// 已发布状态:只有查看按钮
if (record.status === '3') {
return (
<Button
type="link"
onClick={() => handleView(record)}
>
</Button>
)
}
// 已保存状态:有发布、编辑、删除三个按钮
if (record.status === '2') {
return (
<Space>
<Popconfirm
key="publish"
title="您确定要发布这条公告吗?"
onConfirm={() => handlePublish(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link">
</Button>
</Popconfirm>
<Button
key="edit"
type="link"
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
key="delete"
title="您确定要删除这条公告吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
danger
>
</Button>
</Popconfirm>
</Space>
)
}
// 编辑中状态:有起草、删除两个按钮
if (record.status === '1') {
return (
<Space>
<Button
key="edit"
type="link"
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
key="delete"
title="您确定要删除这条公告吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
danger
>
</Button>
</Popconfirm>
</Space>
)
}
return [];
},
},
];
// 查询功能
const handleSearch = async (params: AnnouncementSearchParams ) => {
setLoading(true);
const requestParams = {
...params,
pageNo: params.current,
}
const res = await getAnnouncementList(requestParams);
setLoading(false);
return {
data: res.data.records,
total: res.data.total,
success: res.success,
};
};
// 新增公告导航
const handleAdd = () => {
history.push('/AnnouncementCenter/add');
};
// 编辑功能
const handleEdit = (record: DataType) => {
history.push(`/AnnouncementCenter/edit?id=${record.id}`);
};
// 查看功能
const handleView = (record: DataType) => {
history.push(`/AnnouncementCenter/view?id=${record.id}`);
};
// 删除功能
const handleDelete = async (id: string) => {
setLoading(true);
try {
const res = await deleteAnnouncement(id);
if (res.code === 200) {
message.success('删除成功!');
actionRef.current?.reload();
} else {
message.error('删除失败,请重试');
}
} catch (error) {
message.error('删除失败,请重试');
}
setLoading(false);
};
// 发布功能
const handlePublish = async (id: string) => {
setLoading(true);
try {
const res = await publishAnnouncement(id);
if (res.code === 200) {
message.success('发布成功!');
actionRef.current?.reload();
} else {
message.error('发布失败,请重试');
}
} catch (error) {
message.error('发布失败,请重试');
}
setLoading(false);
};
useEffect(() => {
getAnnouncementTypeDict().then(res => {
setAnnouncementTypeDict(res.data);
});
}, []);
return (
<Card title="公告中心">
<ProTable<DataType, AnnouncementSearchParams>
actionRef={actionRef}
loading={loading}
className={styles['announcement-list']}
columns={columns}
headerTitle="我发布的公告"
toolBarRender={() => [
<Button
key="add"
type="primary"
style={{ width: 150 }}
onClick={handleAdd}
>
</Button>,
]}
request={handleSearch}
{...tableProps}
options={false}
rowKey="id"
/>
</Card>
);
};
export default AnnouncementList;

View File

@ -0,0 +1,17 @@
.announcement-list {
:global {
.ant-pro-table-search-query-filter {
padding-left: 6px;
padding-right: 6px;
}
.ant-card {
.ant-card-body {
padding: 0;
.ant-pro-table-list-toolbar-container {
padding: 12px 6px;
background: #F2F2F2;
}
}
}
}
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import AnnouncementList from './components/AnnouncementList';
const Index: React.FC = () => {
return (
<>
<AnnouncementList />
</>
);
};
export default Index;

View File

@ -0,0 +1,175 @@
import request from '@/utils/request';
import { AnnouncementSearchParams } from './types';
// 获取公告列表,支持分页和搜索
export async function getAnnouncementList(params: AnnouncementSearchParams) {
console.log('getAnnouncementList params', params);
await new Promise(resolve => setTimeout(resolve, 1000));
return {
data: {
records: [
{
id: '1',
announcementNo: '123456',
announcementName: '测试公告',
announcementType: '1',
publishTimeStart: '2021-01-01',
publishTimeEnd: '2021-01-02',
status: '1',
},
{
id: '2',
announcementNo: '123457',
announcementName: '测试公告2',
announcementType: '2',
publishTimeStart: '2021-01-02',
publishTimeEnd: '2021-01-03',
status: '2',
},
{
id: '3',
announcementNo: '123458',
announcementName: '测试公告3',
announcementType: '3',
publishTimeStart: '2021-01-03',
publishTimeEnd: '2021-01-04',
status: '3',
},
],
total: 3,
},
success: true,
}
// return request('', {
// method: 'GET',
// data: params,
// });
}
// 获取公告类型字典
export async function getAnnouncementTypeDict() {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
data: [
{
label: '需求公示',
value: '1',
},
{
label: '项目公告',
value: '2',
},
{
label: '邀请函内容披露',
value: '3',
},
{
label: '中标候选人公示',
value: '4',
},
{
label: '中标结果公示',
value: '5',
},
{
label: '失败公告',
value: '6',
},
{
label: '变更公告',
value: '7',
},
{
label: '其他',
value: '8',
},
],
success: true,
}
// return request('', {
// method: 'GET',
// });
}
// 发布公告
export async function publishAnnouncement(id: string) {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
data: {},
code: 200,
success: true,
}
// return request(`/api/v1//announcement/publish/${id}`, {
// method: '',
// });
}
// 删除公告
export async function deleteAnnouncement(id: string) {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
data: {},
code: 200,
success: true,
}
// return request(`/api//v1/announcement/delete/${id}`, {
// method: 'DELETE',
// });
}
// 创建新公告
export async function createAnnouncement(data: any) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('createAnnouncement data', data);
return {
data: {},
code: 200,
success: true,
}
// return request('/api/v1/announcement/create', {
// method: 'POST',
// data,
// });
}
// 更新公告
export async function updateAnnouncement(data: any) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('updateAnnouncement data', data);
return {
data: {},
code: 200,
success: true,
}
// return request(`/api/v1/announcement/update/${id}`, {
// method: 'PUT',
// data,
// });
}
// 获取公告详情
export async function getAnnouncementDetail(id: string) {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
data: {
id: '1',
announcementNo: '123456',
announcementName: '测试公告',
announcementType: '3',
publishStartTime: '2021-01-01',
publishEndTime: '2021-01-02',
publishWebsites: ['1'],
content: '<p>测试公告内容</p>',
},
code: 200,
success: true,
}
// return request(`/api//v1/announcement/detail/${id}`, {
// method: 'GET',
// });
}

View File

@ -0,0 +1,15 @@
export interface AnnouncementSearchParams {
announcementName?: string;
publishTimeStart?: string;
publishTimeEnd?: string;
announcementType?: string;
pageSize?: number;
current?: number;
}
export interface DataType extends AnnouncementSearchParams {
id: string;
announcementNo: string;
status: string;
content?: string;
}