diff --git a/src/pages/Analysis/Industrytrend/components/chartcards.tsx b/src/pages/Analysis/Industrytrend/components/chartcards.tsx index 2cf3f32..9771c73 100644 --- a/src/pages/Analysis/Industrytrend/components/chartcards.tsx +++ b/src/pages/Analysis/Industrytrend/components/chartcards.tsx @@ -1,7 +1,9 @@ import React from 'react'; -import { Card, Empty, Select, Spin } from 'antd'; +import { Card, Empty, Select, Spin, Tag } from 'antd'; import { Bar, Heatmap, Line, Pie } from '@ant-design/charts'; +const { Option } = Select; + export const IndustryTrendCard = ({ loading, currentIndustryData, @@ -11,17 +13,33 @@ export const IndustryTrendCard = ({ onIndustryChange, }) => ( + 📊 行业趋势分析 + + 趋势图 + + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} + bodyStyle={{ + padding: '16px', + height: 280, + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8 + }} extra={ } > - + {currentIndustryData.length > 0 ? ( ) : ( @@ -40,6 +58,7 @@ export const IndustryTrendCard = ({ description={ loading ? '数据加载中...' : selectedIndustry ? '当前时间段无数据' : '请先选择行业' } + style={{ margin: '40px 0' }} /> )} @@ -48,23 +67,36 @@ export const IndustryTrendCard = ({ export const AreaAnalysisCard = ({ loading, areaData, config }) => ( + 🌍 区域分析 + 热力图 + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} bodyStyle={{ - padding: 12, - height: 250, + padding: 16, + height: 280, + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8, position: 'relative', }} > - {loading ? ( - - ) : areaData.length > 0 ? ( -
- -
- ) : ( - - )} + + {areaData.length > 0 ? ( +
+ +
+ ) : ( + + )} +
); @@ -77,9 +109,22 @@ export const SalaryTrendCard = ({ onSalaryRangeChange, }) => ( + 💰 薪资区间趋势分析 + 趋势图 + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} + bodyStyle={{ + padding: '16px', + height: 280, + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8 + }} extra={ } > - + {currentSalaryData.length > 0 ? ( ) : ( @@ -110,6 +156,7 @@ export const SalaryTrendCard = ({ ? '当前时间段无数据' : '请先选择薪资区间' } + style={{ margin: '40px 0' }} /> )} @@ -125,11 +172,21 @@ export const WorkYearCard = ({ onWorkYearRangeChange, }) => ( + ⏳ 工作经验要求分布 + 饼图 + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} bodyStyle={{ - padding: '12px', - height: 250, + padding: '16px', + height: 280, + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8, display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -139,10 +196,11 @@ export const WorkYearCard = ({ } > - + {workYearData && workYearData.length > 0 ? (
@@ -161,6 +219,7 @@ export const WorkYearCard = ({ )} @@ -176,17 +235,31 @@ export const EducationCard = ({ onEducationLevelChange, }) => ( + 🎓 学历要求分布 + 柱状图 +
+ } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} + bodyStyle={{ + padding: '16px', + height: 280, + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8 + }} extra={ } > - + {educationData.length > 0 ? ( ) : ( )}
-); +); \ No newline at end of file diff --git a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx index 75f9dc7..cd1de64 100644 --- a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx +++ b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx @@ -2,6 +2,20 @@ import { ChartConfig } from '@/types/analysis/industry'; import dayjs from 'dayjs'; import { formatDateForDisplay } from '../utils'; +// 统一的颜色方案 +const COLOR_PALETTE = [ + '#1890ff', + '#36cfc9', + '#faad14', + '#f5222d', + '#722ed1', + '#fa8c16', + '#52c41a', + '#eb2f96', + '#13c2c2', + '#eb2f96', +]; + export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { const sortedData = [...areaData].sort((a, b) => { if (timeDimension === '年') { @@ -19,17 +33,20 @@ export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { return { data: sortedData, - height: 240, + height: 280, autoFit: true, xField: 'name', yField: 'time', - colorField: 'value', - shapeField: 'square', - sizeField: 'value', + colorField: 'value', + shapeField: 'square', + sizeField: 'value', xAxis: { title: { text: '区域', - style: { fontSize: 12 }, + style: { + fontSize: 12, + fill: '#666', + }, }, label: { style: { @@ -44,7 +61,10 @@ export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { yAxis: { title: { text: '时间', - style: { fontSize: 12 }, + style: { + fontSize: 12, + fill: '#666', + }, }, label: { formatter: (text: string) => { @@ -59,39 +79,56 @@ export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { }, }, label: { - text: (d: { value: number }) => d.value.toString(), + text: (d: { value: number }) => (d.value > 0 ? d.value.toString() : ''), position: 'inside', style: { fill: '#fff', - pointerEvents: 'none', + fontSize: 10, + fontWeight: 'bold', }, }, + // 关键配置:值越大,颜色越深;值越大,方块越大 scale: { - size: { range: [14, 14] }, - color: { range: ['#dddddd', '#9ec8e0', '#5fa4cd', '#2e7ab6', '#114d90'] }, + size: { + range: [12, 20], // 小值对应小方块,大值对应大方块 + }, + color: { + range: [ + '#b7c2c3ff', // 浅色,对应小值 + '#afd4fbff', + '#8ecaf0ff', + '#40a9ff', + '#1890ff', + '#0050b3', // 深色,对应大值 + ], + }, }, 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', - }, - }, + title: '区域岗位分布', + items: [ + { field: 'name', name: '区域' }, + { field: 'time', name: '时间' }, + { field: 'value', name: '岗位数量', valueFormatter: (v) => `${v} 个` }, + ], }, interactions: [{ type: 'element-active' }], responsive: true, + animation: { + appear: { + animation: 'scale-in', + duration: 1000, + }, + }, }; }; export const getIndustryChartConfig = (currentIndustryData: any[], type: string): ChartConfig => ({ data: currentIndustryData, - height: 200, + height: 280, xField: 'date', yField: 'value', seriesField: 'category', + color: COLOR_PALETTE, xAxis: { type: 'cat', label: { @@ -107,66 +144,37 @@ export const getIndustryChartConfig = (currentIndustryData: any[], type: string) size: 4, shape: 'circle', }, + line: { + size: 3, + style: { + lineCap: 'round', + }, + }, animation: { appear: { animation: 'path-in', - duration: 1000, + duration: 1500, }, }, 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, + title: '行业趋势', + items: [ + { field: 'category', name: '行业' }, + { field: 'date', name: '时间' }, + { + field: 'value', + name: type === '招聘增长率' ? '增长率' : '岗位数量', + valueFormatter: (v) => `${v}${type === '招聘增长率' ? '%' : ''}`, + }, + ], }, }); export const getSalaryChartConfig = (currentSalaryData: any[]): ChartConfig => ({ data: currentSalaryData, - height: 240, + height: 280, xField: 'date', yField: 'value', seriesField: 'category', @@ -252,26 +260,41 @@ export const getWorkYearPieConfig = (workYearData: any[], selectedWorkYearRange: return { data: filteredData, angleField: 'value', - colorField: 'type', + colorField: 'category', radius: 0.2, innerRadius: 0.5, label: { text: (d: { type: any; value: any }) => `${d.type}\n ${d.value}`, position: 'spider', + transform: [ + { + type: 'overlapDodgeY', + }, + ], }, legend: false, tooltip: { - showTitle: true, title: '工作经验分布', - fields: ['type', 'value'], - formatter: (datum: { type: any; value: any }) => ({ - name: datum.type, - value: datum.value, - }), + items: [ + { field: 'date', name: '时间' }, + { field: 'category', name: '工作经验' }, + { + field: 'value', + name: '岗位数量', + valueFormatter: (v) => `${v} 个`, + }, + ], }, interactions: [{ type: 'element-active' }], - padding: 'auto', + padding: [20, 0, 40, 0], autoFit: true, + color: COLOR_PALETTE, + animation: { + appear: { + animation: 'wave-in', + duration: 1000, + }, + }, }; }; @@ -285,172 +308,86 @@ export const getEducationBarConfig = (educationData: any[], selectedEducationLev '本科', '硕士', '博士', - 'MBA/EMBA', - '留学-学士', - '留学-硕士', - '留学-博士', ]; - const educationColorMap: Record = { - 不限: '#8884d8', - 初中及以下: '#82ca9d', - '中专/中技': '#ffc658', - 高中: '#ff8042', - 大专: '#0088FE', - 本科: '#00C49F', - 硕士: '#FFBB28', - 博士: '#FF8042', - 'MBA/EMBA': '#8884d8', - '留学-学士': '#82ca9d', - '留学-硕士': '#ffc658', - '留学-博士': '#ff8042', - }; + // 汇总数据 + const summaryData = educationData.reduce((acc, item) => { + const existing = acc.find((d: any) => d.name === item.category); + if (existing) { + existing.value += item.value; + } else { + acc.push({ + name: item.category, + value: item.value, + }); + } + return acc; + }, [] as any[]); - 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; - }); + const barData = summaryData + .filter((item) => item.value > 0) + .sort((a, b) => educationLevelOrder.indexOf(a.name) - educationLevelOrder.indexOf(b.name)); return { - data: timeData, - height: 200, - xField: 'date', - yField: 'value', - seriesField: 'category', - color: educationColorMap[selectedEducationLevel] || '#999', + data: barData, + height: 280, + xField: 'value', + yField: 'name', + seriesField: 'name', + color: COLOR_PALETTE, 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; - }), + name: { + alias: '学历要求', + }, + value: { + alias: '岗位数量', }, - 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', + xAxis: { + label: { + style: { + fill: '#666', + fontSize: 11, }, }, + grid: { + line: { + style: { + stroke: '#f0f0f0', + lineDash: [3, 3], + }, + }, + }, + }, + yAxis: { + label: { + style: { + fill: '#666', + fontSize: 11, + }, + }, + }, + barStyle: { + radius: [4, 4, 0, 0], + }, + tooltip: { + title: '学历要求分布', + items: [ + { field: 'name', name: '学历要求' }, + { + field: 'value', + name: '岗位数量', + valueFormatter: (v) => `${v} 个`, + }, + ], }, interactions: [{ type: 'element-active' }], legend: false, animation: { appear: { - animation: 'scale-in-y', + animation: 'scale-in-x', 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 e61e37c..a76af4f 100644 --- a/src/pages/Analysis/Industrytrend/index.tsx +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -64,7 +64,8 @@ const IndustryTrendPage: React.FC = () => { const [params, setParams] = useState({ timeDimension: '月', type: '岗位发布数量', - startTime: dayjs().subtract(5, 'month').format('YYYY-MM'), + // startTime: dayjs().subtract(5, 'month').format('YYYY-MM'), + startTime:dayjs().month(0).format('YYYY-MM'), endTime: dayjs().format('YYYY-MM'), selectedIndustry: '', selectedSalaryRange: '', @@ -374,7 +375,8 @@ const IndustryTrendPage: React.FC = () => { let newStartTime = ''; if (value === '月') { - newStartTime = now.subtract(5, 'month').format('YYYY-MM'); + // newStartTime = now.subtract(5, 'month').format('YYYY-MM'); + newStartTime = dayjs().month(0).format('YYYY-MM'); } else if (value === '季度') { newStartTime = now.subtract(6, 'quarter').format('YYYY-Q'); } else { @@ -451,14 +453,14 @@ const IndustryTrendPage: React.FC = () => { }, [params.timeDimension, params.startTime, params.endTime, params.type]); return ( -
- -
+
+
setParams((p) => ({ ...p, type: value }))} - style={{ width: 100 }} + style={{ width: 120 }} + size="middle" > @@ -497,13 +501,19 @@ const IndustryTrendPage: React.FC = () => { fetchAreaData(); }} loading={industryLoading || areaLoading} + size="middle" + style={{ + background: 'linear-gradient(45deg, #1890ff, #36cfc9)', + border: 'none', + borderRadius: 6 + }} > 查询
- + {/* 行业趋势图表 - 全宽显示 */} { /> - {/* 区域分析和薪资趋势 - 中等屏幕下分成两列 */} + {/* 区域分析和薪资趋势 */} - + { } /> - {/* 工作经验和学历要求 - 中等屏幕下分成两列 */} + + {/* 工作经验和学历要求 */} { /> - +
); }; diff --git a/src/pages/Analysis/User/components/AreaStatics.tsx b/src/pages/Analysis/User/components/AreaStatics.tsx index 91172ab..752f0f1 100644 --- a/src/pages/Analysis/User/components/AreaStatics.tsx +++ b/src/pages/Analysis/User/components/AreaStatics.tsx @@ -1,10 +1,14 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Table, Card, Select } from 'antd'; +import { Card, Select, Row, Col, Statistic, Tag, Empty, Spin } from 'antd'; import { getUserStaticsByArea } from '@/services/analysis/user'; -import { Pie, Column } from '@ant-design/charts'; +import { Column, Pie } from '@ant-design/charts'; + +const { Option } = Select; + export default function AreaStatics({ data }) { const [selectedArea, setSelectedArea] = useState(''); const [byArea, setByArea] = useState({}); + const [loading, setLoading] = useState(false); useEffect(() => { if (data.length > 0) { @@ -14,22 +18,28 @@ export default function AreaStatics({ data }) { }, [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); + setLoading(true); + try { + const resData = await getUserStaticsByArea(areaCode); + if (resData.code === 200) { + const processedData = { + 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), + }; + setByArea(processedData); + } + } catch (error) { + console.error('获取区域数据失败:', error); + } finally { + setLoading(false); } }; const transformNum = useCallback((datas) => { - return datas.map((item) => ({ ...item, data: parseInt(item.data) })); + return (datas || []).map((item) => ({ ...item, data: parseInt(item.data) || 0 })); }, []); const handleAreaChange = (value) => { @@ -37,161 +47,249 @@ export default function AreaStatics({ data }) { fetchAreaStatics(value); }; - const educationColumns = [ - { title: '学历', dataIndex: 'name', key: 'name' }, - { title: '数量', dataIndex: 'data', key: 'data' }, - ]; + // 统一的柱状图配置 + const getColumnConfig = (data: any[], color: string, title: string) => ({ + data: data || [], + xField: 'name', + yField: 'data', + height: 220, + autoFit: true, + style:{ + columnWidthRatio: 0.4, // 统一柱状图宽度比例 + }, + color: color, + xAxis: { + label: { + style: { + fontSize: 12, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 4 ? `${text.substring(0, 4)}...` : text; + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, + }, + yAxis: { + label: { + style: { + fontSize: 12, + fill: '#666', + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, + }, + tooltip: { + title: title, + items: [ + { field: 'name', name: '分类' }, + { field: 'data', name: '人数', valueFormatter: (v) => `${v} 人` }, + ], + }, + interactions: [{ type: 'element-active' }], + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, + }, + }); - const genderColumns = [ - { title: '性别', dataIndex: 'name', key: 'name' }, - { title: '数量', dataIndex: 'data', key: 'data' }, - ]; + // 教育程度柱状图配置 + const educationConfig = getColumnConfig(byArea.education, '#36cfc9', '学历分布'); - const positionColumns = [ - { title: '求职岗位', dataIndex: 'name', key: 'name' }, - { title: '数量', dataIndex: 'data', key: 'data' }, - ]; + // 期望薪资柱状图配置 + const salaryConfig = getColumnConfig(byArea.salary, '#faad14', '期望薪资分布'); - const salaryColumns = [ - { title: '期望薪资', dataIndex: 'name', key: 'name' }, - { title: '数量', dataIndex: 'data', key: 'data' }, - ]; + // 年龄分布柱状图配置 + const ageConfig = getColumnConfig(byArea.age, '#722ed1', '年龄分布'); - const ageColumns = [ - { title: '年龄阶段', dataIndex: 'name', key: 'name' }, - { title: '数量', dataIndex: 'data', key: 'data' }, - ]; + // 性别饼图配置 + const genderConfig = { + data: byArea.gender || [], + angleField: 'data', + colorField: 'name', + radius: 0.6, + height: 220, + label: { + text: (d) => `${d.name}\n${d.data}人`, + position: 'spider', + style: { + fontSize: 12, + textAlign: 'center', + }, + transform: [ + { + type: 'overlapDodgeY', + }, + ], + }, + tooltip: { + title: '性别分布', + items: [ + { field: 'name', name: '性别' }, + { field: 'data', name: '人数', valueFormatter: (v) => `${v} 人` }, + ], + }, + interactions: [{ type: 'element-active' }], + animation: { + appear: { + animation: 'wave-in', + duration: 1000, + }, + }, + }; + + // 计算用户总数 + const totalUsers = byArea.gender ? byArea.gender.reduce((acc, cur) => acc + cur.data, 0) : 0; return ( -
- -
-
- -
-
- {byArea.gender && byArea.gender.length > 0 ? ( -
-
-

用户数量:

  {byArea.gender.reduce((acc, cur) => acc + cur.data, 0)} + + 🌍 区域用户统计 + 区域分析 +
+ } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} + bodyStyle={{ + padding: '16px', + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8 + }} + > + +
+ + + + + + {byArea.gender && byArea.gender.length > 0 ? ( + + + + + + + + + item.name === '男')?.data || 0} + suffix="人" + /> + + + + + item.name === '女')?.data || 0} + suffix="人" + /> + + + + ) : ( +
+ 暂无用户数据
-
男性:  {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, - }} - /> - - - - + {byArea.education && byArea.education.length > 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: 5, - radiusTopRight: 5, - columnWidthRatio: 0.3, - inset: 0.5, - }} - /> - - - - + {/* 年龄分布 */} + + + + + + + {/* 期望薪资 */} + + + + + + + ) : ( + + )} + + ); -} +} \ No newline at end of file diff --git a/src/pages/Analysis/User/components/KeywordChart.tsx b/src/pages/Analysis/User/components/KeywordChart.tsx index b3b743a..e67c165 100644 --- a/src/pages/Analysis/User/components/KeywordChart.tsx +++ b/src/pages/Analysis/User/components/KeywordChart.tsx @@ -1,98 +1,239 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Column } from '@ant-design/charts'; +import { Column, WordCloud } from '@ant-design/charts'; import { getKeyWordSearchRank } from '@/services/analysis/user'; +import { Card, Select, Empty, Spin, Tag, Row, Col, Statistic } from 'antd'; -interface KeywordChartProps { - data?: any[]; +const { Option } = Select; + +interface KeywordData { + keyWord: string; + searchCount: number; + rank?: number; } -export default function KeywordChart({ data: initialData }: KeywordChartProps) { - const [data, setData] = useState(initialData || []); +export default function KeywordChart() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [timeType, setTimeType] = useState<'1' | '2' | '3'>('3'); + const [displayMode, setDisplayMode] = useState<'column' | 'wordcloud'>('column'); + const [totalSearches, setTotalSearches] = useState(0); - const fetchData = useCallback( - async (params?: { searchDate?: string; searchCount?: number; searchType?: 1 | 2 | 3 }) => { + const fetchData = useCallback(async (searchType: '1' | '2' | '3' = '3') => { + setLoading(true); + try { const resData = await getKeyWordSearchRank({ - searchDate: params?.searchDate || '2025-11-03', - // searchCount: params?.searchCount || 4, - searchType: params?.searchType || 3, + searchDate: '2025-11-03', + searchType: searchType, }); if (resData.code === 200) { - setData(resData.rows); + const formattedData = resData.rows + .sort((a, b) => b.searchCount - a.searchCount) + .slice(0, 15) + .map((item, index) => ({ + ...item, + searchCount: Number(item.searchCount) || 0, + rank: index + 1, + })); + + setData(formattedData); + + // 计算总搜索次数 + const total = formattedData.reduce((sum, item) => sum + item.searchCount, 0); + setTotalSearches(total); } - }, - [], - ); + } catch (error) { + console.error('获取关键词数据失败:', error); + } finally { + setLoading(false); + } + }, []); useEffect(() => { - fetchData(); - }, [fetchData]); - const config = { - data, + fetchData(timeType); + }, [timeType]); + + // + const columnConfig = { + data: data, + stack:true, xField: 'keyWord', yField: 'searchCount', - height: 350, - label: { - position: 'middle', - style: { - fill: '#FFFFFF', - opacity: 0.6, - }, + height: 280, + autoFit: true, + style:{ + columnWidthRatio:0.3, }, + // labels: [ + // { + // text: 'searchCount', + // style: { + // fill: '#666', + // fontSize: 12, + // fontWeight: 600, + // textBaseline: 'bottom', + // dy: -6, // 向上偏移,避免与柱状图顶部重叠 + // }, + // position: 'top', + // } + // ], xAxis: { label: { autoHide: true, autoRotate: false, + style: { + fontSize: 12, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 6 ? `${text.substring(0, 6)}...` : text; + }, + }, + title: { + text: '关键词', + style: { + fontSize: 14, + fill: '#333', + fontWeight: 'bold', + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, }, }, - meta: { - keyWord: { alias: '关键词' }, - searchCount: { alias: '搜索次数' }, + yAxis: { + title: { + text: '搜索次数', + style: { + fontSize: 14, + fill: '#333', + fontWeight: 'bold', + }, + }, + label: { + style: { + fontSize: 12, + fill: '#666', + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, + }, + tooltip: { + title: '关键词搜索统计', + items: [ + { field: 'keyWord', name: '关键词' }, + { field: 'rank', name: '排名' }, + { field: 'searchCount', name: '搜索次数', valueFormatter: (v) => `${v} 次` }, + ], + }, + interactions: [{ type: 'element-active' }], + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, }, }; + + + const getTimeText = () => { + switch(timeType) { + case '1': return '今日'; + case '2': return '本周'; + case '3': return '本月'; + default: return ''; + } + }; + return ( - <> -
-

查询关键词搜索排行

- -
-
- {data?.map((item, index) => ( -
- 关键词:{item.keyWord} - 搜索次数:{item.searchCount} -
- ))} -
- + + 🔍 查询关键词搜索排行 + 搜索分析 + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)' + }} + bodyStyle={{ + padding: '16px', + borderRadius: 8 + }} + extra={ + +
+ + + + } + > + + {/* 统计信息 */} + + + + + + + + + + + + + + + + + + + {/* 图表显示区域 */} + {data.length > 0 ? ( + + ) : ( + + )} + + ); -} +} \ No newline at end of file diff --git a/src/pages/Analysis/User/components/WantedPositionStatics.tsx b/src/pages/Analysis/User/components/WantedPositionStatics.tsx index 6c61227..f8cad03 100644 --- a/src/pages/Analysis/User/components/WantedPositionStatics.tsx +++ b/src/pages/Analysis/User/components/WantedPositionStatics.tsx @@ -1,84 +1,447 @@ import React, { useState, useEffect, useCallback } from 'react'; import { getWantedPositionStaticsCount } from '@/services/analysis/user'; -import { Card, Table } from 'antd'; +import { Card, Row, Col, Select, Empty, Spin, Tag, Statistic } from 'antd'; +import { Column } from '@ant-design/charts'; -interface PositionStatics { +const { Option } = Select; + +interface PositionItem { + jobId: number; jobName: string; - count: number; + sexCode?: string; // 可能为 '男' 或 '女' + staticsCount?: number; } -interface getWantedPositionStaticsCountStaticsResult { - male: PositionStatics[]; - female: PositionStatics[]; +interface PositionStatics { + jobId: number; + jobName: string; + count: number; + percentage: number; } const WantedPositionStatics: React.FC = () => { - const [staticsData, setStaticsData] = useState({ male: [], female: [] }); + const [malePositions, setMalePositions] = useState([]); + const [femalePositions, setFemalePositions] = useState([]); + const [loading, setLoading] = useState(false); + const [topCount, setTopCount] = useState<10 | 15 | 20>(10); + const [totalMale, setTotalMale] = useState(0); + const [totalFemale, setTotalFemale] = useState(0); + const [allPositions, setAllPositions] = useState([]); const fetchStatics = useCallback(async () => { - const resData = await getWantedPositionStaticsCount({ sexCode: 0 }); - if (resData.code === 200) { - const malePositions: Record = {}; - const femalePositions: Record = {}; + setLoading(true); + try { + const resData = await getWantedPositionStaticsCount(); + if (resData.code === 200) { + const positions = resData.data as PositionItem[]; + setAllPositions(positions); + + // 统计每个岗位的男女数量 + const maleCounts: Record = {}; + const femaleCounts: Record = {}; + + positions.forEach((item,index) => { + if (item.sexCode === '男') { + maleCounts[item.jobId] = (maleCounts[item.jobId] || 0) + 1; + } else if (item.sexCode === '女') { + femaleCounts[item.jobId] = (femaleCounts[item.jobId] || 0) + 1; + } + }); - 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 maleTotal = Object.values(maleCounts).reduce((a, b) => a + b, 0); + const femaleTotal = Object.values(femaleCounts).reduce((a, b) => a + b, 0); + + setTotalMale(maleTotal); + setTotalFemale(femaleTotal); - const result: StaticsResult = { - male: Object.entries(malePositions).map(([jobName, count]) => ({ jobName, count })), - female: Object.entries(femalePositions).map(([jobName, count]) => ({ jobName, count })), - }; + // 获取所有岗位的唯一列表 + const uniqueJobs = Array.from( + new Map(positions.map(item => [item.jobId, item])).values() + ); - setStaticsData(result); + // 处理男性数据 + const processedMale = uniqueJobs + .map(job => ({ + jobId: job.jobId, + jobName: job.jobName, + count: maleCounts[job.jobId] || 0, + percentage: maleTotal > 0 ? Math.round(((maleCounts[job.jobId] || 0) / maleTotal) * 100) : 0 + })) + .filter(item => item.count > 0) // 只显示有数据的岗位 + .sort((a, b) => b.count - a.count) + .slice(0, topCount); + + // 处理女性数据 + const processedFemale = uniqueJobs + .map(job => ({ + jobId: job.jobId, + jobName: job.jobName, + count: femaleCounts[job.jobId] || 0, + percentage: femaleTotal > 0 ? Math.round(((femaleCounts[job.jobId] || 0) / femaleTotal) * 100) : 0 + })) + .filter(item => item.count > 0) // 只显示有数据的岗位 + .sort((a, b) => b.count - a.count) + .slice(0, topCount); + + console.log(processedMale,processedFemale,'++++++++') + + setMalePositions(processedMale); + setFemalePositions(processedFemale); + } + } catch (error) { + console.error('获取期望岗位数据失败:', error); + } finally { + setLoading(false); } - }, []); + }, [topCount]); useEffect(() => { fetchStatics(); }, [fetchStatics]); - const columns = [ - { - title: '岗位名称', - dataIndex: 'jobName', - key: 'jobName', + // 横向柱状图配置 - 男性 + const maleColumnConfig = { + data: malePositions, + xField: 'count', + yField: 'jobName', + seriesField: 'jobName', + isGroup: false, + isStack: false, + height: 280, + autoFit: true, + columnWidthRatio: 0.6, + color: '#1890ff', + labels: [ + { + text: 'count', + style: { + fill: '#666', + fontSize: 12, + fontWeight: 600, + }, + position: 'right', + formatter: (datum: any) => `${datum.count}人`, + } + ], + xAxis: { + title: { + text: '人数', + style: { + fontSize: 14, + fill: '#333', + fontWeight: 'bold', + }, + }, + label: { + style: { + fontSize: 12, + fill: '#666', + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, }, - { - title: '数量', - dataIndex: 'count', - key: 'count', + yAxis: { + title: null, + label: { + style: { + fontSize: 12, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 10 ? `${text.substring(0, 10)}...` : text; + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, }, - ]; + tooltip: { + title: '男性期望岗位', + items: [ + { field: 'jobName', name: '岗位名称' }, + { field: 'count', name: '人数', valueFormatter: (v) => `${v} 人` }, + { field: 'percentage', name: '占比', valueFormatter: (v) => `${v}%` }, + ], + }, + interactions: [{ type: 'element-active' }], + animation: { + appear: { + animation: 'scale-in-x', + duration: 1000, + }, + }, + }; + + // 横向柱状图配置 - 女性 + const femaleColumnConfig = { + data: femalePositions, + xField: 'count', + yField: 'jobName', + seriesField: 'jobName', + isGroup: false, + isStack: false, + height: 280, + autoFit: true, + columnWidthRatio: 0.6, + color: '#eb2f96', + labels: [ + { + text: 'count', + style: { + fill: '#666', + fontSize: 12, + fontWeight: 600, + }, + position: 'right', + formatter: (datum: any) => `${datum.count}人`, + } + ], + xAxis: { + title: { + text: '人数', + style: { + fontSize: 14, + fill: '#333', + fontWeight: 'bold', + }, + }, + label: { + style: { + fontSize: 12, + fill: '#666', + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, + }, + yAxis: { + title: null, + label: { + style: { + fontSize: 12, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 10 ? `${text.substring(0, 10)}...` : text; + }, + }, + grid: { + line: { + style: { + stroke: '#e8e8e8', + lineWidth: 1, + lineDash: [3, 3], + }, + }, + alignTick: true, + }, + }, + tooltip: { + title: '女性期望岗位', + items: [ + { field: 'jobName', name: '岗位名称' }, + { field: 'count', name: '人数', valueFormatter: (v) => `${v} 人` }, + { field: 'percentage', name: '占比', valueFormatter: (v) => `${v}%` }, + ], + }, + interactions: [{ type: 'element-active' }], + animation: { + appear: { + animation: 'scale-in-x', + duration: 1000, + }, + }, + }; + + // 如果没有数据,显示所有岗位的占位图 + const showPlaceholder = malePositions.length === 0 && femalePositions.length === 0; return ( -
- -
+ + + 🎯 期望岗位统计分析 + 岗位分析 + + } + style={{ + borderRadius: 12, + boxShadow: '0 4px 12px rgba(0,0,0,0.1)', + marginBottom: 16 + }} + bodyStyle={{ + padding: '16px', + background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', + borderRadius: 8 + }} + extra={ + + } + > + {/* 统计数据行 */} + + + + + + + + + + + + + + + + + + + {showPlaceholder ? ( + + + + 👨 男性热门岗位 + TOP {topCount} + + } + size="small" + style={{ height: '100%' }} + > + + + + + + 👩 女性热门岗位 + TOP {topCount} + + } + size="small" + style={{ height: '100%' }} + > + + + + + ) : ( + + {/* 男性期望岗位 - 横向柱状图 */} + + + 👨 男性热门岗位 + TOP {topCount} + + } + size="small" + style={{ height: '100%' }} + bodyStyle={{ padding: '12px', height: '100%' }} + > + {malePositions.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+ + + {/* 女性期望岗位 - 横向柱状图 */} + + + 👩 女性热门岗位 + TOP {topCount} + + } + size="small" + style={{ height: '100%' }} + bodyStyle={{ padding: '12px', height: '100%' }} + > + {femalePositions.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+ + + )} - -
- - + ); }; -export default WantedPositionStatics; +export default WantedPositionStatics; \ No newline at end of file diff --git a/src/pages/Analysis/User/index.tsx b/src/pages/Analysis/User/index.tsx index 34faffd..74716c3 100644 --- a/src/pages/Analysis/User/index.tsx +++ b/src/pages/Analysis/User/index.tsx @@ -43,18 +43,19 @@ export default function userAnalysis() { }, []); return ( -
-
-
- -
-
- -
-
+
+
+
+ +
+
+ +
+
+
); } diff --git a/src/pages/Mobileusers/List/index.tsx b/src/pages/Mobileusers/List/index.tsx index 6bab3f4..d1f3852 100644 --- a/src/pages/Mobileusers/List/index.tsx +++ b/src/pages/Mobileusers/List/index.tsx @@ -1,81 +1,77 @@ -import React, { Fragment, useEffect, useRef, useState } from 'react'; -import { FormattedMessage, useAccess } from '@umijs/max'; +import React, { Fragment, useRef, useState, useEffect } from 'react'; +import { useAccess } from '@umijs/max'; +import { getCmsAppUserList } from '@/services/mobileusers/list'; +import { getResumeDetail } from '@/services/resumeLibrary/resumeList'; // 简历库的简历详情接口 import { Button, FormInstance, message } from 'antd'; import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; -import { PlusOutlined } from '@ant-design/icons'; +import { EyeOutlined, UserOutlined } from '@ant-design/icons'; import { getDictValueEnum } from '@/services/system/dict'; import DictTag from '@/components/DictTag'; -import { exportCmsAppUserExport, getCmsAppUserList } from '@/services/mobileusers/list'; +import ResumeDetail from '../components/detail'; -const handleExport = async (values: API.MobileUser.ListParams) => { - const hide = message.loading('正在导出'); - try { - await exportCmsAppUserExport(values); - hide(); - message.success('导出成功'); - return true; - } catch (error) { - hide(); - message.error('导出失败,请重试'); - return false; - } -}; - -function ManagementList() { +function MobileUserList() { const access = useAccess(); const formTableRef = useRef(); const actionRef = useRef(); - const [educationEnum, setEducationEnum] = useState([]); - const [experienceEnum, setExperienceEnum] = useState([]); - const [areaEnum, setAreaEnum] = useState([]); - const [sexEnum, setSexEnum] = useState([]); - const [hotEnum, setHotEnum] = useState([]); - const [politicalEnum, setPoliticalEnum] = useState([]); const [currentRow, setCurrentRow] = useState(); const [modalVisible, setModalVisible] = useState(false); + const [loading, setLoading] = useState(false); + // 字典枚举值 + const [sexEnum, setSexEnum] = useState([]); + const [educationEnum, setEducationEnum] = useState([]); + const [politicalEnum, setPoliticalEnum] = useState([]); + const [areaEnum, setAreaEnum] = useState([]); + + // 获取字典数据 useEffect(() => { - getDictValueEnum('education', true, true).then((data) => { - setEducationEnum(data); - }); - getDictValueEnum('experience', true, true).then((data) => { - setExperienceEnum(data); - }); - getDictValueEnum('area', true, true).then((data) => { - setAreaEnum(data); - }); getDictValueEnum('sys_user_sex', true).then((data) => { setSexEnum(data); }); + getDictValueEnum('education', true, true).then((data) => { + setEducationEnum(data); + }); getDictValueEnum('political_affiliation', true, true).then((data) => { setPoliticalEnum(data); }); - // getDictValueEnum('job_hot',true).then((data) => { - // setHotEnum(data) - // }) + getDictValueEnum('area', true, true).then((data) => { + setAreaEnum(data); + }); }, []); + // 查看简历详情 + const handleViewResume = async (userId: any) => { + setLoading(true); + try { + const res = await getResumeDetail(userId); + if (res.code === 200) { + setCurrentRow(res.data); + setModalVisible(true); + } else { + message.error(res.msg); + } + } catch (error) { + message.error('获取简历详情失败'); + } finally { + setLoading(false); + } + }; + + // 查看用户行为(待定功能) + const handleViewBehavior = (userId: any) => { + message.info('用户行为查看功能开发中'); + // TODO: 待后续开发 + }; + const columns: ProColumns[] = [ - { + { title: '用户名', dataIndex: 'name', valueType: 'text', align: 'center', }, - { - title: '期望薪资', - dataIndex: 'minSalary', - valueType: 'text', - hideInSearch: true, - align: 'center', - render: (_, record) => ( - <> - {record.salaryMin}-{record.salaryMax} - - ), - }, { title: '出生日期', dataIndex: 'birthDate', @@ -122,13 +118,49 @@ function ManagementList() { return ; }, }, + { + title: '期望薪资', + dataIndex: 'minSalary', + valueType: 'text', + hideInSearch: true, + align: 'center', + render: (_, record) => ( + + {record.salaryMin || '面议'} - {record.salaryMax || '面议'} + + ), + }, { title: '操作', hideInSearch: true, align: 'center', - dataIndex: 'jobId', - width: 200, - render: (jobId, record) => [], + dataIndex: 'userId', + width: 240, + render: (userId, record) => ( +
+ + +
+ ), }, ]; @@ -136,42 +168,39 @@ function ManagementList() {
- // params 是需要自带的参数 - // 这个参数优先级更高,会覆盖查询表单的参数 actionRef={actionRef} formRef={formTableRef} - rowKey="jobId" - key="index" + rowKey="userId" + key="mobileUserIndex" columns={columns} - request={(params) => - getCmsAppUserList({ ...params } as API.MobileUser.ListParams).then((res) => { - console.log(params); - const result = { - data: res.rows, - total: res.total, - success: true, - }; - return result; - }) - } - toolBarRender={() => [ - , - ]} + search={{ + labelWidth: 120, + }} + request={async ( + params: API.MobileUser.ListParams & { + pageSize?: number; + current?: number; + }, + ) => { + const res = await getCmsAppUserList({ ...params } as API.MobileUser.ListParams); + return { + data: res.rows, + total: res.total, + success: true, + }; + }} />
+ { + setModalVisible(false); + setCurrentRow(undefined); + }} + />
); } -export default ManagementList; +export default MobileUserList; \ No newline at end of file diff --git a/src/pages/Mobileusers/components/detail.tsx b/src/pages/Mobileusers/components/detail.tsx new file mode 100644 index 0000000..3e4691a --- /dev/null +++ b/src/pages/Mobileusers/components/detail.tsx @@ -0,0 +1,402 @@ +import { Modal, Card, Row, Col, Divider, Tag, Typography, Avatar, Empty } from 'antd'; +import React, { useEffect, useState } from 'react'; +import { getDictValueEnum } from '@/services/system/dict'; +import DictTag from '@/components/DictTag'; + +const { Text, Paragraph, Title } = Typography; + +export type ResumeDetailProps = { + onCancel: (flag?: boolean, formVals?: unknown) => void; + open: boolean; + values?: any; +}; + +const ResumeDetail: React.FC = (props) => { + const [sexEnum, setSexEnum] = useState([]); + const [educationEnum, setEducationEnum] = useState([]); + const [politicalEnum, setPoliticalEnum] = useState([]); + const [areaEnum, setAreaEnum] = useState([]); + + // 获取字典数据 + useEffect(() => { + getDictValueEnum('sys_user_sex', true).then((data) => { + setSexEnum(data); + }); + getDictValueEnum('education', true, true).then((data) => { + setEducationEnum(data); + }); + getDictValueEnum('political_affiliation', true, true).then((data) => { + setPoliticalEnum(data); + }); + getDictValueEnum('area', true, true).then((data) => { + setAreaEnum(data); + }); + }, []); + + const handleCancel = () => { + props.onCancel(); + }; + + const { values } = props; + + // 格式化时间显示 + const formatTimeRange = (start: string, end: string) => { + if (!start && !end) return ''; + return `${start || ''} - ${end || '至今'}`; + }; + + // 渲染空状态 + const renderEmpty = (description: string) => ( + + ); + + return ( + + {values?.name || '用户'} - 简历详情 +
+ } + open={props.open} + width={1100} + onCancel={handleCancel} + footer={null} + bodyStyle={{ maxHeight: '80vh', overflowY: 'auto', padding: '24px' }} + > + {/* 头部基本信息卡片 */} + + + {/* 头像区域 */} + +
+ {values?.avatar ? ( + + ) : ( + + {values?.name?.charAt(0) || 'U'} + + )} +
+ {values?.name || '未填写'} +
+
+ {values?.jobTitle?.join(' / ') || '暂无期望岗位'} +
+
+ + + {/* 详细信息区域 */} + + + +
+
性别:
+
+ +
+
+ + +
+
年龄:
+
{values?.age || '未填写'}
+
+ + +
+
学历:
+
+ +
+
+ + +
+
手机号:
+
{values?.phone || '未填写'}
+
+ + +
+
政治面貌:
+
+ +
+
+ + +
+
出生日期:
+
{values?.birthDate || '未填写'}
+
+ + +
+
工作年限:
+
{values?.workYears || '未填写'}
+
+ + +
+
期望工作地:
+
+ +
+
+ + +
+
期望薪资:
+
+ {values?.salaryMin || '面议'} - {values?.salaryMax || '面议'} 元/月 +
+
+ + + + {/* 附加信息 */} + + + +
+
毕业院校:
+
{values?.graduationSchool || '未填写'}
+
+ + +
+
就读专业:
+
{values?.major || '未填写'}
+
+ + +
+
居住地址:
+
{values?.residenceAddress || '未填写'}
+
+ + + + + + + {/* 个人介绍与求职意向 - 新布局 */} + + 📋 个人介绍与求职意向 + + } + style={{ + marginBottom: 24, + borderRadius: 8, + border: '1px solid #f0f0f0', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', + }} + > + + + + <span style={{ color: '#1890ff', marginRight: 8 }}>●</span> + 个人简要介绍 + + + {values?.introduction || '暂无个人介绍'} + + + + + + <span style={{ color: '#1890ff', marginRight: 8 }}>●</span> + 求职意向岗位 + +
+ {values?.jobTitle?.length > 0 ? ( +
+ {values.jobTitle.map((job: string, index: number) => ( + + {job} + + ))} +
+ ) : ( + + {values?.jobIntention || '暂无求职意向'} + + )} +
+ + + + + <span style={{ color: '#1890ff', marginRight: 8 }}>●</span> + 自我评价 + + + {values?.selfEvaluation || '暂无自我评价'} + + + + + + {/* 工作经历 - 新布局 */} + + 💼 工作经历 ({values?.workExp?.length || 0}) + + } + style={{ + marginBottom: 24, + borderRadius: 8, + border: '1px solid #f0f0f0', + boxShadow: '0 2px 8px rgba(0,0,0,0.06)', + }} + > + {values?.workExp?.length > 0 ? ( +
+ {values.workExp.map((work: any, index: number) => ( +
+ +
+
+ + {work.company || '未填写公司'} + + {work.isFullTime !== null && ( + + {work.isFullTime ? '全职' : '兼职'} + + )} +
+
+ + {work.position || '未填写职位'} + + 部门:{work.department || '未填写'} +
+ {work.duty && ( + + {work.duty} + + )} + + +
+
+ {formatTimeRange(work.startTime, work.endTime)} +
+ {work.salary && ( +
+ 薪资:{work.salary} +
+ )} +
+ + + + ))} + + ) : ( + renderEmpty('暂无工作经历') + )} + + + ); +}; + +export default ResumeDetail; \ No newline at end of file