Files
shz-admin/src/pages/Jobfair/FairStatistics/index.tsx
lapuda 7b9e8426de
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
feat: add attendee and device management tabs, and implement outdoor fair statistics
2026-06-26 11:26:08 +08:00

210 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState } from 'react';
import {
Card,
Col,
DatePicker,
Empty,
Row,
Segmented,
Spin,
Statistic,
Table,
Typography,
message,
} from 'antd';
import { ArrowDownOutlined, ArrowUpOutlined } 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';
const { RangePicker } = DatePicker;
const { Text } = Typography;
type Granularity = 'day' | 'week' | 'month' | 'year';
const GRAN_OPTIONS: { label: string; value: Granularity }[] = [
{ label: '按日', value: 'day' },
{ label: '按周', value: 'week' },
{ label: '按月', value: 'month' },
{ label: '按年', value: 'year' },
];
const formatBound = (value: any, suffix: string): string | undefined => {
if (value && typeof value.format === 'function') {
return `${value.format('YYYY-MM-DD')} ${suffix}`;
}
return undefined;
};
const FairStatistics: React.FC = () => {
const [granularity, setGranularity] = useState<Granularity>('month');
const [dateRange, setDateRange] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<OutdoorFairStatisticsResult | null>(null);
const fetchStats = async (gran: Granularity, range: any) => {
setLoading(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);
} else {
message.error(res?.msg || '统计失败');
}
} catch (e) {
message.error('统计失败');
} finally {
setLoading(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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [granularity, dateRange]);
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]);
const columns: ColumnsType<OutdoorFairStatisticsSeriesItem> = [
{ title: '时间', dataIndex: 'time', width: 140 },
{ title: '招聘会数', dataIndex: 'fairCount', width: 120 },
{ title: '参会单位数', dataIndex: 'companyCount', width: 120 },
{ title: '职位数', dataIndex: 'jobCount', width: 120 },
];
return (
<PageContainer
header={{ title: '招聘会数据统计' }}
content="按年/周/月/日维度统计时间区间内的招聘会趋势,含累计发布招聘会数及同比、环比。"
>
<Card style={{ marginBottom: 16 }}>
<Row gutter={16} align="middle">
<Col>
<span style={{ marginRight: 8 }}></span>
<Segmented
value={granularity}
onChange={(v) => setGranularity(v as Granularity)}
options={GRAN_OPTIONS}
/>
</Col>
<Col flex="auto">
<span style={{ marginRight: 8, marginLeft: 16 }}></span>
<RangePicker
value={dateRange}
onChange={(v) => setDateRange(v)}
style={{ width: 360 }}
/>
</Col>
</Row>
</Card>
<Spin spinning={loading}>
{result ? (
<>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={8}>
<Card>
<Statistic title="累计发布招聘会数" value={result.totals.fairCount} />
<div style={{ marginTop: 8 }}>
<RateText label="同比" rate={result.yoy?.rate} />
<span style={{ marginLeft: 16 }}>
<RateText label="环比" rate={result.mom?.rate} />
</span>
</div>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="参会单位数" value={result.totals.companyCount} />
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic title="职位数" value={result.totals.jobCount} />
</Card>
</Col>
</Row>
<Card title="招聘会趋势" style={{ marginBottom: 16 }} bodyStyle={{ padding: 12 }}>
{chartData.length > 0 ? (
<Line
height={320}
data={chartData}
xField="time"
yField="value"
seriesField="category"
smooth
legend={{ position: 'bottom' }}
tooltip={{
fields: ['value'],
}}
/>
) : (
<Empty description="所选区间无数据" />
)}
</Card>
<Card title="分时段明细" bordered={false}>
<Table<OutdoorFairStatisticsSeriesItem>
rowKey="time"
dataSource={result.series || []}
columns={columns}
pagination={false}
size="small"
/>
</Card>
</>
) : (
<Empty description="暂无数据" />
)}
</Spin>
</PageContainer>
);
};
const RateText: React.FC<{ label: string; rate?: number | null }> = ({ label, rate }) => {
if (rate === null || rate === undefined) {
return (
<Text type="secondary">
{label}--
</Text>
);
}
const up = rate >= 0;
const color = up ? '#cf1322' : '#3f8600';
const pct = `${(rate * 100).toFixed(1)}%`;
return (
<Text style={{ color }}>
{label}
{up ? <ArrowUpOutlined /> : <ArrowDownOutlined />} {pct}
</Text>
);
};
export default FairStatistics;