From 279011e6526702a40da69a239c9eb66a61934d5e Mon Sep 17 00:00:00 2001 From: francis-fh Date: Fri, 24 Jul 2026 19:28:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=9B=86=E6=88=90?= =?UTF-8?q?=E7=AE=80=E5=8E=86=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/JobPortal/Resume/index.less | 28 +- src/pages/JobPortal/Resume/index.tsx | 1492 ++++++------------- src/pages/Usermgmt/Appuser/Resume/index.tsx | 56 +- src/pages/Usermgmt/Companyuser/index.tsx | 19 +- src/types/jobportal/user.d.ts | 24 + 5 files changed, 566 insertions(+), 1053 deletions(-) diff --git a/src/pages/JobPortal/Resume/index.less b/src/pages/JobPortal/Resume/index.less index ba7bb27..e8b1d6c 100644 --- a/src/pages/JobPortal/Resume/index.less +++ b/src/pages/JobPortal/Resume/index.less @@ -8,31 +8,11 @@ .jp-page-container(); padding: 20px 20px 40px; } +} - .section { - .jp-card-base(); - margin-bottom: 16px; - padding: 0; - - .ant-card-head { - border-bottom: 1px solid @jp-border; - - .ant-card-head-title { - color: @jp-primary; - font-weight: 600; - } - } - } - - .card-profile .name { - color: @jp-text-primary; - font-weight: 600; - } - - .ant-btn-primary { - background: @jp-primary; - border-color: @jp-primary; - } +// iframe 区域确保撑满 +#resume-generator { + display: block; } @media (max-width: 1366px) { diff --git a/src/pages/JobPortal/Resume/index.tsx b/src/pages/JobPortal/Resume/index.tsx index e5bf049..115e2f3 100644 --- a/src/pages/JobPortal/Resume/index.tsx +++ b/src/pages/JobPortal/Resume/index.tsx @@ -1,1077 +1,515 @@ -import React from 'react'; -import { Card, Row, Col, Avatar, Typography, Button, Space, Divider, Tag, List, message, Tooltip, Modal, Form, Select, Input, DatePicker, AutoComplete, Spin } from 'antd'; -import { UserOutlined, EditOutlined, PlusOutlined, EnvironmentOutlined, PhoneOutlined, ManOutlined, WomanOutlined, DeleteOutlined, CheckOutlined } from '@ant-design/icons'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { history, request } from '@umijs/max'; +import { Button, Card, message, Space, Spin, Typography } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import Iframe from 'iframe-js'; import JobPortalHeader from '@/components/JobPortalHeader'; -import defaultAvatar from '@/assets/avatar.jpg'; -import './index.less'; -import JobTitleSelector from '@/components/JobTitleSelector'; -import moment from 'moment'; -import 'moment/locale/zh-cn'; -import zhCN from 'antd/es/date-picker/locale/zh_CN'; -import { addUserWorkExperience } from '@/services/common/jobTitle'; -import { request } from '@umijs/max'; +import useAppUserModel from '@/models/jobportal/user'; import { getDictValueEnum } from '@/services/system/dict'; -import { debounce } from '@/utils/tools'; -moment.locale('zh-cn'); +import './index.less'; -const { Title, Text } = Typography; +const { Text } = Typography; -const ResumePage: React.FC = () => { - // 字典数据 - const [dictData, setDictData] = React.useState<{ - sex?: any; - education?: any; - area?: any; - age?: any; - }>({}); +/** 字典 code → 汉字 的映射表 */ +type DictMap = Record; - // profile 数据可编辑 - const [profile, setProfile] = React.useState({ - userId: null as number | string | null, - name: '', - gender: '', - age: '', - education: '', - phone: '', - avatar: '', - desiredSalary: '', - desiredCity: '', - desiredTitle: '', - birthDate: null as string | null, - politicalAffiliation: '', - nation: '', - idCard: '', - salaryMin: '', - salaryMax: '', - workExperience: '', - }); - const [editMode, setEditMode] = React.useState(false); - const [draft, setDraft] = React.useState({desiredSalary: '', desiredCity: '', desiredTitle: ''}); +/** + * 将后端简历数据转换为简历生成器需要的格式(匹配 EMBED_INTEGRATION.md 第6节 schema) + */ +function toGeneratorFormat(data: API.AppUserInfo, dicts: { sex: DictMap; education: DictMap; area: DictMap; political: DictMap; nation: DictMap }): Record { + const sections: any[] = []; + + // 教育经历 + const education = (data.educationsList || []).map((item: any) => ({ + id: String(item.id), + school: item.schoolName || '', + degree: item.degree || '', + major: item.major || '', + startDate: item.startTime || '', + endDate: item.endTime || '', + description: item.content || '', + })); // 工作经历 - const [experiences, setExperiences] = React.useState>([]); + const experience = (data.experiencesList || []).map((item: any) => ({ + id: String(item.id), + company: item.companyName || '', + position: item.position || '', + startDate: item.startDate || '', + endDate: item.endDate || '', + description: item.description || '', + })); - // 新增经历弹窗控制 - const [addExpVisible, setAddExpVisible] = React.useState(false); - const [addExpForm, setAddExpForm] = React.useState<{ - company: string; - jobId: number | undefined; - jobTitle: string; - period: any; // [start, end] - DatePicker使用Dayjs但这里用moment兼容 - duties: string; - }>({ - company: '', - jobId: undefined, - jobTitle: '', - period: null, // [start, end] - duties: '', - }); - const [addExpTitleModal, setAddExpTitleModal] = React.useState(false); - const [addExpLoading, setAddExpLoading] = React.useState(false); + // 技能 + const skills = (data.appSkillsList || []).map((item: any) => ({ + id: String(item.id), + name: item.name || '', + level: item.levels || '', + })); - // 编辑弹窗控制 - const [editVisible, setEditVisible] = React.useState(false); - const [form] = Form.useForm(); - const [jobTitleModalOpen, setJobTitleModalOpen] = React.useState(false); - const [jobSelected, setJobSelected] = React.useState<{id?:number,label?:string}>({}); - // jobTitle树形数据 - const [jobTitleTree, setJobTitleTree] = React.useState; - }>>([]); - // jobTitleId映射表(用于快速查找) - const [jobTitleMap, setJobTitleMap] = React.useState>({}); - // 保存初始的jobTitleId,用于后续映射 - const [currentJobTitleId, setCurrentJobTitleId] = React.useState(null); + // 培训经历(生成器原生 training 字段) + const training = (data.trainsList || []).map((t: any) => ({ + id: String(t.id), + institution: t.trainOrg || '', + course: t.trainCourse || '', + startDate: t.startTime || '', + endDate: t.endTime || '', + description: t.trainContent || '', + })); - const [editExpId, setEditExpId] = React.useState(null); - const [editExpForm, setEditExpForm] = React.useState<{ - company: string; - jobId: number | undefined; - jobTitle: string; - period: any; // DatePicker使用Dayjs但这里用moment兼容 - duties: string; - }>({company:'',jobId:undefined,jobTitle:'',period:null,duties:''}); - const [editExpTitleModal, setEditExpTitleModal] = React.useState(false); + // sections 决定区块显示顺序和显隐 + sections.push({ id: 'basics', type: 'basics', title: '基本信息', hidden: false }); + if (education.length > 0) sections.push({ id: 'education', type: 'education', title: '教育经历', hidden: false }); + if (experience.length > 0) sections.push({ id: 'experience', type: 'experience', title: '工作经历', hidden: false }); + if (training.length > 0) sections.push({ id: 'training', type: 'training', title: '培训经历', hidden: false }); + if (skills.length > 0) sections.push({ id: 'skills', type: 'skills', title: '技能特长', hidden: false }); - const skillLevels = ['初级', '中级', '高级']; - const [skills, setSkills] = React.useState>([]); - const [editSkillIdx, setEditSkillIdx] = React.useState(null); - const [addSkillMode, setAddSkillMode] = React.useState(false); - const [skillDraft, setSkillDraft] = React.useState({ name: '', level: '初级' }); - // 技能名称模糊查询相关 - const [skillOptions, setSkillOptions] = React.useState>([]); - const [skillSearchLoading, setSkillSearchLoading] = React.useState(false); + // code → 汉字映射 + const sexLabel = dicts.sex[data.sex || ''] || ''; + const educationLabel = dicts.education[data.education || ''] || ''; + const politicalLabel = dicts.political[data.politicalAffiliation || ''] || ''; + const nationLabel = dicts.nation[data.nation || ''] || data.nation || ''; - // 获取字典值的显示文本(支持 dictValue / dictCode 多种存储格式) - const getDictText = (dictType: string, value: string | number | null | undefined): string => { - if (value === null || value === undefined || value === '') return ''; - const dict = dictData[dictType as keyof typeof dictData]; - if (!dict) return String(value); + // summary:展示性别、学历、工作年限等补充信息 + const summaryParts: string[] = []; + if (sexLabel) summaryParts.push(sexLabel); + if (educationLabel) summaryParts.push(educationLabel); + if (data.workExperience) summaryParts.push(`${data.workExperience}年工作经验`); - const strVal = String(value); - const direct = dict[strVal] ?? dict[Number(value)]; - if (direct?.text) return direct.text; - if (direct?.label) return direct.label; + const salaryStr = data.salaryMin || data.salaryMax + ? `${data.salaryMin || ''}${data.salaryMax ? '-' + data.salaryMax : ''}` + : ''; - const matched = Object.entries(dict).find(([dictKey, item]) => { - const entry = item as { value?: string | number; key?: string | number; text?: string; label?: string }; - return ( - dictKey === strVal || - String(entry.value) === strVal || - String(entry.key) === strVal - ); + // 将生成器不支持的标准字段放入 customSections + const supplementaryItems: any[] = []; + let itemIndex = 0; + if (nationLabel) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '民族', value: nationLabel }); + if (data.birthDate) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '出生日期', value: data.birthDate }); + if (data.age) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '年龄', value: data.age }); + if (politicalLabel) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '政治面貌', value: politicalLabel }); + if (salaryStr) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '期望薪资', value: salaryStr }); + if (data.area) supplementaryItems.push({ id: `sup_${itemIndex++}`, label: '所在区域', value: dicts.area[data.area || ''] || data.area }); + + const customSections: any[] = []; + if (supplementaryItems.length > 0) { + customSections.push({ id: 'supplementary', title: '补充信息', items: supplementaryItems }); + sections.push({ id: 'supplementary', type: 'customSection', title: '补充信息', hidden: false }); + } + + return { + basics: { + name: data.name || '', + phone: data.phone || '', + email: data.email || '', + avatarUrl: data.avatar || '', + title: data.jobTitle || '', + location: data.area ? (dicts.area[data.area] || data.area) : '', + summary: summaryParts.join(' | '), + _gender: sexLabel, + _educationLevel: educationLabel, + _yearsOfExperience: data.workExperience || '', + _politicalStatus: politicalLabel, + _ethnicity: nationLabel, + _age: data.age || '', + _birthDate: data.birthDate || '', + _expectedSalary: salaryStr, + }, + education, + experience, + training, + projects: [], + skills, + certificates: [], + customSections, + settings: { + templateId: 'modern', + primaryColor: '#0f766e', + pageSize: 'A4', + language: 'zh', + fontFamily: '', + sections, + }, + }; +} + +/** + * 将简历生成器返回的数据转为后端保存格式(汉字 → code 反向映射) + */ +function fromGeneratorFormat(resume: any, dicts: { sex: DictMap; education: DictMap; area: DictMap; political: DictMap; nation: DictMap }): { + appUser: Record; + appUserEducations: any[]; + appUserTrains: any[]; + experiencesList: any[]; + appSkillsList: any[]; +} { + const basics = resume?.basics || {}; + + // 从 customSections 中按 label 查找值 + const getCustomSectionValue = (label: string): string => { + const supSection = (resume?.customSections || []).find((s: any) => s.id === 'supplementary'); + if (!supSection?.items) return ''; + const item = supSection.items.find((i: any) => i.label === label); + return item?.value || ''; + }; + + // 反向 lookup:汉字 → code + const reverseLookup = (map: DictMap, text: string): string => { + if (!text) return ''; + for (const [code, label] of Object.entries(map)) { + if (label === text) return code; + } + return text; + }; + + // 性别:优先从 _gender 读取 + const sexText = basics._gender || ''; + const sexFromSummary = !sexText ? ((basics.summary || '').includes('男') ? '男' : (basics.summary || '').includes('女') ? '女' : '') : ''; + const sexFinal = sexText || sexFromSummary; + + // 学历、政治面貌、民族等 + const educationText = basics._educationLevel || ''; + const politicalText = basics._politicalStatus || getCustomSectionValue('政治面貌') || ''; + const nationText = basics._ethnicity || getCustomSectionValue('民族') || ''; + const ageText = basics._age || getCustomSectionValue('年龄') || ''; + const birthDateText = basics._birthDate || getCustomSectionValue('出生日期') || ''; + const salaryText = basics._expectedSalary || getCustomSectionValue('期望薪资') || ''; + + // 工作年限 + const yearsFromField = basics._yearsOfExperience || ''; + const yearsFromSummary = !yearsFromField ? ((basics.summary || '').match(/(\d+)年工作经验/)?.[1] || '') : ''; + const workExperienceFinal = yearsFromField || yearsFromSummary; + + return { + appUser: { + name: basics.name || undefined, + phone: basics.phone || undefined, + email: basics.email || undefined, + avatar: basics.avatarUrl || undefined, + sex: reverseLookup(dicts.sex, sexFinal) || (sexFinal === '男' ? '0' : sexFinal === '女' ? '1' : '') || undefined, + age: ageText || undefined, + birthDate: birthDateText || undefined, + education: reverseLookup(dicts.education, educationText) || educationText || undefined, + workExperience: workExperienceFinal || undefined, + politicalAffiliation: reverseLookup(dicts.political, politicalText) || politicalText || undefined, + nation: reverseLookup(dicts.nation, nationText) || nationText || undefined, + address: basics.location || undefined, + salaryMin: salaryText ? salaryText.split('-')[0] : undefined, + salaryMax: salaryText ? salaryText.split('-')[1] : undefined, + area: reverseLookup(dicts.area, basics.location || '') || basics.location || undefined, + }, + appUserEducations: (resume?.education || []).map((e: any) => ({ + id: e.id ? Number(e.id) : undefined, + schoolName: e.school || undefined, + major: e.major || undefined, + degree: e.degree || undefined, + startTime: e.startDate || undefined, + endTime: e.endDate || undefined, + content: e.description || undefined, + sort: 0, + })), + appUserTrains: (resume?.training || []).map((t: any) => ({ + id: t.id ? Number(t.id) : undefined, + trainOrg: t.institution || undefined, + trainCourse: t.course || undefined, + startTime: t.startDate || undefined, + endTime: t.endDate || undefined, + trainContent: t.description || undefined, + sort: 0, + })), + experiencesList: (resume?.experience || []).map((exp: any) => ({ + id: exp.id ? Number(exp.id) : undefined, + companyName: exp.company || undefined, + position: exp.position || undefined, + startDate: exp.startDate || undefined, + endDate: exp.endDate || undefined, + description: exp.description || undefined, + })), + appSkillsList: (resume?.skills || []).map((s: any) => ({ + id: s.id ? Number(s.id) : undefined, + name: s.name, + levels: s.level, + })), + }; +} + +const ResumePage: React.FC = () => { + const { userInfo, fetchUserInfo } = useAppUserModel(); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [resumeData, setResumeData] = useState(null); + const [generatorReady, setGeneratorReady] = useState(false); + const hostRef = useRef