diff --git a/src/pages/Analysis/Industrytrend/components/chartcards.tsx b/src/pages/Analysis/Industrytrend/components/chartcards.tsx index f28cd47..2cf3f32 100644 --- a/src/pages/Analysis/Industrytrend/components/chartcards.tsx +++ b/src/pages/Analysis/Industrytrend/components/chartcards.tsx @@ -1,215 +1,210 @@ -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 +import React from 'react'; +import { Card, Empty, Select, Spin } from 'antd'; +import { Bar, Heatmap, Line, Pie } 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 ? ( + + ) : ( + + )} + + +); diff --git a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx index e1922ec..75f9dc7 100644 --- a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx +++ b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx @@ -1,456 +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 +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) => selectedWorkYearRange === '全部' || 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) => selectedEducationLevel === '全部' || 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, + }, + }, + }; +}; diff --git a/src/pages/Analysis/Industrytrend/index.tsx b/src/pages/Analysis/Industrytrend/index.tsx index 0c563d0..e61e37c 100644 --- a/src/pages/Analysis/Industrytrend/index.tsx +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -1,570 +1,564 @@ -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'; - -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, selectedSalaryRange: value })) - } - /> - - - {/* 工作经验和学历要求 - 中等屏幕下分成两列 */} - - - setParams((p) => ({ ...p, selectedWorkYearRange: value })) - } - /> - - - - - -
-
- ); -}; - -export default IndustryTrendPage; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Button, Card, Col, DatePicker, message, Row, Select, Space } from 'antd'; +import { + getEducationTrend, + getIndustryAreaTrend, + getIndustryTrend, + getSalaryTrend, + getWorkYearTrend, +} from '@/services/analysis/industry'; +import dayjs from 'dayjs'; +import { useRequest } from '@umijs/max'; +import { IndustryDataItem, IndustryTrendState, TimeDimension } from '@/types/analysis/industry'; +import { + convertApiData, + convertEducationData, + convertSalaryData, + convertWorkYearData, + formatDateForDisplay, + formatQuarter, +} from './utils'; +import { + getEducationBarConfig, + getHeatmapConfig, + getIndustryChartConfig, + getSalaryChartConfig, + getWorkYearPieConfig, +} from './components/chartconfigs'; +import { + AreaAnalysisCard, + EducationCard, + IndustryTrendCard, + SalaryTrendCard, + WorkYearCard, +} from './components/chartcards'; + +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); + }); + ranges.unshift('全部'); + 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); + }); + ranges.unshift('全部'); + 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); + }); + levels.unshift('全部'); + 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) => + params.selectedSalaryRange === '全部' || 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, selectedSalaryRange: value })) + } + /> + + {/* 工作经验和学历要求 - 中等屏幕下分成两列 */} + + { + return setParams((p) => ({ ...p, selectedWorkYearRange: value })); + }} + /> + + + + + +
+
+ ); +}; + +export default IndustryTrendPage; diff --git a/src/pages/Analysis/User/components/AreaStatics.tsx b/src/pages/Analysis/User/components/AreaStatics.tsx new file mode 100644 index 0000000..91172ab --- /dev/null +++ b/src/pages/Analysis/User/components/AreaStatics.tsx @@ -0,0 +1,197 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Table, Card, Select } from 'antd'; +import { getUserStaticsByArea } from '@/services/analysis/user'; +import { Pie, Column } from '@ant-design/charts'; +export default function AreaStatics({ data }) { + const [selectedArea, setSelectedArea] = useState(''); + const [byArea, setByArea] = useState({}); + + useEffect(() => { + if (data.length > 0) { + setSelectedArea(data[0].dictValue); + fetchAreaStatics(data[0].dictValue); + } + }, [data]); + + const fetchAreaStatics = async (areaCode) => { + const resData = await getUserStaticsByArea(areaCode); + if (resData.code === 200) { + const data = { + gender: transformNum(resData.data.gender), + education: transformNum(resData.data.education), + position: transformNum(resData.data.position), + salary: transformNum(resData.data.salary), + age: transformNum(resData.data.age), + }; + console.log(data.education); + setByArea(data); + } + }; + + const transformNum = useCallback((datas) => { + return datas.map((item) => ({ ...item, data: parseInt(item.data) })); + }, []); + + const handleAreaChange = (value) => { + setSelectedArea(value); + fetchAreaStatics(value); + }; + + const educationColumns = [ + { title: '学历', dataIndex: 'name', key: 'name' }, + { title: '数量', dataIndex: 'data', key: 'data' }, + ]; + + const genderColumns = [ + { title: '性别', dataIndex: 'name', key: 'name' }, + { title: '数量', dataIndex: 'data', key: 'data' }, + ]; + + const positionColumns = [ + { title: '求职岗位', dataIndex: 'name', key: 'name' }, + { title: '数量', dataIndex: 'data', key: 'data' }, + ]; + + const salaryColumns = [ + { title: '期望薪资', dataIndex: 'name', key: 'name' }, + { title: '数量', dataIndex: 'data', key: 'data' }, + ]; + + const ageColumns = [ + { title: '年龄阶段', dataIndex: 'name', key: 'name' }, + { title: '数量', dataIndex: 'data', key: 'data' }, + ]; + + return ( +
+ +
+
+ +
+
+ {byArea.gender && byArea.gender.length > 0 ? ( +
+
+

用户数量:

  {byArea.gender.reduce((acc, cur) => acc + cur.data, 0)} +
+
男性:  {byArea.gender.find((item) => item.name === '男')?.data || 0}
+
女性:  {byArea.gender.find((item) => item.name === '女')?.data || 0}
+
+ ) : ( +
暂无数据
+ )} +
+
+
+ + `${d.data}人`, + textBaseline: 'bottom', + }} + axis={{ + y: { + label: { + formatter: (v) => `${v}k`, + }, + }, + }} + style={{ + radiusTopLeft: 3, + radiusTopRight: 3, + columnWidthRatio: 0.6, + inset: 0.5, + }} + /> + + + + + + + + `${d.data}人`, + textBaseline: 'bottom', + }} + axis={{ + y: { + label: { + formatter: (v) => `${v}k`, + }, + }, + }} + style={{ + radiusTopLeft: 3, + radiusTopRight: 3, + columnWidthRatio: 0.6, + inset: 0.5, + }} + /> + + + + `${d.data}人`, + textBaseline: 'bottom', + }} + axis={{ + y: { + label: { + formatter: (v) => `${v}k`, + }, + }, + }} + style={{ + radiusTopLeft: 5, + radiusTopRight: 5, + columnWidthRatio: 0.3, + inset: 0.5, + }} + /> + + + + + ); +} diff --git a/src/pages/Analysis/User/components/KeywordChart.tsx b/src/pages/Analysis/User/components/KeywordChart.tsx new file mode 100644 index 0000000..b3b743a --- /dev/null +++ b/src/pages/Analysis/User/components/KeywordChart.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Column } from '@ant-design/charts'; +import { getKeyWordSearchRank } from '@/services/analysis/user'; + +interface KeywordChartProps { + data?: any[]; +} + +export default function KeywordChart({ data: initialData }: KeywordChartProps) { + const [data, setData] = useState(initialData || []); + + const fetchData = useCallback( + async (params?: { searchDate?: string; searchCount?: number; searchType?: 1 | 2 | 3 }) => { + const resData = await getKeyWordSearchRank({ + searchDate: params?.searchDate || '2025-11-03', + // searchCount: params?.searchCount || 4, + searchType: params?.searchType || 3, + }); + if (resData.code === 200) { + setData(resData.rows); + } + }, + [], + ); + + useEffect(() => { + fetchData(); + }, [fetchData]); + const config = { + data, + xField: 'keyWord', + yField: 'searchCount', + height: 350, + label: { + position: 'middle', + style: { + fill: '#FFFFFF', + opacity: 0.6, + }, + }, + xAxis: { + label: { + autoHide: true, + autoRotate: false, + }, + }, + meta: { + keyWord: { alias: '关键词' }, + searchCount: { alias: '搜索次数' }, + }, + }; + + return ( + <> +
+

查询关键词搜索排行

+ +
+
+ {data?.map((item, index) => ( +
+ 关键词:{item.keyWord} + 搜索次数:{item.searchCount} +
+ ))} +
+ + ); +} diff --git a/src/pages/Analysis/User/components/WantedPositionStatics.tsx b/src/pages/Analysis/User/components/WantedPositionStatics.tsx new file mode 100644 index 0000000..6c61227 --- /dev/null +++ b/src/pages/Analysis/User/components/WantedPositionStatics.tsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { getWantedPositionStaticsCount } from '@/services/analysis/user'; +import { Card, Table } from 'antd'; + +interface PositionStatics { + jobName: string; + count: number; +} + +interface getWantedPositionStaticsCountStaticsResult { + male: PositionStatics[]; + female: PositionStatics[]; +} + +const WantedPositionStatics: React.FC = () => { + const [staticsData, setStaticsData] = useState({ male: [], female: [] }); + + const fetchStatics = useCallback(async () => { + const resData = await getWantedPositionStaticsCount({ sexCode: 0 }); + if (resData.code === 200) { + const malePositions: Record = {}; + const femalePositions: Record = {}; + + resData.data.forEach((item) => { + if (item.sexCode === '男') { + malePositions[item.jobName] = (malePositions[item.jobName] || 0) + 1; + } else if (item.sexCode === '女') { + femalePositions[item.jobName] = (femalePositions[item.jobName] || 0) + 1; + } + }); + + const result: StaticsResult = { + male: Object.entries(malePositions).map(([jobName, count]) => ({ jobName, count })), + female: Object.entries(femalePositions).map(([jobName, count]) => ({ jobName, count })), + }; + + setStaticsData(result); + } + }, []); + + useEffect(() => { + fetchStatics(); + }, [fetchStatics]); + + const columns = [ + { + title: '岗位名称', + dataIndex: 'jobName', + key: 'jobName', + }, + { + title: '数量', + dataIndex: 'count', + key: 'count', + }, + ]; + + return ( +
+ +
+ + +
+ + + ); +}; + +export default WantedPositionStatics; diff --git a/src/pages/Analysis/User/index.tsx b/src/pages/Analysis/User/index.tsx index adddf9c..34faffd 100644 --- a/src/pages/Analysis/User/index.tsx +++ b/src/pages/Analysis/User/index.tsx @@ -1 +1,60 @@ - \ No newline at end of file +import React, { useCallback, useEffect, useState } from 'react'; +import { + getUserLoginCountStatics, + getWantedPositionStaticsCount, + getUserStaticsByArea, + getPercentOfResumeCompletionRate, +} from '@/services/analysis/user'; +import KeywordChart from './components/KeywordChart'; +import { getDictDataList } from '@/services/system/dictdata'; +import AreaStatics from './components/AreaStatics'; +import WantedPositionStatics from './components/WantedPositionStatics'; + +export default function userAnalysis() { + const [data, setData] = useState([]); + + const [PofrcRate, setPofrcRate] = useState([]); + const [areaDict, setAreaDict] = useState([]); + + useEffect(() => { + // loginCounts(); + dictDataList(); + percentOfResumeCompletionRate(); + }, []); + + const loginCounts = useCallback(async () => { + let resData = await getUserLoginCountStatics(); + console.log(resData); + }, []); + + const dictDataList = useCallback(async () => { + let resData = await getDictDataList({ dictType: 'area' }); + if (resData.code === 200) { + setAreaDict(resData.rows); + } + }, []); + + const percentOfResumeCompletionRate = useCallback(async () => { + let resData = await getPercentOfResumeCompletionRate(); + if (resData.code === 200) { + console.log(resData.data); + setPofrcRate(resData.data); + } + }, []); + + return ( +
+
+
+ +
+
+ +
+
+
+ +
+
+ ); +} diff --git a/src/pages/Area/Business/UpLine/edit.tsx b/src/pages/Area/Business/UpLine/edit.tsx index 1cc3bae..2641490 100644 --- a/src/pages/Area/Business/UpLine/edit.tsx +++ b/src/pages/Area/Business/UpLine/edit.tsx @@ -70,7 +70,7 @@ const SubWayEdit: React.FC = (props) => { company: string; stationOrder: number; }> - title={`${props.values ? '编辑' : '新增'}站点`} + title={`${props.values ? '编辑' : '新增'}商圈`} form={form} // layout="inline" autoFocusFirstInput diff --git a/src/pages/Area/Business/edit.tsx b/src/pages/Area/Business/edit.tsx index 6506149..da741c9 100644 --- a/src/pages/Area/Business/edit.tsx +++ b/src/pages/Area/Business/edit.tsx @@ -47,7 +47,7 @@ const SubWayEdit: React.FC = (props) => { name: string; company: string; }> - title={`${props.values ? '编辑' : '新增'}线路`} + title={`${props.values ? '编辑' : '新增'}区域`} form={form} // layout="inline" autoFocusFirstInput diff --git a/src/pages/Company/List/index.tsx b/src/pages/Company/List/index.tsx index 86188d7..f4a61a6 100644 --- a/src/pages/Company/List/index.tsx +++ b/src/pages/Company/List/index.tsx @@ -1,8 +1,8 @@ import React, { Fragment, useEffect, useRef, useState } from 'react'; import { FormattedMessage, useAccess } from '@umijs/max'; -import { Button, FormInstance, message, Modal, Descriptions } from 'antd'; +import { Button, Descriptions, FormInstance, message, Modal } from 'antd'; import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; -import { DeleteOutlined, FormOutlined, PlusOutlined, EyeOutlined, AlignLeftOutlined } from '@ant-design/icons'; +import { AlignLeftOutlined, DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; import EditCompanyListRow from './edit'; import { addCmsCompanyList, @@ -15,14 +15,14 @@ import { getDictValueEnum } from '@/services/system/dict'; import DictTag from '@/components/DictTag'; // 详情查看组件 -const CompanyDetailModal = ({ - visible, - onCancel, +const CompanyDetailModal = ({ + visible, + onCancel, record, - scaleEnum -}: { - visible: boolean; - onCancel: () => void; + scaleEnum, +}: { + visible: boolean; + onCancel: () => void; record?: API.CompanyList.Company; scaleEnum: Record; }) => { @@ -107,12 +107,13 @@ function ManagementList() { valueType: 'text', align: 'center', }, - { - title: '公司行业', - dataIndex: 'industry', - valueType: 'text', - align: 'center', - }, + // { + // title: '公司行业', + // hideInSearch: true, + // dataIndex: 'industry', + // valueType: 'text', + // align: 'center', + // }, { title: '公司规模', dataIndex: 'scale', @@ -276,4 +277,4 @@ function ManagementList() { ); } -export default ManagementList; \ No newline at end of file +export default ManagementList; diff --git a/src/pages/Management/List/index.tsx b/src/pages/Management/List/index.tsx index e7ccd74..17fbe12 100644 --- a/src/pages/Management/List/index.tsx +++ b/src/pages/Management/List/index.tsx @@ -106,13 +106,13 @@ function ManagementList() { align: 'center', }, { - title: '最小薪资', + title: '最小薪资(元/月)', dataIndex: 'minSalary', valueType: 'text', align: 'center', }, { - title: '最大薪资', + title: '最大薪资(元/月)', dataIndex: 'maxSalary', valueType: 'text', align: 'center', diff --git a/src/services/analysis/user.ts b/src/services/analysis/user.ts new file mode 100644 index 0000000..b3c04ef --- /dev/null +++ b/src/services/analysis/user.ts @@ -0,0 +1,43 @@ +import { request } from '@umijs/max'; + +// 行业 +export async function getUserLoginCountStatics(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/getUserLoginCountStatics', { + method: 'GET', + params, + }); +} + + +// 查询关键词搜索排行 +export async function getKeyWordSearchRank(params?: { + searchDate?: string; + searchCount?: number; + searchType?: 1 | 2 | 3; +}) { + return request('/api/cms/statics/getKeyWordSearchRank', { + method: 'GET', + params, + }); +} + +// 统计男性和女性用户偏好的期望岗位 +export async function getWantedPositionStaticsCount(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/getWantedPositionStaticsCount', { + method: 'GET', + params, + }); +} +// 获取区域内的用户类别统计 +export async function getUserStaticsByArea(area: string) { + return request(`/api/cms/statics/getUserStaticsByArea/${area}`, { + method: 'GET', + }); +} +// 获取区域内的用户类别统计 +export async function getPercentOfResumeCompletionRate(params?: API.Analysis.IndustryParams) { + return request(`/api/app/user/getPercentOfResumeCompletionRate`, { + method: 'GET', + params, + }); +} \ No newline at end of file