527 lines
19 KiB
TypeScript
527 lines
19 KiB
TypeScript
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||
|
|
import { useParams, useNavigate, useSearchParams } from '@umijs/max';
|
|||
|
|
import { Button, Card, message, Space, Switch, Typography } from 'antd';
|
|||
|
|
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
|
|||
|
|
import Iframe from 'iframe-js';
|
|||
|
|
import { getAppUserResume } from '@/services/cms/appuser';
|
|||
|
|
import { getDictValueEnum } from '@/services/system/dict';
|
|||
|
|
import { request } from '@umijs/max';
|
|||
|
|
import './index.less';
|
|||
|
|
|
|||
|
|
const { Text } = Typography;
|
|||
|
|
|
|||
|
|
/** 字典 code → 汉字 的映射表 */
|
|||
|
|
type DictMap = Record<string, string>;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将后端简历数据转换为简历生成器需要的格式(匹配 EMBED_INTEGRATION.md 第6节 schema)
|
|||
|
|
*/
|
|||
|
|
function toGeneratorFormat(data: API.CmsAppUser.ResumeData, dicts: { sex: DictMap; education: DictMap; area: DictMap; political: DictMap; nation: DictMap }): Record<string, any> {
|
|||
|
|
const sections: any[] = [];
|
|||
|
|
|
|||
|
|
// 教育经历
|
|||
|
|
const education = (data.educationsList || []).map((item) => ({
|
|||
|
|
id: String(item.id),
|
|||
|
|
school: item.schoolName || '',
|
|||
|
|
degree: item.degree || '',
|
|||
|
|
major: item.major || '',
|
|||
|
|
startDate: item.startTime || '',
|
|||
|
|
endDate: item.endTime || '',
|
|||
|
|
description: item.content || '',
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 工作经历
|
|||
|
|
const experience = (data.experiencesList || []).map((item) => ({
|
|||
|
|
id: String(item.id),
|
|||
|
|
company: item.companyName || '',
|
|||
|
|
position: item.position || '',
|
|||
|
|
startDate: item.startDate || '',
|
|||
|
|
endDate: item.endDate || '',
|
|||
|
|
description: item.description || '',
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 技能
|
|||
|
|
const skills = (data.appSkillsList || []).map((item) => ({
|
|||
|
|
id: String(item.id),
|
|||
|
|
name: item.name || '',
|
|||
|
|
level: item.levels || '',
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 培训经历(生成器原生 training 字段)
|
|||
|
|
const training = (data.trainsList || []).map((t) => ({
|
|||
|
|
id: String(t.id),
|
|||
|
|
institution: t.trainOrg || '',
|
|||
|
|
course: t.trainCourse || '',
|
|||
|
|
startDate: t.startTime || '',
|
|||
|
|
endDate: t.endTime || '',
|
|||
|
|
description: t.trainContent || '',
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 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 });
|
|||
|
|
|
|||
|
|
// 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 || '';
|
|||
|
|
|
|||
|
|
// summary:展示性别、学历、工作年限等补充信息(生成器 schema 仅 name/title/phone/email/location/summary 等标准字段,自定义字段会被 Zod 校验丢弃)
|
|||
|
|
const summaryParts: string[] = [];
|
|||
|
|
if (sexLabel) summaryParts.push(sexLabel);
|
|||
|
|
if (educationLabel) summaryParts.push(educationLabel);
|
|||
|
|
if (data.workExperience) summaryParts.push(`${data.workExperience}年工作经验`);
|
|||
|
|
|
|||
|
|
const salaryStr = data.salaryMin || data.salaryMax
|
|||
|
|
? `${data.salaryMin || ''}${data.salaryMax ? '-' + data.salaryMax : ''}`
|
|||
|
|
: '';
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
basics: {
|
|||
|
|
name: data.name || '',
|
|||
|
|
phone: data.phone || '',
|
|||
|
|
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,
|
|||
|
|
summary: summaryParts.join(' | '),
|
|||
|
|
// 以下为自定义扩展字段,生成器 Zod schema 会丢弃,但 save 时从 summary 回读
|
|||
|
|
_gender: sexLabel,
|
|||
|
|
_educationLevel: educationLabel,
|
|||
|
|
_yearsOfExperience: data.workExperience || '',
|
|||
|
|
},
|
|||
|
|
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<string, any>;
|
|||
|
|
appUserEducations: any[];
|
|||
|
|
appUserTrains: any[];
|
|||
|
|
experiencesList: any[];
|
|||
|
|
appSkillsList: any[];
|
|||
|
|
} {
|
|||
|
|
const basics = resume?.basics || {};
|
|||
|
|
|
|||
|
|
// 反向 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 读取,生成器丢弃后从 summary 回退
|
|||
|
|
const sexText = basics._gender || '';
|
|||
|
|
const sexFromSummary = !sexText ? ((basics.summary || '').includes('男') ? '男' : (basics.summary || '').includes('女') ? '女' : '') : '';
|
|||
|
|
const sexFinal = sexText || sexFromSummary;
|
|||
|
|
|
|||
|
|
// 学历:优先从 _educationLevel,回退到 summary 中的学历标签
|
|||
|
|
const educationText = basics._educationLevel || '';
|
|||
|
|
const politicalText = basics.politicalStatus || '';
|
|||
|
|
const nationText = basics.ethnicity || '';
|
|||
|
|
|
|||
|
|
// 工作年限:从 _yearsOfExperience 或 summary 中匹配 "X年工作经验"
|
|||
|
|
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: basics.age || undefined,
|
|||
|
|
birthDate: basics.birthDate || 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,
|
|||
|
|
},
|
|||
|
|
appUserEducations: (resume?.education || []).map((e: any) => ({
|
|||
|
|
id: e.id ? Number(e.id) : undefined,
|
|||
|
|
schoolName: e.school || undefined,
|
|||
|
|
educationLevel: undefined,
|
|||
|
|
major: e.major || undefined,
|
|||
|
|
degree: e.degree || undefined,
|
|||
|
|
studyType: 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,
|
|||
|
|
trainCert: 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,
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function AppUserResume() {
|
|||
|
|
console.log('===== AppUserResume 组件渲染 =====');
|
|||
|
|
const params = useParams<{ userId: string }>();
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
const [searchParams] = useSearchParams();
|
|||
|
|
const forceReadOnly = searchParams.get('readOnly') === '1';
|
|||
|
|
const userId = Number(params.userId);
|
|||
|
|
console.log('userId from params:', userId);
|
|||
|
|
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
const [saving, setSaving] = useState(false);
|
|||
|
|
const [readOnly, setReadOnly] = useState(forceReadOnly);
|
|||
|
|
const [resumeData, setResumeData] = useState<API.CmsAppUser.ResumeData | null>(null);
|
|||
|
|
const [generatorReady, setGeneratorReady] = useState(false);
|
|||
|
|
const hostRef = useRef<Iframe | null>(null);
|
|||
|
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|||
|
|
const resumeDataRef = useRef<API.CmsAppUser.ResumeData | null>(null);
|
|||
|
|
|
|||
|
|
// 字典数据(code → 汉字)
|
|||
|
|
const [sexDict, setSexDict] = useState<DictMap>({});
|
|||
|
|
const [educationDict, setEducationDict] = useState<DictMap>({});
|
|||
|
|
const [areaDict, setAreaDict] = useState<DictMap>({});
|
|||
|
|
const [politicalDict, setPoliticalDict] = useState<DictMap>({});
|
|||
|
|
const [nationDict, setNationDict] = useState<DictMap>({});
|
|||
|
|
|
|||
|
|
// 加载字典
|
|||
|
|
useEffect(() => {
|
|||
|
|
getDictValueEnum('sys_user_sex', true).then((res: any) => {
|
|||
|
|
const map: DictMap = {};
|
|||
|
|
if (res) Object.keys(res).forEach(k => { map[k] = res[k]?.text || ''; });
|
|||
|
|
setSexDict(map);
|
|||
|
|
});
|
|||
|
|
getDictValueEnum('education', true, true).then((res: any) => {
|
|||
|
|
const map: DictMap = {};
|
|||
|
|
if (res) Object.keys(res).forEach(k => { map[k] = res[k]?.text || ''; });
|
|||
|
|
setEducationDict(map);
|
|||
|
|
});
|
|||
|
|
getDictValueEnum('area', true, true).then((res: any) => {
|
|||
|
|
const map: DictMap = {};
|
|||
|
|
if (res) Object.keys(res).forEach(k => { map[k] = res[k]?.text || ''; });
|
|||
|
|
setAreaDict(map);
|
|||
|
|
});
|
|||
|
|
getDictValueEnum('political_affiliation', true, true).then((res: any) => {
|
|||
|
|
const map: DictMap = {};
|
|||
|
|
if (res) Object.keys(res).forEach(k => { map[k] = res[k]?.text || ''; });
|
|||
|
|
setPoliticalDict(map);
|
|||
|
|
});
|
|||
|
|
getDictValueEnum('nation', false, true).then((res: any) => {
|
|||
|
|
const map: DictMap = {};
|
|||
|
|
if (res) Object.keys(res).forEach(k => { map[k] = res[k]?.text || ''; });
|
|||
|
|
setNationDict(map);
|
|||
|
|
});
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const dicts = { sex: sexDict, education: educationDict, area: areaDict, political: politicalDict, nation: nationDict };
|
|||
|
|
const dictsRef = useRef(dicts);
|
|||
|
|
dictsRef.current = dicts;
|
|||
|
|
|
|||
|
|
// 加载完整简历数据
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!userId || isNaN(userId)) return;
|
|||
|
|
setLoading(true);
|
|||
|
|
getAppUserResume(userId)
|
|||
|
|
.then((res) => {
|
|||
|
|
console.log('简历API返回:', res);
|
|||
|
|
if (res?.code === 200) {
|
|||
|
|
if (res?.data) {
|
|||
|
|
setResumeData(res.data);
|
|||
|
|
resumeDataRef.current = res.data;
|
|||
|
|
} else {
|
|||
|
|
console.warn('简历data为空,该用户可能没有简历信息');
|
|||
|
|
resumeDataRef.current = null;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
message.error('获取简历数据失败: ' + (res?.msg || '未知错误'));
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
.catch((err) => {
|
|||
|
|
console.error('获取简历数据异常:', err);
|
|||
|
|
message.error('获取简历数据异常');
|
|||
|
|
})
|
|||
|
|
.finally(() => setLoading(false));
|
|||
|
|
}, [userId]);
|
|||
|
|
|
|||
|
|
// 初始化 iframe-js 通信(独立于数据加载)
|
|||
|
|
const initIframe = useCallback(() => {
|
|||
|
|
if (!iframeRef.current || hostRef.current) return;
|
|||
|
|
|
|||
|
|
const generatorUrl = window.location.origin + '/resumegenerator';
|
|||
|
|
console.log('[Resume] 开始创建iframe通信,URL:', generatorUrl);
|
|||
|
|
|
|||
|
|
let host;
|
|||
|
|
try {
|
|||
|
|
host = new Iframe({
|
|||
|
|
container: iframeRef.current,
|
|||
|
|
url: generatorUrl,
|
|||
|
|
whiteList: [location.origin],
|
|||
|
|
timeout: 15000,
|
|||
|
|
});
|
|||
|
|
console.log('[Resume] Iframe实例创建成功');
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('[Resume] Iframe创建失败:', err);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hostRef.current = host;
|
|||
|
|
|
|||
|
|
// 暴露保存方法给生成器
|
|||
|
|
host.expose('resume:save', async ({ resume }: { resume: any }) => {
|
|||
|
|
console.log('[Resume] 收到保存请求:', resume);
|
|||
|
|
setSaving(true);
|
|||
|
|
try {
|
|||
|
|
const mapped = fromGeneratorFormat(resume, dictsRef.current);
|
|||
|
|
// 注入 userId
|
|||
|
|
mapped.appUser.userId = userId;
|
|||
|
|
mapped.appUserEducations.forEach((e: any) => (e.userId = userId));
|
|||
|
|
mapped.appUserTrains.forEach((t: any) => (t.userId = userId));
|
|||
|
|
mapped.experiencesList.forEach((exp: any) => (exp.userId = userId));
|
|||
|
|
mapped.appSkillsList.forEach((s: any) => (s.userId = userId));
|
|||
|
|
|
|||
|
|
console.log('[Resume] 映射后的保存数据:', mapped);
|
|||
|
|
|
|||
|
|
const result = await request<API.Result>('/api/cms/appUser/saveResume', {
|
|||
|
|
method: 'POST',
|
|||
|
|
data: mapped,
|
|||
|
|
});
|
|||
|
|
if (result.code === 200) {
|
|||
|
|
message.success('简历保存成功');
|
|||
|
|
// 返回用户列表
|
|||
|
|
setTimeout(() => navigate('/usermgmt/appuser/index', { replace: true }), 800);
|
|||
|
|
return { ok: true };
|
|||
|
|
}
|
|||
|
|
message.error(result.msg || '保存失败');
|
|||
|
|
return { ok: false };
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('[Resume] 保存异常:', err);
|
|||
|
|
message.error('保存异常');
|
|||
|
|
return { ok: false };
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 暴露头像上传方法给生成器
|
|||
|
|
host.expose('avatar:upload', async ({ file }: { file: any }) => {
|
|||
|
|
// 文件上传:由生成器将文件传给父页面,父页面调用后端上传
|
|||
|
|
// 这里使用通用的文件上传接口
|
|||
|
|
try {
|
|||
|
|
const formData = new FormData();
|
|||
|
|
formData.append('file', file);
|
|||
|
|
const response = await fetch('/api/common/upload', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: formData,
|
|||
|
|
});
|
|||
|
|
const result = await response.json();
|
|||
|
|
if (result.code === 200 && result.url) {
|
|||
|
|
return { url: result.url };
|
|||
|
|
}
|
|||
|
|
// 如果通用上传不可用,尝试以base64返回
|
|||
|
|
message.warning('头像上传接口不可用,将使用本地预览');
|
|||
|
|
return { url: '' };
|
|||
|
|
} catch {
|
|||
|
|
message.error('头像上传失败');
|
|||
|
|
return { url: '' };
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 生成器就绪
|
|||
|
|
host.action('resume:ready', () => {
|
|||
|
|
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);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (readOnly) {
|
|||
|
|
host.callRemote('resume:setReadOnly', { readOnly: true }, 8000).catch((err: any) => {
|
|||
|
|
console.error('[Resume] resume:setReadOnly 失败:', err);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听 iframe 内的下载/导出按钮点击,记录下载日志
|
|||
|
|
try {
|
|||
|
|
const iframeDoc = iframeRef.current?.contentDocument;
|
|||
|
|
if (iframeDoc) {
|
|||
|
|
let lastLogTime = 0;
|
|||
|
|
iframeDoc.addEventListener('click', (e: MouseEvent) => {
|
|||
|
|
const target = e.target as HTMLElement;
|
|||
|
|
const text = (target.textContent || '').trim();
|
|||
|
|
const isDownloadBtn =
|
|||
|
|
/^(导出|下载|打印|Export|Download|Print|PDF|导出PDF|下载PDF)$/i.test(text) ||
|
|||
|
|
/export|download|pdf|print/i.test(target.className || '') ||
|
|||
|
|
target.getAttribute('aria-label')?.match(/export|download|pdf|print|导出|下载|打印/i) ||
|
|||
|
|
target.closest('[class*="export"],[class*="download"],[aria-label*="导出"],[aria-label*="下载"],[aria-label*="PDF"]');
|
|||
|
|
if (isDownloadBtn) {
|
|||
|
|
const now = Date.now();
|
|||
|
|
if (now - lastLogTime < 3000) return; // 3秒内防抖
|
|||
|
|
lastLogTime = now;
|
|||
|
|
console.log('[Resume] 检测到下载操作:', text);
|
|||
|
|
request('/api/cms/resumeAccessLog/download', {
|
|||
|
|
method: 'POST',
|
|||
|
|
data: { userId },
|
|||
|
|
}).catch(() => {});
|
|||
|
|
}
|
|||
|
|
}, true);
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('[Resume] 无法监听iframe下载事件:', e);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听简历变化
|
|||
|
|
host.action('resume:change', (event: any) => {
|
|||
|
|
console.log('简历内容已更新', event?.data?.resume);
|
|||
|
|
});
|
|||
|
|
}, [readOnly, userId]);
|
|||
|
|
|
|||
|
|
// 当 resumeData 变化且生成器已就绪时,推送新数据到生成器
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (hostRef.current && generatorReady && resumeData) {
|
|||
|
|
const genData = toGeneratorFormat(resumeData, dictsRef.current);
|
|||
|
|
console.log('[Resume] userId变化,推送新简历数据:', genData);
|
|||
|
|
hostRef.current.callRemote('resume:load', { resume: genData }, 8000).catch((err: any) => {
|
|||
|
|
console.error('[Resume] resume:load 失败:', err);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, [resumeData, generatorReady]);
|
|||
|
|
|
|||
|
|
// 页面加载时初始化 iframe
|
|||
|
|
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) => {
|
|||
|
|
setReadOnly(checked);
|
|||
|
|
if (hostRef.current && generatorReady) {
|
|||
|
|
try {
|
|||
|
|
await hostRef.current.callRemote('resume:setReadOnly', { readOnly: checked }, 8000);
|
|||
|
|
} catch {
|
|||
|
|
// 忽略
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 重新加载最新数据到生成器
|
|||
|
|
const reloadResumeData = () => {
|
|||
|
|
if (hostRef.current && generatorReady && resumeData) {
|
|||
|
|
const genData = toGeneratorFormat(resumeData, dictsRef.current);
|
|||
|
|
hostRef.current.callRemote('resume:load', { resume: genData }, 8000);
|
|||
|
|
message.success('已重新加载简历');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="appuser-resume-page">
|
|||
|
|
{/* 顶部工具栏 */}
|
|||
|
|
<Card size="small" style={{ marginBottom: 8 }}>
|
|||
|
|
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
|||
|
|
<Space>
|
|||
|
|
<Button
|
|||
|
|
icon={<ArrowLeftOutlined />}
|
|||
|
|
onClick={() => navigate(-1)}
|
|||
|
|
>
|
|||
|
|
返回列表
|
|||
|
|
</Button>
|
|||
|
|
<Text strong>
|
|||
|
|
{resumeData?.name || '加载中...'} 的简历
|
|||
|
|
</Text>
|
|||
|
|
</Space>
|
|||
|
|
<Space>
|
|||
|
|
{!forceReadOnly && (
|
|||
|
|
<span>
|
|||
|
|
<EyeOutlined /> 只读模式{' '}
|
|||
|
|
<Switch checked={readOnly} onChange={toggleReadOnly} />
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
{generatorReady && (
|
|||
|
|
<Button size="small" onClick={reloadResumeData}>
|
|||
|
|
重新加载
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
{saving && <Text type="secondary">正在保存...</Text>}
|
|||
|
|
</Space>
|
|||
|
|
</Space>
|
|||
|
|
</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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default AppUserResume;
|