11
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled

This commit is contained in:
francis-fh
2026-06-04 16:25:38 +08:00
parent 3815dd63da
commit 03f26885c5
32 changed files with 2642 additions and 3062 deletions

View File

@@ -0,0 +1,121 @@
@import '../../theme.less';
.policy-detail-page {
min-height: 100vh;
background: @jp-bg-page;
.detail-content {
padding: 24px 20px 40px;
.detail-container {
.jp-page-container();
}
.back-button {
padding: 0;
margin-bottom: 16px;
color: @jp-primary;
&:hover {
color: @jp-primary-dark;
}
}
.detail-card {
.jp-card-base();
padding: 28px 32px;
}
.detail-title {
margin: 0 0 16px;
color: @jp-text-primary;
line-height: 1.5;
}
.detail-meta-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
.type-tag {
.jp-job-tag();
}
.level-tag {
background: #f6ffed !important;
border: 1px solid #b7eb8f !important;
color: #52c41a !important;
}
}
.detail-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 20px;
}
.detail-summary {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
font-size: 14px;
.summary-source {
line-height: 1.6;
}
.anticon {
margin-right: 6px;
}
}
.policy-article {
margin-bottom: 28px;
padding: 24px;
background: #fafbfc;
border: 1px solid @jp-border;
border-radius: @jp-radius;
.policy-content {
margin: 0;
font-family: inherit;
font-size: 15px;
line-height: 1.9;
color: @jp-text-primary;
white-space: pre-wrap;
word-break: break-word;
background: transparent;
border: none;
}
}
.content-empty {
margin: 32px 0;
}
.detail-desc {
margin-bottom: 24px;
}
.extra-block {
margin-top: 24px;
padding-top: 20px;
border-top: 1px dashed @jp-border;
h5 {
margin-bottom: 12px;
color: @jp-text-primary;
}
.extra-text {
margin: 0;
color: @jp-text-secondary;
white-space: pre-wrap;
line-height: 1.8;
}
}
}
}

View File

