From 7cdaedcaa77c914f4c6ea5117a273c19892e7af5 Mon Sep 17 00:00:00 2001
From: Apcallover <1503963513@qq.com>
Date: Thu, 6 Nov 2025 10:02:54 +0800
Subject: [PATCH] =?UTF-8?q?flat:=20=E6=9A=82=E5=AD=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Industrytrend/components/chartcards.tsx | 425 +++---
.../Industrytrend/components/chartconfigs.tsx | 912 ++++++-------
src/pages/Analysis/Industrytrend/index.tsx | 1134 ++++++++---------
.../Analysis/User/components/AreaStatics.tsx | 197 +++
.../Analysis/User/components/KeywordChart.tsx | 98 ++
.../User/components/WantedPositionStatics.tsx | 84 ++
src/pages/Analysis/User/index.tsx | 61 +-
src/pages/Area/Business/UpLine/edit.tsx | 2 +-
src/pages/Area/Business/edit.tsx | 2 +-
src/pages/Company/List/index.tsx | 33 +-
src/pages/Management/List/index.tsx | 4 +-
src/services/analysis/user.ts | 43 +
12 files changed, 1733 insertions(+), 1262 deletions(-)
create mode 100644 src/pages/Analysis/User/components/AreaStatics.tsx
create mode 100644 src/pages/Analysis/User/components/KeywordChart.tsx
create mode 100644 src/pages/Analysis/User/components/WantedPositionStatics.tsx
create mode 100644 src/services/analysis/user.ts
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