建立生成器功能
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled

This commit is contained in:
francis-fh
2026-07-23 12:30:28 +08:00
parent cd92dd4628
commit cd9a5b4c35
6 changed files with 257 additions and 49 deletions

View File

@@ -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() {
</Card>
{/* 简历生成器 iframe 区域 */}
{loading && <div style={{ textAlign: 'center', padding: 20 }}>...</div>}
<iframe
ref={iframeRef}
id="resume-generator"
title="简历生成器"
src="/resumegenerator"
style={{ width: '100%', height: 'calc(100vh - 110px)', border: '1px solid #f0f0f0', borderRadius: 4 }}
/>
<div style={{ position: 'relative', width: '100%', height: 'calc(100vh - 110px)' }}>
{loading && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff', zIndex: 1 }}>
...
</div>
)}
{!generatorReady && !loading && (
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff', zIndex: 1, color: '#999' }}>
...
</div>
)}
<iframe
ref={initHost}
id="resume-generator"
title="简历生成器"
src="/resumegenerator"
style={{ width: '100%', height: '100%', border: '1px solid #f0f0f0', borderRadius: 4 }}
/>
</div>
</div>
);
}