import React, { useEffect, useState, useMemo, useRef } from 'react'; import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker } from 'antd'; import { Line, Bar, Pie, Heatmap } from '@ant-design/charts'; import { getIndustryTrend, getIndustryAreaTrend, getSalaryTrend, getWorkYearTrend, getEducationTrend, } from '@/services/analysis/industry'; import dayjs from 'dayjs'; import { useRequest } from '@umijs/max'; import { TimeDimension, AnalysisType, IndustryTrendState, IndustryDataItem, ChartConfig, } from '@/types/analysis/industry'; import { formatQuarter, formatDateForDisplay, convertApiData, convertSalaryData, convertWorkYearData, convertEducationData, } from './utils'; import { getHeatmapConfig, getIndustryChartConfig, getSalaryChartConfig, getWorkYearPieConfig, getEducationBarConfig, } from './components/chartconfigs'; import { IndustryTrendCard, AreaAnalysisCard, SalaryTrendCard, WorkYearCard, EducationCard, } from './components/chartcards'; import Chartline from './components/chartline' import Chartbar from './components/chartbar' const { Option } = Select; const { RangePicker } = DatePicker; const flattenAreaData = (apiResponse: any) => { if (!apiResponse || typeof apiResponse !== 'object') { return []; } const flattenedData: { name: any; time: any; value: number }[] = []; for (const month in apiResponse) { if (apiResponse.hasOwnProperty(month)) { const areas = apiResponse[month]; areas.forEach((area: { name: any; time: any; data: string }) => { flattenedData.push({ name: area.name, time: area.time, value: parseInt(area.data) || 0, }); }); } } return flattenedData; }; const IndustryTrendPage: React.FC = () => { const [params, setParams] = useState({ timeDimension: '月', type: '岗位发布数量', startTime: dayjs().subtract(5, 'month').format('YYYY-MM'), endTime: dayjs().format('YYYY-MM'), selectedIndustry: '', selectedSalaryRange: '', selectedWorkYearRange: '', }); const [allData, setAllData] = useState([]); const [areaData, setAreaData] = useState([]); const [salaryData, setSalaryData] = useState([]); const [availableIndustries, setAvailableIndustries] = useState([]); const [availableSalaryRanges, setAvailableSalaryRanges] = useState([]); const heatmapRef = useRef(null); const containerRef = useRef(null); const [workYearData, setWorkYearData] = useState([]); const [availableWorkYearRanges, setAvailableWorkYearRanges] = useState([]); const [educationData, setEducationData] = useState([]); const [availableEducationLevels, setAvailableEducationLevels] = useState([]); const [selectedEducationLevel, setSelectedEducationLevel] = useState(''); // 获取行业趋势数据 const { loading: industryLoading, run: fetchIndustryData } = useRequest( async () => { let { startTime, endTime, timeDimension, type } = params; if (timeDimension === '季度') { startTime = formatQuarter(startTime); endTime = formatQuarter(endTime); } return await getIndustryTrend({ timeDimension, type, startTime, endTime, }); }, { manual: true, onSuccess: (data) => { const formattedData = convertApiData(data); setAllData(formattedData); const industries = Array.from(new Set(formattedData.map((item: any) => item.category))) .filter(Boolean) .sort(); setAvailableIndustries(industries); if (industries.length > 0 && !industries.includes(params.selectedIndustry)) { setParams((p) => ({ ...p, selectedIndustry: industries[0] })); } }, onError: (error) => { message.error('行业数据加载失败'); }, }, ); // 获取地区趋势数据 const { loading: areaLoading, run: fetchAreaData } = useRequest( async () => { let { startTime, endTime, timeDimension, type } = params; if (timeDimension === '季度') { startTime = formatQuarter(startTime); endTime = formatQuarter(endTime); } return await getIndustryAreaTrend({ timeDimension, type, startTime, endTime, }); }, { manual: true, onSuccess: (res) => { const formattedData = flattenAreaData(res); setAreaData(formattedData); }, onError: (error) => { message.error('地区数据加载失败'); }, }, ); // 获取薪资趋势数据 const { loading: salaryLoading, run: fetchSalaryData } = useRequest( async () => { const now = dayjs(); let startTime = now.subtract(1, 'year').format('YYYY-MM'); let endTime = now.format('YYYY-MM'); const timeDimension = '月'; return await getSalaryTrend({ timeDimension, type: '岗位发布数量', startTime, endTime, }); }, { manual: true, onSuccess: (data) => { const formattedData = convertSalaryData(data); setSalaryData(formattedData); const ranges = Array.from(new Set(formattedData.map((item: any) => item.category))) .filter(Boolean) .sort((a, b) => { const extractNumber = (str: string) => parseInt(str.replace(/[^0-9]/g, '') || 0); return extractNumber(a) - extractNumber(b); }); setAvailableSalaryRanges(ranges); if (ranges.length > 0 && !ranges.includes(params.selectedSalaryRange)) { setParams((p) => ({ ...p, selectedSalaryRange: ranges[0] })); } }, onError: (error) => { message.error('薪资数据加载失败'); }, }, ); // 获取工作年限数据 const { loading: workYearLoading, run: fetchWorkYearData } = useRequest( async () => { const now = dayjs(); let startTime = now.subtract(1, 'year').format('YYYY-MM'); let endTime = now.format('YYYY-MM'); const timeDimension = '月'; return await getWorkYearTrend({ timeDimension, type: '岗位发布数量', startTime, endTime, }); }, { manual: true, onSuccess: (data) => { const formattedData = convertWorkYearData(data); setWorkYearData(formattedData); const ranges = Array.from(new Set(formattedData.map((item: any) => item.category))) .filter(Boolean) .sort((a, b) => { const order = ['应届', '1-3年', '3-5年', '5年以上']; return order.indexOf(a) - order.indexOf(b); }); setAvailableWorkYearRanges(ranges); if (ranges.length > 0 && !ranges.includes(params.selectedWorkYearRange)) { setParams((p) => ({ ...p, selectedWorkYearRange: ranges[0] })); } }, onError: (error) => { message.error('工作经验数据加载失败'); }, }, ); // 获取学历数据 const { loading: educationLoading, run: fetchEducationData } = useRequest( async () => { const now = dayjs(); let startTime = now.subtract(1, 'year').format('YYYY-MM'); let endTime = now.format('YYYY-MM'); const timeDimension = '月'; return await getEducationTrend({ timeDimension, type: '岗位发布数量', startTime, endTime, }); }, { manual: true, onSuccess: (data) => { const formattedData = convertEducationData(data); setEducationData(formattedData); const levels = Array.from(new Set(formattedData.map((item: any) => item.category))) .filter(Boolean) .sort((a, b) => { const order = ['不限', '初中及以下', '中专/中技', '大专', '本科', '硕士', '博士']; return order.indexOf(a) - order.indexOf(b); }); setAvailableEducationLevels(levels); if (levels.length > 0 && !levels.includes(selectedEducationLevel)) { setSelectedEducationLevel(levels[0]); } }, onError: (error) => { message.error('学历数据加载失败'); }, }, ); const currentWorkYearData = useMemo(() => { if (!params.selectedWorkYearRange || workYearData.length === 0) return []; return workYearData .filter((item) => item.category === params.selectedWorkYearRange) .map((item) => ({ ...item, originalDate: item.date, date: formatDateForDisplay(item.date, '月'), })) .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); }, [workYearData, params.selectedWorkYearRange]); const currentIndustryData = useMemo(() => { if (!params.selectedIndustry || allData.length === 0) return []; return allData .filter((item) => item.category === params.selectedIndustry) .map((item) => ({ ...item, originalDate: item.date, date: formatDateForDisplay(item.date, params.timeDimension), })) .sort((a, b) => { if (params.timeDimension === '季度') { const [yearA, quarterA] = a.originalDate.split('-'); const [yearB, quarterB] = b.originalDate.split('-'); const quarterToNumber = (q: string) => { if (q.includes('第一')) return 1; if (q.includes('第二')) return 2; if (q.includes('第三')) return 3; if (q.includes('第四')) return 4; return 0; }; return yearA === yearB ? quarterToNumber(quarterA) - quarterToNumber(quarterB) : parseInt(yearA) - parseInt(yearB); } else if (params.timeDimension === '年') { return parseInt(a.originalDate) - parseInt(b.originalDate); } else { return dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf(); } }); }, [allData, params.selectedIndustry, params.timeDimension]); const currentSalaryData = useMemo(() => { if (!params.selectedSalaryRange || salaryData.length === 0) return []; return salaryData .filter((item) => item.category === params.selectedSalaryRange) .map((item) => ({ ...item, originalDate: item.date, date: formatDateForDisplay(item.date, '月'), })) .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); }, [salaryData, params.selectedSalaryRange]); const currentEducationData = useMemo(() => { if (!selectedEducationLevel || educationData.length === 0) return []; return educationData .filter((item) => item.category === selectedEducationLevel) .map((item) => ({ ...item, originalDate: item.date, date: formatDateForDisplay(item.date, '月'), })) .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); }, [educationData, selectedEducationLevel]); const heatmapConfig = useMemo( () => getHeatmapConfig(areaData, params.timeDimension), [areaData, params.timeDimension], ); const industryChartConfig = useMemo( () => getIndustryChartConfig(currentIndustryData, params.type), [currentIndustryData, params.type], ); const salaryChartConfig = useMemo( () => getSalaryChartConfig(currentSalaryData), [currentSalaryData], ); const workYearPieConfig = useMemo( () => getWorkYearPieConfig(workYearData, params.selectedWorkYearRange), [workYearData, params.selectedWorkYearRange], ); const educationBarConfig = useMemo( () => getEducationBarConfig(educationData, selectedEducationLevel), [educationData, selectedEducationLevel], ); const handleTimeDimensionChange = (value: TimeDimension) => { const now = dayjs(); let newStartTime = ''; if (value === '月') { newStartTime = now.subtract(5, 'month').format('YYYY-MM'); } else if (value === '季度') { newStartTime = now.subtract(6, 'quarter').format('YYYY-Q'); } else { newStartTime = now.subtract(5, 'year').format('YYYY'); } setParams((p) => ({ ...p, timeDimension: value, startTime: newStartTime, endTime: value === '月' ? now.format('YYYY-MM') : value === '季度' ? now.format('YYYY-Q') : now.format('YYYY'), selectedIndustry: '', })); }; const handleDateRangeChange = (dates: any, dateStrings: [string, string]) => { if (dates && dates[0] && dates[1]) { setParams((p) => ({ ...p, startTime: dateStrings[0], endTime: dateStrings[1], })); } }; const disabledDate = (current: dayjs.Dayjs) => { const now = dayjs(); if (params.timeDimension === '月') { return current.isAfter(now.endOf('month')); } else if (params.timeDimension === '季度') { return current.isAfter(now.endOf('quarter')); } else { return current.isAfter(now.endOf('year')); } }; const getPickerValue = () => { try { return [ dayjs( params.startTime, params.timeDimension === '年' ? 'YYYY' : params.timeDimension === '季度' ? 'YYYY-Q' : 'YYYY-MM', ), dayjs( params.endTime, params.timeDimension === '年' ? 'YYYY' : params.timeDimension === '季度' ? 'YYYY-Q' : 'YYYY-MM', ), ]; } catch (e) { return null; } }; useEffect(() => { fetchIndustryData(); fetchAreaData(); fetchSalaryData(); fetchWorkYearData(); fetchEducationData(); }, [params.timeDimension, params.startTime, params.endTime, params.type]); return (
{/* 行业趋势图表 - 全宽显示 */} setParams((p) => ({ ...p, selectedIndustry: value }))} /> {/* 工作经验和学历要求 - 中等屏幕下分成两列 */} setParams((p) => ({ ...p, selectedWorkYearRange: value })) } /> {/* */} {/* 区域分析和薪资趋势 - 中等屏幕下分成两列 */} {/* */} setParams((p) => ({ ...p, selectedSalaryRange: value })) } />
); }; export default IndustryTrendPage;