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,6 +1,6 @@
import React from 'react';
import { Card, Select, Spin, Empty, Row, Col } from 'antd';
import { Line, Bar, Pie, Heatmap } from '@ant-design/charts';
import { Card, Empty, Select, Spin } from 'antd';
import { Bar, Heatmap, Line, Pie } from '@ant-design/charts';
export const IndustryTrendCard = ({
loading,
@@ -38,11 +38,7 @@ export const IndustryTrendCard = ({
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
loading
? '数据加载中...'
: selectedIndustry
? '当前时间段无数据'
: '请先选择行业'
loading ? '数据加载中...' : selectedIndustry ? '当前时间段无数据' : '请先选择行业'
}
/>
)}
@@ -192,7 +188,6 @@ export const EducationCard = ({
placeholder="选择学历要求"
disabled={availableEducationLevels.length === 0}
>
<Option value=""></Option>
{availableEducationLevels.map((level) => (
<Option key={level} value={level}>
{level}

View File

@@ -241,7 +241,7 @@ export const getSalaryChartConfig = (currentSalaryData: any[]): ChartConfig => (
export const getWorkYearPieConfig = (workYearData: any[], selectedWorkYearRange: string) => {
const filteredData = workYearData
.filter((item) => item.category === selectedWorkYearRange)
.filter((item) => selectedWorkYearRange === '全部' || item.category === selectedWorkYearRange)
.map((item) => ({
...item,
date: formatDateForDisplay(item.date, '月'),
@@ -394,7 +394,7 @@ export const getEducationBarConfig = (educationData: any[], selectedEducationLev
}
const timeData = cleanEducationData
.filter((item) => item.category === selectedEducationLevel)
.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();

View File

@@ -1,43 +1,36 @@
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 React, { useEffect, useMemo, useRef, useState } from 'react';
import { Button, Card, Col, DatePicker, message, Row, Select, Space } from 'antd';
import {
getIndustryTrend,
getEducationTrend,
getIndustryAreaTrend,
getIndustryTrend,
getSalaryTrend,
getWorkYearTrend,
getEducationTrend,
} from '@/services/analysis/industry';
import dayjs from 'dayjs';
import { useRequest } from '@umijs/max';
import { IndustryDataItem, IndustryTrendState, TimeDimension } from '@/types/analysis/industry';
import {
TimeDimension,
AnalysisType,
IndustryTrendState,
IndustryDataItem,
ChartConfig,
} from '@/types/analysis/industry';
import {
formatQuarter,
formatDateForDisplay,
convertApiData,
convertEducationData,
convertSalaryData,
convertWorkYearData,
convertEducationData,
formatDateForDisplay,
formatQuarter,
} from './utils';
import {
getEducationBarConfig,
getHeatmapConfig,
getIndustryChartConfig,
getSalaryChartConfig,
getWorkYearPieConfig,
getEducationBarConfig,
} from './components/chartconfigs';
import {
IndustryTrendCard,
AreaAnalysisCard,
EducationCard,
IndustryTrendCard,
SalaryTrendCard,
WorkYearCard,
EducationCard,
} from './components/chartcards';
const { Option } = Select;
@@ -186,7 +179,7 @@ const IndustryTrendPage: React.FC = () => {
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)) {
@@ -226,7 +219,7 @@ const IndustryTrendPage: React.FC = () => {
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] }));
@@ -265,7 +258,7 @@ const IndustryTrendPage: React.FC = () => {
const order = ['不限', '初中及以下', '中专/中技', '大专', '本科', '硕士', '博士'];
return order.indexOf(a) - order.indexOf(b);
});
levels.unshift('全部');
setAvailableEducationLevels(levels);
if (levels.length > 0 && !levels.includes(selectedEducationLevel)) {
setSelectedEducationLevel(levels[0]);
@@ -326,9 +319,11 @@ const IndustryTrendPage: React.FC = () => {
const currentSalaryData = useMemo(() => {
if (!params.selectedSalaryRange || salaryData.length === 0) return [];
return salaryData
.filter((item) => item.category === params.selectedSalaryRange)
.filter(
(item) =>
params.selectedSalaryRange === '全部' || item.category === params.selectedSalaryRange,
)
.map((item) => ({
...item,
originalDate: item.date,
@@ -537,7 +532,6 @@ const IndustryTrendPage: React.FC = () => {
}
/>
</Col>
{/* 工作经验和学历要求 - 中等屏幕下分成两列 */}
<Col xs={24} md={12} lg={12}>
<WorkYearCard
@@ -546,9 +540,9 @@ const IndustryTrendPage: React.FC = () => {
config={workYearPieConfig}
availableWorkYearRanges={availableWorkYearRanges}
selectedWorkYearRange={params.selectedWorkYearRange}
onWorkYearRangeChange={(value) =>
setParams((p) => ({ ...p, selectedWorkYearRange: value }))
}
onWorkYearRangeChange={(value) => {
return setParams((p) => ({ ...p, selectedWorkYearRange: value }));
}}
/>
</Col>
<Col xs={24} md={12} lg={12}>

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,
@@ -19,7 +19,7 @@ const CompanyDetailModal = ({
visible,
onCancel,
record,
scaleEnum
scaleEnum,
}: {
visible: boolean;
onCancel: () => void;
@@ -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',

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',

View File

@@ -0,0 +1,43 @@
import { request } from '@umijs/max';
// 行业
export async function getUserLoginCountStatics(params?: API.Analysis.IndustryParams) {
return request<API.Analysis.User>('/api/cms/statics/getUserLoginCountStatics', {
method: 'GET',
params,
});
}
// 查询关键词搜索排行
export async function getKeyWordSearchRank(params?: {
searchDate?: string;
searchCount?: number;
searchType?: 1 | 2 | 3;
}) {
return request<API.Analysis.User>('/api/cms/statics/getKeyWordSearchRank', {
method: 'GET',
params,
});
}
// 统计男性和女性用户偏好的期望岗位
export async function getWantedPositionStaticsCount(params?: API.Analysis.IndustryParams) {
return request<API.Analysis.User>('/api/cms/statics/getWantedPositionStaticsCount', {
method: 'GET',
params,
});
}
// 获取区域内的用户类别统计
export async function getUserStaticsByArea(area: string) {
return request<API.Analysis.User>(`/api/cms/statics/getUserStaticsByArea/${area}`, {
method: 'GET',
});
}
// 获取区域内的用户类别统计
export async function getPercentOfResumeCompletionRate(params?: API.Analysis.IndustryParams) {
return request<API.Analysis.User>(`/api/app/user/getPercentOfResumeCompletionRate`, {
method: 'GET',
params,
});
}