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
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
This commit is contained in:
@@ -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<CitySelectTabProps> = ({
|
||||
</Spin>
|
||||
<div style={{ marginTop: 12, color: '#888' }}>
|
||||
已选择 {selectedCities.length > 0 ? selectedCities.length + ' 个区域' : '全部区域'}
|
||||
<span style={{ marginLeft: 8 }}>(选择区域后,到「各地市就业信息」查看对应岗位)</span>
|
||||
{/* 暂时隐藏各地市就业信息栏目,保留原提示便于后续恢复。 */}
|
||||
{/* <span style={{ marginLeft: 8 }}>(选择区域后,到「各地市就业信息」查看对应岗位)</span> */}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -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<Record<string, number>>((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 (
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ padding: '0 0 24px 0' }}>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Space wrap>
|
||||
<span>统计时间</span>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
allowClear={false}
|
||||
presets={getDateRangePresets()}
|
||||
onChange={(value) => {
|
||||
if (value?.[0] && value?.[1]) {
|
||||
setDateRange(value as [Dayjs, Dayjs]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} sm={6}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="联盟城市数"
|
||||
value={stats.totalCities}
|
||||
prefix={<EnvironmentOutlined />}
|
||||
suffix="个"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="参会企业总数"
|
||||
title="参会企业"
|
||||
value={stats.totalCompanies}
|
||||
prefix={<BankOutlined />}
|
||||
suffix="家"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={6}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="发布岗位总数"
|
||||
title="发布岗位数"
|
||||
value={stats.totalJobs}
|
||||
prefix={<FileTextOutlined />}
|
||||
suffix="个"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={6}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="招聘需求人数"
|
||||
@@ -498,6 +528,16 @@ const DataStatsTab: React.FC = () => {
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="岗位浏览量"
|
||||
value={stats.totalViews}
|
||||
prefix={<EyeOutlined />}
|
||||
suffix="次"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -524,6 +564,11 @@ const DataStatsTab: React.FC = () => {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card size="small" title="统计口径">
|
||||
仅统计时间范围内招聘会类型为“线上”且标记为“跨域”的招聘会;参会企业按有效报名企业统计,即使企业未选择岗位也计入。岗位、招聘需求人数和浏览量均按岗位
|
||||
ID 去重,招聘数为空、填写“若干”或非正数时按 1 人计算。
|
||||
</Card>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
@@ -563,6 +608,7 @@ const CrossCityFair: React.FC = () => {
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
/* 暂时隐藏各地市就业信息栏目,保留原实现便于后续恢复。
|
||||
{
|
||||
key: 'city-jobs',
|
||||
label: (
|
||||
@@ -573,6 +619,7 @@ const CrossCityFair: React.FC = () => {
|
||||
),
|
||||
children: <JobsTab selectedCities={selectedCities} />,
|
||||
},
|
||||
*/
|
||||
{
|
||||
key: 'statistics',
|
||||
label: (
|
||||
@@ -612,7 +659,7 @@ const CrossCityFair: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Card>
|
||||
<Tabs defaultActiveKey="city-jobs" items={tabItems} tabBarStyle={{ marginBottom: 0 }} />
|
||||
<Tabs defaultActiveKey="statistics" items={tabItems} tabBarStyle={{ marginBottom: 0 }} />
|
||||
</Card>
|
||||
|
||||
<AllianceManageModal
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -37,12 +38,19 @@ 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 getDateRangePresets = () => [
|
||||
{ label: '近7天', value: [dayjs().subtract(6, 'day'), dayjs()] as [Dayjs, Dayjs] },
|
||||
{ label: '近1个月', value: getDefaultDateRange() },
|
||||
];
|
||||
|
||||
const FairStatistics: React.FC = () => {
|
||||
const [fairType, setFairType] = useState<RecruitmentFairType>('online');
|
||||
const [scope, setScope] = useState<StatisticsScope>('single');
|
||||
const [scope, setScope] = useState<StatisticsScope>('range');
|
||||
const [fairOptions, setFairOptions] = useState<FairOption[]>([]);
|
||||
const [selectedFairId, setSelectedFairId] = useState<string>();
|
||||
const [dateRange, setDateRange] = useState<any>(null);
|
||||
const [dateRange, setDateRange] = useState<any>(getDefaultDateRange());
|
||||
const [loadingFairs, setLoadingFairs] = useState(false);
|
||||
const [loadingSummary, setLoadingSummary] = useState(false);
|
||||
const [summary, setSummary] = useState<FairStatisticsSummary>();
|
||||
@@ -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 = () => {
|
||||
) : (
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
allowClear={false}
|
||||
presets={getDateRangePresets()}
|
||||
onChange={(value) => setDateRange(value)}
|
||||
style={{ width: '100%', minWidth: 320 }}
|
||||
placeholder={['起始日期', '结束日期']}
|
||||
|
||||
@@ -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) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />,
|
||||
},
|
||||
*/
|
||||
{
|
||||
title: '区域类型',
|
||||
dataIndex: 'jobFairRegionType',
|
||||
|
||||
@@ -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<API.PublicJobFair.ListResult>(`${BASE_URL}/list`, {
|
||||
|
||||
Reference in New Issue
Block a user