flat: 暂存

This commit is contained in:
Apcallover
2025-11-06 10:02:54 +08:00
parent 203b7d4ef7
commit 7cdaedcaa7
12 changed files with 1733 additions and 1262 deletions

View File

@@ -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,
}) => (
<Card
title="行业趋势分析"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedIndustry}
onChange={onIndustryChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择行业"
disabled={availableIndustries.length === 0}
>
{availableIndustries.map((industry: any) => (
<Option key={industry} value={industry}>
{industry}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{currentIndustryData.length > 0 ? (
<Line {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading
? '数据加载中...'
: selectedIndustry
? '当前时间段无数据'
: '请先选择行业'
}
/>
)}
</Spin>
</Card>
);
export const AreaAnalysisCard = ({ loading, areaData, config }) => (
<Card
title="区域分析"
style={{ marginBottom: 16 }}
bodyStyle={{
padding: 12,
height: 250,
position: 'relative',
}}
>
{loading ? (
<Spin tip="数据加载中..." size="large" />
) : areaData.length > 0 ? (
<div style={{ height: '100%', width: '100%', minHeight: 300 }}>
<Heatmap {...config} />
</div>
) : (
<Empty description="暂无区域数据" />
)}
</Card>
);
export const SalaryTrendCard = ({
loading,
currentSalaryData,
config,
availableSalaryRanges,
selectedSalaryRange,
onSalaryRangeChange,
}) => (
<Card
title="薪资区间趋势分析"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedSalaryRange}
onChange={onSalaryRangeChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择薪资区间"
disabled={availableSalaryRanges.length === 0}
>
{availableSalaryRanges.map((range) => (
<Option key={range} value={range}>
{range}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{currentSalaryData.length > 0 ? (
<Line {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading
? '数据加载中...'
: selectedSalaryRange
? '当前时间段无数据'
: '请先选择薪资区间'
}
/>
)}
</Spin>
</Card>
);
export const WorkYearCard = ({
loading,
workYearData,
config,
availableWorkYearRanges,
selectedWorkYearRange,
onWorkYearRangeChange,
}) => (
<Card
title="工作经验要求分布"
style={{ marginBottom: 16 }}
bodyStyle={{
padding: '12px',
height: 250,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
}}
extra={
<Select
value={selectedWorkYearRange}
onChange={onWorkYearRangeChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择经验要求"
disabled={availableWorkYearRanges.length === 0}
>
{availableWorkYearRanges.map((range: any) => (
<Option key={range} value={range}>
{range}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{workYearData && workYearData.length > 0 ? (
<div style={{ width: '100%', height: '100%', padding: 8 }}>
<Pie {...config} />
</div>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={loading ? '数据加载中...' : '暂无工作经验数据'}
/>
)}
</Spin>
</Card>
);
export const EducationCard = ({
loading,
educationData,
config,
availableEducationLevels,
selectedEducationLevel,
onEducationLevelChange,
}) => (
<Card
title="学历要求分布"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedEducationLevel}
onChange={onEducationLevelChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择学历要求"
disabled={availableEducationLevels.length === 0}
>
<Option value=""></Option>
{availableEducationLevels.map((level) => (
<Option key={level} value={level}>
{level}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{educationData.length > 0 ? (
<Bar {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={loading ? '数据加载中...' : '暂无学历数据'}
/>
)}
</Spin>
</Card>
);
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,
}) => (
<Card
title="行业趋势分析"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedIndustry}
onChange={onIndustryChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择行业"
disabled={availableIndustries.length === 0}
>
{availableIndustries.map((industry: any) => (
<Option key={industry} value={industry}>
{industry}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{currentIndustryData.length > 0 ? (
<Line {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading ? '数据加载中...' : selectedIndustry ? '当前时间段无数据' : '请先选择行业'
}
/>
)}
</Spin>
</Card>
);
export const AreaAnalysisCard = ({ loading, areaData, config }) => (
<Card
title="区域分析"
style={{ marginBottom: 16 }}
bodyStyle={{
padding: 12,
height: 250,
position: 'relative',
}}
>
{loading ? (
<Spin tip="数据加载中..." size="large" />
) : areaData.length > 0 ? (
<div style={{ height: '100%', width: '100%', minHeight: 300 }}>
<Heatmap {...config} />
</div>
) : (
<Empty description="暂无区域数据" />
)}
</Card>
);
export const SalaryTrendCard = ({
loading,
currentSalaryData,
config,
availableSalaryRanges,
selectedSalaryRange,
onSalaryRangeChange,
}) => (
<Card
title="薪资区间趋势分析"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedSalaryRange}
onChange={onSalaryRangeChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择薪资区间"
disabled={availableSalaryRanges.length === 0}
>
{availableSalaryRanges.map((range) => (
<Option key={range} value={range}>
{range}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{currentSalaryData.length > 0 ? (
<Line {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading
? '数据加载中...'
: selectedSalaryRange
? '当前时间段无数据'
: '请先选择薪资区间'
}
/>
)}
</Spin>
</Card>
);
export const WorkYearCard = ({
loading,
workYearData,
config,
availableWorkYearRanges,
selectedWorkYearRange,
onWorkYearRangeChange,
}) => (
<Card
title="工作经验要求分布"
style={{ marginBottom: 16 }}
bodyStyle={{
padding: '12px',
height: 250,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
}}
extra={
<Select
value={selectedWorkYearRange}
onChange={onWorkYearRangeChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择经验要求"
disabled={availableWorkYearRanges.length === 0}
>
{availableWorkYearRanges.map((range: any) => (
<Option key={range} value={range}>
{range}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{workYearData && workYearData.length > 0 ? (
<div style={{ width: '100%', height: '100%', padding: 8 }}>
<Pie {...config} />
</div>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={loading ? '数据加载中...' : '暂无工作经验数据'}
/>
)}
</Spin>
</Card>
);
export const EducationCard = ({
loading,
educationData,
config,
availableEducationLevels,
selectedEducationLevel,
onEducationLevelChange,
}) => (
<Card
title="学历要求分布"
style={{ marginBottom: 16 }}
bodyStyle={{ padding: '12px', height: 250 }}
extra={
<Select
value={selectedEducationLevel}
onChange={onEducationLevelChange}
style={{ width: 180 }}
loading={loading}
placeholder="选择学历要求"
disabled={availableEducationLevels.length === 0}
>
{availableEducationLevels.map((level) => (
<Option key={level} value={level}>
{level}
</Option>
))}
</Select>
}
>
<Spin spinning={loading}>
{educationData.length > 0 ? (
<Bar {...config} />
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={loading ? '数据加载中...' : '暂无学历数据'}
/>
)}
</Spin>
</Card>
);

View File

@@ -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 (
<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}
{type === '招聘增长率' ? '%' : ''}
</b>
</div>
);
})}
</div>
);
},
},
},
],
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 (
<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}</b>
</div>
);
})}
</div>
);
},
},
},
],
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<string, string> = {
: '#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<string, number> = {};
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,
},
},
};
};
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 (
<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}
{type === '招聘增长率' ? '%' : ''}
</b>
</div>
);
})}
</div>
);
},
},
},
],
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 (
<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}</b>
</div>
);
})}
</div>
);
},
},
},
],
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<string, string> = {
: '#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<string, number> = {};
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,
},
},
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -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 (
<div style={{ padding: '8px' }}>
<Card title="区域用户统计" style={{ marginBottom: '8px' }}>
<div
style={{
display: 'flex',
gap: '16px',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>
<Select
style={{ width: '200px', marginBottom: '8px' }}
placeholder="选择区域"
onChange={handleAreaChange}
value={selectedArea}
>
{data.map((item) => (
<Select.Option key={item.dictValue} value={item.dictValue}>
{item.dictLabel}
</Select.Option>
))}
</Select>
</div>
<div>
{byArea.gender && byArea.gender.length > 0 ? (
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: '16px' }}>
<div style={{ display: 'flex', flexWrap: 'nowrap' }}>
<h4>: </h4>&nbsp; {byArea.gender.reduce((acc, cur) => acc + cur.data, 0)}
</div>
<div>: &nbsp;{byArea.gender.find((item) => item.name === '男')?.data || 0}</div>
<div>: &nbsp;{byArea.gender.find((item) => item.name === '女')?.data || 0}</div>
</div>
) : (
<div></div>
)}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
<Card title="学历分布" style={{ marginBottom: '8px' }}>
<Column
data={byArea.education}
xField="name"
yField="data"
label={{
text: (d) => `${d.data}`,
textBaseline: 'bottom',
}}
axis={{
y: {
label: {
formatter: (v) => `${v}k`,
},
},
}}
style={{
radiusTopLeft: 3,
radiusTopRight: 3,
columnWidthRatio: 0.6,
inset: 0.5,
}}
/>
</Card>
<Card
title="求职岗位排行"
style={{ marginBottom: '8px', height: '600px', overflowY: 'auto' }}
>
<Table
columns={positionColumns}
dataSource={byArea.position}
pagination={false}
rowKey="name"
size="small"
scroll={{ y: 440 }}
/>
</Card>
<Card title="期望薪资阶段" style={{ marginBottom: '8px' }}>
<Column
data={byArea.salary}
xField="name"
yField="data"
label={{
text: (d) => `${d.data}`,
textBaseline: 'bottom',
}}
axis={{
y: {
label: {
formatter: (v) => `${v}k`,
},
},
}}
style={{
radiusTopLeft: 3,
radiusTopRight: 3,
columnWidthRatio: 0.6,
inset: 0.5,
}}
/>
</Card>
<Card title="年龄阶段分布">
<Column
data={byArea.age}
xField="name"
yField="data"
label={{
text: (d) => `${d.data}`,
textBaseline: 'bottom',
}}
axis={{
y: {
label: {
formatter: (v) => `${v}k`,
},
},
}}
style={{
radiusTopLeft: 5,
radiusTopRight: 5,
columnWidthRatio: 0.3,
inset: 0.5,
}}
/>
</Card>
</div>
</Card>
</div>
);
}

