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
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:
121
src/pages/JobPortal/Policy/Detail/index.less
Normal file
121
src/pages/JobPortal/Policy/Detail/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
src/pages/JobPortal/Policy/Detail/index.tsx
Normal file
191
src/pages/JobPortal/Policy/Detail/index.tsx
Normal 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;
|
||||
129
src/pages/JobPortal/Policy/index.less
Normal file
129
src/pages/JobPortal/Policy/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/pages/JobPortal/Policy/index.tsx
Normal file
158
src/pages/JobPortal/Policy/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user