添加企业服务管理、企业数据管理页面
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
This commit is contained in:
574
src/pages/Company/DataManagement/index.tsx
Normal file
574
src/pages/Company/DataManagement/index.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker } from 'antd';
|
||||
import { Line, Bar, Pie, Heatmap } from '@ant-design/charts';
|
||||
import {
|
||||
getIndustryTrend,
|
||||
getIndustryAreaTrend,
|
||||
getSalaryTrend,
|
||||
getWorkYearTrend,
|
||||
getEducationTrend,
|
||||
} from '@/services/analysis/industry';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRequest } from '@umijs/max';
|
||||
import {
|
||||
TimeDimension,
|
||||
AnalysisType,
|
||||
IndustryTrendState,
|
||||
IndustryDataItem,
|
||||
ChartConfig,
|
||||
} from '@/types/analysis/industry';
|
||||
import {
|
||||
formatQuarter,
|
||||
formatDateForDisplay,
|
||||
convertApiData,
|
||||
convertSalaryData,
|
||||
convertWorkYearData,
|
||||
convertEducationData,
|
||||
} from './utils';
|
||||
import {
|
||||
getHeatmapConfig,
|
||||
getIndustryChartConfig,
|
||||
getSalaryChartConfig,
|
||||
getWorkYearPieConfig,
|
||||
getEducationBarConfig,
|
||||
} from './components/chartconfigs';
|
||||
import {
|
||||
IndustryTrendCard,
|
||||
AreaAnalysisCard,
|
||||
SalaryTrendCard,
|
||||
WorkYearCard,
|
||||
EducationCard,
|
||||
} from './components/chartcards';
|
||||
import Chartline from './components/chartline'
|
||||
import Chartbar from './components/chartbar'
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const flattenAreaData = (apiResponse: any) => {
|
||||
if (!apiResponse || typeof apiResponse !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flattenedData: { name: any; time: any; value: number }[] = [];
|
||||
|
||||
for (const month in apiResponse) {
|
||||
if (apiResponse.hasOwnProperty(month)) {
|
||||
const areas = apiResponse[month];
|
||||
|
||||
areas.forEach((area: { name: any; time: any; data: string }) => {
|
||||
flattenedData.push({
|
||||
name: area.name,
|
||||
time: area.time,
|
||||
value: parseInt(area.data) || 0,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return flattenedData;
|
||||
};
|
||||
|
||||
const IndustryTrendPage: React.FC = () => {
|
||||
const [params, setParams] = useState<IndustryTrendState>({
|
||||
timeDimension: '月',
|
||||
type: '岗位发布数量',
|
||||
startTime: dayjs().subtract(5, 'month').format('YYYY-MM'),
|
||||
endTime: dayjs().format('YYYY-MM'),
|
||||
selectedIndustry: '',
|
||||
selectedSalaryRange: '',
|
||||
selectedWorkYearRange: '',
|
||||
});
|
||||
|
||||
const [allData, setAllData] = useState<IndustryDataItem[]>([]);
|
||||
const [areaData, setAreaData] = useState<any[]>([]);
|
||||
const [salaryData, setSalaryData] = useState<any[]>([]);
|
||||
const [availableIndustries, setAvailableIndustries] = useState<string[]>([]);
|
||||
const [availableSalaryRanges, setAvailableSalaryRanges] = useState<string[]>([]);
|
||||
const heatmapRef = useRef(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [workYearData, setWorkYearData] = useState<any[]>([]);
|
||||
const [availableWorkYearRanges, setAvailableWorkYearRanges] = useState<string[]>([]);
|
||||
const [educationData, setEducationData] = useState<any[]>([]);
|
||||
const [availableEducationLevels, setAvailableEducationLevels] = useState<string[]>([]);
|
||||
const [selectedEducationLevel, setSelectedEducationLevel] = useState<string>('');
|
||||
|
||||
// 获取行业趋势数据
|
||||
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 (
|
||||
<div style={{ padding: 16 }} ref={containerRef}>
|
||||
<Card title="企业数据分析" style={{ marginBottom: 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Space size="middle" wrap>
|
||||
<Select
|
||||
value={params.timeDimension}
|
||||
onChange={handleTimeDimensionChange}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
<Option value="月">按月</Option>
|
||||
<Option value="季度">按季度</Option>
|
||||
<Option value="年">按年</Option>
|
||||
</Select>
|
||||
|
||||
<RangePicker
|
||||
picker={
|
||||
params.timeDimension === '月'
|
||||
? 'month'
|
||||
: params.timeDimension === '季度'
|
||||
? 'quarter'
|
||||
: 'year'
|
||||
}
|
||||
value={getPickerValue()}
|
||||
onChange={handleDateRangeChange}
|
||||
style={{ width: 180 }}
|
||||
disabled={industryLoading || areaLoading}
|
||||
allowClear={false}
|
||||
disabledDate={disabledDate}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={params.type}
|
||||
onChange={(value) => setParams((p) => ({ ...p, type: value }))}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
<Option value="岗位发布数量">岗位数量</Option>
|
||||
<Option value="招聘增长率">增长率</Option>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
fetchIndustryData();
|
||||
fetchAreaData();
|
||||
}}
|
||||
loading={industryLoading || areaLoading}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 行业趋势图表 - 全宽显示 */}
|
||||
<Col xs={24} lg={24}>
|
||||
<IndustryTrendCard
|
||||
loading={industryLoading}
|
||||
currentIndustryData={currentIndustryData}
|
||||
config={industryChartConfig}
|
||||
availableIndustries={availableIndustries}
|
||||
selectedIndustry={params.selectedIndustry}
|
||||
onIndustryChange={(value) => setParams((p) => ({ ...p, selectedIndustry: value }))}
|
||||
/>
|
||||
</Col>
|
||||
{/* 工作经验和学历要求 - 中等屏幕下分成两列 */}
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<WorkYearCard
|
||||
loading={workYearLoading}
|
||||
workYearData={workYearData}
|
||||
config={workYearPieConfig}
|
||||
availableWorkYearRanges={availableWorkYearRanges}
|
||||
selectedWorkYearRange={params.selectedWorkYearRange}
|
||||
onWorkYearRangeChange={(value) =>
|
||||
setParams((p) => ({ ...p, selectedWorkYearRange: value }))
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<Chartbar />
|
||||
{/* <EducationCard
|
||||
loading={educationLoading}
|
||||
educationData={educationData}
|
||||
config={educationBarConfig}
|
||||
availableEducationLevels={availableEducationLevels}
|
||||
selectedEducationLevel={selectedEducationLevel}
|
||||
onEducationLevelChange={setSelectedEducationLevel}
|
||||
/> */}
|
||||
</Col>
|
||||
{/* 区域分析和薪资趋势 - 中等屏幕下分成两列 */}
|
||||
{/* <Col xs={24} md={12} lg={12}>
|
||||
<AreaAnalysisCard loading={areaLoading} areaData={areaData} config={heatmapConfig} />
|
||||
</Col> */}
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<SalaryTrendCard
|
||||
loading={salaryLoading}
|
||||
currentSalaryData={currentSalaryData}
|
||||
config={salaryChartConfig}
|
||||
availableSalaryRanges={availableSalaryRanges}
|
||||
selectedSalaryRange={params.selectedSalaryRange}
|
||||
onSalaryRangeChange={(value) =>
|
||||
setParams((p) => ({ ...p, selectedSalaryRange: value }))
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12} lg={12}>
|
||||
<Chartline />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndustryTrendPage;
|
||||
Reference in New Issue
Block a user