View File

@@ -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 (
<>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
<h3 style={{ marginRight: 16 }}></h3>
<select
onChange={(e) => {
fetchData({
searchDate: '2025-11-03',
searchCount: 10,
searchType: Number(e.target.value) as 1 | 2 | 3,
});
}}
defaultValue="3"
>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
</select>
</div>
<div
style={{
height: '400px',
overflowY: 'auto',
border: '1px solid #eee',
borderRadius: '4px',
padding: '8px',
}}
>
{data?.map((item, index) => (
<div
key={index}
style={{
padding: '8px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-around',
}}
>
<span>{item.keyWord}</span>
<span>{item.searchCount}</span>
</div>
))}
</div>
</>
);
}

View File

@@ -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<StaticsResult>({ male: [], female: [] });
const fetchStatics = useCallback(async () => {
const resData = await getWantedPositionStaticsCount({ sexCode: 0 });
if (resData.code === 200) {
const malePositions: Record<string, number> = {};
const femalePositions: Record<string, number> = {};
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 (
<div style={{ padding: '8px', display: 'flex', width: '100%', height: '100%' }}>
<Card title="男性用户期望岗位统计" style={{ width: '50%' }}>
<Table
columns={columns}
dataSource={staticsData.male}
pagination={false}
rowKey="jobName"
size="small"
scroll={{ y: 440 }}
/>
</Card>
<Card title="女性用户期望岗位统计" style={{ width: '50%' }}>
<Table
columns={columns}
dataSource={staticsData.female}
pagination={false}
rowKey="jobName"
size="small"
scroll={{ y: 440 }}
/>
</Card>
</div>
);
};
export default WantedPositionStatics;

View File

@@ -1 +1,60 @@
<button>111111111111</button>
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 (
<div style={{ padding: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px' }}>
<div style={{ width: '100%' }}>
<KeywordChart />
</div>
<div style={{ width: '100%' }}>
<WantedPositionStatics />
</div>
</div>
<div style={{ width: '100%' }}>
<AreaStatics data={areaDict} />
</div>
</div>
);
}

View File

@@ -70,7 +70,7 @@ const SubWayEdit: React.FC<ListFormProps> = (props) => {
company: string;
stationOrder: number;
}>
title={`${props.values ? '编辑' : '新增'}站点`}
title={`${props.values ? '编辑' : '新增'}商圈`}
form={form}
// layout="inline"
autoFocusFirstInput

View File

@@ -47,7 +47,7 @@ const SubWayEdit: React.FC<ListFormProps> = (props) => {
name: string;
company: string;
}>
title={`${props.values ? '编辑' : '新增'}线路`}
title={`${props.values ? '编辑' : '新增'}区域`}
form={form}
// layout="inline"
autoFocusFirstInput

View File

@@ -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<string, any>;
}) => {
@@ -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;
export default ManagementList;

View File

@@ -106,13 +106,13 @@ function ManagementList() {
align: 'center',
},
{
title: '最小薪资',
title: '最小薪资(元/月)',
dataIndex: 'minSalary',
valueType: 'text',
align: 'center',
},
{
title: '最大薪资',
title: '最大薪资(元/月)',
dataIndex: 'maxSalary',
valueType: 'text',
align: 'center',