From 4db32559971ec8b8288ccdecbd7f88778921983f Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Fri, 24 Jul 2026 09:46:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=8B=9B=E8=81=98?= =?UTF-8?q?=E4=BC=9A=E7=BB=9F=E8=AE=A1=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Jobfair/Crosscityfair/index.tsx | 111 +++++++++++++++------ src/pages/Jobfair/FairStatistics/index.tsx | 22 +++- src/pages/Jobfair/PublicJobFair/index.tsx | 2 + src/services/jobfair/publicJobFair.ts | 29 ++++++ 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/src/pages/Jobfair/Crosscityfair/index.tsx b/src/pages/Jobfair/Crosscityfair/index.tsx index 63096bb..0167c1a 100644 --- a/src/pages/Jobfair/Crosscityfair/index.tsx +++ b/src/pages/Jobfair/Crosscityfair/index.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; +import dayjs, { Dayjs } from 'dayjs'; import { useAccess } from '@umijs/max'; import { PageContainer, @@ -11,6 +12,7 @@ import { import { Tabs, Card, + DatePicker, Row, Col, Statistic, @@ -32,6 +34,7 @@ import { DeleteOutlined, EditOutlined, SettingOutlined, + EyeOutlined, } from '@ant-design/icons'; import { getDictValueEnum } from '@/services/system/dict'; import { @@ -44,13 +47,21 @@ import type { CrossCityAllianceItem, CrossCityAllianceForm, } from '@/services/jobfair/crossCityAlliance'; -import { getCrossDomainJobs } from '@/services/jobfair/publicJobFair'; +import { getCrossDomainJobs, getCrossDomainStatistics } from '@/services/jobfair/publicJobFair'; import type { CrossDomainJobItem } from '@/services/jobfair/publicJobFair'; const { Title, Text } = Typography; +const { RangePicker } = DatePicker; 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) => { if (value === undefined || value === null || value === '') return '-'; const option = valueEnum[String(value)] || valueEnum[Number(value)]; @@ -273,7 +284,8 @@ const CitySelectTab: React.FC = ({
已选择 {selectedCities.length > 0 ? selectedCities.length + ' 个区域' : '全部区域'} - (选择区域后,到「各地市就业信息」查看对应岗位) + {/* 暂时隐藏各地市就业信息栏目,保留原提示便于后续恢复。 */} + {/* (选择区域后,到「各地市就业信息」查看对应岗位) */}
@@ -379,12 +391,13 @@ const JobsTab: React.FC<{ selectedCities: string[] }> = ({ selectedCities }) => // ==================== 数据统计 Tab ==================== const DataStatsTab: React.FC = () => { + const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultDateRange()); const [loading, setLoading] = useState(false); const [stats, setStats] = useState({ - totalCities: 0, totalCompanies: 0, totalJobs: 0, totalVacancies: 0, + totalViews: 0, industryDistribution: [] as { industry: string; count: number; percentage: string }[], monthlyJobs: [] as { month: string; count: number }[], }); @@ -393,17 +406,26 @@ const DataStatsTab: React.FC = () => { const loadStats = async () => { setLoading(true); try { - const [cityRes, jobRes] = await Promise.all([ - getCrossCityAllianceList({ pageSize: PAGE_SIZE_FOR_STATS }), - getCrossDomainJobs({ current: 1, pageSize: PAGE_SIZE_FOR_STATS }), + 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, + }), ]); - const jobs = jobRes.rows || []; - const companyIds = new Set( - jobs - .map((job) => job.companyId) - .filter((companyId) => companyId !== undefined && companyId !== null), + 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 = jobRes.total || jobs.length; + const totalJobs = statisticsRes.data.jobCount; const industryCount = jobs.reduce>((acc, job) => { const industry = job.industry || '其他'; acc[industry] = (acc[industry] || 0) + 1; @@ -417,10 +439,10 @@ const DataStatsTab: React.FC = () => { }, {}); setStats({ - totalCities: cityRes.total || cityRes.rows?.length || 0, - totalCompanies: companyIds.size, + totalCompanies: statisticsRes.data.companyCount, 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]) => ({ industry, count, @@ -430,12 +452,14 @@ const DataStatsTab: React.FC = () => { .sort(([a], [b]) => a.localeCompare(b)) .map(([month, count]) => ({ month, count })), }); + } catch (error: any) { + message.error(error?.message || '跨域招聘会统计加载失败,请稍后重试'); } finally { setLoading(false); } }; loadStats(); - }, []); + }, [dateRange]); const industryColumns = [ { title: '行业', dataIndex: 'industry', key: 'industry' }, @@ -457,38 +481,44 @@ const DataStatsTab: React.FC = () => { return (
+ + + 统计时间 + { + if (value?.[0] && value?.[1]) { + setDateRange(value as [Dayjs, Dayjs]); + } + }} + /> + + + - + } - suffix="个" - /> - - - - - } suffix="家" /> - + } suffix="个" /> - + { /> + + + } + suffix="次" + /> + + @@ -524,6 +564,11 @@ const DataStatsTab: React.FC = () => { + + + 仅统计时间范围内招聘会类型为“线上”且标记为“跨域”的招聘会;参会企业按有效报名企业统计,即使企业未选择岗位也计入。岗位、招聘需求人数和浏览量均按岗位 + ID 去重,招聘数为空、填写“若干”或非正数时按 1 人计算。 +
); @@ -563,6 +608,7 @@ const CrossCityFair: React.FC = () => { }; const tabItems = [ + /* 暂时隐藏各地市就业信息栏目,保留原实现便于后续恢复。 { key: 'city-jobs', label: ( @@ -573,6 +619,7 @@ const CrossCityFair: React.FC = () => { ), children: , }, + */ { key: 'statistics', label: ( @@ -612,7 +659,7 @@ const CrossCityFair: React.FC = () => { }} > - + 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 [fairType, setFairType] = useState('online'); - const [scope, setScope] = useState('single'); + const [scope, setScope] = useState('range'); const [fairOptions, setFairOptions] = useState([]); const [selectedFairId, setSelectedFairId] = useState(); - const [dateRange, setDateRange] = useState(null); + const [dateRange, setDateRange] = useState(getDefaultDateRange()); const [loadingFairs, setLoadingFairs] = useState(false); const [loadingSummary, setLoadingSummary] = useState(false); const [summary, setSummary] = useState(); @@ -97,7 +105,7 @@ const FairStatistics: React.FC = () => { // 切换类型时同步清空条件,避免新类型携带旧类型招聘会 ID 发起查询。 setSelectedFairId(undefined); - setDateRange(null); + setDateRange(getDefaultDateRange()); setSummary(undefined); setFairOptions([]); setFairType(type); @@ -190,7 +198,11 @@ const FairStatistics: React.FC = () => { { label: '按起始时间统计', value: 'range' }, ]} onChange={(value) => { - setScope(value as StatisticsScope); + const nextScope = value as StatisticsScope; + if (nextScope === 'range' && !dateRange) { + setDateRange(getDefaultDateRange()); + } + setScope(nextScope); setSummary(undefined); }} /> @@ -211,6 +223,8 @@ const FairStatistics: React.FC = () => { ) : ( setDateRange(value)} style={{ width: '100%', minWidth: 320 }} placeholder={['起始日期', '结束日期']} diff --git a/src/pages/Jobfair/PublicJobFair/index.tsx b/src/pages/Jobfair/PublicJobFair/index.tsx index f765eca..dacc8a4 100644 --- a/src/pages/Jobfair/PublicJobFair/index.tsx +++ b/src/pages/Jobfair/PublicJobFair/index.tsx @@ -123,6 +123,7 @@ const PublicJobFairList: React.FC = () => { hideInSearch: true, ellipsis: true, }, + /* 暂时隐藏线上招聘会列表中的招聘会类型字段,保留原实现便于后续恢复。 { title: '类型', dataIndex: 'jobFairType', @@ -130,6 +131,7 @@ const PublicJobFairList: React.FC = () => { valueEnum: jobFairTypeEnum, render: (_, record) => , }, + */ { title: '区域类型', dataIndex: 'jobFairRegionType', diff --git a/src/services/jobfair/publicJobFair.ts b/src/services/jobfair/publicJobFair.ts index 1926531..6d8efad 100644 --- a/src/services/jobfair/publicJobFair.ts +++ b/src/services/jobfair/publicJobFair.ts @@ -30,6 +30,7 @@ export interface PublicJobFairJobForm { export interface CrossDomainJobItem { jobId: number; jobTitle: string; + view?: number; minSalary?: number; maxSalary?: number; education?: string; @@ -49,6 +50,10 @@ export interface CrossDomainJobItem { export interface CrossDomainJobParams { /** 逗号分隔的城市名称 */ cityNames?: string; + /** 招聘会起始日期(含当天) */ + startDate?: string; + /** 招聘会起始日期(含当天) */ + endDate?: string; current?: 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) { return request(`${BASE_URL}/list`, {