@@ -0,0 +1,191 @@
import React, { useEffect, useState } from 'react';
import {
Button,
Descriptions,
Spin,
Tag,
Typography,
message,
Empty,
} from 'antd';
import {
ArrowLeftOutlined,
CalendarOutlined,
EyeOutlined,
FileTextOutlined,
BankOutlined,
} from '@ant-design/icons';
import { history, useSearchParams } from '@umijs/max';
import JobPortalHeader from '@/components/JobPortalHeader';
import { getPolicyInfoDetail } from '@/services/cms/policyInfo';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag, { DictValueEnumObj } from '@/components/DictTag';
import './index.less';
const { Title, Paragraph, Text } = Typography;
/** 解析 GET /cms/policyInfo/{id} 响应 */
const parsePolicyDetail = (
response: API.PolicyInfo.PolicyInfoDetailResult | undefined,
): API.PolicyInfo.PolicyInfoItem | null => {
if (!response) return null;
if (response.code === 200 && response.data) {
return response.data;
}
return null;
};
const PolicyDetailPage: React.FC = () => {
const [searchParams] = useSearchParams();
const policyId = Number(searchParams.get('id'));
const [detail, setDetail] = useState<API.PolicyInfo.PolicyInfoItem | null>(null);
const [loading, setLoading] = useState(false);
const [userTypeEnum, setUserTypeEnum] = useState<DictValueEnumObj>({});
useEffect(() => {
getDictValueEnum('user_type', false, true).then(setUserTypeEnum);
}, []);
useEffect(() => {
if (!policyId || Number.isNaN(policyId)) {
return;
}
const fetchDetail = async () => {
try {
setLoading(true);
setDetail(null);
const response = await getPolicyInfoDetail(policyId);
const policy = parsePolicyDetail(response);
if (policy) {
setDetail(policy);
} else {
message.error(response?.msg || '获取政策详情失败');
}
} catch (error) {
console.error('获取政策详情失败:', error);
message.error('获取政策详情失败');
} finally {
setLoading(false);
}
};
fetchDetail();
}, [policyId]);
const renderPolicyTags = () => {
if (!detail?.policyTag) return null;
const tags = String(detail.policyTag).split(',').filter(Boolean);
return (
<div className="detail-tags">
{tags.map((tag) => (
<DictTag key={tag} enums={userTypeEnum} value={tag.trim()} />
))}
</div>
);
};
const renderExtraBlock = (label: string, content?: string | null) => {
if (!content?.trim()) return null;
return (
<section className="extra-block">
<Title level={5}>{label}</Title>
<Paragraph className="extra-text">{content}</Paragraph>
</section>
);
};
const invalidId = !policyId || Number.isNaN(policyId);
return (
<div className="policy-detail-page">
<JobPortalHeader showSearch={false} showHotJobs={false} />
<div className="detail-content">
<div className="detail-container">
<Button
type="link"
icon={<ArrowLeftOutlined />}
className="back-button"
onClick={() => history.push('/job-portal/policy')}
>
</Button>
<Spin spinning={loading}>
{invalidId ? (
<Empty description="政策 ID 无效">
<Button type="primary" onClick={() => history.push('/job-portal/policy')}>
</Button>
</Empty>
) : detail ? (
<div className="detail-card">
<Title level={3} className="detail-title">{detail.zcmc}</Title>
<div className="detail-meta-tags">
{detail.zclx && <Tag className="type-tag">{detail.zclx}</Tag>}
{detail.zcLevel && <Tag className="level-tag">{detail.zcLevel}</Tag>}
</div>
{renderPolicyTags()}
<div className="detail-summary">
{detail.publishTime && (
<Text type="secondary">
<CalendarOutlined /> {detail.publishTime}
</Text>
)}
{detail.sourceUnit && (
<Text type="secondary" className="summary-source">
<BankOutlined /> {detail.sourceUnit}
</Text>
)}
<Text type="secondary">
<EyeOutlined /> {detail.viewNum ?? 0}
</Text>
</div>
{detail.zcContent?.trim() ? (
<article className="policy-article">
<pre className="policy-content">{detail.zcContent}</pre>
</article>
) : (
<Empty className="content-empty" description="暂无政策正文" />
)}
{(detail.acceptUnit || detail.fileUrl) && (
<Descriptions column={1} bordered size="middle" className="detail-desc">
{detail.acceptUnit && (
<Descriptions.Item label="受理单位">{detail.acceptUnit}</Descriptions.Item>
)}
{detail.fileUrl && (
<Descriptions.Item label="政策文件">
<a href={detail.fileUrl} target="_blank" rel="noopener noreferrer">
<FileTextOutlined /> {detail.fileName || '查看附件'}
</a>
</Descriptions.Item>
)}
</Descriptions>
)}
{renderExtraBlock('补贴标准', detail.subsidyStandard)}
{renderExtraBlock('经办渠道', detail.handleChannel)}
{renderExtraBlock('申报条件', detail.applyCondition)}
</div>
) : (
!loading && (
<Empty description="未找到政策信息">
<Button type="primary" onClick={() => history.push('/job-portal/policy')}>
</Button>
</Empty>
)
)}
</Spin>
</div>
</div>
</div>
);
};
export default PolicyDetailPage;

View File

@@ -0,0 +1,129 @@
@import '../theme.less';
.policy-page {
min-height: 100vh;
background: @jp-bg-page;
.policy-content {
padding: 24px 20px 40px;
.policy-container {
.jp-page-container();
}
.policy-card {
.jp-card-base();
padding: 24px 28px 16px;
}
.policy-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid @jp-border;
.page-title {
.jp-section-title();
margin: 0;
.anticon {
margin-right: 8px;
color: @jp-primary;
}
}
.policy-search {
width: 320px;
max-width: 100%;
.search-icon {
color: @jp-text-muted;
cursor: pointer;
&:hover {
color: @jp-primary;
}
}
}
}
.policy-list {
.policy-item {
padding: 20px 0;
cursor: pointer;
border-bottom: 1px solid @jp-border;
transition: background 0.2s;
&:hover {
background: @jp-primary-light;
margin: 0 -12px;
padding-left: 12px;
padding-right: 12px;
border-radius: @jp-radius;
}
&:last-child {
border-bottom: none;
}
}
.policy-title {
margin: 0 0 10px;
color: @jp-text-primary;
font-weight: 600;
line-height: 1.5;
}
.policy-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
.type-tag {
.jp-job-tag();
}
.level-tag {
background: #f6ffed !important;
border: 1px solid #b7eb8f !important;
color: #52c41a !important;
}
}
.policy-source {
display: block;
margin-bottom: 8px;
font-size: 13px;
.anticon {
margin-right: 4px;
}
}
.policy-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
font-size: 13px;
color: @jp-text-muted;
.anticon {
margin-right: 4px;
}
}
}
.policy-pagination {
display: flex;
justify-content: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid @jp-border;
}
}
}

