建立生成器功能
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

@@ -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<any>([]);
const [hotEnum, setHotEnum] = useState<any>([]);
const [politicalEnum, setPoliticalEnum] = useState<any>([]);
const [nationEnum, setNationEnum] = useState<any>([]);
const [userTypeEnum, setUserTypeEnum] = useState<any>([]);
const [currentRow, setCurrentRow] = useState<API.MobileUser.ListRow>();
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [inviteVisible, setInviteVisible] = useState<boolean>(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 <DictTag enums={nationEnum} value={record.nation} />;
},
},
{
title: '个人标签',
dataIndex: 'userType',
valueType: 'select',
align: 'center',
valueEnum: userTypeEnum,
render: (_, record) => {
return <DictTag enums={userTypeEnum} value={record.userType} />;
},
},
{
title: '工作经验',
dataIndex: 'workExperience',
valueType: 'select',
align: 'center',
valueEnum: experienceEnum,
render: (_, record) => {
return <DictTag enums={experienceEnum} value={record.workExperience} />;
},
},
{
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;
// 过滤空值,避免空字符串干扰后端查询

View File

@@ -23,6 +23,8 @@ function AppUserList() {
const [areaEnum, setAreaEnum] = useState<Record<string, any>>({});
const [politicalEnum, setPoliticalEnum] = useState<Record<string, any>>({});
const [nationEnum, setNationEnum] = useState<Record<string, any>>({});
const [userTypeEnum, setUserTypeEnum] = useState<Record<string, any>>({});
const [workExperienceEnum, setWorkExperienceEnum] = useState<Record<string, any>>({});
/** 修改身份证 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<API.CmsAppUser.AppUserRow>[] = [
@@ -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) => <DictTag enums={educationEnum} value={record.education} />,
},
{
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) => <DictTag enums={nationEnum} value={record.nation ?? undefined} />,
},
{
title: '个人标签',
dataIndex: 'userType',
valueType: 'select',
align: 'center',
width: 110,
valueEnum: userTypeEnum,
render: (_, record) => <DictTag enums={userTypeEnum} value={record.userType ?? undefined} />,
},
{
title: '工作经验',
dataIndex: 'workExperience',
valueType: 'select',
align: 'center',
width: 110,
valueEnum: workExperienceEnum,
render: (_, record) => <DictTag enums={workExperienceEnum} value={record.workExperience ?? undefined} />,
},
{
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="个人用户管理"

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>
);
}

View File

@@ -44,14 +44,16 @@ function patchRouteItems(route: any, menu: any, parentPath: string) {
if (path.length > 0) {
path += '/';
}
if (name !== 'index') {
path += name.at(0)?.toUpperCase() + name.substr(1);
// 不区分大小写判断 "index",避免服务端返回 "Index"大写I时在 Linux 上找不到模块
// 统一输出小写 "index" 以匹配 webpack 模块注册路径
if (name.toLowerCase() === 'index') {
path += 'index';
} else {
path += name;
path += name.at(0)?.toUpperCase() + name.substr(1);
}
})
});
if (!path.endsWith('.tsx')) {
path += '.tsx'
path += '.tsx';
}
if (route.routes === undefined) {
route.routes = [];

View File

@@ -38,6 +38,8 @@ declare namespace API.CmsAppUser {
unionid: string | null;
nation: string | null;
workExperience: string | null;
schoolName: string | null;
endTime: string | null;
company: string | null;
experiencesList: any;
appSkillsList: any;
@@ -59,10 +61,17 @@ declare namespace API.CmsAppUser {
name?: string;
phone?: string;
sex?: string;
age?: string;
birthDate?: string;
education?: string;
schoolName?: string;
endTime?: string;
area?: string;
status?: string;
politicalAffiliation?: string;
nation?: string;
userType?: string;
workExperience?: string;
idCard?: string;
orderByColumn?: string;
isAsc?: string;

View File

@@ -33,6 +33,11 @@ declare namespace API.MobileUser {
idCard?:string;
interviewStatus?: string;
interviewId?: number;
schoolName?: string;
endTime?: string;
nation?: string;
userType?: string;
workExperience?: string;
}
export interface ListParams {
@@ -44,5 +49,12 @@ declare namespace API.MobileUser {
politicalAffiliation?: string;
phone?: string;
jobName?: string;
jobTitleId?: string;
schoolName?: string;
endTime?: string;
nation?: string;
userType?: string;
workExperience?: string;
area?: string;
}
}