Compare commits

...

2 Commits

Author SHA1 Message Date
4db3255997 feat: 完善招聘会统计页面
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-24 09:46:54 +08:00
c6d665c370 feat: 增加招聘会机构管理和场地机构选择 2026-07-24 09:42:37 +08:00
11 changed files with 554 additions and 58 deletions

View File

@@ -301,6 +301,11 @@ export default [
path: '/jobfair/venue-info', path: '/jobfair/venue-info',
component: './Jobfair/Venueinfo', component: './Jobfair/Venueinfo',
}, },
{
name: '机构管理',
path: '/jobfair/institution',
component: './Jobfair/Institution',
},
{ {
name: '场地信息详情', name: '场地信息详情',
path: '/jobfair/venue-info/detail', path: '/jobfair/venue-info/detail',

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { useAccess } from '@umijs/max'; import { useAccess } from '@umijs/max';
import { import {
PageContainer, PageContainer,
@@ -11,6 +12,7 @@ import {
import { import {
Tabs, Tabs,
Card, Card,
DatePicker,
Row, Row,
Col, Col,
Statistic, Statistic,
@@ -32,6 +34,7 @@ import {
DeleteOutlined, DeleteOutlined,
EditOutlined, EditOutlined,
SettingOutlined, SettingOutlined,
EyeOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict'; import { getDictValueEnum } from '@/services/system/dict';
import { import {
@@ -44,13 +47,21 @@ import type {
CrossCityAllianceItem, CrossCityAllianceItem,
CrossCityAllianceForm, CrossCityAllianceForm,
} from '@/services/jobfair/crossCityAlliance'; } from '@/services/jobfair/crossCityAlliance';
import { getCrossDomainJobs } from '@/services/jobfair/publicJobFair'; import { getCrossDomainJobs, getCrossDomainStatistics } from '@/services/jobfair/publicJobFair';
import type { CrossDomainJobItem } from '@/services/jobfair/publicJobFair'; import type { CrossDomainJobItem } from '@/services/jobfair/publicJobFair';
const { Title, Text } = Typography; const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const PAGE_SIZE_FOR_STATS = 10000; const PAGE_SIZE_FOR_STATS = 10000;
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'month'), dayjs()];
const getDateRangePresets = () => [
{ label: '近7天', value: [dayjs().subtract(6, 'day'), dayjs()] as [Dayjs, Dayjs] },
{ label: '近1个月', value: getDefaultDateRange() },
];
const getValueEnumText = (valueEnum: any, value?: string | number) => { const getValueEnumText = (valueEnum: any, value?: string | number) => {
if (value === undefined || value === null || value === '') return '-'; if (value === undefined || value === null || value === '') return '-';
const option = valueEnum[String(value)] || valueEnum[Number(value)]; const option = valueEnum[String(value)] || valueEnum[Number(value)];
@@ -273,7 +284,8 @@ const CitySelectTab: React.FC<CitySelectTabProps> = ({
</Spin> </Spin>
<div style={{ marginTop: 12, color: '#888' }}> <div style={{ marginTop: 12, color: '#888' }}>
{selectedCities.length > 0 ? selectedCities.length + ' 个区域' : '全部区域'} {selectedCities.length > 0 ? selectedCities.length + ' 个区域' : '全部区域'}
<span style={{ marginLeft: 8 }}></span> {/* 暂时隐藏各地市就业信息栏目,保留原提示便于后续恢复。 */}
{/* <span style={{ marginLeft: 8 }}>(选择区域后,到「各地市就业信息」查看对应岗位)</span> */}
</div> </div>
</Card> </Card>
</div> </div>
@@ -379,12 +391,13 @@ const JobsTab: React.FC<{ selectedCities: string[] }> = ({ selectedCities }) =>
// ==================== 数据统计 Tab ==================== // ==================== 数据统计 Tab ====================
const DataStatsTab: React.FC = () => { const DataStatsTab: React.FC = () => {
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultDateRange());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalCities: 0,
totalCompanies: 0, totalCompanies: 0,
totalJobs: 0, totalJobs: 0,
totalVacancies: 0, totalVacancies: 0,
totalViews: 0,
industryDistribution: [] as { industry: string; count: number; percentage: string }[], industryDistribution: [] as { industry: string; count: number; percentage: string }[],
monthlyJobs: [] as { month: string; count: number }[], monthlyJobs: [] as { month: string; count: number }[],
}); });
@@ -393,17 +406,26 @@ const DataStatsTab: React.FC = () => {
const loadStats = async () => { const loadStats = async () => {
setLoading(true); setLoading(true);
try { try {
const [cityRes, jobRes] = await Promise.all([ const startDate = dateRange[0].format('YYYY-MM-DD');
getCrossCityAllianceList({ pageSize: PAGE_SIZE_FOR_STATS }), const endDate = dateRange[1].format('YYYY-MM-DD');
getCrossDomainJobs({ current: 1, pageSize: PAGE_SIZE_FOR_STATS }), const [statisticsRes, jobRes] = await Promise.all([
getCrossDomainStatistics({ startDate, endDate }),
getCrossDomainJobs({
startDate,
endDate,
current: 1,
pageSize: PAGE_SIZE_FOR_STATS,
}),
]); ]);
const jobs = jobRes.rows || []; if (statisticsRes.code !== 200 || !statisticsRes.data) {
const companyIds = new Set( throw new Error(statisticsRes.msg || '跨域招聘会统计加载失败');
jobs }
.map((job) => job.companyId)
.filter((companyId) => companyId !== undefined && companyId !== null), // 岗位可能被多场跨域招聘会引用,图表也必须按岗位 ID 去重。
const jobs = Array.from(
new Map((jobRes.rows || []).map((job) => [String(job.jobId), job])).values(),
); );
const totalJobs = jobRes.total || jobs.length; const totalJobs = statisticsRes.data.jobCount;
const industryCount = jobs.reduce<Record<string, number>>((acc, job) => { const industryCount = jobs.reduce<Record<string, number>>((acc, job) => {
const industry = job.industry || '其他'; const industry = job.industry || '其他';
acc[industry] = (acc[industry] || 0) + 1; acc[industry] = (acc[industry] || 0) + 1;
@@ -417,10 +439,10 @@ const DataStatsTab: React.FC = () => {
}, {}); }, {});
setStats({ setStats({
totalCities: cityRes.total || cityRes.rows?.length || 0, totalCompanies: statisticsRes.data.companyCount,
totalCompanies: companyIds.size,
totalJobs, totalJobs,
totalVacancies: jobs.reduce((sum, job) => sum + (Number(job.vacancies) || 0), 0), totalVacancies: statisticsRes.data.demandCount,
totalViews: statisticsRes.data.viewCount,
industryDistribution: Object.entries(industryCount).map(([industry, count]) => ({ industryDistribution: Object.entries(industryCount).map(([industry, count]) => ({
industry, industry,
count, count,
@@ -430,12 +452,14 @@ const DataStatsTab: React.FC = () => {
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([month, count]) => ({ month, count })), .map(([month, count]) => ({ month, count })),
}); });
} catch (error: any) {
message.error(error?.message || '跨域招聘会统计加载失败,请稍后重试');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
loadStats(); loadStats();
}, []); }, [dateRange]);
const industryColumns = [ const industryColumns = [
{ title: '行业', dataIndex: 'industry', key: 'industry' }, { title: '行业', dataIndex: 'industry', key: 'industry' },
@@ -457,38 +481,44 @@ const DataStatsTab: React.FC = () => {
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<div style={{ padding: '0 0 24px 0' }}> <div style={{ padding: '0 0 24px 0' }}>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}> <Card style={{ marginBottom: 16 }}>
<Col xs={24} sm={6}> <Space wrap>
<Card> <span></span>
<Statistic <RangePicker
title="联盟城市数" value={dateRange}
value={stats.totalCities} allowClear={false}
prefix={<EnvironmentOutlined />} presets={getDateRangePresets()}
suffix="个" onChange={(value) => {
if (value?.[0] && value?.[1]) {
setDateRange(value as [Dayjs, Dayjs]);
}
}}
/> />
</Space>
</Card> </Card>
</Col>
<Col xs={24} sm={6}> <Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={12} lg={6}>
<Card> <Card>
<Statistic <Statistic
title="参会企业总数" title="参会企业"
value={stats.totalCompanies} value={stats.totalCompanies}
prefix={<BankOutlined />} prefix={<BankOutlined />}
suffix="家" suffix="家"
/> />
</Card> </Card>
</Col> </Col>
<Col xs={24} sm={6}> <Col xs={24} sm={12} lg={6}>
<Card> <Card>
<Statistic <Statistic
title="发布岗位数" title="发布岗位数"
value={stats.totalJobs} value={stats.totalJobs}
prefix={<FileTextOutlined />} prefix={<FileTextOutlined />}
suffix="个" suffix="个"
/> />
</Card> </Card>
</Col> </Col>
<Col xs={24} sm={6}> <Col xs={24} sm={12} lg={6}>
<Card> <Card>
<Statistic <Statistic
title="招聘需求人数" title="招聘需求人数"
@@ -498,6 +528,16 @@ const DataStatsTab: React.FC = () => {
/> />
</Card> </Card>
</Col> </Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="岗位浏览量"
value={stats.totalViews}
prefix={<EyeOutlined />}
suffix="次"
/>
</Card>
</Col>
</Row> </Row>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
@@ -524,6 +564,11 @@ const DataStatsTab: React.FC = () => {
</Card> </Card>
</Col> </Col>
</Row> </Row>
<Card size="small" title="统计口径">
线使
ID 1
</Card>
</div> </div>
</Spin> </Spin>
); );
@@ -563,6 +608,7 @@ const CrossCityFair: React.FC = () => {
}; };
const tabItems = [ const tabItems = [
/* 暂时隐藏各地市就业信息栏目,保留原实现便于后续恢复。
{ {
key: 'city-jobs', key: 'city-jobs',
label: ( label: (
@@ -573,6 +619,7 @@ const CrossCityFair: React.FC = () => {
), ),
children: <JobsTab selectedCities={selectedCities} />, children: <JobsTab selectedCities={selectedCities} />,
}, },
*/
{ {
key: 'statistics', key: 'statistics',
label: ( label: (
@@ -612,7 +659,7 @@ const CrossCityFair: React.FC = () => {
}} }}
> >
<Card> <Card>
<Tabs defaultActiveKey="city-jobs" items={tabItems} tabBarStyle={{ marginBottom: 0 }} /> <Tabs defaultActiveKey="statistics" items={tabItems} tabBarStyle={{ marginBottom: 0 }} />
</Card> </Card>
<AllianceManageModal <AllianceManageModal

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { import {
Button, Button,
Card, Card,
@@ -37,12 +38,19 @@ interface FairOption {
const formatDate = (value: any): string | undefined => const formatDate = (value: any): string | undefined =>
value && typeof value.format === 'function' ? value.format('YYYY-MM-DD') : undefined; value && typeof value.format === 'function' ? value.format('YYYY-MM-DD') : undefined;
const getDefaultDateRange = (): [Dayjs, Dayjs] => [dayjs().subtract(1, 'month'), dayjs()];
const getDateRangePresets = () => [
{ label: '近7天', value: [dayjs().subtract(6, 'day'), dayjs()] as [Dayjs, Dayjs] },
{ label: '近1个月', value: getDefaultDateRange() },
];
const FairStatistics: React.FC = () => { const FairStatistics: React.FC = () => {
const [fairType, setFairType] = useState<RecruitmentFairType>('online'); const [fairType, setFairType] = useState<RecruitmentFairType>('online');
const [scope, setScope] = useState<StatisticsScope>('single'); const [scope, setScope] = useState<StatisticsScope>('range');
const [fairOptions, setFairOptions] = useState<FairOption[]>([]); const [fairOptions, setFairOptions] = useState<FairOption[]>([]);
const [selectedFairId, setSelectedFairId] = useState<string>(); const [selectedFairId, setSelectedFairId] = useState<string>();
const [dateRange, setDateRange] = useState<any>(null); const [dateRange, setDateRange] = useState<any>(getDefaultDateRange());
const [loadingFairs, setLoadingFairs] = useState(false); const [loadingFairs, setLoadingFairs] = useState(false);
const [loadingSummary, setLoadingSummary] = useState(false); const [loadingSummary, setLoadingSummary] = useState(false);
const [summary, setSummary] = useState<FairStatisticsSummary>(); const [summary, setSummary] = useState<FairStatisticsSummary>();
@@ -97,7 +105,7 @@ const FairStatistics: React.FC = () => {
// 切换类型时同步清空条件,避免新类型携带旧类型招聘会 ID 发起查询。 // 切换类型时同步清空条件,避免新类型携带旧类型招聘会 ID 发起查询。
setSelectedFairId(undefined); setSelectedFairId(undefined);
setDateRange(null); setDateRange(getDefaultDateRange());
setSummary(undefined); setSummary(undefined);
setFairOptions([]); setFairOptions([]);
setFairType(type); setFairType(type);
@@ -190,7 +198,11 @@ const FairStatistics: React.FC = () => {
{ label: '按起始时间统计', value: 'range' }, { label: '按起始时间统计', value: 'range' },
]} ]}
onChange={(value) => { onChange={(value) => {
setScope(value as StatisticsScope); const nextScope = value as StatisticsScope;
if (nextScope === 'range' && !dateRange) {
setDateRange(getDefaultDateRange());
}
setScope(nextScope);
setSummary(undefined); setSummary(undefined);
}} }}
/> />
@@ -211,6 +223,8 @@ const FairStatistics: React.FC = () => {
) : ( ) : (
<RangePicker <RangePicker
value={dateRange} value={dateRange}
allowClear={false}
presets={getDateRangePresets()}
onChange={(value) => setDateRange(value)} onChange={(value) => setDateRange(value)}
style={{ width: '100%', minWidth: 320 }} style={{ width: '100%', minWidth: 320 }}
placeholder={['起始日期', '结束日期']} placeholder={['起始日期', '结束日期']}

View File

@@ -0,0 +1,172 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, message, Modal } from 'antd';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
addJobFairInstitution,
deleteJobFairInstitution,
getJobFairInstitutionList,
updateJobFairInstitution,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
const InstitutionManagement: React.FC = () => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [formOpen, setFormOpen] = useState(false);
const [currentRow, setCurrentRow] = useState<JobFairInstitutionItem>();
const handleDelete = (record: JobFairInstitutionItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除机构「${record.institutionName}」吗?如果该机构已被场地使用,需要先解除场地关联。`,
onOk: async () => {
const res = await deleteJobFairInstitution(record.id);
if (res.code === 200) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error(res.msg || '删除失败');
}
},
});
};
const columns: ProColumns<JobFairInstitutionItem>[] = [
{
title: '机构名称',
dataIndex: 'institutionName',
ellipsis: true,
width: 260,
},
{
title: '机构简介',
dataIndex: 'institutionIntro',
ellipsis: true,
hideInSearch: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
hideInSearch: true,
width: 180,
},
{
title: '操作',
valueType: 'option',
width: 160,
fixed: 'right',
render: (_, record) => [
<Button
key="edit"
type="link"
size="small"
icon={<EditOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:edit')}
onClick={() => {
setCurrentRow(record);
setFormOpen(true);
}}
>
</Button>,
<Button
key="delete"
type="link"
size="small"
danger
icon={<DeleteOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:remove')}
onClick={() => handleDelete(record)}
>
</Button>,
],
},
];
return (
<PageContainer>
<ProTable<JobFairInstitutionItem>
headerTitle="机构列表"
actionRef={actionRef}
rowKey="id"
columns={columns}
search={{ labelWidth: 'auto' }}
scroll={{ x: 'max-content' }}
request={async (params) => {
const res = await getJobFairInstitutionList({
institutionName: params.institutionName,
current: params.current,
pageSize: params.pageSize,
});
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
}}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:add')}
onClick={() => {
setCurrentRow(undefined);
setFormOpen(true);
}}
>
</Button>,
]}
/>
<ModalForm<JobFairInstitutionForm>
title={currentRow ? '编辑机构' : '新增机构'}
open={formOpen}
initialValues={currentRow}
modalProps={{ destroyOnClose: true, onCancel: () => setFormOpen(false) }}
onFinish={async (values) => {
const res = currentRow
? await updateJobFairInstitution({ ...values, id: currentRow.id })
: await addJobFairInstitution(values);
if (res.code === 200) {
message.success(currentRow ? '修改成功' : '新增成功');
setFormOpen(false);
setCurrentRow(undefined);
actionRef.current?.reload();
return true;
}
message.error(res.msg || '操作失败');
return false;
}}
>
<ProFormText
name="institutionName"
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
fieldProps={{ maxLength: 200, showCount: true }}
placeholder="请输入机构名称"
/>
<ProFormTextArea
name="institutionIntro"
label="机构简介"
fieldProps={{ maxLength: 2000, showCount: true, rows: 5 }}
placeholder="请输入机构简介"
/>
</ModalForm>
</PageContainer>
);
};
export default InstitutionManagement;

View File

@@ -123,6 +123,7 @@ const PublicJobFairList: React.FC = () => {
hideInSearch: true, hideInSearch: true,
ellipsis: true, ellipsis: true,
}, },
/* 暂时隐藏线上招聘会列表中的招聘会类型字段,保留原实现便于后续恢复。
{ {
title: '类型', title: '类型',
dataIndex: 'jobFairType', dataIndex: 'jobFairType',
@@ -130,6 +131,7 @@ const PublicJobFairList: React.FC = () => {
valueEnum: jobFairTypeEnum, valueEnum: jobFairTypeEnum,
render: (_, record) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />, render: (_, record) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />,
}, },
*/
{ {
title: '区域类型', title: '区域类型',
dataIndex: 'jobFairRegionType', dataIndex: 'jobFairRegionType',

View File

@@ -8,9 +8,21 @@ import {
getVenueInfo, getVenueInfo,
updateVenueInfo, updateVenueInfo,
} from '@/services/jobportal/venueInfo'; } from '@/services/jobportal/venueInfo';
import type { VenueInfoDictForm, VenueInfoForm, VenueInfoItem } from '@/services/jobportal/venueInfo'; import type {
VenueInfoDictForm,
VenueInfoForm,
VenueInfoItem,
} from '@/services/jobportal/venueInfo';
import { getCmsLineList } from '@/services/area/subway'; import { getCmsLineList } from '@/services/area/subway';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict'; import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import {
addJobFairInstitution,
getJobFairInstitutionList,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import EditModal from '../components/EditModal'; import EditModal from '../components/EditModal';
const VENUE_TYPE_DICT = 'venue_info_type'; const VENUE_TYPE_DICT = 'venue_info_type';
@@ -25,6 +37,7 @@ const VenueInfoDetail: React.FC = () => {
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]); const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({}); const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
const [lineOptions, setLineOptions] = useState<any[]>([]); const [lineOptions, setLineOptions] = useState<any[]>([]);
const [institutionOptions, setInstitutionOptions] = useState<JobFairInstitutionItem[]>([]);
const fetchVenueInfo = useCallback(async () => { const fetchVenueInfo = useCallback(async () => {
if (!venueId) { if (!venueId) {
@@ -45,10 +58,11 @@ const VenueInfoDetail: React.FC = () => {
}, [venueId]); }, [venueId]);
const refreshOptions = useCallback(async () => { const refreshOptions = useCallback(async () => {
const [valueEnum, options, lineRes] = await Promise.all([ const [valueEnum, options, lineRes, institutionRes] = await Promise.all([
getDictValueEnum(VENUE_TYPE_DICT), getDictValueEnum(VENUE_TYPE_DICT),
getDictSelectOption(VENUE_TYPE_DICT), getDictSelectOption(VENUE_TYPE_DICT),
getCmsLineList({ current: '1', pageSize: '999' } as any), getCmsLineList({ current: '1', pageSize: '999' } as any),
getJobFairInstitutionList({ current: 1, pageSize: 9999 }),
]); ]);
setVenueTypeValueEnum(valueEnum); setVenueTypeValueEnum(valueEnum);
setVenueTypeOptions(options); setVenueTypeOptions(options);
@@ -60,8 +74,23 @@ const VenueInfoDetail: React.FC = () => {
})), })),
); );
} }
if (institutionRes.code === 200) {
setInstitutionOptions(institutionRes.rows || []);
}
}, []); }, []);
const handleCreateInstitution = async (values: JobFairInstitutionForm) => {
const res = await addJobFairInstitution(values);
if (res.code === 200 && res.data) {
const listRes = await getJobFairInstitutionList({ current: 1, pageSize: 9999 });
if (listRes.code === 200) setInstitutionOptions(listRes.rows || []);
message.success('机构新增成功');
return res.data;
}
message.error(res.msg || '机构新增失败');
return undefined;
};
useEffect(() => { useEffect(() => {
fetchVenueInfo(); fetchVenueInfo();
refreshOptions(); refreshOptions();
@@ -80,11 +109,7 @@ const VenueInfoDetail: React.FC = () => {
const venueTypeLabel = (val?: string) => { const venueTypeLabel = (val?: string) => {
if (!val) return '--'; if (!val) return '--';
return ( return venueTypeValueEnum[val]?.text || venueTypeValueEnum[val]?.label || val;
venueTypeValueEnum[val]?.text ||
venueTypeValueEnum[val]?.label ||
val
);
}; };
return ( return (
@@ -120,27 +145,20 @@ const VenueInfoDetail: React.FC = () => {
<Descriptions.Item label="场地面积"> <Descriptions.Item label="场地面积">
{venueInfo.venueArea != null ? `${venueInfo.venueArea}` : '--'} {venueInfo.venueArea != null ? `${venueInfo.venueArea}` : '--'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="展位数量"> <Descriptions.Item label="展位数量">{venueInfo.floorCount ?? '--'}</Descriptions.Item>
{venueInfo.floorCount ?? '--'}
</Descriptions.Item>
<Descriptions.Item label="场地地址" span={2}> <Descriptions.Item label="场地地址" span={2}>
{venueInfo.venueAddress || '--'} {venueInfo.venueAddress || '--'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="联系电话"> <Descriptions.Item label="联系电话">{venueInfo.contactPhone || '--'}</Descriptions.Item>
{venueInfo.contactPhone || '--'}
</Descriptions.Item>
<Descriptions.Item label="乘坐线路" span={3}> <Descriptions.Item label="乘坐线路" span={3}>
{venueInfo.routeLineNames || '--'} {venueInfo.routeLineNames || '--'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="经办日期"> <Descriptions.Item label="经办日期">{venueInfo.handleDate || '--'}</Descriptions.Item>
{venueInfo.handleDate || '--'} <Descriptions.Item label="经办机构">{venueInfo.handleOrg || '--'}</Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="所属机构" span={3}>
<Descriptions.Item label="经办机构"> {venueInfo.institutionNames || '--'}
{venueInfo.handleOrg || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办人">
{venueInfo.handler || '--'}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="经办人">{venueInfo.handler || '--'}</Descriptions.Item>
<Descriptions.Item label="备注" span={3}> <Descriptions.Item label="备注" span={3}>
{venueInfo.remark || '--'} {venueInfo.remark || '--'}
</Descriptions.Item> </Descriptions.Item>
@@ -155,6 +173,9 @@ const VenueInfoDetail: React.FC = () => {
values={venueInfo} values={venueInfo}
venueTypeOptions={venueTypeOptions} venueTypeOptions={venueTypeOptions}
lineOptions={lineOptions} lineOptions={lineOptions}
institutionOptions={institutionOptions}
canCreateInstitution={access.hasPerms('cms:jobFairInstitution:add')}
onCreateInstitution={handleCreateInstitution}
onCreateDictOption={handleCreateDictOption} onCreateDictOption={handleCreateDictOption}
onCancel={() => setEditModalOpen(false)} onCancel={() => setEditModalOpen(false)}
onSubmit={async (values: VenueInfoForm) => { onSubmit={async (values: VenueInfoForm) => {

View File

@@ -10,6 +10,10 @@ import {
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Divider, Form, Input, Modal, Space } from 'antd'; import { Button, Divider, Form, Input, Modal, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import type { import type {
VenueInfoDictForm, VenueInfoDictForm,
VenueInfoForm, VenueInfoForm,
@@ -21,8 +25,13 @@ interface EditModalProps {
values?: VenueInfoItem; values?: VenueInfoItem;
venueTypeOptions: any[]; venueTypeOptions: any[];
lineOptions: any[]; lineOptions: any[];
institutionOptions: JobFairInstitutionItem[];
canCreateInstitution: boolean;
defaultHandler?: string; defaultHandler?: string;
onCreateDictOption: (values: VenueInfoDictForm) => Promise<boolean>; onCreateDictOption: (values: VenueInfoDictForm) => Promise<boolean>;
onCreateInstitution: (
values: JobFairInstitutionForm,
) => Promise<JobFairInstitutionItem | undefined>;
onCancel: () => void; onCancel: () => void;
onSubmit: (values: VenueInfoForm) => Promise<void>; onSubmit: (values: VenueInfoForm) => Promise<void>;
} }
@@ -44,8 +53,11 @@ const EditModal: React.FC<EditModalProps> = ({
values, values,
venueTypeOptions, venueTypeOptions,
lineOptions, lineOptions,
institutionOptions,
canCreateInstitution,
defaultHandler, defaultHandler,
onCreateDictOption, onCreateDictOption,
onCreateInstitution,
onCancel, onCancel,
onSubmit, onSubmit,
}) => { }) => {
@@ -53,6 +65,9 @@ const EditModal: React.FC<EditModalProps> = ({
const [dictModalOpen, setDictModalOpen] = useState(false); const [dictModalOpen, setDictModalOpen] = useState(false);
const [dictInputValue, setDictInputValue] = useState(''); const [dictInputValue, setDictInputValue] = useState('');
const [dictSubmitLoading, setDictSubmitLoading] = useState(false); const [dictSubmitLoading, setDictSubmitLoading] = useState(false);
const [institutionModalOpen, setInstitutionModalOpen] = useState(false);
const [institutionSubmitLoading, setInstitutionSubmitLoading] = useState(false);
const [institutionForm] = Form.useForm<JobFairInstitutionForm>();
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -61,6 +76,7 @@ const EditModal: React.FC<EditModalProps> = ({
form.setFieldsValue({ form.setFieldsValue({
...values, ...values,
routeLineIds: splitRouteLineIds(values.routeLineIds), routeLineIds: splitRouteLineIds(values.routeLineIds),
institutionIds: values.institutionIds || [],
}); });
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
@@ -111,6 +127,45 @@ const EditModal: React.FC<EditModalProps> = ({
} }
}; };
const renderInstitutionDropdown = (menu: React.ReactElement): React.ReactElement => (
<>
{menu}
{canCreateInstitution && (
<>
<Divider style={{ margin: '8px 0' }} />
<Space style={{ padding: '0 8px 8px' }}>
<Button
type="link"
icon={<PlusOutlined />}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
institutionForm.resetFields();
setInstitutionModalOpen(true);
}}
>
</Button>
</Space>
</>
)}
</>
);
const handleInstitutionModalOk = async () => {
try {
const values = await institutionForm.validateFields();
setInstitutionSubmitLoading(true);
const created = await onCreateInstitution(values);
if (created) {
const selectedIds = (form.getFieldValue('institutionIds') || []) as number[];
form.setFieldValue('institutionIds', [...selectedIds, created.id]);
setInstitutionModalOpen(false);
}
} finally {
setInstitutionSubmitLoading(false);
}
};
return ( return (
<ModalForm <ModalForm
title={values ? '编辑场地' : '添加场地'} title={values ? '编辑场地' : '添加场地'}
@@ -190,6 +245,21 @@ const EditModal: React.FC<EditModalProps> = ({
}} }}
placeholder="请选择乘坐线路" placeholder="请选择乘坐线路"
/> />
<ProFormSelect
name="institutionIds"
label="所属机构"
mode="multiple"
options={institutionOptions.map((item) => ({
label: item.institutionName,
value: item.id,
}))}
fieldProps={{
showSearch: true,
optionFilterProp: 'label',
dropdownRender: renderInstitutionDropdown,
}}
placeholder="请选择所属机构,可多选"
/>
<ProFormTextArea <ProFormTextArea
name="remark" name="remark"
label="备注" label="备注"
@@ -224,6 +294,27 @@ const EditModal: React.FC<EditModalProps> = ({
onPressEnter={handleDictModalOk} onPressEnter={handleDictModalOk}
/> />
</Modal> </Modal>
<Modal
title="快速新增机构"
open={institutionModalOpen}
confirmLoading={institutionSubmitLoading}
onOk={handleInstitutionModalOk}
onCancel={() => setInstitutionModalOpen(false)}
destroyOnClose
>
<Form form={institutionForm} layout="vertical">
<Form.Item
name="institutionName"
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
>
<Input maxLength={200} showCount placeholder="请输入机构名称" />
</Form.Item>
<Form.Item name="institutionIntro" label="机构简介">
<Input.TextArea maxLength={2000} showCount rows={4} placeholder="请输入机构简介" />
</Form.Item>
</Form>
</Modal>
</ModalForm> </ModalForm>
); );
}; };

View File

@@ -18,6 +18,14 @@ import type {
} from '@/services/jobportal/venueInfo'; } from '@/services/jobportal/venueInfo';
import { getCmsLineList } from '@/services/area/subway'; import { getCmsLineList } from '@/services/area/subway';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict'; import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import {
addJobFairInstitution,
getJobFairInstitutionList,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import EditModal from './components/EditModal'; import EditModal from './components/EditModal';
import VenueBoothMapModal from './components/VenueBoothMapModal'; import VenueBoothMapModal from './components/VenueBoothMapModal';
@@ -33,6 +41,7 @@ const VenueInfoList: React.FC = () => {
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({}); const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]); const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
const [lineOptions, setLineOptions] = useState<any[]>([]); const [lineOptions, setLineOptions] = useState<any[]>([]);
const [institutionOptions, setInstitutionOptions] = useState<JobFairInstitutionItem[]>([]);
const defaultHandler = const defaultHandler =
initialState?.currentUser?.nickName || initialState?.currentUser?.userName || undefined; initialState?.currentUser?.nickName || initialState?.currentUser?.userName || undefined;
@@ -58,10 +67,29 @@ const VenueInfoList: React.FC = () => {
} }
}, []); }, []);
const refreshInstitutionOptions = useCallback(async () => {
const res = await getJobFairInstitutionList({ current: 1, pageSize: 9999 });
if (res.code === 200) {
setInstitutionOptions(res.rows || []);
}
}, []);
useEffect(() => { useEffect(() => {
refreshVenueTypeOptions(); refreshVenueTypeOptions();
refreshLineOptions(); refreshLineOptions();
}, [refreshVenueTypeOptions, refreshLineOptions]); refreshInstitutionOptions();
}, [refreshVenueTypeOptions, refreshLineOptions, refreshInstitutionOptions]);
const handleCreateInstitution = async (values: JobFairInstitutionForm) => {
const res = await addJobFairInstitution(values);
if (res.code === 200 && res.data) {
await refreshInstitutionOptions();
message.success('机构新增成功');
return res.data;
}
message.error(res.msg || '机构新增失败');
return undefined;
};
const handleCreateDictOption = async (values: VenueInfoDictForm) => { const handleCreateDictOption = async (values: VenueInfoDictForm) => {
const res = await addVenueInfoDictOption(values); const res = await addVenueInfoDictOption(values);
@@ -118,6 +146,12 @@ const VenueInfoList: React.FC = () => {
dataIndex: 'venueAddress', dataIndex: 'venueAddress',
ellipsis: true, ellipsis: true,
}, },
{
title: '所属机构',
dataIndex: 'institutionNames',
ellipsis: true,
hideInSearch: true,
},
{ {
title: '联系电话', title: '联系电话',
dataIndex: 'contactPhone', dataIndex: 'contactPhone',
@@ -234,6 +268,9 @@ const VenueInfoList: React.FC = () => {
values={currentRow} values={currentRow}
venueTypeOptions={venueTypeOptions} venueTypeOptions={venueTypeOptions}
lineOptions={lineOptions} lineOptions={lineOptions}
institutionOptions={institutionOptions}
canCreateInstitution={access.hasPerms('cms:jobFairInstitution:add')}
onCreateInstitution={handleCreateInstitution}
defaultHandler={defaultHandler} defaultHandler={defaultHandler}
onCreateDictOption={handleCreateDictOption} onCreateDictOption={handleCreateDictOption}
onCancel={() => { onCancel={() => {

View File

@@ -30,6 +30,7 @@ export interface PublicJobFairJobForm {
export interface CrossDomainJobItem { export interface CrossDomainJobItem {
jobId: number; jobId: number;
jobTitle: string; jobTitle: string;
view?: number;
minSalary?: number; minSalary?: number;
maxSalary?: number; maxSalary?: number;
education?: string; education?: string;
@@ -49,6 +50,10 @@ export interface CrossDomainJobItem {
export interface CrossDomainJobParams { export interface CrossDomainJobParams {
/** 逗号分隔的城市名称 */ /** 逗号分隔的城市名称 */
cityNames?: string; cityNames?: string;
/** 招聘会起始日期(含当天) */
startDate?: string;
/** 招聘会起始日期(含当天) */
endDate?: string;
current?: number; current?: number;
pageSize?: number; pageSize?: number;
} }
@@ -69,6 +74,30 @@ export async function getCrossDomainJobs(params?: CrossDomainJobParams) {
}); });
} }
/** 查询跨域线上招聘会的去重统计。 */
export interface CrossDomainStatisticsParams {
startDate: string;
endDate: string;
}
export interface CrossDomainStatistics {
fairCount: number;
companyCount: number;
jobCount: number;
demandCount: number;
viewCount: number;
}
export async function getCrossDomainStatistics(params: CrossDomainStatisticsParams) {
return request<{ code: number; msg?: string; data?: CrossDomainStatistics }>(
`${BASE_URL}/cross-domain-statistics`,
{
method: 'GET',
params,
},
);
}
/** 查询招聘会列表 */ /** 查询招聘会列表 */
export async function getPublicJobFairList(params?: API.PublicJobFair.ListParams) { export async function getPublicJobFairList(params?: API.PublicJobFair.ListParams) {
return request<API.PublicJobFair.ListResult>(`${BASE_URL}/list`, { return request<API.PublicJobFair.ListResult>(`${BASE_URL}/list`, {

View File

@@ -0,0 +1,72 @@
import { request } from '@umijs/max';
export interface JobFairInstitutionItem {
id: number;
/** 机构名称 */
institutionName: string;
/** 机构简介 */
institutionIntro?: string;
createTime?: string;
updateTime?: string;
}
export interface JobFairInstitutionForm {
id?: number;
institutionName: string;
institutionIntro?: string;
}
export interface JobFairInstitutionListParams {
institutionName?: string;
current?: number;
pageSize?: number;
}
export interface JobFairInstitutionListResult {
code: number;
msg?: string;
total: number;
rows: JobFairInstitutionItem[];
}
export interface JobFairInstitutionResult {
code: number;
msg?: string;
data?: JobFairInstitutionItem;
}
const BASE_URL = '/api/cms/job-fair-institution';
export async function getJobFairInstitutionList(params?: JobFairInstitutionListParams) {
return request<JobFairInstitutionListResult>(`${BASE_URL}/list`, {
method: 'GET',
params,
});
}
export async function getJobFairInstitution(id: number) {
return request<JobFairInstitutionResult>(`${BASE_URL}/${id}`, {
method: 'GET',
});
}
export async function addJobFairInstitution(data: JobFairInstitutionForm) {
return request<JobFairInstitutionResult>(BASE_URL, {
method: 'POST',
data,
});
}
export async function updateJobFairInstitution(data: JobFairInstitutionForm) {
return request<JobFairInstitutionResult>(BASE_URL, {
method: 'PUT',
data,
});
}
export async function deleteJobFairInstitution(ids: number | number[]) {
const idPath = Array.isArray(ids) ? ids.join(',') : ids;
return request<JobFairInstitutionResult>(`${BASE_URL}/${idPath}`, {
method: 'DELETE',
});
}

View File

@@ -27,6 +27,10 @@ export interface VenueInfoItem {
handler?: string; handler?: string;
/** 备注 */ /** 备注 */
remark?: string; remark?: string;
/** 所属机构ID支持多个 */
institutionIds?: number[];
/** 所属机构名称,多个用顿号分隔 */
institutionNames?: string;
createTime?: string; createTime?: string;
updateTime?: string; updateTime?: string;
} }
@@ -63,6 +67,7 @@ export interface VenueInfoForm {
handleOrg?: string; handleOrg?: string;
handler?: string; handler?: string;
remark?: string; remark?: string;
institutionIds?: number[];
} }
/** 通用响应 */ /** 通用响应 */
@@ -103,6 +108,7 @@ const formatDate = (value: any) => {
const toApiPayload = (data: VenueInfoForm) => ({ const toApiPayload = (data: VenueInfoForm) => ({
...data, ...data,
routeLineIds: Array.isArray(data.routeLineIds) ? data.routeLineIds.join(',') : data.routeLineIds, routeLineIds: Array.isArray(data.routeLineIds) ? data.routeLineIds.join(',') : data.routeLineIds,
institutionIds: Array.isArray(data.institutionIds) ? data.institutionIds : [],
handleDate: formatDate(data.handleDate), handleDate: formatDate(data.handleDate),
}); });