Compare commits

...

3 Commits

Author SHA1 Message Date
3faced1751 Merge branch 'master' of http://124.243.245.42:3000/sh/shz-admin
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
2026-07-25 22:21:54 +08:00
e86621aeed 修复招聘会前端交互与无障碍问题 2026-07-25 22:16:55 +08:00
527fcea91f feat: 新增招聘会企业签到与未参会统计 2026-07-24 12:35:30 +08:00
30 changed files with 1192 additions and 341 deletions

View File

@@ -259,6 +259,17 @@ export default [
name: 'company',
path: '/company',
routes: [
{
name: '企业列表',
path: '/company/list',
component: './Company/List',
},
{
name: '未参加招聘会详情',
path: '/company/list/missed-outdoor-fairs',
component: './Company/List/MissedOutdoorFairs',
hideInMenu: true,
},
{
name: '企业信息',
path: '/company/info/index',

View File

@@ -7,10 +7,7 @@ import { history } from '@umijs/max';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
import { clearSessionToken, getAccessToken, getRefreshToken, getTokenExpireTime } from './access';
import {
exchangeThirdPartyCredential,
isThirdPartyTransitionPage,
} from './utils/thirdPartyLogin';
import { exchangeThirdPartyCredential, isThirdPartyTransitionPage } from './utils/thirdPartyLogin';
import { shouldSkipManagementBootstrap } from './utils/routeContext';
import { saveGetInfoCache } from './utils/jobPortalAuth';
import {
@@ -30,6 +27,7 @@ import { stringify } from 'querystring';
import { message } from 'antd';
import React from 'react';
import { getProductionApiBaseUrl } from './utils/publicUrl';
import defaultAvatar from './assets/avatar.jpg';
const isDev = process.env.NODE_ENV === 'development';
@@ -53,8 +51,6 @@ const loginOut = async () => {
}
};
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
@@ -70,8 +66,7 @@ export async function getInitialState(): Promise<{
skipErrorHandler: true,
});
if (response.user.avatar === '') {
response.user.avatar =
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
response.user.avatar = defaultAvatar;
}
saveGetInfoCache(response as unknown as Record<string, unknown>);
return {
@@ -110,11 +105,13 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
// actionsRender: () => [<Question key="doc" />, <SelectLang key="SelectLang" />],
actionsRender: () => [<SelectLang key="SelectLang" />],
avatarProps: {
src: initialState?.currentUser?.avatar,
src: initialState?.currentUser?.avatar || defaultAvatar,
fallback: defaultAvatar,
onError: () => true,
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown menu>{avatarChildren}</AvatarDropdown>;
},
render: (_, avatarChildren) => {
return <AvatarDropdown menu>{avatarChildren}</AvatarDropdown>;
},
},
waterMarkProps: {
// content: initialState?.currentUser?.nickName,
@@ -138,10 +135,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login但排除过渡页面和求职者页面考虑基础路径 /shihezi/
if (
!initialState?.currentUser &&
!shouldSkipManagementBootstrap(location.pathname)
) {
if (!initialState?.currentUser && !shouldSkipManagementBootstrap(location.pathname)) {
history.push(PageEnum.LOGIN);
}
},
@@ -203,14 +197,16 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
};
};
export async function onRouteChange({ clientRoutes, location }: { clientRoutes: any; location: any }) {
export async function onRouteChange({
clientRoutes,
location,
}: {
clientRoutes: any;
location: any;
}) {
const menus = getRemoteMenu();
const token = getAccessToken();
if (
menus === null &&
token &&
!shouldSkipManagementBootstrap(location.pathname)
) {
if (menus === null && token && !shouldSkipManagementBootstrap(location.pathname)) {
console.log('检测到路由信息未加载,正在重新获取路由信息...');
// 重新获取路由信息,而不是刷新页面

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { history, useSearchParams } from '@umijs/max';
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
import { Button, message } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { getCmsCompanyMissedOutdoorFairs } from '@/services/company/list';
const MissedOutdoorFairs: React.FC = () => {
const [searchParams] = useSearchParams();
const companyId = Number(searchParams.get('companyId'));
const columns: ProColumns<API.CompanyList.MissedOutdoorFair>[] = [
{ title: '招聘会名称', dataIndex: 'title', ellipsis: true },
{ title: '举办单位', dataIndex: 'hostUnit', ellipsis: true },
{ title: '举办地址', dataIndex: 'address', ellipsis: true },
{ title: '举办开始时间', dataIndex: 'holdTime', valueType: 'dateTime', width: 180 },
{ title: '举办结束时间', dataIndex: 'endTime', valueType: 'dateTime', width: 180 },
{ title: '报名时间', dataIndex: 'registrationTime', valueType: 'dateTime', width: 180 },
];
return (
<PageContainer
header={{
title: '未参加招聘会详情',
extra: [
<Button key="back" icon={<ArrowLeftOutlined />} onClick={() => history.back()}>
</Button>,
],
}}
>
<ProTable<API.CompanyList.MissedOutdoorFair>
rowKey="fairId"
columns={columns}
search={false}
options={false}
pagination={{ pageSize: 10 }}
request={async (params) => {
if (!companyId) {
message.error('缺少企业ID');
return { data: [], total: 0, success: false };
}
const res = await getCmsCompanyMissedOutdoorFairs(companyId, {
current: params.current,
pageSize: params.pageSize,
});
return {
data: res.rows || [],
total: res.total || 0,
success: res.code === 200,
};
}}
/>
</PageContainer>
);
};
export default MissedOutdoorFairs;

View File

@@ -1,5 +1,5 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useAccess } from '@umijs/max';
import { FormattedMessage, history, useAccess } from '@umijs/max';
import { Button, FormInstance, message, Modal, Tag } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { DeleteOutlined, FormOutlined, PlusOutlined, AlignLeftOutlined, AuditOutlined } from '@ant-design/icons';
@@ -225,6 +225,34 @@ function ManagementList() {
return <Tag color="orange"></Tag>;
},
},
{
title: '招聘会参会情况',
dataIndex: 'missedOutdoorFairOnly',
valueType: 'select',
hideInTable: true,
valueEnum: {
true: { text: '有未参加招聘会记录' },
},
},
{
title: '未参加招聘会详情',
dataIndex: 'missedOutdoorFairCount',
hideInSearch: true,
align: 'center',
render: (_, record) => {
const count = record.missedOutdoorFairCount || 0;
if (!count) return '0';
return (
<Button
type="link"
size="small"
onClick={() => history.push(`/company/list/missed-outdoor-fairs?companyId=${record.companyId}`)}
>
{count}
</Button>
);
},
},
{
title: '驳回原因',
dataIndex: 'notPassReason',

View File

@@ -255,6 +255,10 @@ const PortalJobFairPage: React.FC = () => {
onChange={(event) => setSearchInput(event.target.value)}
onSearch={handleSearch}
placeholder="搜索招聘会名称"
id="portal-job-fair-search"
name="jobFairTitle"
aria-label="搜索招聘会名称"
autoComplete="off"
value={searchInput}
/>
{selectedDate && (

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { useAccess } from '@umijs/max';
import {
@@ -24,6 +24,7 @@ import {
message,
Modal,
Spin,
Dropdown,
} from 'antd';
import {
EnvironmentOutlined,
@@ -35,6 +36,9 @@ import {
EditOutlined,
SettingOutlined,
EyeOutlined,
InfoCircleOutlined,
DownOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import {
@@ -48,18 +52,29 @@ import type {
CrossCityAllianceForm,
} from '@/services/jobfair/crossCityAlliance';
import { getCrossDomainJobs, getCrossDomainStatistics } from '@/services/jobfair/publicJobFair';
import type { CrossDomainJobItem } from '@/services/jobfair/publicJobFair';
import type {
CrossDomainJobItem,
CrossDomainStatisticsFairItem,
} from '@/services/jobfair/publicJobFair';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const PAGE_SIZE_FOR_STATS = 10000;
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'year'), dayjs()];
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'month'), dayjs()];
type QuickDateRangeKey = '7days' | '30days' | '1year';
const getDateRangePresets = () => [
{ label: '近7天', value: [dayjs().subtract(6, 'day'), dayjs()] as [Dayjs, Dayjs] },
{ label: '近1个月', value: getDefaultDateRange() },
const getQuickDateRange = (key: QuickDateRangeKey): [Dayjs, Dayjs] => {
const end = dayjs();
const days = { '7days': 6, '30days': 29 } as const;
if (key === '1year') return [end.subtract(1, 'year'), end];
return [end.subtract(days[key], 'day'), end];
};
const quickDateRangeItems = [
{ key: '7days', label: '近7天' },
{ key: '30days', label: '近30天' },
{ key: '1year', label: '近1年' },
];
const getValueEnumText = (valueEnum: any, value?: string | number) => {
@@ -107,7 +122,17 @@ const AllianceManageModal: React.FC<AllianceManageModalProps> = ({ open, onClose
};
const columns: ProColumns<CrossCityAllianceItem>[] = [
{ title: '城市名称', dataIndex: 'name', ellipsis: true },
{
title: '城市名称',
dataIndex: 'name',
ellipsis: true,
fieldProps: {
id: 'cross-city-alliance-name-search',
name: 'name',
'aria-label': '城市名称',
autoComplete: 'off',
},
},
{
title: '操作',
valueType: 'option',
@@ -150,7 +175,7 @@ const AllianceManageModal: React.FC<AllianceManageModalProps> = ({ open, onClose
onCancel={onClose}
footer={null}
width={860}
destroyOnClose
destroyOnHidden
>
<ProTable<CrossCityAllianceItem>
actionRef={actionRef}
@@ -189,7 +214,7 @@ const AllianceManageModal: React.FC<AllianceManageModalProps> = ({ open, onClose
<ModalForm<CrossCityAllianceForm>
title={currentRow ? '编辑联盟城市' : '新增联盟城市'}
open={formOpen}
modalProps={{ destroyOnClose: true, onCancel: () => setFormOpen(false) }}
modalProps={{ destroyOnHidden: true, onCancel: () => setFormOpen(false) }}
initialValues={
currentRow
? {
@@ -218,6 +243,12 @@ const AllianceManageModal: React.FC<AllianceManageModalProps> = ({ open, onClose
name="name"
label="城市名称"
placeholder="请输入城市名称,如:乌鲁木齐市"
fieldProps={{
id: 'cross-city-alliance-name-input',
name: 'name',
'aria-label': '城市名称',
autoComplete: 'address-level2',
}}
rules={[{ required: true, message: '请输入城市名称' }]}
/>
</ModalForm>
@@ -262,18 +293,33 @@ const CitySelectTab: React.FC<CitySelectTabProps> = ({
<Spin spinning={citiesLoading} size="small">
<Space wrap style={{ marginTop: 12 }}>
{cities.map((city) => (
<Tag.CheckableTag
<button
key={city.id}
checked={selectedCities.includes(city.name)}
onChange={() => onToggle(city.name)}
type="button"
aria-pressed={selectedCities.includes(city.name)}
aria-label={`${city.name}${selectedCities.includes(city.name) ? ',已选择' : ',未选择'}`}
onClick={() => onToggle(city.name)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onToggle(city.name);
}
}}
style={{
appearance: 'none',
border: selectedCities.includes(city.name)
? '1px solid #1677ff'
: '1px solid #d9d9d9',
background: selectedCities.includes(city.name) ? '#e6f4ff' : '#fafafa',
color: selectedCities.includes(city.name) ? '#1677ff' : 'rgba(0, 0, 0, 0.88)',
cursor: 'pointer',
padding: '4px 12px',
fontSize: 14,
borderRadius: 4,
}}
>
{city.name}
</Tag.CheckableTag>
</button>
))}
{selectedCities.length > 0 && (
<Button type="link" size="small" onClick={onClear}>
@@ -394,88 +440,80 @@ const DataStatsTab: React.FC = () => {
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultDateRange());
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState({
fairCount: 0,
totalCompanies: 0,
totalJobs: 0,
totalVacancies: 0,
totalViews: 0,
industryDistribution: [] as { industry: string; count: number; percentage: string }[],
monthlyJobs: [] as { month: string; count: number }[],
fairs: [] as CrossDomainStatisticsFairItem[],
});
useEffect(() => {
const loadStats = async () => {
setLoading(true);
try {
const startDate = dateRange[0].format('YYYY-MM-DD');
const endDate = dateRange[1].format('YYYY-MM-DD');
const [statisticsRes, jobRes] = await Promise.all([
getCrossDomainStatistics({ startDate, endDate }),
getCrossDomainJobs({
startDate,
endDate,
current: 1,
pageSize: PAGE_SIZE_FOR_STATS,
}),
]);
if (statisticsRes.code !== 200 || !statisticsRes.data) {
throw new Error(statisticsRes.msg || '跨域招聘会统计加载失败');
}
// 岗位可能被多场跨域招聘会引用,图表也必须按岗位 ID 去重。
const jobs = Array.from(
new Map((jobRes.rows || []).map((job) => [String(job.jobId), job])).values(),
);
const totalJobs = statisticsRes.data.jobCount;
const industryCount = jobs.reduce<Record<string, number>>((acc, job) => {
const industry = job.industry || '其他';
acc[industry] = (acc[industry] || 0) + 1;
return acc;
}, {});
const monthlyCount = jobs.reduce<Record<string, number>>((acc, job) => {
if (!job.postingDate) return acc;
const month = String(job.postingDate).slice(0, 7);
acc[month] = (acc[month] || 0) + 1;
return acc;
}, {});
setStats({
totalCompanies: statisticsRes.data.companyCount,
totalJobs,
totalVacancies: statisticsRes.data.demandCount,
totalViews: statisticsRes.data.viewCount,
industryDistribution: Object.entries(industryCount).map(([industry, count]) => ({
industry,
count,
percentage: totalJobs > 0 ? ((count / totalJobs) * 100).toFixed(1) : '0.0',
})),
monthlyJobs: Object.entries(monthlyCount)
.sort(([a], [b]) => a.localeCompare(b))
.map(([month, count]) => ({ month, count })),
});
} catch (error: any) {
message.error(error?.message || '跨域招聘会统计加载失败,请稍后重试');
} finally {
setLoading(false);
const loadStats = useCallback(async (range: [Dayjs, Dayjs]) => {
setLoading(true);
try {
const startDate = range[0].format('YYYY-MM-DD');
const endDate = range[1].format('YYYY-MM-DD');
const statisticsRes = await getCrossDomainStatistics({ startDate, endDate });
if (statisticsRes.code !== 200 || !statisticsRes.data) {
throw new Error(statisticsRes.msg || '跨域招聘会统计加载失败');
}
};
loadStats();
}, [dateRange]);
const industryColumns = [
{ title: '行业', dataIndex: 'industry', key: 'industry' },
{ title: '岗位数量', dataIndex: 'count', key: 'count', align: 'center' as const },
setStats({
fairCount: statisticsRes.data.fairCount,
totalCompanies: statisticsRes.data.companyCount,
totalJobs: statisticsRes.data.jobCount,
totalVacancies: statisticsRes.data.demandCount,
totalViews: statisticsRes.data.viewCount,
fairs: statisticsRes.data.fairs || [],
});
} catch (error: any) {
message.error(error?.message || '跨域招聘会统计加载失败,请稍后重试');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadStats(getDefaultDateRange());
}, [loadStats]);
const fairColumns = [
{ title: '招聘会名称', dataIndex: 'fairTitle', key: 'fairTitle', ellipsis: true },
{
title: '占比',
dataIndex: 'percentage',
key: 'percentage',
align: 'center' as const,
render: (val: string) => `${val}%`,
title: '起始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 180,
render: (value: string) => value || '--',
},
{
title: '参会企业',
dataIndex: 'companyCount',
key: 'companyCount',
width: 110,
align: 'right' as const,
},
{
title: '发布岗位数',
dataIndex: 'jobCount',
key: 'jobCount',
width: 110,
align: 'right' as const,
},
{
title: '招聘需求人数',
dataIndex: 'demandCount',
key: 'demandCount',
width: 120,
align: 'right' as const,
},
{
title: '岗位浏览量',
dataIndex: 'viewCount',
key: 'viewCount',
width: 110,
align: 'right' as const,
},
];
const monthlyColumns = [
{ title: '月份', dataIndex: 'month', key: 'month' },
{ title: '发布岗位数', dataIndex: 'count', key: 'count', align: 'center' as const },
];
return (
@@ -485,89 +523,118 @@ const DataStatsTab: React.FC = () => {
<Space wrap>
<span></span>
<RangePicker
id={{
start: 'cross-city-statistics-start-date',
end: 'cross-city-statistics-end-date',
}}
aria-label="统计起止日期"
value={dateRange}
allowClear={false}
presets={getDateRangePresets()}
onChange={(value) => {
if (value?.[0] && value?.[1]) {
setDateRange(value as [Dayjs, Dayjs]);
}
}}
/>
<Dropdown
menu={{
items: quickDateRangeItems,
onClick: ({ key }) => setDateRange(getQuickDateRange(key as QuickDateRangeKey)),
}}
>
<Button>
<DownOutlined />
</Button>
</Dropdown>
<Button
type="primary"
icon={<SearchOutlined />}
loading={loading}
onClick={() => loadStats(dateRange)}
>
</Button>
</Space>
</Card>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="参会企业"
value={stats.totalCompanies}
prefix={<BankOutlined />}
suffix="家"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="发布岗位数"
value={stats.totalJobs}
prefix={<FileTextOutlined />}
suffix="个"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="招聘需求人数"
value={stats.totalVacancies}
prefix={<RiseOutlined />}
suffix="人"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="岗位浏览量"
value={stats.totalViews}
prefix={<EyeOutlined />}
suffix="次"
/>
</Card>
</Col>
</Row>
<Card title={`统计结果(共 ${stats.fairCount} 场线上招聘会)`} style={{ marginBottom: 16 }}>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card size="small" style={{ background: '#fafafa' }}>
<Statistic
title="参会企业"
value={stats.totalCompanies}
prefix={<BankOutlined />}
suffix="家"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small" style={{ background: '#fafafa' }}>
<Statistic
title="发布岗位数"
value={stats.totalJobs}
prefix={<FileTextOutlined />}
suffix="个"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small" style={{ background: '#fafafa' }}>
<Statistic
title="招聘需求人数"
value={stats.totalVacancies}
prefix={<RiseOutlined />}
suffix="人"
/>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card size="small" style={{ background: '#f0f5ff' }}>
<Statistic
title="岗位浏览量"
value={stats.totalViews}
prefix={<EyeOutlined />}
suffix="次"
/>
</Card>
</Col>
</Row>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} lg={12}>
<Card title="行业分布统计" style={{ marginBottom: 16 }}>
<Table
rowKey="industry"
columns={industryColumns}
dataSource={stats.industryDistribution}
pagination={false}
size="small"
/>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="月度岗位发布趋势" style={{ marginBottom: 16 }}>
<Table
rowKey="month"
columns={monthlyColumns}
dataSource={stats.monthlyJobs}
pagination={false}
size="small"
/>
</Card>
</Col>
</Row>
<Card
title="涉及招聘会明细"
style={{ marginBottom: 16 }}
styles={{ body: { paddingTop: 8 } }}
>
<Table<CrossDomainStatisticsFairItem>
rowKey="fairId"
columns={fairColumns}
dataSource={stats.fairs}
pagination={false}
size="small"
scroll={{ x: 860 }}
/>
</Card>
<Card size="small" title="统计口径">
线使
ID 1
<Card
size="small"
title={
<span>
<InfoCircleOutlined style={{ color: '#1677ff', marginRight: 8 }} />
</span>
}
>
<Row gutter={[24, 8]}>
<Col xs={24} lg={12}>
线使
</Col>
<Col xs={24} lg={12}>
ID 1
</Col>
</Row>
</Card>
</div>
</Spin>

View File

@@ -5,16 +5,18 @@ import {
Card,
Col,
DatePicker,
Dropdown,
Empty,
Row,
Segmented,
Select,
Space,
Spin,
Statistic,
Table,
message,
} from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import { DownOutlined, InfoCircleOutlined, SearchOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import type { ColumnsType } from 'antd/es/table';
import { getPublicJobFairList } from '@/services/jobfair/publicJobFair';
@@ -29,6 +31,7 @@ import {
const { RangePicker } = DatePicker;
type StatisticsScope = 'single' | 'range';
type QuickDateRangeKey = '7days' | '30days' | '1year';
interface FairOption {
value: string;
@@ -38,11 +41,25 @@ interface FairOption {
const formatDate = (value: any): string | undefined =>
value && typeof value.format === 'function' ? value.format('YYYY-MM-DD') : undefined;
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'month'), dayjs()];
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'year'), dayjs()];
const getDateRangePresets = () => [
{ label: '近7天', value: [dayjs().subtract(6, 'day'), dayjs()] as [Dayjs, Dayjs] },
{ label: '近1个月', value: getDefaultDateRange() },
{ label: '近30天', value: [dayjs().subtract(29, 'day'), dayjs()] as [Dayjs, Dayjs] },
{ label: '近1年', value: getDefaultDateRange() },
];
const getQuickDateRange = (key: QuickDateRangeKey): [Dayjs, Dayjs] => {
const end = dayjs();
if (key === '7days') return [end.subtract(6, 'day'), end];
if (key === '30days') return [end.subtract(29, 'day'), end];
return [end.subtract(1, 'year'), end];
};
const quickDateRangeItems = [
{ key: '7days', label: '近7天' },
{ key: '30days', label: '近30天' },
{ key: '1year', label: '近1年' },
];
const FairStatistics: React.FC = () => {
@@ -50,7 +67,8 @@ const FairStatistics: React.FC = () => {
const [scope, setScope] = useState<StatisticsScope>('range');
const [fairOptions, setFairOptions] = useState<FairOption[]>([]);
const [selectedFairId, setSelectedFairId] = useState<string>();
const [dateRange, setDateRange] = useState<any>(getDefaultDateRange());
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultDateRange());
const [queryDateRange, setQueryDateRange] = useState<[Dayjs, Dayjs]>(getDefaultDateRange());
const [loadingFairs, setLoadingFairs] = useState(false);
const [loadingSummary, setLoadingSummary] = useState(false);
const [summary, setSummary] = useState<FairStatisticsSummary>();
@@ -106,6 +124,7 @@ const FairStatistics: React.FC = () => {
// 切换类型时同步清空条件,避免新类型携带旧类型招聘会 ID 发起查询。
setSelectedFairId(undefined);
setDateRange(getDefaultDateRange());
setQueryDateRange(getDefaultDateRange());
setSummary(undefined);
setFairOptions([]);
setFairType(type);
@@ -113,8 +132,8 @@ const FairStatistics: React.FC = () => {
const queryReady = useMemo(() => {
if (scope === 'single') return Boolean(selectedFairId);
return Boolean(dateRange?.[0] && dateRange?.[1]);
}, [dateRange, scope, selectedFairId]);
return Boolean(queryDateRange?.[0] && queryDateRange?.[1]);
}, [queryDateRange, scope, selectedFairId]);
const loadSummary = useCallback(async () => {
if (!queryReady) {
@@ -126,8 +145,8 @@ const FairStatistics: React.FC = () => {
const res = await getFairStatisticsSummary({
fairType,
fairId: scope === 'single' ? selectedFairId : undefined,
startDate: scope === 'range' ? formatDate(dateRange?.[0]) : undefined,
endDate: scope === 'range' ? formatDate(dateRange?.[1]) : undefined,
startDate: scope === 'range' ? formatDate(queryDateRange?.[0]) : undefined,
endDate: scope === 'range' ? formatDate(queryDateRange?.[1]) : undefined,
});
if (res.code === 200 && res.data) {
setSummary(res.data);
@@ -141,7 +160,7 @@ const FairStatistics: React.FC = () => {
} finally {
setLoadingSummary(false);
}
}, [dateRange, fairType, queryReady, scope, selectedFairId]);
}, [fairType, queryDateRange, queryReady, scope, selectedFairId]);
useEffect(() => {
loadSummary();
@@ -174,7 +193,7 @@ const FairStatistics: React.FC = () => {
<Card style={{ marginBottom: 16 }}>
<Row gutter={[16, 16]} align="middle">
<Col>
<Button.Group>
<Space.Compact>
<Button
type={fairType === 'online' ? 'primary' : 'default'}
onClick={() => handleFairTypeChange('online')}
@@ -187,7 +206,7 @@ const FairStatistics: React.FC = () => {
>
</Button>
</Button.Group>
</Space.Compact>
</Col>
<Col>
<span style={{ marginRight: 8 }}></span>
@@ -221,14 +240,40 @@ const FairStatistics: React.FC = () => {
style={{ width: '100%', minWidth: 320 }}
/>
) : (
<RangePicker
value={dateRange}
allowClear={false}
presets={getDateRangePresets()}
onChange={(value) => setDateRange(value)}
style={{ width: '100%', minWidth: 320 }}
placeholder={['起始日期', '结束日期']}
/>
<Space wrap style={{ width: '100%' }}>
<RangePicker
id={{ start: 'fair-statistics-start-date', end: 'fair-statistics-end-date' }}
aria-label="统计起止日期"
value={dateRange}
allowClear={false}
presets={getDateRangePresets()}
onChange={(value) => {
if (value?.[0] && value?.[1]) {
setDateRange(value as [Dayjs, Dayjs]);
}
}}
style={{ minWidth: 320 }}
placeholder={['起始日期', '结束日期']}
/>
<Dropdown
menu={{
items: quickDateRangeItems,
onClick: ({ key }) => setDateRange(getQuickDateRange(key as QuickDateRangeKey)),
}}
>
<Button>
<DownOutlined />
</Button>
</Dropdown>
<Button
type="primary"
icon={<SearchOutlined />}
loading={loadingSummary}
onClick={() => setQueryDateRange([...dateRange])}
>
</Button>
</Space>
)}
</Col>
</Row>
@@ -266,7 +311,11 @@ const FairStatistics: React.FC = () => {
</Card>
{summary && (
<Card title="涉及招聘会明细" style={{ marginBottom: 16 }} bodyStyle={{ paddingTop: 8 }}>
<Card
title="涉及招聘会明细"
style={{ marginBottom: 16 }}
styles={{ body: { paddingTop: 8 } }}
>
<Table<FairStatisticsFairItem>
rowKey="fairId"
columns={columns}

View File

@@ -50,6 +50,12 @@ const InstitutionManagement: React.FC = () => {
dataIndex: 'institutionName',
ellipsis: true,
width: 260,
fieldProps: {
id: 'job-fair-institution-name-search',
name: 'institutionName',
'aria-label': '机构名称',
autoComplete: 'off',
},
},
{
title: '机构简介',
@@ -155,13 +161,27 @@ const InstitutionManagement: React.FC = () => {
name="institutionName"
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
fieldProps={{ maxLength: 200, showCount: true }}
fieldProps={{
id: 'job-fair-institution-name-input',
name: 'institutionName',
'aria-label': '机构名称',
autoComplete: 'organization',
maxLength: 200,
showCount: true,
}}
placeholder="请输入机构名称"
/>
<ProFormTextArea
name="institutionIntro"
label="机构简介"
fieldProps={{ maxLength: 2000, showCount: true, rows: 5 }}
fieldProps={{
id: 'job-fair-institution-intro-input',
name: 'institutionIntro',
'aria-label': '机构简介',
maxLength: 2000,
showCount: true,
rows: 5,
}}
placeholder="请输入机构简介"
/>
</ModalForm>

View File

@@ -1,33 +1,81 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, Modal, message } from 'antd';
import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormDateTimePicker } from '@ant-design/pro-components';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Modal, Select, message } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import {
getOutdoorFairAttendeeList,
addOutdoorFairAttendee,
updateOutdoorFairAttendee,
deleteOutdoorFairAttendee,
getAvailableOutdoorFairAttendeeUsers,
getOutdoorFairAttendeeList,
} from '@/services/jobportal/outdoorFairAttendee';
import type {
OutdoorFairAttendeeItem,
OutdoorFairAttendeeForm,
OutdoorFairAttendeeUserOption,
} from '@/services/jobportal/outdoorFairAttendee';
interface Props {
fairId: number;
}
const getUserLabel = (user: OutdoorFairAttendeeUserOption) => {
const name = user.name || '未填写姓名';
return user.phone ? `${name}${user.phone}` : name;
};
const AttendeeTab: React.FC<Props> = ({ fairId }) => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<OutdoorFairAttendeeItem>();
const [userLoading, setUserLoading] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [userOptions, setUserOptions] = useState<OutdoorFairAttendeeUserOption[]>([]);
const loadUserOptions = async (keyword?: string) => {
setUserLoading(true);
try {
const res = await getAvailableOutdoorFairAttendeeUsers({ fairId, keyword });
if (res.code === 200) {
setUserOptions(res.data || []);
} else {
message.error(res.msg || '移动端用户加载失败');
}
} catch {
setUserOptions([]);
message.error('移动端用户加载失败');
} finally {
setUserLoading(false);
}
};
const openAddModal = () => {
setUserOptions([]);
setModalVisible(true);
loadUserOptions();
};
const handleUserSelect = async (userId: number) => {
setSubmitLoading(true);
try {
const res = await addOutdoorFairAttendee({ fairId, userId });
if (res.code === 200) {
message.success('签到成功');
setModalVisible(false);
actionRef.current?.reload();
return;
}
message.error(res.msg || '签到失败');
} catch {
message.error('签到失败,请重试');
} finally {
setSubmitLoading(false);
}
};
const handleDelete = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该签到人员吗?',
content: '确定要删除该签到记录吗?',
onOk: async () => {
const res = await deleteOutdoorFairAttendee(id);
if (res.code === 200) {
@@ -41,9 +89,19 @@ const AttendeeTab: React.FC<Props> = ({ fairId }) => {
};
const columns: ProColumns<OutdoorFairAttendeeItem>[] = [
{ title: '人员', dataIndex: 'name', ellipsis: true },
{
title: '姓名',
dataIndex: 'name',
ellipsis: true,
fieldProps: {
id: 'outdoor-attendee-name-search',
name: 'name',
'aria-label': '参会人员姓名',
autoComplete: 'off',
},
},
{ title: '手机号', dataIndex: 'phone', ellipsis: true, hideInSearch: true },
{ title: '址', dataIndex: 'address', ellipsis: true, hideInSearch: true },
{ title: '址', dataIndex: 'address', ellipsis: true, hideInSearch: true },
{
title: '签到时间',
dataIndex: 'checkInTime',
@@ -53,22 +111,9 @@ const AttendeeTab: React.FC<Props> = ({ fairId }) => {
{
title: '操作',
valueType: 'option',
width: 160,
width: 100,
fixed: 'right',
render: (_, record) => [
<Button
key="edit"
type="link"
size="small"
icon={<EditOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:edit')}
onClick={() => {
setCurrentRow(record);
setModalVisible(true);
}}
>
</Button>,
<Button
key="delete"
type="link"
@@ -100,10 +145,7 @@ const AttendeeTab: React.FC<Props> = ({ fairId }) => {
type="primary"
icon={<PlusOutlined />}
hidden={!access.hasPerms('cms:outdoorFair:add')}
onClick={() => {
setCurrentRow(undefined);
setModalVisible(true);
}}
onClick={openAddModal}
>
</Button>,
@@ -120,53 +162,34 @@ const AttendeeTab: React.FC<Props> = ({ fairId }) => {
columns={columns}
/>
<ModalForm<OutdoorFairAttendeeForm>
title={currentRow ? '编辑签到人员' : '录入签到人员'}
<Modal
title="录入签到人员"
open={modalVisible}
modalProps={{ destroyOnClose: true, onCancel: () => setModalVisible(false) }}
initialValues={
currentRow
? {
id: currentRow.id,
fairId,
name: currentRow.name,
phone: currentRow.phone,
address: currentRow.address,
checkInTime: currentRow.checkInTime,
}
: { fairId }
}
onFinish={async (values) => {
const payload = { ...values, fairId };
const res = values.id
? await updateOutdoorFairAttendee(payload)
: await addOutdoorFairAttendee(payload);
if (res.code === 200) {
message.success(values.id ? '修改成功' : '新增成功');
setModalVisible(false);
setCurrentRow(undefined);
actionRef.current?.reload();
return true;
}
message.error(res.msg || '操作失败');
return false;
}}
destroyOnClose
footer={null}
onCancel={() => setModalVisible(false)}
>
<ProFormText
name="name"
label="人员"
placeholder="请输入人员姓名"
rules={[{ required: true, message: '请输入人员姓名' }]}
<div style={{ marginBottom: 8 }}></div>
<Select<number>
showSearch
allowClear
autoFocus
loading={userLoading || submitLoading}
disabled={submitLoading}
placeholder="请选择移动端用户"
id="outdoor-attendee-user-select"
aria-label="选择移动端用户"
style={{ width: '100%' }}
optionFilterProp="label"
options={userOptions.map((user) => ({
value: user.userId,
label: getUserLabel(user),
}))}
onSearch={loadUserOptions}
onSelect={handleUserSelect}
notFoundContent={userLoading ? '加载中...' : '暂无可签到用户'}
/>
<ProFormText name="phone" label="手机号" placeholder="请输入手机号" />
<ProFormText name="address" label="住址" placeholder="请输入住址" />
<ProFormDateTimePicker
name="checkInTime"
label="签到时间"
placeholder="请选择签到时间"
fieldProps={{ style: { width: '100%' } }}
/>
</ModalForm>
</Modal>
</>
);
};

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { Card, Col, Row, Tag } from 'antd';
import { ProColumns, ProTable } from '@ant-design/pro-components';
import { getOutdoorFairCompanyAttendanceList } from '@/services/jobportal/outdoorFairDetail';
import type { OutdoorFairCompanyAttendanceItem } from '@/services/jobportal/outdoorFairDetail';
interface Props {
fairId: number;
}
const getColumns = (checkedIn: boolean): ProColumns<OutdoorFairCompanyAttendanceItem>[] => [
{
title: '企业名称',
dataIndex: 'companyName',
ellipsis: true,
fieldProps: {
id: `outdoor-company-attendance-${checkedIn ? 'checked-in' : 'not-checked-in'}-search`,
name: `companyName${checkedIn ? 'CheckedIn' : 'NotCheckedIn'}`,
'aria-label': `${checkedIn ? '已签到' : '未签到'}企业名称`,
autoComplete: 'off',
},
},
{
title: '行业',
dataIndex: 'industry',
hideInSearch: true,
render: (_, record) => record.industry || '--',
},
{
title: '联系人',
dataIndex: 'contactPerson',
hideInSearch: true,
render: (_, record) => record.contactPerson || '--',
},
{
title: '签到时间',
dataIndex: 'checkInTime',
valueType: 'dateTime',
hideInSearch: true,
render: (_, record) => record.checkInTime || '--',
},
];
const CompanyAttendanceList: React.FC<{ fairId: number; checkedIn: boolean }> = ({
fairId,
checkedIn,
}) => (
<ProTable<OutdoorFairCompanyAttendanceItem>
rowKey="id"
columns={getColumns(checkedIn)}
search={{ labelWidth: 'auto' }}
options={false}
pagination={{ pageSize: 8 }}
request={async (params) => {
const res = await getOutdoorFairCompanyAttendanceList(fairId, {
current: params.current,
pageSize: params.pageSize,
companyName: params.companyName,
checkedIn,
});
return {
data: res.rows || [],
total: res.total || 0,
success: res.code === 200,
};
}}
/>
);
const CompanyAttendanceTab: React.FC<Props> = ({ fairId }) => (
<Row gutter={16} align="top">
<Col span={12}>
<Card title="已签到企业" extra={<Tag color="green"></Tag>} bodyStyle={{ padding: 0 }}>
<CompanyAttendanceList fairId={fairId} checkedIn />
</Card>
</Col>
<Col span={12}>
<Card title="未签到企业" extra={<Tag color="orange"></Tag>} bodyStyle={{ padding: 0 }}>
<CompanyAttendanceList fairId={fairId} checkedIn={false} />
</Card>
</Col>
</Row>
);
export default CompanyAttendanceTab;

View File

@@ -15,6 +15,12 @@ const CompanyUnsubscribeTab: React.FC<Props> = ({ fairId }) => {
dataIndex: 'companyName',
ellipsis: true,
order: 1,
fieldProps: {
id: 'outdoor-company-unsubscribe-name-search',
name: 'companyName',
'aria-label': '企业名称',
autoComplete: 'off',
},
},
{ title: '联系人', dataIndex: 'contactPerson', width: 120, hideInSearch: true },
{ title: '联系电话', dataIndex: 'contactPhone', width: 150, hideInSearch: true },

View File

@@ -67,6 +67,12 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
title: '设备码',
dataIndex: 'deviceCode',
ellipsis: true,
fieldProps: {
id: 'outdoor-device-code-search',
name: 'deviceCode',
'aria-label': '设备码',
autoComplete: 'off',
},
render: (_, record) => (
<Typography.Text copyable code>
{record.deviceCode}
@@ -191,7 +197,13 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
name="deviceCode"
label="设备码"
placeholder="请输入设备码"
fieldProps={{ maxLength: 64 }}
fieldProps={{
id: 'outdoor-device-code-input',
name: 'deviceCode',
'aria-label': '设备码',
autoComplete: 'off',
maxLength: 64,
}}
rules={[
{ required: true, message: '请输入设备码' },
{ max: 64, message: '设备码长度不能超过64个字符' },
@@ -201,6 +213,7 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
name="companyId"
label="绑定企业"
placeholder="选填,不选则创建未绑定的设备码"
fieldProps={{ id: 'outdoor-device-company-input', 'aria-label': '绑定企业' }}
showSearch
allowClear
request={async () => {
@@ -247,6 +260,7 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
label="参会企业"
placeholder="请选择参会企业"
showSearch
fieldProps={{ id: 'outdoor-device-company-bind-select', 'aria-label': '参会企业' }}
request={async () => {
const [companies, devicesRes] = await Promise.all([
getOutdoorFairCompanyOptions(fairId),

View File

@@ -8,6 +8,7 @@ import { getVenueInfo, getVenueInfoList } from '@/services/jobportal/venueInfo';
import type { VenueInfoItem } from '@/services/jobportal/venueInfo';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import EditModal from '@/pages/Jobfair/Outdoorfair/components/EditModal';
import { normalizePublicUrl } from '@/utils/publicUrl';
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
@@ -138,6 +139,7 @@ const FairInfoTab: React.FC<Props> = ({ fairInfo, onFairUpdate }) => {
{fairInfo.address}
</Descriptions.Item>
<Descriptions.Item label="展位数量">{fairInfo.boothCount}</Descriptions.Item>
<Descriptions.Item label="展位图数量">{fairInfo.boothMapCount ?? 0}</Descriptions.Item>
<Descriptions.Item label="举办时间">{fairInfo.holdTime}</Descriptions.Item>
<Descriptions.Item label="结束时间">{fairInfo.endTime}</Descriptions.Item>
<Descriptions.Item label="报名开始时间">{fairInfo.applyStartTime}</Descriptions.Item>
@@ -148,7 +150,7 @@ const FairInfoTab: React.FC<Props> = ({ fairInfo, onFairUpdate }) => {
<Descriptions.Item label="招聘会照片" span={3}>
{fairInfo.photoUrl ? (
<Image
src={fairInfo.photoUrl}
src={normalizePublicUrl(fairInfo.photoUrl)}
alt={fairInfo.title}
width={240}
style={{ objectFit: 'cover', borderRadius: 4 }}
@@ -192,9 +194,7 @@ const FairInfoTab: React.FC<Props> = ({ fairInfo, onFairUpdate }) => {
<Descriptions.Item label="场地面积">
{venueDetail.venueArea != null ? `${venueDetail.venueArea}` : '--'}
</Descriptions.Item>
<Descriptions.Item label="展位数量">
{venueDetail.floorCount ?? '--'}
</Descriptions.Item>
<Descriptions.Item label="展位数量">{venueDetail.floorCount ?? '--'}</Descriptions.Item>
<Descriptions.Item label="场地地址" span={2}>
{venueDetail.venueAddress || '--'}
</Descriptions.Item>
@@ -204,15 +204,9 @@ const FairInfoTab: React.FC<Props> = ({ fairInfo, onFairUpdate }) => {
<Descriptions.Item label="乘坐线路" span={3}>
{venueDetail.routeLineNames || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办日期">
{venueDetail.handleDate || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办机构">
{venueDetail.handleOrg || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办人">
{venueDetail.handler || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办日期">{venueDetail.handleDate || '--'}</Descriptions.Item>
<Descriptions.Item label="经办机构">{venueDetail.handleOrg || '--'}</Descriptions.Item>
<Descriptions.Item label="经办人">{venueDetail.handler || '--'}</Descriptions.Item>
<Descriptions.Item label="备注" span={3}>
{venueDetail.remark || '--'}
</Descriptions.Item>

View File

@@ -32,6 +32,15 @@ type ReviewTarget =
record: API.OutdoorFairDetail.PostedJob;
};
const REVIEW_STATUS_ENUM = {
'0': { text: '待审核', status: 'Processing' },
'1': { text: '已通过', status: 'Success' },
'2': { text: '已驳回', status: 'Error' },
};
const formatSearchDateTime = (value: any) =>
value && typeof value.format === 'function' ? value.format('YYYY-MM-DD HH:mm:ss') : value;
const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
const companyActionRef = useRef<ActionType>();
const jobActionRef = useRef<ActionType>();
@@ -264,11 +273,54 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
current: params.current,
pageSize: params.pageSize,
companyName: params.companyName,
reviewStatus: params.reviewStatus,
startTime: formatSearchDateTime(params.startTime),
endTime: formatSearchDateTime(params.endTime),
});
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
}}
columns={[
{ title: '企业名称', dataIndex: 'companyName', ellipsis: true },
{
title: '企业名称',
dataIndex: 'companyName',
ellipsis: true,
fieldProps: {
id: 'outdoor-participating-company-name-search',
name: 'companyName',
'aria-label': '企业名称',
autoComplete: 'off',
},
},
{
title: '报名开始时间',
dataIndex: 'startTime',
hideInTable: true,
valueType: 'dateTime',
fieldProps: {
id: 'outdoor-participating-company-start-time-search',
name: 'startTime',
'aria-label': '报名开始时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ startTime: formatSearchDateTime(value) }),
},
},
{
title: '报名结束时间',
dataIndex: 'endTime',
hideInTable: true,
valueType: 'dateTime',
fieldProps: {
id: 'outdoor-participating-company-end-time-search',
name: 'endTime',
'aria-label': '报名结束时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ endTime: formatSearchDateTime(value) }),
},
},
{
title: '行业',
dataIndex: 'industry',
@@ -298,9 +350,22 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
{
title: '审核状态',
dataIndex: 'reviewStatus',
hideInSearch: true,
valueType: 'select',
valueEnum: REVIEW_STATUS_ENUM,
fieldProps: {
id: 'outdoor-participating-company-review-status-search',
name: 'reviewStatus',
'aria-label': '审核状态',
},
render: (_, record) => renderReviewStatus(record.reviewStatus),
},
{
title: '报名时间',
dataIndex: 'registrationTime',
valueType: 'dateTime',
hideInSearch: true,
width: 170,
},
{
title: '展位号',
dataIndex: 'boothNumber',

View File

@@ -105,7 +105,17 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
};
const columns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
{
title: '岗位名称',
dataIndex: 'jobTitle',
ellipsis: true,
fieldProps: {
id: 'outdoor-participating-job-title-search',
name: 'jobTitle',
'aria-label': '岗位名称',
autoComplete: 'off',
},
},
{
title: '企业名称',
dataIndex: 'companyId',
@@ -126,6 +136,10 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
);
},
fieldProps: {
id: 'outdoor-participating-job-company-search',
name: 'companyId',
'aria-label': '企业名称',
autoComplete: 'off',
showSearch: true,
optionFilterProp: 'label',
placeholder: '请选择或搜索企业',
@@ -144,6 +158,11 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
valueType: 'select',
valueEnum: educationEnum,
width: 110,
fieldProps: {
id: 'outdoor-participating-job-education-search',
name: 'education',
'aria-label': '学历要求',
},
},
{
title: '工作经验',
@@ -151,6 +170,11 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
valueType: 'select',
valueEnum: experienceEnum,
width: 110,
fieldProps: {
id: 'outdoor-participating-job-experience-search',
name: 'experience',
'aria-label': '工作经验',
},
},
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
{ title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true },

View File

@@ -11,6 +11,7 @@ import ParticipatingCompaniesTab from './components/ParticipatingCompaniesTab';
import ParticipatingJobsTab from './components/ParticipatingJobsTab';
import CompanyUnsubscribeTab from './components/CompanyUnsubscribeTab';
import AttendeeTab from './components/AttendeeTab';
import CompanyAttendanceTab from './components/CompanyAttendanceTab';
import DeviceTab from './components/DeviceTab';
const OutdoorFairDetail: React.FC = () => {
@@ -43,6 +44,7 @@ const OutdoorFairDetail: React.FC = () => {
const tabItems = [
{ key: 'fair-info', label: '招聘会管理' },
{ key: 'participating-companies', label: '参会企业' },
{ key: 'company-attendance', label: '企业签到' },
{ key: 'company-unsubscribe', label: '企业退订' },
{ key: 'participating-jobs', label: '参会岗位' },
{ key: 'attendee', label: '参会人员管理' },
@@ -88,6 +90,8 @@ const OutdoorFairDetail: React.FC = () => {
return <BoothTab fairId={fairId} fairInfo={fairInfo} />;
case 'participating-companies':
return <ParticipatingCompaniesTab fairId={fairId} />;
case 'company-attendance':
return <CompanyAttendanceTab fairId={fairId} />;
case 'company-unsubscribe':
return <CompanyUnsubscribeTab fairId={fairId} />;
case 'participating-jobs':

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Descriptions, Divider, Image, Modal, Tag } from 'antd';
import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair';
import { normalizePublicUrl } from '@/utils/publicUrl';
interface Props {
fair?: OutdoorFairItem | null;
@@ -33,17 +34,25 @@ const CompanyFairDetailModal: React.FC<Props> = ({ fair, open, onCancel }) => (
{fair.address || '--'}
</Descriptions.Item>
<Descriptions.Item label="展位数量">{fair.boothCount ?? '--'}</Descriptions.Item>
<Descriptions.Item label="展位图数量">{fair.boothMapCount ?? 0}</Descriptions.Item>
<Descriptions.Item label="已预定摊位数">{fair.reservedBoothCount ?? 0}</Descriptions.Item>
<Descriptions.Item label="举办时间">{fair.holdTime || '--'}</Descriptions.Item>
<Descriptions.Item label="结束时间">{fair.endTime || '--'}</Descriptions.Item>
<Descriptions.Item label="报名开始时间">{fair.applyStartTime || '--'}</Descriptions.Item>
<Descriptions.Item label="报名截止时间">{fair.applyEndTime || '--'}</Descriptions.Item>
<Descriptions.Item label="线上申请">
<Tag color={fair.onlineApply ? 'green' : 'default'}>{fair.onlineApply ? '是' : '否'}</Tag>
<Tag color={fair.onlineApply ? 'green' : 'default'}>
{fair.onlineApply ? '是' : '否'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="招聘会照片">
{fair.photoUrl ? (
<Image src={fair.photoUrl} alt={fair.title} width={180} style={{ objectFit: 'cover' }} />
<Image
src={normalizePublicUrl(fair.photoUrl)}
alt={fair.title}
width={180}
style={{ objectFit: 'cover' }}
/>
) : (
'--'
)}
@@ -59,7 +68,9 @@ const CompanyFairDetailModal: React.FC<Props> = ({ fair, open, onCancel }) => (
</Divider>
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="需携带资料">{content(fair.requiredMaterials)}</Descriptions.Item>
<Descriptions.Item label="需携带资料">
{content(fair.requiredMaterials)}
</Descriptions.Item>
<Descriptions.Item label="到场须知">{content(fair.attendanceNotes)}</Descriptions.Item>
</Descriptions>
</>

View File

@@ -17,6 +17,7 @@ import type {
OutdoorFairDictForm,
} from '@/services/jobportal/outdoorFair';
import { uploadOutdoorFairPhoto } from '@/services/jobportal/outdoorFair';
import { normalizePublicUrl } from '@/utils/publicUrl';
interface EditModalProps {
open: boolean;
@@ -356,7 +357,7 @@ const EditModal: React.FC<EditModalProps> = ({
</Upload>
{photoUrl ? (
<Image
src={photoUrl}
src={normalizePublicUrl(photoUrl)}
width={180}
height={120}
style={{ objectFit: 'cover', borderRadius: 6 }}

View File

@@ -10,6 +10,7 @@ import {
QrcodeOutlined,
UnorderedListOutlined,
ExportOutlined,
CheckOutlined,
} from '@ant-design/icons';
import {
getOutdoorFairList,
@@ -20,6 +21,7 @@ import {
signupOutdoorFair,
unsubscribeOutdoorFair,
getMyOutdoorFairQrCode,
checkInOutdoorFair,
getOutdoorFairMiniProgramQrCode,
exportOutdoorFair,
} from '@/services/jobportal/outdoorFair';
@@ -45,6 +47,9 @@ import type { RecruitmentFairExportParams } from '@/services/jobfair/recruitment
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
const formatSearchDateTime = (value: any) =>
value && typeof value.format === 'function' ? value.format('YYYY-MM-DD HH:mm:ss') : value;
const OutdoorFairList: React.FC = () => {
const access = useAccess();
const { initialState } = useModel('@@initialState');
@@ -183,13 +188,31 @@ const OutdoorFairList: React.FC = () => {
jobCount: 0,
});
const handleSignup = async (record: OutdoorFairItem) => {
const res = await signupOutdoorFair(record.id);
const handleSignup = (record: OutdoorFairItem) => {
Modal.confirm({
title: '报名户外招聘会',
content: `确定报名「${record.title}」吗?报名后需要等待管理员审核。`,
okText: '确认报名',
cancelText: '取消',
onOk: async () => {
const res = await signupOutdoorFair(record.id);
if (res.code === 200) {
message.success('报名成功,等待审核');
actionRef.current?.reload();
} else {
message.error(res.msg || '报名失败');
}
},
});
};
const handleCheckIn = async (record: OutdoorFairItem) => {
const res = await checkInOutdoorFair(record.id);
if (res.code === 200) {
message.success('报名成功,等待审核');
message.success('签到成功');
actionRef.current?.reload();
} else {
message.error(res.msg || '报名失败');
message.error(res.msg || '签到失败');
}
};
@@ -292,7 +315,17 @@ const OutdoorFairList: React.FC = () => {
};
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
{
title: '岗位名称',
dataIndex: 'jobTitle',
ellipsis: true,
fieldProps: {
id: 'outdoor-company-job-title-search',
name: 'jobTitle',
'aria-label': '岗位名称',
autoComplete: 'off',
},
},
{
title: '薪资范围',
hideInSearch: true,
@@ -304,6 +337,11 @@ const OutdoorFairList: React.FC = () => {
valueType: 'select',
valueEnum: educationEnum,
width: 110,
fieldProps: {
id: 'outdoor-company-job-education-search',
name: 'education',
'aria-label': '学历要求',
},
},
{
title: '工作经验',
@@ -311,6 +349,11 @@ const OutdoorFairList: React.FC = () => {
valueType: 'select',
valueEnum: experienceEnum,
width: 110,
fieldProps: {
id: 'outdoor-company-job-experience-search',
name: 'experience',
'aria-label': '工作经验',
},
},
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
{
@@ -328,11 +371,23 @@ const OutdoorFairList: React.FC = () => {
title: '招聘会标题',
dataIndex: 'title',
ellipsis: true,
fieldProps: {
id: 'outdoor-fair-title-search',
name: 'title',
'aria-label': '招聘会标题',
autoComplete: 'off',
},
},
{
title: '举办单位',
dataIndex: 'hostUnit',
ellipsis: true,
fieldProps: {
id: 'outdoor-fair-host-unit-search',
name: 'hostUnit',
'aria-label': '举办单位',
autoComplete: 'off',
},
},
{
title: '招聘会类型',
@@ -340,6 +395,11 @@ const OutdoorFairList: React.FC = () => {
ellipsis: true,
valueType: 'select',
valueEnum: fairTypeValueEnum,
fieldProps: {
id: 'outdoor-fair-type-search',
name: 'fairType',
'aria-label': '招聘会类型',
},
},
{
title: '举办区域',
@@ -347,6 +407,11 @@ const OutdoorFairList: React.FC = () => {
ellipsis: true,
valueType: 'select',
valueEnum: regionValueEnum,
fieldProps: {
id: 'outdoor-fair-region-search',
name: 'region',
'aria-label': '举办区域',
},
},
{
title: '场地名称',
@@ -354,6 +419,9 @@ const OutdoorFairList: React.FC = () => {
ellipsis: true,
valueType: 'select',
fieldProps: {
id: 'outdoor-fair-venue-search',
name: 'venueId',
'aria-label': '场地名称',
options: venueOptions,
allowClear: true,
showSearch: true,
@@ -375,6 +443,12 @@ const OutdoorFairList: React.FC = () => {
dataIndex: 'boothCount',
hideInSearch: true,
},
{
title: '展位图数量',
dataIndex: 'boothMapCount',
hideInSearch: true,
render: (_, record) => record.boothMapCount ?? 0,
},
{
title: '已预定摊位数',
dataIndex: 'reservedBoothCount',
@@ -382,28 +456,60 @@ const OutdoorFairList: React.FC = () => {
render: (_, record) => record.reservedBoothCount ?? 0,
},
{
title: '举办时间',
title: '开始时间',
dataIndex: 'holdTime',
valueType: 'dateTime',
hideInSearch: true,
fieldProps: {
id: 'outdoor-fair-start-time-search',
name: 'startTime',
'aria-label': '开始时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ startTime: formatSearchDateTime(value) }),
},
},
{
title: '结束时间',
dataIndex: 'endTime',
valueType: 'dateTime',
hideInSearch: true,
fieldProps: {
id: 'outdoor-fair-end-time-search',
name: 'endTime',
'aria-label': '结束时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ endTime: formatSearchDateTime(value) }),
},
},
{
title: '报名开始时间',
dataIndex: 'applyStartTime',
valueType: 'dateTime',
hideInSearch: true,
fieldProps: {
id: 'outdoor-fair-apply-start-time-search',
name: 'applyStartTime',
'aria-label': '报名开始时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ applyStartTime: formatSearchDateTime(value) }),
},
},
{
title: '报名截止时间',
dataIndex: 'applyEndTime',
valueType: 'dateTime',
hideInSearch: true,
fieldProps: {
id: 'outdoor-fair-apply-end-time-search',
name: 'applyEndTime',
'aria-label': '报名截止时间',
format: 'YYYY-MM-DD HH:mm:ss',
},
search: {
transform: (value) => ({ applyEndTime: formatSearchDateTime(value) }),
},
},
{
title: '线上申请',
@@ -440,7 +546,7 @@ const OutdoorFairList: React.FC = () => {
render: (_, record) =>
record.photoUrl ? (
<img
src={record.photoUrl}
src={normalizePublicUrl(record.photoUrl)}
alt={record.title}
style={{ width: 60, height: 45, objectFit: 'cover', borderRadius: 4 }}
/>
@@ -451,7 +557,7 @@ const OutdoorFairList: React.FC = () => {
{
title: '操作',
valueType: 'option',
width: isEnterprise ? 520 : 260,
width: isEnterprise ? 620 : 260,
fixed: 'right',
render: (_, record) => [
isEnterprise && !record.signedUp ? (
@@ -475,6 +581,29 @@ const OutdoorFairList: React.FC = () => {
</Button>
) : null,
isEnterprise &&
record.signedUp &&
record.reviewStatus === '1' &&
record.companyCheckInStatus === 'checked_in' ? (
<Button key="checkedIn" type="link" size="small" icon={<CheckOutlined />} disabled>
</Button>
) : null,
isEnterprise &&
record.signedUp &&
record.reviewStatus === '1' &&
record.companyCheckInStatus !== 'checked_in' &&
record.companyCheckInAllowed ? (
<Button
key="checkIn"
type="link"
size="small"
icon={<CheckOutlined />}
onClick={() => handleCheckIn(record)}
>
</Button>
) : null,
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
<Button
key="jobs"
@@ -587,6 +716,10 @@ const OutdoorFairList: React.FC = () => {
region: params.region,
venueId: params.venueId,
onlineApply: params.onlineApply,
startTime: formatSearchDateTime(params.startTime),
endTime: formatSearchDateTime(params.endTime),
applyStartTime: formatSearchDateTime(params.applyStartTime),
applyEndTime: formatSearchDateTime(params.applyEndTime),
} as OutdoorFairListParams);
return { data: res.rows, total: res.total, success: true };
}}

View File

@@ -54,6 +54,8 @@ const JobFairDetail: React.FC = () => {
const [reviewCompany, setReviewCompany] = useState<API.PublicJobFair.CompanyInfo>();
const [reviewSubmitting, setReviewSubmitting] = useState(false);
const [jobFairRegionTypeEnum, setJobFairRegionTypeEnum] = useState<any>({});
const [educationEnum, setEducationEnum] = useState<any>({});
const [experienceEnum, setExperienceEnum] = useState<any>({});
const [reviewForm] = Form.useForm<ReviewFormValues>();
const reviewStatus = Form.useWatch('reviewStatus', reviewForm);
@@ -77,8 +79,16 @@ const JobFairDetail: React.FC = () => {
useEffect(() => {
getDictValueEnum('job_fair_region_type').then(setJobFairRegionTypeEnum);
getDictValueEnum('education', true, true).then(setEducationEnum);
getDictValueEnum('experience', true, true).then(setExperienceEnum);
}, []);
const getJobDictLabel = (value: string | number | undefined, enums: any) => {
if (value === undefined || value === null || value === '') return '未说明';
const item = enums[String(value)] || enums[value];
return item?.label || item?.text || '待配置';
};
const handleRemoveCompany = (company: API.PublicJobFair.CompanyInfo) => {
Modal.confirm({
title: '确认移除',
@@ -320,8 +330,12 @@ const JobFairDetail: React.FC = () => {
? `${job.minSalary || 0}-${job.maxSalary || 0} 元/月`
: '面议'}
</Tag>
{job.education && <Tag>{job.education}</Tag>}
{job.experience && <Tag>{job.experience}</Tag>}
{job.education && (
<Tag>{getJobDictLabel(job.education, educationEnum)}</Tag>
)}
{job.experience && (
<Tag>{getJobDictLabel(job.experience, experienceEnum)}</Tag>
)}
{job.vacancies !== undefined && <Tag>{job.vacancies}</Tag>}
</Space>
</List.Item>

View File

@@ -103,6 +103,9 @@ const PublicJobFairList: React.FC = () => {
hideInTable: true,
valueType: 'dateRange',
fieldProps: {
id: { start: 'public-job-fair-start-date', end: 'public-job-fair-end-date' },
name: ['publicJobFairStartDate', 'publicJobFairEndDate'],
'aria-label': '招聘会时间范围',
format: 'YYYY-MM-DD',
},
search: {
@@ -116,6 +119,12 @@ const PublicJobFairList: React.FC = () => {
title: '招聘会标题',
dataIndex: 'jobFairTitle',
ellipsis: true,
fieldProps: {
id: 'public-job-fair-title-search',
name: 'jobFairTitle',
'aria-label': '招聘会标题',
autoComplete: 'off',
},
},
{
title: '地址',

View File

@@ -195,6 +195,8 @@ const EditModal: React.FC<EditModalProps> = ({
width="md"
options={venueTypeOptions}
fieldProps={{
id: 'venue-info-type-input',
'aria-label': '场地类型',
showSearch: true,
dropdownRender: renderVenueTypeDropdown,
}}
@@ -207,13 +209,24 @@ const EditModal: React.FC<EditModalProps> = ({
width="md"
rules={[{ required: true, message: '请输入场地名称' }]}
placeholder="请输入场地名称"
fieldProps={{
id: 'venue-info-name-input',
name: 'venueName',
'aria-label': '场地名称',
autoComplete: 'off',
}}
/>
<ProFormDigit
name="venueArea"
label="场地面积"
width="sm"
min={0}
fieldProps={{ precision: 2 }}
fieldProps={{
id: 'venue-info-area-input',
name: 'venueArea',
'aria-label': '场地面积(平方米)',
precision: 2,
}}
placeholder="请输入场地面积"
/>
</ProForm.Group>
@@ -223,16 +236,38 @@ const EditModal: React.FC<EditModalProps> = ({
label="展位数量"
width="md"
min={0}
fieldProps={{ precision: 0 }}
fieldProps={{
id: 'venue-info-booth-count-input',
name: 'floorCount',
'aria-label': '展位数量',
precision: 0,
}}
placeholder="请输入展位数量"
/>
<ProFormText name="contactPhone" label="联系电话" width="md" placeholder="请输入联系电话" />
<ProFormText
name="contactPhone"
label="联系电话"
width="md"
placeholder="请输入联系电话"
fieldProps={{
id: 'venue-info-contact-phone-input',
name: 'contactPhone',
'aria-label': '联系电话',
autoComplete: 'tel',
}}
/>
</ProForm.Group>
<ProFormText
name="venueAddress"
label="场地地址"
rules={[{ required: true, message: '请输入场地地址' }]}
placeholder="请输入场地地址"
fieldProps={{
id: 'venue-info-address-input',
name: 'venueAddress',
'aria-label': '场地地址',
autoComplete: 'street-address',
}}
/>
<ProFormSelect
name="routeLineIds"
@@ -240,6 +275,8 @@ const EditModal: React.FC<EditModalProps> = ({
mode="multiple"
options={lineOptions}
fieldProps={{
id: 'venue-info-route-line-select',
'aria-label': '乘坐线路',
showSearch: true,
optionFilterProp: 'label',
}}
@@ -254,6 +291,8 @@ const EditModal: React.FC<EditModalProps> = ({
value: item.id,
}))}
fieldProps={{
id: 'venue-info-institution-select',
'aria-label': '所属机构',
showSearch: true,
optionFilterProp: 'label',
dropdownRender: renderInstitutionDropdown,
@@ -263,7 +302,13 @@ const EditModal: React.FC<EditModalProps> = ({
<ProFormTextArea
name="remark"
label="备注"
fieldProps={{ maxLength: 100, showCount: true }}
fieldProps={{
id: 'venue-info-remark-input',
name: 'remark',
'aria-label': '备注',
maxLength: 100,
showCount: true,
}}
placeholder="请输入备注"
/>
<ProForm.Group>
@@ -271,10 +316,37 @@ const EditModal: React.FC<EditModalProps> = ({
name="handleDate"
label="经办日期"
width="md"
fieldProps={{ format: 'YYYY-MM-DD' }}
fieldProps={{
id: 'venue-info-handle-date-input',
name: 'handleDate',
'aria-label': '经办日期',
format: 'YYYY-MM-DD',
}}
/>
<ProFormText
name="handleOrg"
label="经办机构"
width="md"
placeholder="请输入经办机构"
fieldProps={{
id: 'venue-info-handle-org-input',
name: 'handleOrg',
'aria-label': '经办机构',
autoComplete: 'organization',
}}
/>
<ProFormText
name="handler"
label="经办人"
width="md"
placeholder="请输入经办人"
fieldProps={{
id: 'venue-info-handler-input',
name: 'handler',
'aria-label': '经办人',
autoComplete: 'name',
}}
/>
<ProFormText name="handleOrg" label="经办机构" width="md" placeholder="请输入经办机构" />
<ProFormText name="handler" label="经办人" width="md" placeholder="请输入经办人" />
</ProForm.Group>
<Modal
title="新增场地类型"
@@ -290,6 +362,10 @@ const EditModal: React.FC<EditModalProps> = ({
maxLength={100}
showCount
placeholder="请输入场地类型"
id="venue-info-type-option-input"
name="venueTypeOption"
aria-label="新增场地类型"
autoComplete="off"
onChange={(event) => setDictInputValue(event.target.value)}
onPressEnter={handleDictModalOk}
/>
@@ -308,10 +384,26 @@ const EditModal: React.FC<EditModalProps> = ({
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
>
<Input maxLength={200} showCount placeholder="请输入机构名称" />
<Input
id="venue-info-institution-name-input"
name="institutionName"
aria-label="机构名称"
autoComplete="organization"
maxLength={200}
showCount
placeholder="请输入机构名称"
/>
</Form.Item>
<Form.Item name="institutionIntro" label="机构简介">
<Input.TextArea maxLength={2000} showCount rows={4} placeholder="请输入机构简介" />
<Input.TextArea
id="venue-info-institution-intro-input"
name="institutionIntro"
aria-label="机构简介"
maxLength={2000}
showCount
rows={4}
placeholder="请输入机构简介"
/>
</Form.Item>
</Form>
</Modal>

View File

@@ -125,11 +125,22 @@ const VenueInfoList: React.FC = () => {
ellipsis: true,
valueType: 'select',
valueEnum: venueTypeValueEnum,
fieldProps: {
id: 'venue-info-type-search',
name: 'venueType',
'aria-label': '场地类型',
},
},
{
title: '场地名称',
dataIndex: 'venueName',
ellipsis: true,
fieldProps: {
id: 'venue-info-name-search',
name: 'venueName',
'aria-label': '场地名称',
autoComplete: 'off',
},
},
{
title: '场地面积',

View File

@@ -59,6 +59,22 @@ export async function getCmsCompanyDetail(companyId: number | string) {
});
}
/** 获取企业未参加的已结束户外招聘会明细。 */
export async function getCmsCompanyMissedOutdoorFairs(
companyId: number | string,
params?: { current?: number; pageSize?: number },
) {
return request<{
code: number;
msg?: string;
total: number;
rows: API.CompanyList.MissedOutdoorFair[];
}>(`/api/cms/company/${companyId}/missed-outdoor-fairs`, {
method: 'GET',
params,
});
}
/** 获取当前企业用户关联的企业信息 */
export async function getCmsCurrentCompany() {
return request<{

View File

@@ -86,6 +86,18 @@ export interface CrossDomainStatistics {
jobCount: number;
demandCount: number;
viewCount: number;
fairs: CrossDomainStatisticsFairItem[];
}
/** 跨域线上招聘会统计中的单场明细 */
export interface CrossDomainStatisticsFairItem {
fairId: string;
fairTitle: string;
startTime?: string;
companyCount: number;
jobCount: number;
demandCount: number;
viewCount: number;
}
export async function getCrossDomainStatistics(params: CrossDomainStatisticsParams) {

View File

@@ -24,6 +24,8 @@ export interface OutdoorFairItem {
boothCount: number;
/** 已预定摊位数 */
reservedBoothCount?: number;
/** 招聘会实际展位图中的展位数量 */
boothMapCount?: number;
/** 举办时间 */
holdTime: string;
/** 招聘会结束时间 */
@@ -58,6 +60,12 @@ export interface OutdoorFairItem {
reviewStatus?: string;
/** 当前企业报名审核备注 */
reviewRemark?: string;
/** 当前企业签到状态not_checked_in/checked_in */
companyCheckInStatus?: 'not_checked_in' | 'checked_in';
/** 当前企业签到时间 */
companyCheckInTime?: string;
/** 当前企业当前是否允许签到 */
companyCheckInAllowed?: boolean;
createTime?: string;
updateTime?: string;
}
@@ -70,6 +78,14 @@ export interface OutdoorFairListParams {
region?: string;
venueId?: number;
onlineApply?: boolean;
/** 举办开始时间下限 */
startTime?: string;
/** 举办结束时间上限 */
endTime?: string;
/** 报名开始时间下限 */
applyStartTime?: string;
/** 报名截止时间上限 */
applyEndTime?: string;
current?: number;
pageSize?: number;
}
@@ -343,6 +359,25 @@ export async function getMyOutdoorFairQrCode(fairId: number) {
);
}
/**
* 当前企业在招聘会举办期间签到
* POST /api/cms/outdoor-fair/{fairId}/company/check-in
*/
export async function checkInOutdoorFair(fairId: number) {
return request<{
code: number;
msg?: string;
data?: {
fairId: number;
companyId: number;
checkInTime: string;
attendanceStatus: 'checked_in';
};
}>(`${CMS_OUTDOOR_FAIR_BASE}/${fairId}/company/check-in`, {
method: 'POST',
});
}
/**
* 管理员获取招聘会微信小程序码
* GET /api/cms/outdoor-fair/{fairId}/mini-program-qrcode

View File

@@ -4,6 +4,8 @@ import { request } from '@umijs/max';
export interface OutdoorFairAttendeeItem {
id: number;
fairId: number;
/** 关联的移动端用户 ID */
userId?: number;
/** 人员姓名 */
name: string;
/** 手机号 */
@@ -36,14 +38,18 @@ export interface OutdoorFairAttendeeListResult {
rows: OutdoorFairAttendeeItem[];
}
/** 签到人员新增/编辑表单 */
/** 后台录入签到参数 */
export interface OutdoorFairAttendeeForm {
id?: number;
fairId: number;
name: string;
userId: number;
}
/** 后台录入签到时的移动端用户选项 */
export interface OutdoorFairAttendeeUserOption {
userId: number;
name?: string;
phone?: string;
address?: string;
checkInTime?: string;
}
/** 通用响应 */
@@ -53,14 +59,14 @@ export interface OutdoorFairAttendeeCommonResult {
data?: OutdoorFairAttendeeItem;
}
const BASE = '/api/cms/outdoor-fair-attendee';
/** 可录入签到的移动端用户列表响应 */
export interface OutdoorFairAttendeeUserOptionResult {
code: number;
msg?: string;
data?: OutdoorFairAttendeeUserOption[];
}
const formatDateTime = (value: any) => {
if (value && typeof value.format === 'function') {
return value.format('YYYY-MM-DD HH:mm:ss');
}
return value;
};
const BASE = '/api/cms/outdoor-fair-attendee';
/**
* 获取签到人员列表
@@ -74,24 +80,27 @@ export async function getOutdoorFairAttendeeList(params?: OutdoorFairAttendeeLis
}
/**
* 新增签到人员
* 获取当前招聘会尚未签到的移动端用户
* GET /api/cms/outdoor-fair-attendee/available-users
*/
export async function getAvailableOutdoorFairAttendeeUsers(params: {
fairId: number;
keyword?: string;
}) {
return request<OutdoorFairAttendeeUserOptionResult>(`${BASE}/available-users`, {
method: 'GET',
params,
});
}
/**
* 选择移动端用户并立即完成签到
* POST /api/cms/outdoor-fair-attendee
*/
export async function addOutdoorFairAttendee(data: OutdoorFairAttendeeForm) {
return request<OutdoorFairAttendeeCommonResult>(BASE, {
method: 'POST',
data: { ...data, checkInTime: formatDateTime(data.checkInTime) },
});
}
/**
* 修改签到人员
* PUT /api/cms/outdoor-fair-attendee
*/
export async function updateOutdoorFairAttendee(data: OutdoorFairAttendeeForm) {
return request<OutdoorFairAttendeeCommonResult>(BASE, {
method: 'PUT',
data: { ...data, checkInTime: formatDateTime(data.checkInTime) },
data,
});
}

View File

@@ -19,6 +19,21 @@ type GateEntryLog = API.OutdoorFairDetail.GateEntryLog;
type FairStatistics = API.OutdoorFairDetail.FairStatistics;
type CompanyQrCodeInfo = API.OutdoorFairDetail.CompanyQrCodeInfo;
export interface OutdoorFairCompanyAttendanceItem {
id: number;
fairId: number;
companyId: number;
companyName: string;
industry?: string;
scale?: string;
contactPerson?: string;
contactPhone?: string;
reviewTime?: string;
registrationTime?: string;
attendanceStatus: '0' | '1';
checkInTime?: string;
}
// ==================== 模块1: 招聘会管理 假数据 ====================
const MOCK_COMPANIES: ParticipatingCompany[] = [
@@ -1016,6 +1031,11 @@ export async function getParticipatingCompanies(
current?: number;
pageSize?: number;
companyName?: string;
reviewStatus?: string;
/** 报名时间下限 */
startTime?: string;
/** 报名时间上限 */
endTime?: string;
},
) {
return request<{ code: number; msg?: string; total: number; rows: ParticipatingCompany[] }>(
@@ -1027,6 +1047,27 @@ export async function getParticipatingCompanies(
);
}
/** 获取招聘会企业签到列表。 */
export async function getOutdoorFairCompanyAttendanceList(
fairId: number,
params?: {
current?: number;
pageSize?: number;
companyName?: string;
checkedIn?: boolean;
},
) {
return request<{
code: number;
msg?: string;
total: number;
rows: OutdoorFairCompanyAttendanceItem[];
}>(`/api/cms/outdoor-fair/${fairId}/company-attendance`, {
method: 'GET',
params,
});
}
/** 获取招聘会企业退订记录。 */
export async function getCompanyUnsubscribes(
fairId: number,

View File

@@ -1,4 +1,14 @@
declare namespace API.CompanyList {
export interface MissedOutdoorFair {
fairId: number;
title: string;
hostUnit?: string;
address?: string;
holdTime?: string;
endTime?: string;
registrationTime?: string;
}
export interface CompanyListResult {
total: number;
rows: RootObjectRows[];
@@ -30,6 +40,7 @@ declare namespace API.CompanyList {
notPassReason?: string;
updateTime?: string;
status?: 0 | 1 | 2; // 0 待审核, 1 通过, 2 驳回
missedOutdoorFairCount?: number;
}
export interface Params {
@@ -51,5 +62,7 @@ declare namespace API.CompanyList {
status?: 0 | 1 | 2;
startDate?: string;
endDate?: string;
/** 仅查看有未参加已结束户外招聘会记录的企业。 */
missedOutdoorFairOnly?: boolean;
}
}

View File

@@ -30,6 +30,12 @@ declare namespace API.OutdoorFairDetail {
reviewBy?: string;
reviewTime?: string;
reviewRemark?: string;
/** 企业报名时间。 */
registrationTime?: string;
/** 企业签到状态not_checked_in/checked_in。 */
companyCheckInStatus?: 'not_checked_in' | 'checked_in';
/** 企业签到时间。 */
companyCheckInTime?: string;
/** 仅兼容旧 mock 数据,真实企业接口不再返回岗位列表。 */
jobList?: PostedJob[];
}