From 1c9216dbbb651777df46136cf38ad6b2af6099ae Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 23 Jul 2026 18:13:30 +0800 Subject: [PATCH] feat: add recruitment fair statistics dashboard --- src/pages/Jobfair/FairStatistics/index.tsx | 377 ++++++++++++--------- src/services/jobfair/fairStatistics.ts | 45 +++ 2 files changed, 271 insertions(+), 151 deletions(-) create mode 100644 src/services/jobfair/fairStatistics.ts diff --git a/src/pages/Jobfair/FairStatistics/index.tsx b/src/pages/Jobfair/FairStatistics/index.tsx index cb8f84a..f555d8d 100644 --- a/src/pages/Jobfair/FairStatistics/index.tsx +++ b/src/pages/Jobfair/FairStatistics/index.tsx @@ -1,209 +1,284 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + Button, Card, Col, DatePicker, Empty, Row, Segmented, + Select, Spin, Statistic, Table, - Typography, message, } from 'antd'; -import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; -import { Line } from '@ant-design/charts'; import type { ColumnsType } from 'antd/es/table'; -import { getOutdoorFairStatistics } from '@/services/jobportal/outdoorFair'; -import type { - OutdoorFairStatisticsResult, - OutdoorFairStatisticsSeriesItem, -} from '@/services/jobportal/outdoorFair'; +import { getPublicJobFairList } from '@/services/jobfair/publicJobFair'; +import { getOutdoorFairList } from '@/services/jobportal/outdoorFair'; +import { + getFairStatisticsSummary, + type FairStatisticsFairItem, + type FairStatisticsSummary, + type RecruitmentFairType, +} from '@/services/jobfair/fairStatistics'; const { RangePicker } = DatePicker; -const { Text } = Typography; -type Granularity = 'day' | 'week' | 'month' | 'year'; +type StatisticsScope = 'single' | 'range'; -const GRAN_OPTIONS: { label: string; value: Granularity }[] = [ - { label: '按日', value: 'day' }, - { label: '按周', value: 'week' }, - { label: '按月', value: 'month' }, - { label: '按年', value: 'year' }, -]; +interface FairOption { + value: string; + label: string; +} -const formatBound = (value: any, suffix: string): string | undefined => { - if (value && typeof value.format === 'function') { - return `${value.format('YYYY-MM-DD')} ${suffix}`; - } - return undefined; -}; +const formatDate = (value: any): string | undefined => + value && typeof value.format === 'function' ? value.format('YYYY-MM-DD') : undefined; const FairStatistics: React.FC = () => { - const [granularity, setGranularity] = useState('month'); + const [fairType, setFairType] = useState('online'); + const [scope, setScope] = useState('single'); + const [fairOptions, setFairOptions] = useState([]); + const [selectedFairId, setSelectedFairId] = useState(); const [dateRange, setDateRange] = useState(null); - const [loading, setLoading] = useState(false); - const [result, setResult] = useState(null); + const [loadingFairs, setLoadingFairs] = useState(false); + const [loadingSummary, setLoadingSummary] = useState(false); + const [summary, setSummary] = useState(); - const fetchStats = async (gran: Granularity, range: any) => { - setLoading(true); + const interactionLabel = fairType === 'online' ? '投递岗位数' : '签到数'; + const interactionUnit = fairType === 'online' ? '个' : '人次'; + + const loadFairOptions = useCallback(async (type: RecruitmentFairType) => { + setLoadingFairs(true); try { - const res = await getOutdoorFairStatistics({ - granularity: gran, - startTime: formatBound(range?.[0], '00:00:00'), - endTime: formatBound(range?.[1], '23:59:59'), - }); - if (res?.code === 200 && res.data) { - setResult(res.data); + if (type === 'online') { + const res = await getPublicJobFairList({ pageNum: 1, pageSize: 1000, jobFairType: '1' }); + if (res.code !== 200) { + message.error(res.msg || '线上招聘会列表加载失败'); + setFairOptions([]); + return; + } + setFairOptions( + (res.rows || []).map((item) => ({ + value: String(item.jobFairId), + label: `${item.jobFairTitle}${item.jobFairStartTime ? `(${item.jobFairStartTime})` : ''}`, + })), + ); } else { - message.error(res?.msg || '统计失败'); + const res = await getOutdoorFairList({ current: 1, pageSize: 1000 }); + if (res.code !== 200) { + message.error(res.msg || '户外招聘会列表加载失败'); + setFairOptions([]); + return; + } + setFairOptions( + (res.rows || []).map((item) => ({ + value: String(item.id), + label: `${item.title}${item.holdTime ? `(${item.holdTime})` : ''}`, + })), + ); } - } catch (e) { - message.error('统计失败'); + } catch { + message.error('招聘会列表加载失败,请稍后重试'); + setFairOptions([]); } finally { - setLoading(false); + setLoadingFairs(false); } - }; + }, []); - // 粒度或日期变化即查询;区间半选(只选了一端)不查询,避免中途无效查询 useEffect(() => { - const complete = dateRange && dateRange[0] && dateRange[1]; - const empty = !dateRange || (!dateRange[0] && !dateRange[1]); - if (complete) { - fetchStats(granularity, dateRange); - } else if (empty) { - fetchStats(granularity, null); + setSelectedFairId(undefined); + setDateRange(null); + setSummary(undefined); + loadFairOptions(fairType); + }, [fairType, loadFairOptions]); + + const queryReady = useMemo(() => { + if (scope === 'single') return Boolean(selectedFairId); + return Boolean(dateRange?.[0] && dateRange?.[1]); + }, [dateRange, scope, selectedFairId]); + + const loadSummary = useCallback(async () => { + if (!queryReady) { + setSummary(undefined); + return; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [granularity, dateRange]); + setLoadingSummary(true); + try { + const res = await getFairStatisticsSummary({ + fairType, + fairId: scope === 'single' ? selectedFairId : undefined, + startDate: scope === 'range' ? formatDate(dateRange?.[0]) : undefined, + endDate: scope === 'range' ? formatDate(dateRange?.[1]) : undefined, + }); + if (res.code === 200 && res.data) { + setSummary(res.data); + } else { + setSummary(undefined); + message.error(res.msg || '统计数据加载失败'); + } + } catch { + setSummary(undefined); + message.error('统计数据加载失败,请稍后重试'); + } finally { + setLoadingSummary(false); + } + }, [dateRange, fairType, queryReady, scope, selectedFairId]); - const chartData = useMemo(() => { - if (!result?.series) return []; - const rows: { time: string; category: string; value: number }[] = []; - result.series.forEach((item: OutdoorFairStatisticsSeriesItem) => { - rows.push({ time: item.time, category: '招聘会数', value: item.fairCount }); - rows.push({ time: item.time, category: '参会单位数', value: item.companyCount }); - rows.push({ time: item.time, category: '职位数', value: item.jobCount }); - }); - return rows; - }, [result]); + useEffect(() => { + loadSummary(); + }, [loadSummary]); - const columns: ColumnsType = [ - { title: '时间', dataIndex: 'time', width: 140 }, - { title: '招聘会数', dataIndex: 'fairCount', width: 120 }, - { title: '参会单位数', dataIndex: 'companyCount', width: 120 }, - { title: '职位数', dataIndex: 'jobCount', width: 120 }, + const columns: ColumnsType = useMemo( + () => [ + { title: '招聘会名称', dataIndex: 'fairTitle', ellipsis: true }, + { title: '起始时间', dataIndex: 'startTime', width: 180, render: (value) => value || '--' }, + { title: '参会单位数', dataIndex: 'companyCount', width: 110, align: 'right' }, + { title: '职位数', dataIndex: 'jobCount', width: 90, align: 'right' }, + { title: '需求人数', dataIndex: 'demandCount', width: 100, align: 'right' }, + { title: interactionLabel, dataIndex: 'interactionCount', width: 120, align: 'right' }, + ], + [interactionLabel], + ); + + const statisticsCards = [ + { title: '参会单位数', value: summary?.companyCount ?? 0, suffix: '家' }, + { title: '职位数', value: summary?.jobCount ?? 0, suffix: '个' }, + { title: '需求人数', value: summary?.demandCount ?? 0, suffix: '人' }, + { title: interactionLabel, value: summary?.interactionCount ?? 0, suffix: interactionUnit }, ]; return ( - + - 统计粒度 + + + + + + + 统计范围 setGranularity(v as Granularity)} - options={GRAN_OPTIONS} + value={scope} + options={[ + { label: '统计单场', value: 'single' }, + { label: '按起始时间统计', value: 'range' }, + ]} + onChange={(value) => { + setScope(value as StatisticsScope); + setSummary(undefined); + }} /> - 时间区间 - setDateRange(v)} - style={{ width: 360 }} - /> + {scope === 'single' ? ( +