View File

@@ -0,0 +1,158 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
Input,
List,
Pagination,
Spin,
Tag,
Typography,
Empty,
message,
} from 'antd';
import {
CalendarOutlined,
EyeOutlined,
FileTextOutlined,
SearchOutlined,
BankOutlined,
} from '@ant-design/icons';
import { history } from '@umijs/max';
import JobPortalHeader from '@/components/JobPortalHeader';
import { getPolicyInfoList } from '@/services/cms/policyInfo';
import './index.less';
const { Title, Text } = Typography;
const PolicyListPage: React.FC = () => {
const [list, setList] = useState<API.PolicyInfo.PolicyInfoItem[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [pageNum, setPageNum] = useState(1);
const [pageSize] = useState(10);
const [searchValue, setSearchValue] = useState('');
const [keyword, setKeyword] = useState('');
const fetchList = useCallback(async () => {
try {
setLoading(true);
const response = await getPolicyInfoList({
pageNum,
pageSize,
searchValue: keyword || undefined,
});
if (response?.code === 200) {
setList(response.rows || []);
setTotal(response.total || 0);
} else {
message.error(response?.msg || '获取政策列表失败');
}
} catch (error) {
console.error('获取政策列表失败:', error);
message.error('获取政策列表失败');
} finally {
setLoading(false);
}
}, [pageNum, pageSize, keyword]);
useEffect(() => {
fetchList();
}, [fetchList]);
const handleSearch = () => {
setPageNum(1);
setKeyword(searchValue.trim());
};
const handleItemClick = (item: API.PolicyInfo.PolicyInfoItem) => {
history.push(`/job-portal/policy/detail?id=${item.id}`);
};
return (
<div className="policy-page">
<JobPortalHeader showSearch={false} showHotJobs={false} />
<div className="policy-content">
<div className="policy-container">
<div className="policy-card">
<div className="policy-header">
<Title level={3} className="page-title">
<FileTextOutlined />
</Title>
<div className="policy-search">
<Input
placeholder="搜索政策名称"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onPressEnter={handleSearch}
allowClear
suffix={
<SearchOutlined
className="search-icon"
onClick={handleSearch}
/>
}
/>
</div>
</div>
<Spin spinning={loading}>
{list.length > 0 ? (
<>
<List
className="policy-list"
dataSource={list}
renderItem={(item) => (
<List.Item
className="policy-item"
onClick={() => handleItemClick(item)}
>
<div className="policy-item-body">
<Title level={5} className="policy-title" ellipsis={{ rows: 2 }}>
{item.zcmc}
</Title>
<div className="policy-tags">
{item.zclx && <Tag className="type-tag">{item.zclx}</Tag>}
{item.zcLevel && <Tag className="level-tag">{item.zcLevel}</Tag>}
</div>
{item.sourceUnit && (
<Text type="secondary" className="policy-source" ellipsis>
<BankOutlined /> {item.sourceUnit}
</Text>
)}
<div className="policy-meta">
{item.publishTime && (
<span>
<CalendarOutlined /> {item.publishTime}
</span>
)}
<span>
<EyeOutlined /> {item.viewNum ?? 0}
</span>
</div>
</div>
</List.Item>
)}
/>
<div className="policy-pagination">
<Pagination
current={pageNum}
pageSize={pageSize}
total={total}
showSizeChanger={false}
showTotal={(t) => `${t}`}
onChange={(page) => setPageNum(page)}
/>
</div>
</>
) : (
!loading && <Empty description="暂无政策信息" />
)}
</Spin>
</div>
</div>
</div>
</div>
);
};
export default PolicyListPage;