Files
shihezi-admin/src/pages/Analysis/Industrytrend/index.tsx
2025-05-26 17:40:35 +08:00

385 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useMemo } from 'react';
import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker } from 'antd';
import { Line } from '@ant-design/charts';
import { getIndustryTrend } 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
} from './utils';
const { Option } = Select;
const { RangePicker } = DatePicker;
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: ''
});
const [allData, setAllData] = useState<IndustryDataItem[]>([]);
const [availableIndustries, setAvailableIndustries] = useState<string[]>([]);
const { loading, run: fetchData } = 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: { category: any; }) => item.category))
).filter(Boolean).sort();
setAvailableIndustries(industries);
if (industries.length > 0 && !industries.includes(params.selectedIndustry)) {
setParams(p => ({ ...p, selectedIndustry: industries[0] }));
}
},
onError: () => message.error('数据加载失败')
}
);
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 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 === '季度') {
// 处理中文季度格式,如 "2024-第一季度" -> ["2024", "第一"]
const [yearA, quarterA] = a.originalDate.split('-');
const [yearB, quarterB] = b.originalDate.split('-');
// 转换中文季度为数字("第一" -> 1, "第二" -> 2...
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 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) {
console.error('日期解析错误:', e);
return null;
}
};
const chartConfig: ChartConfig = {
data: currentIndustryData,
height: 180,
xField: 'date',
yField: 'value',
seriesField: 'category',
xAxis: {
type: 'cat',
label: {
formatter: (text: string) => text,
},
},
yAxis: {
label: {
formatter: (val: string) => `${val}${params.type === '招聘增长率' ? '%' : ''}`,
},
},
point: {
size: 4,
shape: 'circle',
},
animation: {
appear: {
animation: 'path-in',
duration: 1000,
},
},
smooth: true,
interactions: [
{
type: 'tooltip',
cfg: {
render: (e, { title, items }) => {
const list = items.filter((item) => item.value);
return (
<div key={title} style={{ padding: '8px 12px' }}>
<h4 style={{ marginBottom: 8 }}>{title}</h4>
{list.map((item, index) => {
const { name, value, color } = item;
return (
<div key={index} style={{ margin: '4px 0', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span
style={{
display: 'inline-block',
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: color,
marginRight: 8,
}}
></span>
<span>{name}</span>
</div>
<b>{value}{params.type === '招聘增长率' ? '%' : ''}</b>
</div>
);
})}
</div>
);
},
},
},
],
legend: false,
tooltip: {
showTitle: undefined,
title: undefined,
customContent: undefined
}
};
useEffect(() => {
fetchData();
}, [params.timeDimension, params.startTime, params.endTime, params.type]);
return (
<div style={{ padding: 16 }}>
<Card title="行业趋势分析" style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 24 }}>
<Space size="middle" wrap>
<Select
value={params.selectedIndustry}
onChange={(value) => setParams(p => ({ ...p, selectedIndustry: value }))}
style={{ width: 150 }}
loading={loading}
placeholder="选择行业"
disabled={availableIndustries.length === 0}
>
{availableIndustries.map(industry => (
<Option key={industry} value={industry}>{industry}</Option>
))}
</Select>
<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={loading}
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={() => fetchData()}
loading={loading}
>
</Button>
</Space>
</div>
<Card
title={`${params.selectedIndustry || '请选择行业'}趋势 (${params.type})`}
style={{ marginBottom: 24 }}
bodyStyle={{ padding: '24px 12px' }}
>
<Spin spinning={loading}>
{currentIndustryData.length > 0 ? (
<Line {...chartConfig} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading ? '数据加载中...' :
params.selectedIndustry ? '当前时间段无数据' : '请先选择行业'
}
/>
)}
</Spin>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8} lg={8} xl={6}>
<Card
title="区域分析"
bodyStyle={{
padding: 12,
height: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Empty description="热力地图预留位置" />
</Card>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={6}>
<Card
title="薪资趋势"
bodyStyle={{
padding: 12,
height: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Empty description="薪资分析预留位置" />
</Card>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={6}>
<Card
title="经验要求"
bodyStyle={{
padding: 12,
height: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Empty description="经验分析预留位置" />
</Card>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={6}>
<Card
title="技能需求"
bodyStyle={{
padding: 12,
height: 300,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Empty description="技能分析预留位置" />
</Card>
</Col>
</Row>
</Card>
</div>
);
};
export default IndustryTrendPage;