公告公示及详情页面

This commit is contained in:
linxd
2025-06-16 21:36:50 +08:00
parent 7c64b1528f
commit 9056f68998
8 changed files with 901 additions and 25 deletions

View File

@ -1,7 +0,0 @@
@import '../../baseStyle.less';
.spaceBlock {
height: 10px;
margin: 0 -15px;
margin-top: 20px;
background: rgba(@gray, 0.3);
}

View File

@ -1,7 +0,0 @@
import React from 'react';
import './SpaceBlock.less';
const SpaceBlock: React.FC = () => {
return <div className="spaceBlock" />;
};
export default SpaceBlock;

View File

@ -15,8 +15,8 @@ body,
.layout-content-main {
width: @width;
margin: 0 auto;
background: #fff;
padding: 15px;
// background: #fff;
// padding: 15px;
}
}
.colorWeak {

View File

@ -0,0 +1,207 @@
.announcePage {
// padding: 20px;
// background-color: #f5f5f5;
}
.searchSection {
background-color: #fff;
padding: 20px;
// border-radius: 4px;
margin-bottom: 20px;
// box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.searchRow {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.searchLabel {
font-size: 14px;
font-weight: 500;
color: #333;
margin-right: 20px;
min-width: 70px;
}
.filterRow {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
}
.filterLabel {
font-size: 14px;
font-weight: 500;
color: #333;
margin-right: 20px;
min-width: 70px;
padding-top: 5px;
}
.filterOptions {
flex: 1;
}
.locationSelector {
display: flex;
flex-direction: column;
}
.provinceSelector {
margin-top: 10px;
width: 100%;
.provinceRow {
margin-bottom: 8px;
display: flex;
flex-wrap: wrap;
}
}
/* 覆盖antd样式 */
:global {
.ant-radio-button-wrapper {
margin-right: 5px;
margin-bottom: 8px;
border-radius: 0;
height: 32px;
line-height: 30px;
min-width: 60px;
text-align: center;
padding: 0 10px;
}
.ant-radio-button-wrapper:first-child {
border-radius: 0;
}
.ant-radio-button-wrapper:last-child {
border-radius: 0;
}
.ant-radio-button-wrapper-checked {
background-color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
color: #fff;
&:hover {
background-color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
color: #fff;
}
}
.ant-btn-primary {
background-color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
border-radius: 0;
&:hover, &:focus {
background-color: rgba(0, 79, 142, 0.8);
border-color: rgba(0, 79, 142, 0.8);
}
}
.ant-select-selector {
border-radius: 0 !important;
}
.ant-input {
border-radius: 0;
}
.ant-input-affix-wrapper {
border-radius: 0;
}
.ant-list-pagination {
margin-top: 20px;
text-align: center;
}
.ant-pagination-item {
border-radius: 0;
&-active {
border-color: rgb(0, 79, 142);
a {
color: rgb(0, 79, 142);
}
}
}
.ant-pagination-item-link {
border-radius: 0;
}
.ant-pagination-options {
.ant-select-selector {
border-radius: 0;
}
}
.ant-tag {
margin-right: 0;
border-radius: 2px;
}
}
.announceList {
background-color: #fff;
padding: 10px;
}
.listItem {
padding: 16px 10px;
border-bottom: 1px solid #f0f0f0;
&:hover {
background-color: #f9f9f9;
}
}
.itemContent {
width: 100%;
}
.itemHeader {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.typeTag {
margin-right: 10px;
min-width: 90px;
text-align: center;
}
.itemTitle {
font-size: 16px;
font-weight: 500;
color: #333;
flex: 1;
&:hover {
color: rgb(0, 79, 142);
cursor: pointer;
}
}
.itemFooter {
display: flex;
justify-content: space-between;
color: #666;
font-size: 14px;
}
.company {
flex: 1;
}
.date {
min-width: 180px;
text-align: right;
}

View File

@ -1,5 +1,369 @@
import React from 'react';
import React, { useState } from 'react';
import { Input, Button, Select, Radio, DatePicker, List, Tag } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { history } from 'umi';
import styles from './announce.less';
const { Option } = Select;
const { RangePicker } = DatePicker;
// 模拟公告数据
const mockAnnounceData = [
{
id: '1',
type: '招标采购公告',
title: '中远海运集团2023年度办公设备采购项目招标公告',
company: '中远海运集团有限公司',
publishDate: '2023-06-15',
},
{
id: '2',
type: '中标结果公示',
title: '中远海运物流有限公司信息系统建设项目中标结果公示',
company: '中远海运物流有限公司',
publishDate: '2023-06-10',
},
{
id: '3',
type: '采购需求公示',
title: '中远海运集装箱运输有限公司集装箱采购需求公示',
company: '中远海运集装箱运输有限公司',
publishDate: '2023-06-08',
},
{
id: '4',
type: '变更公告',
title: '关于"中远海运能源运输股份有限公司船舶维修项目"的变更公告',
company: '中远海运能源运输股份有限公司',
publishDate: '2023-06-05',
},
{
id: '5',
type: '招标采购公告',
title: '中远海运重工有限公司船舶设备采购项目招标公告',
company: '中远海运重工有限公司',
publishDate: '2023-06-01',
},
];
const AnnouncePage: React.FC = () => {
return <>announcePage</>;
// 状态管理
const [searchKeyword, setSearchKeyword] = useState<string>('');
const [announceType, setAnnounceType] = useState<string>('全部');
const [projectUnit, setProjectUnit] = useState<string>('全选');
const [projectLocation, setProjectLocation] = useState<string>('中国');
const [publishTime, setPublishTime] = useState<string>('不限');
const [projectType, setProjectType] = useState<string>('全部');
const [currentProvince, setCurrentProvince] = useState<string>('全部');
// 分页状态
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(100);
// 公告类型选项
const announceTypeOptions = [
{ label: '全部', value: '全部' },
{ label: '采购需求公示', value: '采购需求公示' },
{ label: '招标采购公告', value: '招标采购公告' },
{ label: '非招标采购公告', value: '非招标采购公告' },
{ label: '资格预审公告', value: '资格预审公告' },
{ label: '招募公告', value: '招募公告' },
{ label: '变更公告', value: '变更公告' },
{ label: '中标(中选)候选人公示', value: '中标(中选)候选人公示' },
{ label: '中标(中选)结果公示', value: '中标(中选)结果公示' },
{ label: '采购失败(流标)公告', value: '采购失败(流标)公告' },
];
// 省份选项 - 按照图片中的顺序排列
const provinceOptions = [
{ label: '全部', value: '全部' },
{ label: '北京', value: '北京' },
{ label: '天津', value: '天津' },
{ label: '河北', value: '河北' },
{ label: '山西', value: '山西' },
{ label: '内蒙古', value: '内蒙古' },
{ label: '辽宁', value: '辽宁' },
{ label: '吉林', value: '吉林' },
{ label: '黑龙江', value: '黑龙江' },
{ label: '上海', value: '上海' },
{ label: '江苏', value: '江苏' },
{ label: '浙江', value: '浙江' },
{ label: '安徽', value: '安徽' },
{ label: '福建', value: '福建' },
{ label: '江西', value: '江西' },
{ label: '山东', value: '山东' },
{ label: '河南', value: '河南' },
{ label: '湖北', value: '湖北' },
{ label: '湖南', value: '湖南' },
{ label: '广东', value: '广东' },
{ label: '广西', value: '广西' },
{ label: '海南', value: '海南' },
{ label: '重庆', value: '重庆' },
{ label: '四川', value: '四川' },
{ label: '贵州', value: '贵州' },
{ label: '云南', value: '云南' },
{ label: '西藏', value: '西藏' },
{ label: '陕西', value: '陕西' },
{ label: '甘肃', value: '甘肃' },
{ label: '青海', value: '青海' },
{ label: '宁夏', value: '宁夏' },
{ label: '新疆', value: '新疆' },
{ label: '香港', value: '香港' },
{ label: '澳门', value: '澳门' },
{ label: '台湾', value: '台湾' },
];
// 第一行省份
const firstRowProvinces = ['全部', '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江', '上海', '江苏', '浙江', '安徽', '福建', '江西'];
// 第二行省份
const secondRowProvinces = ['山东', '河南', '湖北', '湖南', '广东', '广西', '海南', '重庆', '四川', '贵州', '云南', '西藏', '陕西'];
// 第三行省份
const thirdRowProvinces = ['甘肃', '青海', '宁夏', '新疆', '香港', '澳门', '台湾'];
// 发布时间选项
const publishTimeOptions = [
{ label: '不限', value: '不限' },
{ label: '今天', value: '今天' },
{ label: '最近三天', value: '最近三天' },
{ label: '最近一周', value: '最近一周' },
{ label: '最近一月', value: '最近一月' },
{ label: '自定义', value: '自定义' },
];
// 项目类型选项
const projectTypeOptions = [
{ label: '全部', value: '全部' },
{ label: '货物', value: '货物' },
{ label: '工程', value: '工程' },
{ label: '服务', value: '服务' },
{ label: '其他', value: '其他' },
];
// 处理搜索
const handleSearch = () => {
console.log('搜索条件:', {
searchKeyword,
announceType,
projectUnit,
projectLocation,
publishTime,
projectType,
});
// 这里添加搜索逻辑
};
// 处理分页变化
const handlePageChange = (page: number, pageSizeValue?: number) => {
setCurrent(page);
if (pageSizeValue) setPageSize(pageSizeValue);
};
// 获取公告类型对应的颜色
const getTypeColor = (type: string) => {
switch (type) {
case '招标采购公告':
return 'blue';
case '中标结果公示':
return 'green';
case '采购需求公示':
return 'orange';
case '变更公告':
return 'red';
default:
return 'default';
}
};
// 处理点击公告标题
const handleAnnounceClick = (id: string) => {
console.log('点击了公告ID:', id);
history.push({
pathname: '/announce/announceInfo',
query: {
id,
},
});
};
return (
<div className={styles.announcePage}>
<div className={styles.searchSection}>
{/* 公告搜索 */}
<div className={styles.searchRow}>
<div className={styles.searchLabel}></div>
<Input
placeholder="请输入公告关键词"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
style={{ width: '80%', maxWidth: 1000 }}
suffix={
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
</Button>
}
/>
</div>
{/* 公告类型 */}
<div className={styles.filterRow}>
<div className={styles.filterLabel}></div>
<div className={styles.filterOptions}>
<Radio.Group
value={announceType}
onChange={(e) => setAnnounceType(e.target.value)}
buttonStyle="solid"
>
{announceTypeOptions.map((option) => (
<Radio.Button key={option.value} value={option.value}>
{option.label}
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
{/* 项目所属单位 */}
<div className={styles.filterRow}>
<div className={styles.filterLabel}></div>
<Select
style={{ width: 300 }}
value={projectUnit}
onChange={(value) => setProjectUnit(value)}
>
<Option value="全选"></Option>
<Option value="中远海运集团"></Option>
<Option value="中远海运物流"></Option>
<Option value="中远海运集装箱"></Option>
</Select>
</div>
{/* 项目所在地 */}
<div className={styles.filterRow}>
<div className={styles.filterLabel}></div>
<div className={styles.locationSelector}>
<Select
style={{ width: 150, marginRight: 10 }}
value={projectLocation}
onChange={(value) => setProjectLocation(value)}
>
<Option value="中国"></Option>
<Option value="国外"></Option>
</Select>
<div className={styles.provinceSelector}>
<Radio.Group
value={currentProvince}
onChange={(e) => setCurrentProvince(e.target.value)}
buttonStyle="solid"
>
<div className={styles.provinceRow}>
{firstRowProvinces.map((province) => (
<Radio.Button key={province} value={province}>
{province}
</Radio.Button>
))}
</div>
<div className={styles.provinceRow}>
{secondRowProvinces.map((province) => (
<Radio.Button key={province} value={province}>
{province}
</Radio.Button>
))}
</div>
<div className={styles.provinceRow}>
{thirdRowProvinces.map((province) => (
<Radio.Button key={province} value={province}>
{province}
</Radio.Button>
))}
</div>
</Radio.Group>
</div>
</div>
</div>
{/* 发布时间 */}
<div className={styles.filterRow}>
<div className={styles.filterLabel}></div>
<div className={styles.filterOptions}>
<Radio.Group
value={publishTime}
onChange={(e) => setPublishTime(e.target.value)}
buttonStyle="solid"
>
{publishTimeOptions.map((option) => (
<Radio.Button key={option.value} value={option.value}>
{option.label}
</Radio.Button>
))}
</Radio.Group>
{publishTime === '自定义' && (
<RangePicker style={{ marginLeft: 10 }} />
)}
</div>
</div>
{/* 项目类型 */}
<div className={styles.filterRow}>
<div className={styles.filterLabel}></div>
<div className={styles.filterOptions}>
<Radio.Group
value={projectType}
onChange={(e) => setProjectType(e.target.value)}
buttonStyle="solid"
>
{projectTypeOptions.map((option) => (
<Radio.Button key={option.value} value={option.value}>
{option.label}
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
</div>
{/* 公告列表部分 */}
<div className={styles.announceList}>
<List
itemLayout="horizontal"
dataSource={mockAnnounceData}
pagination={{
onChange: handlePageChange,
current,
pageSize,
total,
showTotal: (totalItems) => `${totalItems} 条记录`,
showSizeChanger: true,
showQuickJumper: true,
}}
renderItem={(item) => (
<List.Item
key={item.id}
className={styles.listItem}
>
<div className={styles.itemContent}>
<div className={styles.itemHeader}>
<Tag color={getTypeColor(item.type)} className={styles.typeTag}>
{item.type}
</Tag>
<div
className={styles.itemTitle}
onClick={() => handleAnnounceClick(item.id)}
>
{item.title}
</div>
</div>
<div className={styles.itemFooter}>
<div className={styles.company}>{item.company}</div>
<div className={styles.date}>{item.publishDate}</div>
</div>
</div>
</List.Item>
)}
/>
</div>
</div>
);
};
export default AnnouncePage;

View File

@ -0,0 +1,144 @@
.announceInfoContainer {
padding: 20px;
background-color: #fff;
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
}
.announceInfoHeader {
margin-bottom: 20px;
}
.backButton {
padding-left: 0;
font-size: 16px;
color: rgb(0, 79, 142);
&:hover, &:focus {
color: rgba(0, 79, 142, 0.8);
}
}
.announceInfoContent {
padding: 30px;
}
.titleContainer {
text-align: center;
margin-bottom: 30px;
}
.title {
font-weight: bold !important;
color: #333;
}
.metaInfo {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 20px;
@media (max-width: 768px) {
flex-direction: column;
> div {
margin-bottom: 10px;
}
}
}
.metaLeft {
flex: 1;
}
.metaCenter {
flex: 1;
text-align: center;
@media (max-width: 768px) {
text-align: left;
}
}
.metaRight {
flex: 1;
text-align: right;
@media (max-width: 768px) {
text-align: left;
}
}
.divider {
margin: 20px 0 !important;
border-top-color: #e8e8e8;
}
.contentBody {
font-size: 16px;
line-height: 1.8;
color: #333;
p {
margin-bottom: 16px;
}
}
.attachmentsSection {
margin-top: 40px;
}
.attachmentsTitle {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
}
.attachmentsList {
display: flex;
flex-wrap: wrap;
}
.attachmentButton {
margin-right: 15px;
margin-bottom: 15px;
}
:global {
.ant-btn-primary {
background-color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
border-radius: 0;
&:hover, &:focus {
background-color: rgba(0, 79, 142, 0.8);
border-color: rgba(0, 79, 142, 0.8);
}
}
.ant-btn-primary[disabled] {
background-color: rgba(0, 0, 0, 0.04);
border-color: #d9d9d9;
color: rgba(0, 0, 0, 0.25);
}
.ant-btn-background-ghost.ant-btn-primary {
color: rgb(0, 79, 142);
border-color: rgb(0, 79, 142);
border-radius: 0;
&:hover, &:focus {
color: rgba(0, 79, 142, 0.8);
border-color: rgba(0, 79, 142, 0.8);
}
}
.ant-divider {
border-top-color: #e8e8e8;
}
}

View File

@ -1,8 +1,186 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useLocation } from 'umi';
import { Typography, Button, Space, Divider, Row, Col, Spin, message } from 'antd';
import { DownloadOutlined, ArrowLeftOutlined, FilePdfOutlined, FileWordOutlined, FileExcelOutlined } from '@ant-design/icons';
import styles from './announceInfo.less';
const { Title, Text } = Typography;
// 模拟公告详情数据
const mockAnnounceDetail = {
id: '1',
title: '中远海运集团2023年度办公设备采购项目招标公告',
type: '招标采购公告',
publishDate: '2023-06-15',
publisher: '中远海运集团采购中心',
content: `
<p>一、项目基本情况</p>
<p>1. 项目名称中远海运集团2023年度办公设备采购项目</p>
<p>2. 采购需求台式电脑300台、笔记本电脑150台、打印机50台、投影仪10台等办公设备</p>
<p>3. 项目预算人民币800万元</p>
<p>4. 最高限价人民币800万元</p>
<p>5. 采购方式:公开招标</p>
<p>6. 交付地点:中远海运集团总部及各分支机构</p>
<p>7. 交付期限合同签订后45个工作日内</p>
<p>8. 本项目是否接受联合体投标:否</p>
<p>二、申请人的资格要求</p>
<p>1. 满足《中华人民共和国政府采购法》第二十二条规定;</p>
<p>2. 落实政府采购政策需满足的资格要求:本项目支持小微企业,监狱企业,残疾人福利性单位发展等政府采购政策。</p>
<p>3. 本项目的特定资格要求:</p>
<p>1投标人须具有电子产品销售相关资质</p>
<p>2投标人须提供所投产品的厂家授权</p>
<p>3投标人须具有近三年同类项目业绩不少于3个。</p>
<p>三、获取招标文件</p>
<p>1. 时间2023年6月15日至2023年6月22日每天9:00至17:00北京时间法定节假日除外</p>
<p>2. 地点:中远海运集团采购中心官网</p>
<p>3. 方式:网上下载</p>
<p>4. 售价:免费</p>
<p>四、提交投标文件截止时间、开标时间和地点</p>
<p>1. 提交投标文件截止时间2023年7月6日14:30北京时间</p>
<p>2. 开标时间2023年7月6日15:00北京时间</p>
<p>3. 地点中远海运集团总部1号会议室</p>
<p>五、公告期限</p>
<p>自本公告发布之日起5个工作日。</p>
<p>六、其他补充事宜</p>
<p>1. 本项目需要落实的政府采购政策:节能环保、中小微企业扶持、促进残疾人就业等。</p>
<p>2. 投标保证金人民币10万元。</p>
<p>七、对本次招标提出询问,请按以下方式联系</p>
<p>1. 采购人信息</p>
<p>名称:中远海运集团有限公司</p>
<p>地址上海市东大名路xxx号</p>
<p>联系方式021-xxxxxxxx</p>
<p>2. 采购代理机构信息</p>
<p>名称上海xxx招标代理有限公司</p>
<p>地址上海市浦东新区xxx路xx号</p>
<p>联系方式021-xxxxxxxx</p>
<p>3. 项目联系方式</p>
<p>项目联系人:张先生</p>
<p>电话021-xxxxxxxx</p>
`,
attachments: [
{ name: '招标文件.pdf', url: '/files/招标文件.pdf', type: 'pdf' },
{ name: '投标须知.docx', url: '/files/投标须知.docx', type: 'word' },
{ name: '技术需求说明.xlsx', url: '/files/技术需求说明.xlsx', type: 'excel' }
]
};
const AnnounceInfo: React.FC = () => {
const location = useLocation();
const id = new URLSearchParams(location.search).get("id")
return <>AnnounceInfo -------- {id}</>;
const id = new URLSearchParams(location.search).get("id");
const [announceDetail, setAnnounceDetail] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
// 模拟获取公告详情数据
useEffect(() => {
// 实际项目中应该通过API获取数据
setTimeout(() => {
setAnnounceDetail(mockAnnounceDetail);
setLoading(false);
}, 500);
}, [id]);
// 处理返回列表
const handleBack = () => {
window.history.back();
};
// 处理下载附件
const handleDownload = (url: string, name: string) => {
message.success(`开始下载: ${name}`);
console.log('下载附件:', url, name);
// 实际项目中应该调用下载API
// window.open(url);
};
// 根据文件类型获取图标
const getFileIcon = (type: string) => {
switch (type) {
case 'pdf':
return <FilePdfOutlined />;
case 'word':
return <FileWordOutlined />;
case 'excel':
return <FileExcelOutlined />;
default:
return <DownloadOutlined />;
}
};
if (loading) {
return (
<div className={styles.loadingContainer}>
<Spin size="large" />
</div>
);
}
return (
<div className={styles.announceInfoContainer}>
<div className={styles.announceInfoHeader}>
<Button
type="link"
icon={<ArrowLeftOutlined />}
onClick={handleBack}
className={styles.backButton}
>
</Button>
</div>
<div className={styles.announceInfoContent}>
<div className={styles.titleContainer}>
<Title level={2} className={styles.title}>
{announceDetail.title}
</Title>
</div>
<div className={styles.metaInfo}>
<div className={styles.metaLeft}>
<Text type="secondary">: {announceDetail.publishDate}</Text>
</div>
<div className={styles.metaCenter}>
<Text type="secondary">: {announceDetail.publisher}</Text>
</div>
<div className={styles.metaRight}>
<Text type="secondary">: {announceDetail.type}</Text>
</div>
</div>
<Divider className={styles.divider} />
<div className={styles.contentBody}>
<div dangerouslySetInnerHTML={{ __html: announceDetail.content }} />
</div>
{announceDetail.attachments.length > 0 && (
<div className={styles.attachmentsSection}>
<Divider className={styles.divider} />
<div className={styles.attachmentsTitle}></div>
<div className={styles.attachmentsList}>
{announceDetail.attachments.map((attachment: any, index: number) => (
<Button
key={index}
type="primary"
ghost
icon={getFileIcon(attachment.type)}
onClick={() => handleDownload(attachment.url, attachment.name)}
className={styles.attachmentButton}
>
{attachment.name}
</Button>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default AnnounceInfo;

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { Card, Row, Col, Tabs, Table } from 'antd';
import { useIntl, Link } from 'umi';
import SpaceBlock from '@/components/SpaceBlock/SpaceBlock';
import './index.less';
import IconFont from '@/components/IconFont/IconFont';
import LinkComponent from './Link';
@ -181,7 +180,6 @@ const IndexPage: React.FC = () => {
</Col>
))}
</Row>
<SpaceBlock />
<Tabs onChange={tabChange}>
{tabList.map((item) => (
<Tabs.TabPane tab={item.label} key={item.key} />
@ -200,7 +198,7 @@ const IndexPage: React.FC = () => {
</Link>
</div>
<Row style={{ marginTop: '20px' }}>
<Row style={{ marginTop: '20px', backgroundColor: '#fff' }}>
<Col span={12}>
<div className="blockTitle"></div>
<img src="" alt="" />
@ -235,7 +233,6 @@ const IndexPage: React.FC = () => {
<p>客服1: 400-300-9989</p>
</Col>
</Row>
<SpaceBlock />
<div>
<LinkComponent />