feat: add job fair region type functionality and improve job fair detail pages
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
- Introduced job fair region type selection in EditModal and PublicJobFairList components. - Enhanced JobFairDetail page to display job fair region type using DictTag. - Updated API types to include jobFairRegionType for better data handling. - Created new JobPortal pages for displaying job fair details with improved styling. - Added CSS styles for job fair detail and list pages to enhance user experience. - Implemented loading states and error handling for job fair data fetching.
This commit is contained in:
@@ -43,6 +43,8 @@ export default {
|
||||
// 配置了这个可以从 http 代理到 https
|
||||
// 依赖 origin 的功能可能需要这个,比如 cookie
|
||||
changeOrigin: true,
|
||||
// 测试环境目前使用 IP 访问 HTTPS,证书域名不匹配;仅影响本地开发代理。
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -48,6 +48,14 @@ export default [
|
||||
path: '/job-portal/list', // 职位列表
|
||||
component: './JobPortal/List',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/job-fair', // 招聘会(线上、户外)
|
||||
component: './JobPortal/JobFair',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/job-fair/detail', // 招聘会详情
|
||||
component: './JobPortal/JobFair/Detail',
|
||||
},
|
||||
{
|
||||
path: '/job-portal/detail', // 职位详情
|
||||
component: './JobPortal/Detail',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FileTextOutlined,
|
||||
HomeOutlined,
|
||||
BellOutlined,
|
||||
CalendarOutlined,
|
||||
ReadOutlined,
|
||||
LoginOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
@@ -102,6 +103,7 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
|
||||
// 判断激活导航(简化为 startsWith 匹配)
|
||||
const isHome = /^\/job-portal(\/(list|detail))?$/.test(location.pathname);
|
||||
const isJobFair = location.pathname.startsWith('/job-portal/job-fair');
|
||||
const isResume = location.pathname.startsWith('/job-portal/resume');
|
||||
const isMine = location.pathname.startsWith('/job-portal/personal-center') || location.pathname.startsWith('/job-portal/profile');
|
||||
const isMessage = location.pathname.startsWith('/job-portal/message');
|
||||
@@ -318,6 +320,14 @@ const JobPortalHeader: React.FC<JobPortalHeaderProps> = ({
|
||||
>
|
||||
找工作
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CalendarOutlined />}
|
||||
onClick={() => handleNavClick('/job-portal/job-fair')}
|
||||
className={`nav-btn${isJobFair ? ' active' : ''}`}
|
||||
>
|
||||
招聘会
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FileTextOutlined />}
|
||||
|
||||
62
src/pages/JobPortal/JobFair/Detail/index.less
Normal file
62
src/pages/JobPortal/JobFair/Detail/index.less
Normal file
@@ -0,0 +1,62 @@
|
||||
@import '../../theme.less';
|
||||
|
||||
.portal-job-fair-detail-page {
|
||||
.job-fair-detail-content {
|
||||
.jp-page-container();
|
||||
padding-top: 24px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
.job-fair-back {
|
||||
margin: 0 0 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.job-fair-detail-card {
|
||||
.jp-card-base();
|
||||
|
||||
.ant-card-body {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-detail-title-row {
|
||||
margin-bottom: 26px;
|
||||
|
||||
.ant-tag {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
margin: 0;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-detail-introduction {
|
||||
margin-top: 28px;
|
||||
|
||||
.ant-typography {
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
white-space: pre-wrap;
|
||||
color: @jp-text-secondary;
|
||||
line-height: 1.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.portal-job-fair-detail-page {
|
||||
.job-fair-detail-content {
|
||||
padding: 16px 12px 36px;
|
||||
}
|
||||
|
||||
.job-fair-detail-card .ant-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/pages/JobPortal/JobFair/Detail/index.tsx
Normal file
182
src/pages/JobPortal/JobFair/Detail/index.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Card, Descriptions, Empty, Spin, Tag, Typography } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CalendarOutlined,
|
||||
EnvironmentOutlined,
|
||||
HomeOutlined,
|
||||
PhoneOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation } from '@umijs/max';
|
||||
import dayjs from 'dayjs';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import {
|
||||
getPortalJobFairDetail,
|
||||
type JobFairChannel,
|
||||
type PortalJobFairItem,
|
||||
} from '@/services/jobportal/jobFair';
|
||||
import '../index.less';
|
||||
import './index.less';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
interface JobFairDetailLocationState {
|
||||
fair?: PortalJobFairItem;
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string) {
|
||||
if (!value) {
|
||||
return '暂未公布';
|
||||
}
|
||||
const date = dayjs(value.replace(' ', 'T'));
|
||||
return date.isValid() ? date.format('YYYY年MM月DD日 HH:mm') : value;
|
||||
}
|
||||
|
||||
const PortalJobFairDetailPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const fallbackDetail = (location.state as JobFairDetailLocationState | null)?.fair;
|
||||
const { channel, jobFairId } = useMemo(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
return {
|
||||
channel: params.get('channel') === 'outdoor' ? 'outdoor' : 'online',
|
||||
jobFairId: params.get('jobFairId') || '',
|
||||
} as { channel: JobFairChannel; jobFairId: string };
|
||||
}, [location.search]);
|
||||
const [detail, setDetail] = useState<PortalJobFairItem>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobFairId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const loadDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPortalJobFairDetail(channel, jobFairId);
|
||||
if (!cancelled && response?.code === 200) {
|
||||
setDetail(response.data || fallbackDetail);
|
||||
} else if (!cancelled) {
|
||||
setDetail(fallbackDetail);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setDetail(fallbackDetail);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadDetail();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [channel, fallbackDetail, jobFairId]);
|
||||
|
||||
const backToList = () => history.push('/job-portal/job-fair');
|
||||
|
||||
return (
|
||||
<div className="portal-job-fair-page portal-job-fair-detail-page job-portal-page-root">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
<main className="job-fair-detail-content">
|
||||
<Button
|
||||
className="job-fair-back"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={backToList}
|
||||
type="link"
|
||||
>
|
||||
返回招聘会列表
|
||||
</Button>
|
||||
<Spin spinning={loading}>
|
||||
{detail ? (
|
||||
<Card className="job-fair-detail-card" variant="borderless">
|
||||
<div className="job-fair-detail-title-row">
|
||||
<div>
|
||||
<Tag color={channel === 'outdoor' ? 'cyan' : 'blue'}>
|
||||
{channel === 'outdoor' ? '户外招聘会' : '线上招聘会'}
|
||||
</Tag>
|
||||
<Title level={2}>{detail.jobFairTitle}</Title>
|
||||
</div>
|
||||
</div>
|
||||
<Descriptions bordered column={{ lg: 2, md: 2, sm: 1, xs: 1 }}>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<>
|
||||
<CalendarOutlined /> 举办时间
|
||||
</>
|
||||
}
|
||||
>
|
||||
{formatDateTime(detail.jobFairStartTime)} 至{' '}
|
||||
{formatDateTime(detail.jobFairEndTime)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<>
|
||||
<EnvironmentOutlined /> 举办地点
|
||||
</>
|
||||
}
|
||||
>
|
||||
{detail.jobFairAddress || detail.jobFairVenueName || '暂未公布'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<>
|
||||
<TeamOutlined /> 主办单位
|
||||
</>
|
||||
}
|
||||
>
|
||||
{detail.jobFairHostUnit || detail.jobFairOrganizeUnit || '暂未公布'}
|
||||
</Descriptions.Item>
|
||||
{detail.jobFairRegion && (
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<>
|
||||
<HomeOutlined /> 举办区域
|
||||
</>
|
||||
}
|
||||
>
|
||||
{detail.jobFairRegion}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detail.boothNum && (
|
||||
<Descriptions.Item label="展位数量">{detail.boothNum}</Descriptions.Item>
|
||||
)}
|
||||
{detail.jobFairPhone && (
|
||||
<Descriptions.Item
|
||||
label={
|
||||
<>
|
||||
<PhoneOutlined /> 联系电话
|
||||
</>
|
||||
}
|
||||
>
|
||||
{detail.jobFairPhone}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
<section className="job-fair-detail-introduction">
|
||||
<Title level={4}>招聘会简介</Title>
|
||||
<Paragraph>{detail.jobFairIntroduction || '暂未提供招聘会简介。'}</Paragraph>
|
||||
</section>
|
||||
</Card>
|
||||
) : (
|
||||
!loading && (
|
||||
<Empty description={jobFairId ? '未找到该招聘会信息' : '缺少招聘会参数'}>
|
||||
<Button onClick={backToList} type="primary">
|
||||
返回招聘会列表
|
||||
</Button>
|
||||
</Empty>
|
||||
)
|
||||
)}
|
||||
</Spin>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PortalJobFairDetailPage;
|
||||
333
src/pages/JobPortal/JobFair/index.less
Normal file
333
src/pages/JobPortal/JobFair/index.less
Normal file
@@ -0,0 +1,333 @@
|
||||
@import '../theme.less';
|
||||
|
||||
.portal-job-fair-page {
|
||||
min-height: 100vh;
|
||||
|
||||
.job-fair-content {
|
||||
.jp-page-container();
|
||||
padding-top: 28px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
.job-fair-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 128px;
|
||||
padding: 24px 38px;
|
||||
border-radius: @jp-radius-lg;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #0d6efd, #21a3ee);
|
||||
box-shadow: 0 10px 28px rgba(24, 144, 255, 0.22);
|
||||
|
||||
.ant-typography {
|
||||
color: #fff;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.job-fair-hero-icon {
|
||||
opacity: 0.18;
|
||||
font-size: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-panel,
|
||||
.fair-calendar-card,
|
||||
.fair-summary-card,
|
||||
.fair-card {
|
||||
.jp-card-base();
|
||||
}
|
||||
|
||||
.job-fair-panel {
|
||||
margin-top: 22px;
|
||||
padding: 0 24px 20px;
|
||||
|
||||
.job-fair-tabs {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 16px 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.ant-input-search {
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
margin: 0;
|
||||
padding: 5px 9px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-supporting {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
.fair-calendar-card {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
.ant-card-head,
|
||||
.ant-card-body {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.ant-picker-calendar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.fair-calendar-date {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
margin: 0 3px;
|
||||
border-radius: 4px;
|
||||
color: @jp-text-primary;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ant-picker-cell-selected .fair-calendar-date {
|
||||
color: #fff;
|
||||
background: @jp-primary;
|
||||
}
|
||||
|
||||
.fair-calendar-marker {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 1px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #fa8c16;
|
||||
}
|
||||
|
||||
.clear-date-button {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fair-summary-grid {
|
||||
display: grid;
|
||||
grid-column: span 2;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
|
||||
.fair-summary-card {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
.ant-card-head {
|
||||
border-bottom-color: @jp-border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fair-summary-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fair-summary-item {
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: @jp-text-primary;
|
||||
text-align: left;
|
||||
background: #f8fbff;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: @jp-primary-light;
|
||||
}
|
||||
|
||||
.fair-summary-date {
|
||||
color: @jp-primary;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fair-summary-title {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.fair-summary-empty {
|
||||
display: block;
|
||||
padding-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.job-fair-list-section {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.job-fair-list-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.ant-typography {
|
||||
.jp-section-title();
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fair-card {
|
||||
height: 100%;
|
||||
transition:
|
||||
box-shadow 0.2s,
|
||||
transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @jp-shadow-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.fair-card-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.ant-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fair-card-title {
|
||||
min-height: 48px;
|
||||
margin-bottom: 14px !important;
|
||||
color: @jp-text-primary;
|
||||
line-height: 24px !important;
|
||||
}
|
||||
|
||||
.fair-card-meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
min-height: 82px;
|
||||
color: @jp-text-secondary;
|
||||
font-size: 13px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
line-height: 20px;
|
||||
|
||||
.anticon {
|
||||
flex: 0 0 auto;
|
||||
margin: 3px 8px 0 0;
|
||||
color: @jp-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fair-card-introduction {
|
||||
min-height: 42px;
|
||||
margin: 16px 0 0 !important;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid @jp-border;
|
||||
color: @jp-text-muted;
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.job-fair-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.portal-job-fair-page {
|
||||
.job-fair-supporting,
|
||||
.fair-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fair-summary-grid {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.fair-summary-card {
|
||||
min-height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.portal-job-fair-page {
|
||||
.job-fair-content {
|
||||
padding: 16px 12px 36px;
|
||||
}
|
||||
|
||||
.job-fair-hero {
|
||||
min-height: 112px;
|
||||
padding: 20px;
|
||||
|
||||
.job-fair-hero-icon {
|
||||
font-size: 62px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-fair-panel {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.job-fair-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-input-search {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fair-summary-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.job-fair-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
380
src/pages/JobPortal/JobFair/index.tsx
Normal file
380
src/pages/JobPortal/JobFair/index.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Card,
|
||||
Col,
|
||||
ConfigProvider,
|
||||
Empty,
|
||||
Input,
|
||||
Pagination,
|
||||
Row,
|
||||
Spin,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CalendarOutlined,
|
||||
ClockCircleOutlined,
|
||||
EnvironmentOutlined,
|
||||
SearchOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import {
|
||||
getPortalCurrentMonthJobFairs,
|
||||
getPortalCurrentQuarterJobFairs,
|
||||
getPortalJobFairDates,
|
||||
getPortalJobFairPage,
|
||||
type JobFairChannel,
|
||||
type PortalJobFairItem,
|
||||
} from '@/services/jobportal/jobFair';
|
||||
import './index.less';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
const PAGE_SIZE = 9;
|
||||
|
||||
const fairTabs: Array<{ key: JobFairChannel; label: string }> = [
|
||||
{ key: 'online', label: '线上招聘会' },
|
||||
{ key: 'outdoor', label: '户外招聘会' },
|
||||
];
|
||||
|
||||
function formatDateTime(value?: string) {
|
||||
if (!value) {
|
||||
return '时间待定';
|
||||
}
|
||||
const date = dayjs(value.replace(' ', 'T'));
|
||||
return date.isValid() ? date.format('YYYY年MM月DD日 HH:mm') : value;
|
||||
}
|
||||
|
||||
function formatSummaryDate(value?: string) {
|
||||
if (!value) {
|
||||
return '时间待定';
|
||||
}
|
||||
const date = dayjs(value.replace(' ', 'T'));
|
||||
return date.isValid() ? date.format('MM月DD日') : value.slice(0, 10);
|
||||
}
|
||||
|
||||
function getFairStatus(fair: PortalJobFairItem) {
|
||||
const start = fair.jobFairStartTime ? dayjs(fair.jobFairStartTime.replace(' ', 'T')) : null;
|
||||
const end = fair.jobFairEndTime ? dayjs(fair.jobFairEndTime.replace(' ', 'T')) : null;
|
||||
const now = dayjs();
|
||||
|
||||
if (start?.isValid() && now.isBefore(start)) {
|
||||
return { label: '待开始', color: 'gold' };
|
||||
}
|
||||
if (end?.isValid() && now.isAfter(end)) {
|
||||
return { label: '已结束', color: 'default' };
|
||||
}
|
||||
if (start?.isValid() && end?.isValid()) {
|
||||
return { label: '进行中', color: 'green' };
|
||||
}
|
||||
return fair.jobFairStatus ? { label: fair.jobFairStatus, color: 'blue' } : null;
|
||||
}
|
||||
|
||||
const PortalJobFairPage: React.FC = () => {
|
||||
const [channel, setChannel] = useState<JobFairChannel>('online');
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [selectedDate, setSelectedDate] = useState<string>();
|
||||
const [calendarValue, setCalendarValue] = useState<Dayjs>(() => dayjs().locale('zh-cn'));
|
||||
const [fairDates, setFairDates] = useState<Set<string>>(new Set());
|
||||
const [fairs, setFairs] = useState<PortalJobFairItem[]>([]);
|
||||
const [monthFairs, setMonthFairs] = useState<PortalJobFairItem[]>([]);
|
||||
const [quarterFairs, setQuarterFairs] = useState<PortalJobFairItem[]>([]);
|
||||
const [pageNum, setPageNum] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [summaryLoading, setSummaryLoading] = useState(false);
|
||||
|
||||
const loadFairs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPortalJobFairPage(channel, {
|
||||
pageNum,
|
||||
pageSize: PAGE_SIZE,
|
||||
jobFairTitle: keyword || undefined,
|
||||
zphjbsj: selectedDate,
|
||||
});
|
||||
if (response?.code !== 200) {
|
||||
throw new Error(response?.msg || '招聘会列表加载失败');
|
||||
}
|
||||
setFairs(response.data?.list || []);
|
||||
setTotal(response.data?.total || 0);
|
||||
} catch (error) {
|
||||
setFairs([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [channel, keyword, pageNum, selectedDate]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadFairs();
|
||||
}, [loadFairs]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadSupportingData = async () => {
|
||||
setSummaryLoading(true);
|
||||
try {
|
||||
const [datesResponse, monthResponse, quarterResponse] = await Promise.all([
|
||||
getPortalJobFairDates(channel),
|
||||
getPortalCurrentMonthJobFairs(channel),
|
||||
getPortalCurrentQuarterJobFairs(channel),
|
||||
]);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setFairDates(new Set(Array.isArray(datesResponse?.data) ? datesResponse.data : []));
|
||||
setMonthFairs(Array.isArray(monthResponse?.data) ? monthResponse.data : []);
|
||||
setQuarterFairs(Array.isArray(quarterResponse?.data) ? quarterResponse.data : []);
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setFairDates(new Set());
|
||||
setMonthFairs([]);
|
||||
setQuarterFairs([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setSummaryLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadSupportingData();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
const handleChannelChange = (nextChannel: string) => {
|
||||
setChannel(nextChannel as JobFairChannel);
|
||||
setPageNum(1);
|
||||
setSelectedDate(undefined);
|
||||
};
|
||||
|
||||
const handleSearch = (value?: string) => {
|
||||
setKeyword((value ?? searchInput).trim());
|
||||
setPageNum(1);
|
||||
};
|
||||
|
||||
const handleSelectDate = (value: Dayjs) => {
|
||||
const localizedValue = value.locale('zh-cn');
|
||||
const nextDate = localizedValue.format('YYYY-MM-DD');
|
||||
setCalendarValue(localizedValue);
|
||||
setSelectedDate(nextDate);
|
||||
setPageNum(1);
|
||||
};
|
||||
|
||||
const handleSummaryFairClick = (fair: PortalJobFairItem) => {
|
||||
if (!fair.jobFairId) {
|
||||
return;
|
||||
}
|
||||
history.push(
|
||||
`/job-portal/job-fair/detail?channel=${channel}&jobFairId=${encodeURIComponent(fair.jobFairId)}`,
|
||||
{ fair },
|
||||
);
|
||||
};
|
||||
|
||||
const renderSummary = (title: string, fairsToRender: PortalJobFairItem[]) => (
|
||||
<Card className="fair-summary-card" title={title} variant="borderless">
|
||||
<Spin spinning={summaryLoading}>
|
||||
{fairsToRender.length ? (
|
||||
<div className="fair-summary-list">
|
||||
{fairsToRender.slice(0, 3).map((fair) => (
|
||||
<button
|
||||
className="fair-summary-item"
|
||||
key={fair.jobFairId}
|
||||
onClick={() => handleSummaryFairClick(fair)}
|
||||
title={`查看${fair.jobFairTitle}详情`}
|
||||
type="button"
|
||||
>
|
||||
<span className="fair-summary-date">
|
||||
{formatSummaryDate(fair.jobFairStartTime)}
|
||||
</span>
|
||||
<span className="fair-summary-title">{fair.jobFairTitle}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary" className="fair-summary-empty">
|
||||
暂无招聘会安排
|
||||
</Text>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="portal-job-fair-page job-portal-page-root">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
|
||||
<main className="job-fair-content">
|
||||
<section className="job-fair-hero">
|
||||
<div>
|
||||
<Title level={2}>招聘会</Title>
|
||||
<Text>汇集线上与户外招聘会信息,选择日期即可查看当天的活动安排。</Text>
|
||||
</div>
|
||||
<CalendarOutlined className="job-fair-hero-icon" aria-hidden="true" />
|
||||
</section>
|
||||
|
||||
<section className="job-fair-panel" aria-label="招聘会筛选">
|
||||
<Tabs
|
||||
activeKey={channel}
|
||||
className="job-fair-tabs"
|
||||
items={fairTabs}
|
||||
onChange={handleChannelChange}
|
||||
/>
|
||||
<div className="job-fair-toolbar">
|
||||
<Input.Search
|
||||
allowClear
|
||||
enterButton={
|
||||
<>
|
||||
<SearchOutlined /> 搜索
|
||||
</>
|
||||
}
|
||||
onChange={(event) => setSearchInput(event.target.value)}
|
||||
onSearch={handleSearch}
|
||||
placeholder="搜索招聘会名称"
|
||||
value={searchInput}
|
||||
/>
|
||||
{selectedDate && (
|
||||
<Tag
|
||||
closable
|
||||
color="blue"
|
||||
onClose={() => {
|
||||
setSelectedDate(undefined);
|
||||
setPageNum(1);
|
||||
}}
|
||||
>
|
||||
举办日期:{selectedDate}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="job-fair-supporting" aria-label="招聘会日历与近期安排">
|
||||
<Card className="fair-calendar-card" title="招聘会日历" variant="borderless">
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<Calendar
|
||||
fullCellRender={(current, info) => {
|
||||
if (info.type !== 'date') {
|
||||
return info.originNode;
|
||||
}
|
||||
const date = current.format('YYYY-MM-DD');
|
||||
return (
|
||||
<div className="fair-calendar-date">
|
||||
{current.date()}
|
||||
{fairDates.has(date) && (
|
||||
<span className="fair-calendar-marker" title="当天有招聘会" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
fullscreen={false}
|
||||
onPanelChange={(value) => setCalendarValue(value.locale('zh-cn'))}
|
||||
onSelect={handleSelectDate}
|
||||
value={calendarValue}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
<Button
|
||||
className="clear-date-button"
|
||||
disabled={!selectedDate}
|
||||
onClick={() => {
|
||||
setSelectedDate(undefined);
|
||||
setPageNum(1);
|
||||
}}
|
||||
type="link"
|
||||
>
|
||||
查看全部日期
|
||||
</Button>
|
||||
</Card>
|
||||
<div className="fair-summary-grid">
|
||||
{renderSummary(`本月${channel === 'outdoor' ? '户外' : ''}招聘会`, monthFairs)}
|
||||
{renderSummary(`本季度${channel === 'outdoor' ? '户外' : ''}招聘会`, quarterFairs)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="job-fair-list-section" aria-live="polite">
|
||||
<div className="job-fair-list-heading">
|
||||
<Title level={3}>{channel === 'outdoor' ? '户外招聘会' : '线上招聘会'}</Title>
|
||||
<Text type="secondary">共 {total} 场</Text>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
{fairs.length ? (
|
||||
<>
|
||||
<Row gutter={[20, 20]}>
|
||||
{fairs.map((fair) => {
|
||||
const status = getFairStatus(fair);
|
||||
return (
|
||||
<Col key={fair.jobFairId} lg={8} md={12} sm={24} xs={24}>
|
||||
<Card className="fair-card" hoverable>
|
||||
<div className="fair-card-tags">
|
||||
<Tag color={channel === 'outdoor' ? 'cyan' : 'blue'}>
|
||||
{channel === 'outdoor' ? '户外招聘会' : '线上招聘会'}
|
||||
</Tag>
|
||||
{status && <Tag color={status.color}>{status.label}</Tag>}
|
||||
</div>
|
||||
<Title
|
||||
className="fair-card-title"
|
||||
ellipsis={{ rows: 2 }}
|
||||
level={4}
|
||||
title={fair.jobFairTitle}
|
||||
>
|
||||
{fair.jobFairTitle}
|
||||
</Title>
|
||||
<div className="fair-card-meta">
|
||||
<div>
|
||||
<ClockCircleOutlined />
|
||||
{formatDateTime(fair.jobFairStartTime)}
|
||||
</div>
|
||||
<div>
|
||||
<EnvironmentOutlined />
|
||||
{fair.jobFairAddress || '地点待定'}
|
||||
</div>
|
||||
<div>
|
||||
<TeamOutlined />
|
||||
{fair.jobFairHostUnit || fair.jobFairOrganizeUnit || '主办单位待定'}
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph className="fair-card-introduction" ellipsis={{ rows: 2 }}>
|
||||
{fair.jobFairIntroduction || '暂未提供招聘会简介。'}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
{total > PAGE_SIZE && (
|
||||
<div className="job-fair-pagination">
|
||||
<Pagination
|
||||
current={pageNum}
|
||||
onChange={(nextPage) => setPageNum(nextPage)}
|
||||
pageSize={PAGE_SIZE}
|
||||
showSizeChanger={false}
|
||||
showTotal={(count) => `共 ${count} 场招聘会`}
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!loading && <Empty description="暂未找到符合条件的招聘会" />
|
||||
)}
|
||||
</Spin>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PortalJobFairPage;
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
removeJobFromJobFair,
|
||||
reviewPublicJobFairCompany,
|
||||
} from '@/services/jobfair/publicJobFair';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import DictTag from '@/components/DictTag';
|
||||
import CompanySelectModal from '../components/CompanySelectModal';
|
||||
import JobSelectModal from '../components/JobSelectModal';
|
||||
|
||||
@@ -51,6 +53,7 @@ const JobFairDetail: React.FC = () => {
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [reviewCompany, setReviewCompany] = useState<API.PublicJobFair.CompanyInfo>();
|
||||
const [reviewSubmitting, setReviewSubmitting] = useState(false);
|
||||
const [jobFairRegionTypeEnum, setJobFairRegionTypeEnum] = useState<any>({});
|
||||
const [reviewForm] = Form.useForm<ReviewFormValues>();
|
||||
const reviewStatus = Form.useWatch('reviewStatus', reviewForm);
|
||||
|
||||
@@ -72,6 +75,10 @@ const JobFairDetail: React.FC = () => {
|
||||
}
|
||||
}, [jobFairId, isEnterprise]);
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('job_fair_region_type').then(setJobFairRegionTypeEnum);
|
||||
}, []);
|
||||
|
||||
const handleRemoveCompany = (company: API.PublicJobFair.CompanyInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认移除',
|
||||
@@ -174,6 +181,13 @@ const JobFairDetail: React.FC = () => {
|
||||
{jobFairTypeMap[detail?.jobFairType || '']}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="区域类型">
|
||||
{detail?.jobFairRegionType ? (
|
||||
<DictTag enums={jobFairRegionTypeEnum} value={detail.jobFairRegionType} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="地址">{detail?.jobFairAddress}</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">{detail?.jobFairPhone}</Descriptions.Item>
|
||||
<Descriptions.Item label="招聘会时间">
|
||||
|
||||
@@ -18,6 +18,7 @@ interface EditModalProps {
|
||||
open: boolean;
|
||||
values?: API.PublicJobFair.JobFairItem;
|
||||
jobFairTypeEnum: any;
|
||||
jobFairRegionTypeEnum: any;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: API.PublicJobFair.JobFairForm) => Promise<void>;
|
||||
}
|
||||
@@ -69,6 +70,7 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
open,
|
||||
values,
|
||||
jobFairTypeEnum,
|
||||
jobFairRegionTypeEnum,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) => {
|
||||
@@ -175,6 +177,12 @@ const EditModal: React.FC<EditModalProps> = ({
|
||||
valueEnum={jobFairTypeEnum}
|
||||
rules={[{ required: true, message: '请选择招聘会类型' }]}
|
||||
/>
|
||||
<ProFormRadio.Group
|
||||
name="jobFairRegionType"
|
||||
label="区域类型"
|
||||
valueEnum={jobFairRegionTypeEnum}
|
||||
rules={[{ required: true, message: '请选择区域类型' }]}
|
||||
/>
|
||||
<ProFormRadio.Group name="isCrossDomain" label="是否跨域" valueEnum={yesNoEnum} />
|
||||
{isCrossDomain === 'y' && (
|
||||
<ProFormSelect
|
||||
|
||||
@@ -33,11 +33,13 @@ const PublicJobFairList: React.FC = () => {
|
||||
const [companyJobModalOpen, setCompanyJobModalOpen] = useState(false);
|
||||
const [currentJobFair, setCurrentJobFair] = useState<API.PublicJobFair.JobFairItem>();
|
||||
const [jobFairTypeEnum, setJobFairTypeEnum] = useState<any>({});
|
||||
const [jobFairRegionTypeEnum, setJobFairRegionTypeEnum] = useState<any>({});
|
||||
const [yesNoEnum, setYesNoEnum] = useState<any>({});
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('job_fair_type', true).then(setJobFairTypeEnum);
|
||||
getDictValueEnum('job_fair_region_type').then(setJobFairRegionTypeEnum);
|
||||
getDictValueEnum('sys_yes_no').then(setYesNoEnum);
|
||||
}, []);
|
||||
|
||||
@@ -129,6 +131,26 @@ const PublicJobFairList: React.FC = () => {
|
||||
valueEnum: jobFairTypeEnum,
|
||||
render: (_, record) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />,
|
||||
},
|
||||
{
|
||||
title: '区域类型',
|
||||
dataIndex: 'jobFairRegionType',
|
||||
valueType: 'select',
|
||||
valueEnum: jobFairRegionTypeEnum,
|
||||
render: (_, record) => (
|
||||
<DictTag enums={jobFairRegionTypeEnum} value={record.jobFairRegionType} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '举办状态',
|
||||
dataIndex: 'jobFairStatus',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => {
|
||||
const status = record.jobFairStatus;
|
||||
if (!status) return '-';
|
||||
const color = status === '未举办' ? 'blue' : status === '正在举办' ? 'green' : 'default';
|
||||
return <Tag color={color}>{status}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '报名审核状态',
|
||||
dataIndex: 'enterpriseReviewStatus',
|
||||
@@ -284,6 +306,7 @@ const PublicJobFairList: React.FC = () => {
|
||||
pageSize: params.pageSize,
|
||||
jobFairTitle: params.jobFairTitle,
|
||||
jobFairType: params.jobFairType,
|
||||
jobFairRegionType: params.jobFairRegionType,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
});
|
||||
@@ -322,6 +345,7 @@ const PublicJobFairList: React.FC = () => {
|
||||
open={modalVisible}
|
||||
values={currentRow}
|
||||
jobFairTypeEnum={jobFairTypeEnum}
|
||||
jobFairRegionTypeEnum={jobFairRegionTypeEnum}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
setCurrentRow(undefined);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@ant-design/pro-components';
|
||||
import { Form, Button, Input, Tag, TreeSelect, Switch } from 'antd';
|
||||
import { PlusOutlined, MinusCircleOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import { useModel } from '@umijs/max';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { DictValueEnumObj } from '@/components/DictTag';
|
||||
import { getCmsCompanyList } from '@/services/company/list';
|
||||
@@ -72,6 +73,8 @@ const waitTime = (time: number = 100) => {
|
||||
};
|
||||
|
||||
const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const isEnterprise = initialState?.currentUser?.userType === 'view';
|
||||
const [form] = Form.useForm<ManageFormValues>();
|
||||
const companyNameMap = useRef<Record<number, string>>({});
|
||||
const [jobDetail, setJobDetail] = useState<API.ManagementList.Manage | null>(null);
|
||||
@@ -265,7 +268,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
<ProDescriptions.Item dataIndex="jobLocation" label="工作地点" />
|
||||
<ProDescriptions.Item
|
||||
dataIndex="jobType"
|
||||
label="岗位类型"
|
||||
label="区域"
|
||||
valueEnum={jobTypeEnum}
|
||||
/>
|
||||
<ProDescriptions.Item dataIndex="jobCategory" label="岗位标签" />
|
||||
@@ -434,26 +437,28 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
initialValue={0}
|
||||
fieldProps={{ defaultChecked: false }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
showSearch
|
||||
width="md"
|
||||
name="companyId"
|
||||
request={async ({ keyWords }) => {
|
||||
const resData = await getCmsCompanyList({ name: keyWords });
|
||||
return resData.rows.map((item) => {
|
||||
companyNameMap.current[item.companyId] = item.name;
|
||||
return { label: item.name, value: item.companyId };
|
||||
});
|
||||
}}
|
||||
fieldProps={{
|
||||
onChange: (companyId: number) => {
|
||||
form.setFieldValue('companyName', companyNameMap.current[companyId] ?? '');
|
||||
},
|
||||
}}
|
||||
placeholder="请输入公司名称选择公司"
|
||||
rules={[{ required: true, message: '请输入公司名称选择公司!' }]}
|
||||
label="招聘公司"
|
||||
/>
|
||||
{!isEnterprise && (
|
||||
<ProFormSelect
|
||||
showSearch
|
||||
width="md"
|
||||
name="companyId"
|
||||
request={async ({ keyWords }) => {
|
||||
const resData = await getCmsCompanyList({ name: keyWords });
|
||||
return resData.rows.map((item) => {
|
||||
companyNameMap.current[item.companyId] = item.name;
|
||||
return { label: item.name, value: item.companyId };
|
||||
});
|
||||
}}
|
||||
fieldProps={{
|
||||
onChange: (companyId: number) => {
|
||||
form.setFieldValue('companyName', companyNameMap.current[companyId] ?? '');
|
||||
},
|
||||
}}
|
||||
placeholder="请输入公司名称选择公司"
|
||||
rules={[{ required: true, message: '请输入公司名称选择公司!' }]}
|
||||
label="招聘公司"
|
||||
/>
|
||||
)}
|
||||
<ProFormDigit
|
||||
name="minSalary"
|
||||
width="md"
|
||||
@@ -529,10 +534,10 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
<ProFormSelect
|
||||
width="md"
|
||||
name="jobType"
|
||||
label={'岗位类型'}
|
||||
label={'区域'}
|
||||
valueEnum={jobTypeEnum}
|
||||
placeholder="请选择类型"
|
||||
rules={[{ required: true, message: '请选择类型!' }]}
|
||||
placeholder="请选择区域"
|
||||
rules={[{ required: true, message: '请选择区域!' }]}
|
||||
/>
|
||||
<ProForm.Item name="jobCategory" label="岗位标签" rules={[{ required: true, message: '请选择岗位标签!' }]}>
|
||||
<TreeSelect
|
||||
|
||||
174
src/services/jobportal/jobFair.ts
Normal file
174
src/services/jobportal/jobFair.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { request } from '@umijs/max';
|
||||
|
||||
export type JobFairChannel = 'online' | 'outdoor';
|
||||
|
||||
/** 招聘会公共列表项;线上与户外接口统一为该结构。 */
|
||||
export interface PortalJobFairItem {
|
||||
jobFairId: string;
|
||||
jobFairTitle: string;
|
||||
jobFairAddress?: string;
|
||||
/** 线上接口通常为 1/2,户外接口固定返回 outdoor。 */
|
||||
jobFairType?: string;
|
||||
jobFairStartTime?: string;
|
||||
jobFairEndTime?: string;
|
||||
jobFairStatus?: string;
|
||||
jobFairHostUnit?: string;
|
||||
jobFairOrganizeUnit?: string;
|
||||
jobFairIntroduction?: string;
|
||||
jobFairPhone?: string;
|
||||
jobFairImage?: string;
|
||||
enterpriseNum?: string;
|
||||
boothNum?: string;
|
||||
divisionName?: string;
|
||||
jobFairCategory?: string;
|
||||
jobFairRegion?: string;
|
||||
jobFairVenueName?: string;
|
||||
}
|
||||
|
||||
export interface PortalJobFairPageParams {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
jobFairTitle?: string;
|
||||
/** 招聘会举办日期,格式 yyyy-MM-dd。 */
|
||||
zphjbsj?: string;
|
||||
}
|
||||
|
||||
export interface PortalJobFairPageData {
|
||||
total: number;
|
||||
list: PortalJobFairItem[];
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
pages?: number;
|
||||
}
|
||||
|
||||
export interface PortalJobFairPageResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: PortalJobFairPageData;
|
||||
}
|
||||
|
||||
export interface PortalJobFairListResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: PortalJobFairItem[];
|
||||
}
|
||||
|
||||
export interface PortalJobFairDatesResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: string[];
|
||||
}
|
||||
|
||||
interface PortalOutdoorFairDetailData extends Partial<PortalJobFairItem> {
|
||||
id?: number;
|
||||
title?: string;
|
||||
hostUnit?: string;
|
||||
fairType?: string;
|
||||
region?: string;
|
||||
venueName?: string;
|
||||
address?: string;
|
||||
boothCount?: number;
|
||||
holdTime?: string;
|
||||
endTime?: string;
|
||||
photoUrl?: string;
|
||||
}
|
||||
|
||||
interface PortalOutdoorFairDetailResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
/**
|
||||
* 旧接口返回 { fair: {...} },当前公共接口直接返回招聘会对象。
|
||||
* 两种格式均保留兼容,避免接口升级后户外详情页显示为空。
|
||||
*/
|
||||
data?: PortalOutdoorFairDetailData | { fair?: PortalOutdoorFairDetailData };
|
||||
}
|
||||
|
||||
function isOutdoorFairDetailWrapper(
|
||||
data: NonNullable<PortalOutdoorFairDetailResult['data']>,
|
||||
): data is { fair?: PortalOutdoorFairDetailData } {
|
||||
return 'fair' in data;
|
||||
}
|
||||
|
||||
export interface PortalJobFairDetailResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: PortalJobFairItem;
|
||||
}
|
||||
|
||||
const FAIR_API = {
|
||||
online: '/app/jobfair/public/jobfair',
|
||||
outdoor: '/app/outdoor-fair',
|
||||
} as const;
|
||||
|
||||
/** 获取线上或户外招聘会分页列表。 */
|
||||
export async function getPortalJobFairPage(
|
||||
channel: JobFairChannel,
|
||||
params?: PortalJobFairPageParams,
|
||||
) {
|
||||
return request<PortalJobFairPageResult>(`${FAIR_API[channel]}/page`, {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取有招聘会的日期,用于与小程序一致的日期筛选提示。 */
|
||||
export async function getPortalJobFairDates(channel: JobFairChannel) {
|
||||
return request<PortalJobFairDatesResult>(`${FAIR_API[channel]}/dates`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取当前月招聘会摘要。 */
|
||||
export async function getPortalCurrentMonthJobFairs(channel: JobFairChannel) {
|
||||
return request<PortalJobFairListResult>(`${FAIR_API[channel]}/currentMonth`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取当前季度招聘会摘要。 */
|
||||
export async function getPortalCurrentQuarterJobFairs(channel: JobFairChannel) {
|
||||
return request<PortalJobFairListResult>(`${FAIR_API[channel]}/currentQuarter`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取招聘会详情,并把户外招聘会详情转换为与列表一致的公共字段。 */
|
||||
export async function getPortalJobFairDetail(channel: JobFairChannel, jobFairId: string) {
|
||||
if (channel === 'online') {
|
||||
return request<PortalJobFairDetailResult>(`${FAIR_API.online}/detail`, {
|
||||
method: 'GET',
|
||||
params: { jobFairId },
|
||||
});
|
||||
}
|
||||
|
||||
const response = await request<PortalOutdoorFairDetailResult>(
|
||||
`${FAIR_API.outdoor}/${encodeURIComponent(jobFairId)}`,
|
||||
{ method: 'GET' },
|
||||
);
|
||||
const payload = response.data;
|
||||
const fair = payload && (isOutdoorFairDetailWrapper(payload) ? payload.fair : payload);
|
||||
if (!fair) {
|
||||
return { code: response.code, msg: response.msg } as PortalJobFairDetailResult;
|
||||
}
|
||||
return {
|
||||
code: response.code,
|
||||
msg: response.msg,
|
||||
data: {
|
||||
jobFairId: fair.jobFairId || String(fair.id ?? jobFairId),
|
||||
jobFairTitle: fair.jobFairTitle || fair.title || '户外招聘会',
|
||||
jobFairAddress: fair.jobFairAddress || fair.address || fair.venueName,
|
||||
jobFairType: fair.jobFairType || fair.fairType || 'outdoor',
|
||||
jobFairStartTime: fair.jobFairStartTime || fair.holdTime,
|
||||
jobFairEndTime: fair.jobFairEndTime || fair.endTime,
|
||||
jobFairHostUnit: fair.jobFairHostUnit || fair.hostUnit,
|
||||
jobFairOrganizeUnit: fair.jobFairOrganizeUnit,
|
||||
jobFairIntroduction: fair.jobFairIntroduction,
|
||||
jobFairPhone: fair.jobFairPhone,
|
||||
jobFairImage: fair.jobFairImage || fair.photoUrl,
|
||||
boothNum:
|
||||
fair.boothNum || (fair.boothCount === undefined ? undefined : String(fair.boothCount)),
|
||||
jobFairRegion: fair.jobFairRegion || fair.region,
|
||||
jobFairVenueName: fair.jobFairVenueName || fair.venueName,
|
||||
},
|
||||
} as PortalJobFairDetailResult;
|
||||
}
|
||||
4
src/types/jobfair/publicJobFair.d.ts
vendored
4
src/types/jobfair/publicJobFair.d.ts
vendored
@@ -8,6 +8,7 @@ declare namespace API.PublicJobFair {
|
||||
pageSize?: number;
|
||||
jobFairTitle?: string;
|
||||
jobFairType?: string;
|
||||
jobFairRegionType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
@@ -18,8 +19,10 @@ declare namespace API.PublicJobFair {
|
||||
jobFairTitle: string;
|
||||
jobFairAddress: string;
|
||||
jobFairType: string;
|
||||
jobFairRegionType?: string;
|
||||
jobFairStartTime: string;
|
||||
jobFairEndTime: string;
|
||||
jobFairStatus?: '未举办' | '正在举办' | '已举办';
|
||||
jobFairSignUpStartTime?: string;
|
||||
jobFairSignUpEndTime?: string;
|
||||
jobFairHostUnit?: string;
|
||||
@@ -101,6 +104,7 @@ declare namespace API.PublicJobFair {
|
||||
jobFairTitle: string;
|
||||
jobFairAddress: string;
|
||||
jobFairType: string;
|
||||
jobFairRegionType: string;
|
||||
jobFairStartTime: string;
|
||||
jobFairEndTime: string;
|
||||
jobFairSignUpStartTime?: string;
|
||||
|
||||
Reference in New Issue
Block a user