From cd9a5b4c35086f5fc2911af772a0b544e7038d40 Mon Sep 17 00:00:00 2001 From: francis-fh Date: Thu, 23 Jul 2026 12:30:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BB=BA=E7=AB=8B=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Mobileusers/List/index.tsx | 80 +++++++++++ src/pages/Usermgmt/Appuser/List/index.tsx | 48 ++++++- src/pages/Usermgmt/Appuser/Resume/index.tsx | 145 ++++++++++++++------ src/services/session.ts | 12 +- src/types/cms/appuser.d.ts | 9 ++ src/types/mobileusers/list.d.ts | 12 ++ 6 files changed, 257 insertions(+), 49 deletions(-) diff --git a/src/pages/Mobileusers/List/index.tsx b/src/pages/Mobileusers/List/index.tsx index 7e76c3b..114cc04 100644 --- a/src/pages/Mobileusers/List/index.tsx +++ b/src/pages/Mobileusers/List/index.tsx @@ -6,6 +6,7 @@ import { PlusOutlined, SendOutlined, StarOutlined, StarFilled, FileTextOutlined import { getDictValueEnum } from '@/services/system/dict'; import DictTag from '@/components/DictTag'; import { applyAgree, delHrTalentCollect, exportCmsAppUserExport, getCmsAppUserList, getHrTalentCollectList } from '@/services/mobileusers/list'; +import { getJobTitleTreeSelect } from '@/services/common/jobTitle'; import InterviewInvite from '@/pages/Management/List/ResumeRecommend/InterviewInvite'; import CollectTalent from './CollectTalent'; @@ -36,6 +37,8 @@ function ManagementList() { const [sexEnum, setSexEnum] = useState([]); const [hotEnum, setHotEnum] = useState([]); const [politicalEnum, setPoliticalEnum] = useState([]); + const [nationEnum, setNationEnum] = useState([]); + const [userTypeEnum, setUserTypeEnum] = useState([]); const [currentRow, setCurrentRow] = useState(); const [modalVisible, setModalVisible] = useState(false); const [inviteVisible, setInviteVisible] = useState(false); @@ -62,6 +65,12 @@ function ManagementList() { getDictValueEnum('is_publish', true).then((data) => { setIsPublishEnum(data); }); + getDictValueEnum('nation', false, true).then((data) => { + setNationEnum(data); + }); + getDictValueEnum('user_type', false, true).then((data) => { + setUserTypeEnum(data); + }); // getDictValueEnum('job_hot',true).then((data) => { // setHotEnum(data) // }) @@ -145,6 +154,25 @@ function ManagementList() { valueType: 'text', align: 'center', }, + { + title: '求职期望', + dataIndex: 'jobTitleId', + valueType: 'treeSelect', + hideInTable: true, + width: 240, + fieldProps: { + showSearch: true, + filterTreeNode: (input: string, node: any) => node.label?.includes(input), + treeNodeFilterProp: 'label', + placeholder: '请选择求职期望', + style: { width: 240 }, + fieldNames: { label: 'label', value: 'id', children: 'children' }, + }, + request: async () => { + const res = await getJobTitleTreeSelect(); + return res?.data || []; + }, + }, { title: '期望薪资', dataIndex: 'minSalary', @@ -166,6 +194,57 @@ function ManagementList() { format: 'YYYY-MM-DD', }, }, + { + title: '年龄', + dataIndex: 'age', + valueType: 'text', + align: 'center', + }, + { + title: '毕业院校', + dataIndex: 'schoolName', + valueType: 'text', + align: 'center', + }, + { + title: '毕业时间', + dataIndex: 'endTime', + valueType: 'date', + align: 'center', + fieldProps: { + format: 'YYYY-MM-DD', + }, + }, + { + title: '民族', + dataIndex: 'nation', + valueType: 'select', + align: 'center', + valueEnum: nationEnum, + render: (_, record) => { + return ; + }, + }, + { + title: '个人标签', + dataIndex: 'userType', + valueType: 'select', + align: 'center', + valueEnum: userTypeEnum, + render: (_, record) => { + return ; + }, + }, + { + title: '工作经验', + dataIndex: 'workExperience', + valueType: 'select', + align: 'center', + valueEnum: experienceEnum, + render: (_, record) => { + return ; + }, + }, { title: '学历要求', dataIndex: 'education', @@ -302,6 +381,7 @@ function ManagementList() { rowKey="applyId" key="index" columns={columns} + search={{ defaultCollapsed: false }} request={(params) => { const { current, pageSize, ...searchParams } = params; // 过滤空值,避免空字符串干扰后端查询 diff --git a/src/pages/Usermgmt/Appuser/List/index.tsx b/src/pages/Usermgmt/Appuser/List/index.tsx index 2962316..696f77f 100644 --- a/src/pages/Usermgmt/Appuser/List/index.tsx +++ b/src/pages/Usermgmt/Appuser/List/index.tsx @@ -23,6 +23,8 @@ function AppUserList() { const [areaEnum, setAreaEnum] = useState>({}); const [politicalEnum, setPoliticalEnum] = useState>({}); const [nationEnum, setNationEnum] = useState>({}); + const [userTypeEnum, setUserTypeEnum] = useState>({}); + const [workExperienceEnum, setWorkExperienceEnum] = useState>({}); /** 修改身份证 Modal 状态 */ const [idCardModalOpen, setIdCardModalOpen] = useState(false); @@ -40,6 +42,8 @@ function AppUserList() { getDictValueEnum('area', true, true).then(setAreaEnum); getDictValueEnum('political_affiliation', true, true).then(setPoliticalEnum); getDictValueEnum('nation', false, true).then(setNationEnum); + getDictValueEnum('user_type', false, true).then(setUserTypeEnum); + getDictValueEnum('experience', false, true).then(setWorkExperienceEnum); }, []); const columns: ProColumns[] = [ @@ -80,7 +84,14 @@ function AppUserList() { valueType: 'text', align: 'center', width: 70, - hideInSearch: true, + }, + { + title: '出生年月', + dataIndex: 'birthDate', + valueType: 'date', + align: 'center', + width: 120, + fieldProps: { format: 'YYYY-MM-DD' }, }, { title: '学历', @@ -91,6 +102,21 @@ function AppUserList() { valueEnum: educationEnum, render: (_, record) => , }, + { + title: '毕业院校', + dataIndex: 'schoolName', + valueType: 'text', + align: 'center', + width: 160, + }, + { + title: '毕业时间', + dataIndex: 'endTime', + valueType: 'date', + align: 'center', + width: 120, + fieldProps: { format: 'YYYY-MM-DD' }, + }, { title: '地区', dataIndex: 'area', @@ -118,6 +144,24 @@ function AppUserList() { valueEnum: nationEnum, render: (_, record) => , }, + { + title: '个人标签', + dataIndex: 'userType', + valueType: 'select', + align: 'center', + width: 110, + valueEnum: userTypeEnum, + render: (_, record) => , + }, + { + title: '工作经验', + dataIndex: 'workExperience', + valueType: 'select', + align: 'center', + width: 110, + valueEnum: workExperienceEnum, + render: (_, record) => , + }, { title: '期望薪资', dataIndex: 'salaryRange', @@ -267,7 +311,7 @@ function AppUserList() { success: true, })); }} - search={{ labelWidth: 'auto' }} + search={{ labelWidth: 'auto', defaultCollapsed: false }} scroll={{ x: 'max-content' }} pagination={{ defaultPageSize: 10, showSizeChanger: true }} headerTitle="个人用户管理" diff --git a/src/pages/Usermgmt/Appuser/Resume/index.tsx b/src/pages/Usermgmt/Appuser/Resume/index.tsx index 9f49d00..ea524c1 100644 --- a/src/pages/Usermgmt/Appuser/Resume/index.tsx +++ b/src/pages/Usermgmt/Appuser/Resume/index.tsx @@ -80,6 +80,22 @@ function toGeneratorFormat(data: API.CmsAppUser.ResumeData, dicts: { sex: DictMa ? `${data.salaryMin || ''}${data.salaryMax ? '-' + data.salaryMax : ''}` : ''; + // 将生成器不支持的标准字段放入 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 || '', @@ -87,17 +103,17 @@ function toGeneratorFormat(data: API.CmsAppUser.ResumeData, dicts: { sex: DictMa email: data.email || '', avatarUrl: data.avatar || '', title: data.jobTitle?.[0] || '', - ethnicity: nationLabel, - birthDate: data.birthDate || '', - age: data.age || '', - politicalStatus: politicalLabel, - expectedPosition: data.jobTitle?.[0] || '', - expectedSalary: salaryStr, + location: data.area ? (dicts.area[data.area] || data.area) : '', summary: summaryParts.join(' | '), // 以下为自定义扩展字段,生成器 Zod schema 会丢弃,但 save 时从 summary 回读 _gender: sexLabel, _educationLevel: educationLabel, _yearsOfExperience: data.workExperience || '', + _politicalStatus: politicalLabel, + _ethnicity: nationLabel, + _age: data.age || '', + _birthDate: data.birthDate || '', + _expectedSalary: salaryStr, }, education, experience, @@ -105,7 +121,7 @@ function toGeneratorFormat(data: API.CmsAppUser.ResumeData, dicts: { sex: DictMa projects: [], skills, certificates: [], - customSections: [], + customSections, settings: { templateId: 'modern', primaryColor: '#0f766e', @@ -129,6 +145,14 @@ function fromGeneratorFormat(resume: any, dicts: { sex: DictMap; education: Dict } { const basics = resume?.basics || {}; + // 从 customSections 中按 label 查找值(作为 Zod 丢弃字段后的回退方案) + 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 ''; @@ -145,8 +169,11 @@ function fromGeneratorFormat(resume: any, dicts: { sex: DictMap; education: Dict // 学历:优先从 _educationLevel,回退到 summary 中的学历标签 const educationText = basics._educationLevel || ''; - const politicalText = basics.politicalStatus || ''; - const nationText = basics.ethnicity || ''; + 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('期望薪资') || ''; // 工作年限:从 _yearsOfExperience 或 summary 中匹配 "X年工作经验" const yearsFromField = basics._yearsOfExperience || ''; @@ -160,13 +187,15 @@ function fromGeneratorFormat(resume: any, dicts: { sex: DictMap; education: Dict email: basics.email || undefined, avatar: basics.avatarUrl || undefined, sex: reverseLookup(dicts.sex, sexFinal) || (sexFinal === '男' ? '0' : sexFinal === '女' ? '1' : '') || undefined, - age: basics.age || undefined, - birthDate: basics.birthDate || 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, }, appUserEducations: (resume?.education || []).map((e: any) => ({ id: e.id ? Number(e.id) : undefined, @@ -278,6 +307,8 @@ function AppUserResume() { } else { console.warn('简历data为空,该用户可能没有简历信息'); resumeDataRef.current = null; + setResumeData(null); + message.info('该用户暂无简历信息,生成器将显示空白模板'); } } else { message.error('获取简历数据失败: ' + (res?.msg || '未知错误')); @@ -290,9 +321,11 @@ function AppUserResume() { .finally(() => setLoading(false)); }, [userId]); - // 初始化 iframe-js 通信(独立于数据加载) - const initIframe = useCallback(() => { - if (!iframeRef.current || hostRef.current) return; + // 初始化 iframe-js 通信(通过 ref callback 在 DOM 插入时同步执行,确保监听器在 iframe 加载前就位) + const initHost = useCallback((el: HTMLIFrameElement) => { + if (!el || hostRef.current) return; + + iframeRef.current = el; const generatorUrl = window.location.origin + '/resumegenerator'; console.log('[Resume] 开始创建iframe通信,URL:', generatorUrl); @@ -300,7 +333,7 @@ function AppUserResume() { let host; try { host = new Iframe({ - container: iframeRef.current, + container: el, url: generatorUrl, whiteList: [location.origin], timeout: 15000, @@ -373,19 +406,43 @@ function AppUserResume() { } }); + // 超时保护:15秒内未收到 ready 则报错 + const readyTimeout = setTimeout(() => { + if (!hostRef.current) return; + console.error('[Resume] 简历生成器加载超时(15秒)'); + message.error('简历生成器加载失败,请检查网络连接后刷新页面重试'); + setGeneratorReady(false); + }, 15000); + // 生成器就绪 + let readyFired = false; host.action('resume:ready', () => { + if (readyFired) return; // 防止重复触发 + readyFired = true; + clearTimeout(readyTimeout); + console.log('[Resume] 简历生成器已就绪'); setGeneratorReady(true); - // 有数据就加载 - const data = resumeDataRef.current; - if (data) { - const genData = toGeneratorFormat(data, dictsRef.current); - console.log('加载简历数据到生成器:', genData); - host.callRemote('resume:load', { resume: genData }, 8000).catch((err: any) => { - console.error('[Resume] resume:load 失败:', err); - }); - } + + // 推送数据到生成器(如果数据已加载) + const tryLoadData = (attempts: number = 0) => { + const data = resumeDataRef.current; + if (data) { + const genData = toGeneratorFormat(data, dictsRef.current); + console.log('[Resume] 加载简历数据到生成器:', genData); + host.callRemote('resume:load', { resume: genData }, 8000).catch((err: any) => { + console.error('[Resume] resume:load 失败:', err); + message.warning('简历数据加载失败,请点击"重新加载"按钮重试'); + }); + } else if (attempts < 20) { + // 数据还没加载完,轮询等待(最多 10 秒,每 500ms 一次) + setTimeout(() => tryLoadData(attempts + 1), 500); + } else { + console.warn('[Resume] 等待简历数据超时(10秒),生成器将显示空模板'); + } + }; + tryLoadData(); + if (readOnly) { host.callRemote('resume:setReadOnly', { readOnly: true }, 8000).catch((err: any) => { console.error('[Resume] resume:setReadOnly 失败:', err); @@ -426,7 +483,7 @@ function AppUserResume() { host.action('resume:change', (event: any) => { console.log('简历内容已更新', event?.data?.resume); }); - }, [readOnly, userId]); + }, []); // 仅在首次挂载时初始化,readOnly 和 userId 通过 ref 获取最新值 // 当 resumeData 变化且生成器已就绪时,推送新数据到生成器 useEffect(() => { @@ -439,22 +496,15 @@ function AppUserResume() { } }, [resumeData, generatorReady]); - // 页面加载时初始化 iframe + // 页面卸载时清理 iframe-js 实例 useEffect(() => { - // 先清理旧实例 - if (hostRef.current) { - try { hostRef.current.destroy(); } catch (e) {} - hostRef.current = null; - } - console.log('[Resume] useEffect触发 initIframe'); - initIframe(); return () => { if (hostRef.current) { try { hostRef.current.destroy(); } catch (e) {} hostRef.current = null; } }; - }, []); // 只在 mount 时执行一次 + }, []); // 切换只读模式 const toggleReadOnly = async (checked: boolean) => { @@ -511,14 +561,25 @@ function AppUserResume() { {/* 简历生成器 iframe 区域 */} - {loading &&
正在加载简历数据...
} -