From 1bb796b5eff76070ada8a467cda42a454145824b Mon Sep 17 00:00:00 2001 From: yy <3078169442@qq.com> Date: Mon, 9 Jun 2025 11:18:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E6=96=B0=E5=A2=9E=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E5=88=86=E6=9E=90=E4=BA=94=E4=B8=AA=E5=9B=BE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Industrytrend/components/chartcards.tsx | 215 ++++++ .../Industrytrend/components/chartconfigs.tsx | 456 ++++++++++++ src/pages/Analysis/Industrytrend/index.tsx | 666 ++++++++---------- src/pages/Analysis/Industrytrend/utils.ts | 71 ++ src/services/analysis/industry.ts | 19 +- src/types/analysis/industry.d.ts | 4 +- 6 files changed, 1042 insertions(+), 389 deletions(-) create mode 100644 src/pages/Analysis/Industrytrend/components/chartcards.tsx create mode 100644 src/pages/Analysis/Industrytrend/components/chartconfigs.tsx diff --git a/src/pages/Analysis/Industrytrend/components/chartcards.tsx b/src/pages/Analysis/Industrytrend/components/chartcards.tsx new file mode 100644 index 0000000..f28cd47 --- /dev/null +++ b/src/pages/Analysis/Industrytrend/components/chartcards.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { Card, Select, Spin, Empty, Row, Col } from 'antd'; +import { Line, Bar, Pie, Heatmap } from '@ant-design/charts'; + +export const IndustryTrendCard = ({ + loading, + currentIndustryData, + config, + availableIndustries, + selectedIndustry, + onIndustryChange, +}) => ( + + {availableIndustries.map((industry: any) => ( + + ))} + + } + > + + {currentIndustryData.length > 0 ? ( + + ) : ( + + )} + + +); + +export const AreaAnalysisCard = ({ loading, areaData, config }) => ( + + {loading ? ( + + ) : areaData.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+); + +export const SalaryTrendCard = ({ + loading, + currentSalaryData, + config, + availableSalaryRanges, + selectedSalaryRange, + onSalaryRangeChange, +}) => ( + + {availableSalaryRanges.map((range) => ( + + ))} + + } + > + + {currentSalaryData.length > 0 ? ( + + ) : ( + + )} + + +); + +export const WorkYearCard = ({ + loading, + workYearData, + config, + availableWorkYearRanges, + selectedWorkYearRange, + onWorkYearRangeChange, +}) => ( + + {availableWorkYearRanges.map((range: any) => ( + + ))} + + } + > + + {workYearData && workYearData.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+
+); + +export const EducationCard = ({ + loading, + educationData, + config, + availableEducationLevels, + selectedEducationLevel, + onEducationLevelChange, +}) => ( + + + {availableEducationLevels.map((level) => ( + + ))} + + } + > + + {educationData.length > 0 ? ( + + ) : ( + + )} + + +); \ No newline at end of file diff --git a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx new file mode 100644 index 0000000..e1922ec --- /dev/null +++ b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx @@ -0,0 +1,456 @@ +import { ChartConfig } from '@/types/analysis/industry'; +import dayjs from 'dayjs'; +import { formatDateForDisplay } from '../utils'; + +export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { + const sortedData = [...areaData].sort((a, b) => { + if (timeDimension === '年') { + return parseInt(a.time) - parseInt(b.time); + } else if (timeDimension === '季度') { + const [yearA, quarterA] = a.time.split('-Q'); + const [yearB, quarterB] = b.time.split('-Q'); + return yearA === yearB + ? parseInt(quarterA) - parseInt(quarterB) + : parseInt(yearA) - parseInt(yearB); + } else { + return dayjs(a.time).valueOf() - dayjs(b.time).valueOf(); + } + }); + + return { + data: sortedData, + height: 240, + autoFit: true, + xField: 'name', + yField: 'time', + colorField: 'value', + shapeField: 'square', + sizeField: 'value', + xAxis: { + title: { + text: '区域', + style: { fontSize: 12 }, + }, + label: { + style: { + fontSize: 10, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 4 ? `${text.substring(0, 3)}...` : text; + }, + }, + }, + yAxis: { + title: { + text: '时间', + style: { fontSize: 12 }, + }, + label: { + formatter: (text: string) => { + if (timeDimension === '年') return `${text}年`; + if (timeDimension === '季度') return text.replace('-Q', '年Q'); + return text.replace('-', '年').replace('-', '月'); + }, + style: { + fontSize: 10, + fill: '#666', + }, + }, + }, + label: { + text: (d: { value: number }) => d.value.toString(), + position: 'inside', + style: { + fill: '#fff', + pointerEvents: 'none', + }, + }, + scale: { + size: { range: [14, 14] }, + color: { range: ['#dddddd', '#9ec8e0', '#5fa4cd', '#2e7ab6', '#114d90'] }, + }, + tooltip: { + title: (d: { name: any; time: any }) => `${d.name} - ${d.time}`, + field: 'value', + valueFormatter: (v: number) => v.toString(), + domStyles: { + 'g2-tooltip': { + padding: '8px 12px', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + responsive: true, + }; +}; + +export const getIndustryChartConfig = (currentIndustryData: any[], type: string): ChartConfig => ({ + data: currentIndustryData, + height: 200, + xField: 'date', + yField: 'value', + seriesField: 'category', + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + yAxis: { + label: { + formatter: (val: string) => `${val}${type === '招聘增长率' ? '%' : ''}`, + }, + }, + point: { + size: 4, + shape: 'circle', + }, + animation: { + appear: { + animation: 'path-in', + duration: 1000, + }, + }, + smooth: true, + interactions: [ + { + type: 'tooltip', + cfg: { + render: (e, { title, items }) => { + const list = items.filter((item) => item.value); + return ( +
+

{title}

+ {list.map((item, index) => { + const { name, value, color } = item; + return ( +
+
+ + {name} +
+ + {value} + {type === '招聘增长率' ? '%' : ''} + +
+ ); + })} +
+ ); + }, + }, + }, + ], + legend: false, + tooltip: { + showTitle: undefined, + title: undefined, + customContent: undefined, + }, +}); + +export const getSalaryChartConfig = (currentSalaryData: any[]): ChartConfig => ({ + data: currentSalaryData, + height: 240, + xField: 'date', + yField: 'value', + seriesField: 'category', + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + yAxis: { + label: { + formatter: (val: string) => `${val}`, + }, + }, + point: { + size: 4, + shape: 'circle', + }, + animation: { + appear: { + animation: 'path-in', + duration: 1000, + }, + }, + smooth: true, + interactions: [ + { + type: 'tooltip', + cfg: { + render: (e, { title, items }) => { + const list = items.filter((item) => item.value); + return ( +
+

{title}

+ {list.map((item, index) => { + const { name, value, color } = item; + return ( +
+
+ + {name} +
+ {value} +
+ ); + })} +
+ ); + }, + }, + }, + ], + legend: false, + tooltip: { + showTitle: undefined, + title: undefined, + customContent: undefined, + }, +}); + +export const getWorkYearPieConfig = (workYearData: any[], selectedWorkYearRange: string) => { + const filteredData = workYearData + .filter((item) => item.category === selectedWorkYearRange) + .map((item) => ({ + ...item, + date: formatDateForDisplay(item.date, '月'), + value: item.value || 0, + type: `${formatDateForDisplay(item.date, '月')} ${item.category}`, + })); + + return { + data: filteredData, + angleField: 'value', + colorField: 'type', + radius: 0.2, + innerRadius: 0.5, + label: { + text: (d: { type: any; value: any }) => `${d.type}\n ${d.value}`, + position: 'spider', + }, + legend: false, + tooltip: { + showTitle: true, + title: '工作经验分布', + fields: ['type', 'value'], + formatter: (datum: { type: any; value: any }) => ({ + name: datum.type, + value: datum.value, + }), + }, + interactions: [{ type: 'element-active' }], + padding: 'auto', + autoFit: true, + }; +}; + +export const getEducationBarConfig = (educationData: any[], selectedEducationLevel: string) => { + const educationLevelOrder = [ + '不限', + '初中及以下', + '中专/中技', + '高中', + '大专', + '本科', + '硕士', + '博士', + 'MBA/EMBA', + '留学-学士', + '留学-硕士', + '留学-博士', + ]; + + const educationColorMap: Record = { + 不限: '#8884d8', + 初中及以下: '#82ca9d', + '中专/中技': '#ffc658', + 高中: '#ff8042', + 大专: '#0088FE', + 本科: '#00C49F', + 硕士: '#FFBB28', + 博士: '#FF8042', + 'MBA/EMBA': '#8884d8', + '留学-学士': '#82ca9d', + '留学-硕士': '#ffc658', + '留学-博士': '#ff8042', + }; + + const cleanEducationData = educationData + .filter((item) => item && item.category && item.date && !isNaN(item.value)) + .map((item) => ({ + ...item, + date: formatDateForDisplay(item.date, '月'), + category: item.category || '不限', + value: Number(item.value) || 0, + })); + + if (!selectedEducationLevel) { + const educationSummary: Record = {}; + + cleanEducationData.forEach((item) => { + if (!educationSummary[item.category]) { + educationSummary[item.category] = 0; + } + educationSummary[item.category] += item.value; + }); + + const barData = Object.entries(educationSummary) + .filter(([_, value]) => value > 0) + .map(([name, value]) => ({ + name, + value, + color: educationColorMap[name] || '#999', + })) + .sort((a, b) => educationLevelOrder.indexOf(a.name) - educationLevelOrder.indexOf(b.name)); + + return { + data: barData, + height: 200, + xField: 'value', + yField: 'name', + seriesField: 'name', + color: ({ name }: { name: string }) => educationColorMap[name] || '#999', + meta: { + name: { alias: '学历要求' }, + value: { alias: '岗位数量' }, + }, + xAxis: { + label: { + formatter: (val: string) => `${val}`, + }, + grid: { + line: { + style: { + stroke: '#f0f0f0', + lineDash: [4, 4], + }, + }, + }, + }, + yAxis: { + label: { + formatter: (text: string) => text, + }, + }, + barStyle: { + radius: [2, 2, 0, 0], + }, + tooltip: { + showTitle: true, + title: '学历要求分布', + fields: ['name', 'value'], + formatter: (datum: { name: any; value: any }) => ({ + name: datum.name, + value: datum.value, + }), + domStyles: { + 'g2-tooltip': { + background: 'rgba(255, 255, 255, 0.9)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + legend: false, + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, + }, + }; + } + + const timeData = cleanEducationData + .filter((item) => item.category === selectedEducationLevel) + .sort((a, b) => { + const dateA = dayjs(a.date, 'YYYY年MM月').valueOf(); + const dateB = dayjs(b.date, 'YYYY年MM月').valueOf(); + return dateA - dateB; + }); + + return { + data: timeData, + height: 200, + xField: 'date', + yField: 'value', + seriesField: 'category', + color: educationColorMap[selectedEducationLevel] || '#999', + meta: { + date: { + alias: '时间', + type: 'cat', + values: timeData + .map((item) => item.date) + .sort((a, b) => { + const dateA = dayjs(a, 'YYYY年MM月').valueOf(); + const dateB = dayjs(b, 'YYYY年MM月').valueOf(); + return dateA - dateB; + }), + }, + value: { alias: '岗位数量' }, + }, + barStyle: { + radius: [2, 2, 0, 0], + }, + tooltip: { + showTitle: true, + title: `${selectedEducationLevel}趋势`, + fields: ['date', 'value'], + formatter: (datum: { date: any; value: any }) => ({ name: datum.date, value: datum.value }), + domStyles: { + 'g2-tooltip': { + background: 'rgba(255, 255, 255, 0.9)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + legend: false, + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, + }, + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + }; +}; \ No newline at end of file diff --git a/src/pages/Analysis/Industrytrend/index.tsx b/src/pages/Analysis/Industrytrend/index.tsx index 7e1c5d0..0c563d0 100644 --- a/src/pages/Analysis/Industrytrend/index.tsx +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -1,7 +1,13 @@ import React, { useEffect, useState, useMemo, useRef } from 'react'; -import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker, Tabs } from 'antd'; -import { Line, Heatmap } from '@ant-design/charts'; -import { getIndustryTrend, getIndustryAreaTrend, getSalaryTrend } from '@/services/analysis/industry'; +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 { @@ -11,24 +17,44 @@ import { IndustryDataItem, ChartConfig, } from '@/types/analysis/industry'; -import { formatQuarter, formatDateForDisplay, convertApiData, convertSalaryData } from './utils'; +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'; const { Option } = Select; const { RangePicker } = DatePicker; -const { TabPane } = Tabs; const flattenAreaData = (apiResponse: any) => { if (!apiResponse || typeof apiResponse !== 'object') { return []; } - const flattenedData = []; + const flattenedData: { name: any; time: any; value: number }[] = []; for (const month in apiResponse) { if (apiResponse.hasOwnProperty(month)) { const areas = apiResponse[month]; - areas.forEach((area) => { + areas.forEach((area: { name: any; time: any; data: string }) => { flattenedData.push({ name: area.name, time: area.time, @@ -49,7 +75,7 @@ const IndustryTrendPage: React.FC = () => { endTime: dayjs().format('YYYY-MM'), selectedIndustry: '', selectedSalaryRange: '', - analysisCategory: 'industry', // 默认显示行业分析 + selectedWorkYearRange: '', }); const [allData, setAllData] = useState([]); @@ -58,6 +84,12 @@ const IndustryTrendPage: React.FC = () => { 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( @@ -130,16 +162,14 @@ const IndustryTrendPage: React.FC = () => { // 获取薪资趋势数据 const { loading: salaryLoading, run: fetchSalaryData } = useRequest( async () => { - let { startTime, endTime, timeDimension, type } = params; - - if (timeDimension === '季度') { - startTime = formatQuarter(startTime); - endTime = formatQuarter(endTime); - } + 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, + type: '岗位发布数量', startTime, endTime, }); @@ -152,8 +182,11 @@ const IndustryTrendPage: React.FC = () => { const ranges = Array.from(new Set(formattedData.map((item: any) => item.category))) .filter(Boolean) - .sort(); - + .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)) { @@ -166,183 +199,180 @@ const IndustryTrendPage: React.FC = () => { }, ); - // 根据分析类别获取当前数据 - const currentData = useMemo(() => { - if (params.analysisCategory === 'industry') { - 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 { 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 = '月'; - 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 await getWorkYearTrend({ + timeDimension, + type: '岗位发布数量', + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (data) => { + const formattedData = convertWorkYearData(data); + setWorkYearData(formattedData); - 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(); - } - }); - } else if (params.analysisCategory === 'salary') { - return salaryData - .filter((item) => item.category === params.selectedSalaryRange) - .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 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); + }); - 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(); - } - }); - } - return []; - }, [allData, salaryData, params]); - - // 热力图配置 - const heatmapConfig = useMemo(() => { - const sortedData = [...areaData].sort((a, b) => { - if (params.timeDimension === '年') { - return parseInt(a.time) - parseInt(b.time); - } else if (params.timeDimension === '季度') { - const [yearA, quarterA] = a.time.split('-Q'); - const [yearB, quarterB] = b.time.split('-Q'); - return yearA === yearB - ? parseInt(quarterA) - parseInt(quarterB) - : parseInt(yearA) - parseInt(yearB); - } else { - return dayjs(a.time).valueOf() - dayjs(b.time).valueOf(); - } - }); - - // 计算最大值和阈值 - const maxValue = Math.max(...sortedData.map((item) => item.value), 1); - const hotThreshold = maxValue * 0.8; // 热门阈值(前20%) - const growthThreshold = maxValue * 0.5; // 增长阈值(前50%) - - return { - data: sortedData, - height: 300, - autoFit: false, - xField: 'name', - yField: 'time', - colorField: 'value', - mark: 'cell', - style: { - inset: 1, - fillOpacity: 0.9, + setAvailableWorkYearRanges(ranges); + if (ranges.length > 0 && !ranges.includes(params.selectedWorkYearRange)) { + setParams((p) => ({ ...p, selectedWorkYearRange: ranges[0] })); + } }, - cellSize: [100, 40], - color: ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A'], - label: { - text: (d: { value: number }) => { - if (d.value >= 10000) return (d.value / 10000).toFixed(0) + 'w'; - if (d.value >= 1000) return (d.value / 1000).toFixed(0) + 'k'; - return d.value; - }, - style: { - fill: '#fff', - fontSize: 12, - fontWeight: 'bold', - textShadow: '0 0 3px rgba(0,0,0,0.7)', - }, - position: 'inside', + onError: (error) => { + message.error('工作经验数据加载失败'); }, - xAxis: { - title: { - text: '区域', - style: { fontSize: 12 }, - }, - label: { - autoRotate: true, - style: { - fontSize: 11, - fill: '#666', - }, - }, + }, + ); + + // 获取学历数据 + 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]); + } }, - yAxis: { - title: { - text: '时间', - style: { fontSize: 12 }, - }, - label: { - formatter: (text) => { - if (params.timeDimension === '年') return `${text}年`; - if (params.timeDimension === '季度') return text.replace('-Q', '年Q'); - return text.replace('-', '年').replace('-', '月'); - }, - style: { - fontSize: 11, - fill: '#666', - }, - }, - sortable: false, + onError: (error) => { + message.error('学历数据加载失败'); }, - tooltip: { - title: (d) => `${d.name} - ${d.time}`, - field: 'value', - valueFormatter: (v) => { - if (v >= 10000) return (v / 10000).toFixed(1) + '万'; - if (v >= 1000) return (v / 1000).toFixed(1) + '千'; - return v; - }, - pointerEvents: 'none', - domStyles: { - 'g2-tooltip': { - padding: '8px 12px', - borderRadius: '4px', - }, - }, - }, - interactions: [{ type: 'element-active' }], - legend: { - position: 'bottom', - layout: 'horizontal', - slidable: true, - title: false, - itemName: { - style: { - fill: '#666', - fontSize: 12, - }, - }, - }, - }; - }, [areaData, params.timeDimension]); + }, + ); + + 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(); @@ -366,8 +396,7 @@ const IndustryTrendPage: React.FC = () => { : value === '季度' ? now.format('YYYY-Q') : now.format('YYYY'), - selectedIndustry: params.analysisCategory === 'industry' ? '' : p.selectedIndustry, - selectedSalaryRange: params.analysisCategory === 'salary' ? '' : p.selectedSalaryRange, + selectedIndustry: '', })); }; @@ -418,146 +447,19 @@ const IndustryTrendPage: React.FC = () => { } }; - const chartConfig: ChartConfig = { - data: currentData, - height: 180, - xField: 'date', - yField: 'value', - seriesField: 'category', - xAxis: { - type: 'cat', - label: { - formatter: (text: string) => text, - }, - }, - yAxis: { - label: { - formatter: (val: string) => `${val}${params.type === '招聘增长率' ? '%' : ''}`, - }, - }, - point: { - size: 4, - shape: 'circle', - }, - animation: { - appear: { - animation: 'path-in', - duration: 1000, - }, - }, - smooth: true, - interactions: [ - { - type: 'tooltip', - cfg: { - render: (e, { title, items }) => { - const list = items.filter((item) => item.value); - return ( -
-

{title}

- {list.map((item, index) => { - const { name, value, color } = item; - return ( -
-
- - {name} -
- - {value} - {params.type === '招聘增长率' ? '%' : ''} - -
- ); - })} -
- ); - }, - }, - }, - ], - legend: false, - tooltip: { - showTitle: undefined, - title: undefined, - customContent: undefined, - }, - }; - useEffect(() => { - if (params.analysisCategory === 'industry') { - fetchIndustryData(); - fetchAreaData(); - } else if (params.analysisCategory === 'salary') { - fetchSalaryData(); - fetchAreaData(); - } - }, [params.timeDimension, params.startTime, params.endTime, params.type, params.analysisCategory]); + fetchIndustryData(); + fetchAreaData(); + fetchSalaryData(); + fetchWorkYearData(); + fetchEducationData(); + }, [params.timeDimension, params.startTime, params.endTime, params.type]); return ( -
+
- setParams(p => ({ - ...p, - analysisCategory: key as 'industry' | 'salary' | 'area', - selectedIndustry: key === 'industry' && availableIndustries.length > 0 ? availableIndustries[0] : p.selectedIndustry, - selectedSalaryRange: key === 'salary' && availableSalaryRanges.length > 0 ? availableSalaryRanges[0] : p.selectedSalaryRange - }))} - > - - - -
- {params.analysisCategory === 'industry' && ( - - )} - - {params.analysisCategory === 'salary' && ( - - )} -