Files
zeling_f/src/pages/Resume/Create/index.tsx
2026-03-17 14:30:02 +08:00

372 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useCallback, useEffect } from 'react';
import { Input, Button, Select, message, ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import {
PlusOutlined,
DeleteOutlined,
UserOutlined,
BankOutlined,
ReadOutlined,
FileSearchOutlined,
ArrowLeftOutlined,
LoadingOutlined,
CheckCircleFilled,
ClockCircleOutlined,
RocketOutlined,
} from '@ant-design/icons';
import { useNavigate, useSearchParams, request } from '@umijs/max';
import Navbar from '../../Home/components/Navbar';
import Footer from '../../Home/components/Footer';
import styles from './index.less';
const { TextArea } = Input;
interface WorkExp {
company: string;
position: string;
startDate: string;
endDate: string;
description: string;
}
interface Education {
school: string;
major: string;
degree: string;
startDate: string;
endDate: string;
description: string;
}
const PROGRESS_STEPS = [
{ label: '提交表单数据', duration: 2000 },
{ label: 'AI 正在分析岗位要求', duration: 6000 },
{ label: 'AI 正在生成简历内容', duration: 12000 },
{ label: '正在填充 Word 模板', duration: 8000 },
{ label: '正在上传文件', duration: 3000 },
];
const degreeOptions = [
{ value: '博士', label: '博士' },
{ value: '硕士', label: '硕士' },
{ value: '本科', label: '本科' },
{ value: '大专', label: '大专' },
];
const emptyWork = (): WorkExp => ({ company: '', position: '', startDate: '', endDate: '', description: '' });
const emptyEdu = (): Education => ({ school: '', major: '', degree: '', startDate: '', endDate: '', description: '' });
const ResumeCreate: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId') || '1';
const templateTitle = searchParams.get('templateTitle') || '';
// Form fields
const [resumeName, setResumeName] = useState('');
const [jobIntention, setJobIntention] = useState('');
const [expectedSalary, setExpectedSalary] = useState('');
const [preferredCity, setPreferredCity] = useState('');
const [language, setLanguage] = useState('中文');
const [jobDescription, setJobDescription] = useState('');
const [workList, setWorkList] = useState<WorkExp[]>([emptyWork()]);
const [eduList, setEduList] = useState<Education[]>([emptyEdu()]);
// Generation state
const [generating, setGenerating] = useState(false);
const [progressStep, setProgressStep] = useState(0);
const timerRef = useRef<any>(null);
// Cleanup timers on unmount
useEffect(() => {
return () => { if (timerRef.current) clearTimeout(timerRef.current); };
}, []);
// Work experience helpers
const addWork = () => setWorkList([...workList, emptyWork()]);
const removeWork = (i: number) => setWorkList(workList.filter((_, idx) => idx !== i));
const updateWork = (i: number, field: keyof WorkExp, val: string) => {
const list = [...workList];
list[i] = { ...list[i], [field]: val };
setWorkList(list);
};
// Education helpers
const addEdu = () => setEduList([...eduList, emptyEdu()]);
const removeEdu = (i: number) => setEduList(eduList.filter((_, idx) => idx !== i));
const updateEdu = (i: number, field: keyof Education, val: string) => {
const list = [...eduList];
list[i] = { ...list[i], [field]: val };
setEduList(list);
};
// Animate progress steps
const animateProgress = useCallback(() => {
let step = 0;
setProgressStep(0);
const next = () => {
if (step < PROGRESS_STEPS.length - 1) {
timerRef.current = setTimeout(() => {
step++;
setProgressStep(step);
next();
}, PROGRESS_STEPS[step].duration);
}
};
next();
}, []);
// Generate resume
const handleGenerate = async () => {
if (!resumeName.trim()) { message.warning('请输入简历名称'); return; }
if (!jobIntention.trim()) { message.warning('请输入求职意向'); return; }
if (!jobDescription.trim()) { message.warning('请粘贴岗位描述/JD'); return; }
setGenerating(true);
animateProgress();
try {
const payload: any = {
resumeName: resumeName.trim(),
jobIntention: jobIntention.trim(),
jobDescription: jobDescription.trim(),
templateId,
};
if (expectedSalary.trim()) payload.expectedSalary = expectedSalary.trim();
if (preferredCity.trim()) payload.preferredCity = preferredCity.trim();
if (language) payload.language = language;
const validWork = workList.filter(w => w.company.trim() || w.position.trim());
if (validWork.length) payload.workExperience = validWork;
const validEdu = eduList.filter(e => e.school.trim() || e.major.trim());
if (validEdu.length) payload.education = validEdu;
const res = await request('/api/resume/student/ai/generate', {
method: 'POST',
data: payload,
timeout: 120000,
});
if (res.code === 0 && res.data?.url) {
setProgressStep(PROGRESS_STEPS.length - 1);
if (timerRef.current) clearTimeout(timerRef.current);
message.success('简历生成成功');
// 跳转到预览页
navigate(`/resume/preview?url=${encodeURIComponent(res.data.url)}&name=${encodeURIComponent(resumeName.trim())}`);
} else {
message.error(res.message || '生成失败,请重试');
}
} catch (err: any) {
message.error(err?.message || '请求超时或网络异常,请重试');
} finally {
setGenerating(false);
if (timerRef.current) clearTimeout(timerRef.current);
}
};
const handleCancel = () => navigate(-1);
return (
<ConfigProvider locale={zhCN}>
<div className={styles.createPage}>
<div className={styles.navbarWrap}><Navbar solidBg /></div>
<div className={styles.contentArea}>
{/* Top bar */}
<div className={styles.topBar}>
<span className={styles.backLink} onClick={handleCancel}>
<ArrowLeftOutlined />
</span>
</div>
{/* Header */}
<div className={styles.headerCard}>
<div className={styles.headerCardInner}>
<h1 className={styles.pageTitle}>AI </h1>
{templateTitle && <span className={styles.templateBadge}>{decodeURIComponent(templateTitle)}</span>}
</div>
<p className={styles.headerDesc}>AI </p>
</div>
{/* Loading overlay */}
{generating && (
<div className={styles.loadingOverlay}>
<div className={styles.loadingCard}>
<div className={styles.loadingSpinner}><LoadingOutlined /></div>
<h2 className={styles.loadingTitle}>AI </h2>
<p className={styles.loadingHint}> 10~30 </p>
<div className={styles.progressSteps}>
{PROGRESS_STEPS.map((s, i) => (
<div key={i} className={`${styles.stepItem} ${i < progressStep ? styles.stepDone : ''} ${i === progressStep ? styles.stepActive : ''}`}>
<span className={styles.stepDot}>
{i < progressStep ? <CheckCircleFilled /> : i === progressStep ? <LoadingOutlined /> : <ClockCircleOutlined />}
</span>
<span className={styles.stepLabel}>{s.label}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Section 1: Basic Info */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionIcon}><UserOutlined /></div>
<span className={styles.sectionTitle}></span>
<span className={styles.sectionHint}> * </span>
</div>
<div className={styles.formGrid4}>
<div className={styles.formItem}>
<label className={styles.label}><span className={styles.required}>*</span> </label>
<Input className={styles.input} placeholder="如Java开发工程师简历" value={resumeName} onChange={e => setResumeName(e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}><span className={styles.required}>*</span> </label>
<Input className={styles.input} placeholder="如Java开发工程师" value={jobIntention} onChange={e => setJobIntention(e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如15-20K" value={expectedSalary} onChange={e => setExpectedSalary(e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如:上海" value={preferredCity} onChange={e => setPreferredCity(e.target.value)} />
</div>
</div>
<div className={styles.formRowSingle}>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Select className={styles.select} value={language} onChange={setLanguage}
popupClassName={styles.selectDropdown}
options={[{ value: '中文', label: '中文' }, { value: '英文', label: '英文' }]} />
</div>
</div>
</div>
{/* Section 2: Work Experience */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionIcon}><BankOutlined /></div>
<span className={styles.sectionTitle}>/</span>
<span className={styles.sectionHint}></span>
</div>
{workList.map((w, i) => (
<div key={i} className={styles.expCard}>
<div className={styles.expCardHeader}>
<span className={styles.expBadge}> {i + 1}</span>
{workList.length > 1 && (
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => removeWork(i)}></Button>
)}
</div>
<div className={styles.formGrid4}>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="公司名称" value={w.company} onChange={e => updateWork(i, 'company', e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="职位名称" value={w.position} onChange={e => updateWork(i, 'position', e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如2022.07" value={w.startDate} onChange={e => updateWork(i, 'startDate', e.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如:至今" value={w.endDate} onChange={e => updateWork(i, 'endDate', e.target.value)} />
</div>
</div>
<div className={styles.formItemFull}>
<label className={styles.label}></label>
<TextArea className={styles.textarea} rows={3} placeholder="简要描述工作内容" value={w.description} onChange={e => updateWork(i, 'description', e.target.value)} />
</div>
</div>
))}
<div className={styles.addBtnWrap}>
<Button className={styles.addBtn} icon={<PlusOutlined />} onClick={addWork}></Button>
</div>
</div>
{/* Section 3: Education */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionIcon}><ReadOutlined /></div>
<span className={styles.sectionTitle}></span>
<span className={styles.sectionHint}></span>
</div>
{eduList.map((e, i) => (
<div key={i} className={styles.expCard}>
<div className={styles.expCardHeader}>
<span className={styles.expBadge}> {i + 1}</span>
{eduList.length > 1 && (
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => removeEdu(i)}></Button>
)}
</div>
<div className={styles.formGrid4}>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="学校名称" value={e.school} onChange={ev => updateEdu(i, 'school', ev.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="专业名称" value={e.major} onChange={ev => updateEdu(i, 'major', ev.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Select className={styles.selectFull} value={e.degree || undefined} onChange={v => updateEdu(i, 'degree', v)}
popupClassName={styles.selectDropdown} placeholder="选择学历" options={degreeOptions} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如2018.09" value={e.startDate} onChange={ev => updateEdu(i, 'startDate', ev.target.value)} />
</div>
<div className={styles.formItem}>
<label className={styles.label}></label>
<Input className={styles.input} placeholder="如2022.06" value={e.endDate} onChange={ev => updateEdu(i, 'endDate', ev.target.value)} />
</div>
</div>
<div className={styles.formItemFull}>
<label className={styles.label}>/</label>
<TextArea className={styles.textarea} rows={3} placeholder="主修课程或其他描述" value={e.description} onChange={ev => updateEdu(i, 'description', ev.target.value)} />
</div>
</div>
))}
<div className={styles.addBtnWrap}>
<Button className={styles.addBtn} icon={<PlusOutlined />} onClick={addEdu}></Button>
</div>
</div>
{/* Section 4: Job Description */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionIcon}><FileSearchOutlined /></div>
<span className={styles.sectionTitle}> / JD</span>
<span className={styles.sectionHint}></span>
</div>
<div className={styles.formItemFull}>
<label className={styles.label}><span className={styles.required}>*</span> </label>
<TextArea className={styles.textarea} rows={6}
placeholder="请粘贴目标岗位的招聘要求AI 将根据 JD 为你量身定制简历内容..."
value={jobDescription} onChange={e => setJobDescription(e.target.value)} />
</div>
</div>
{/* Footer actions */}
<div className={styles.footerActions}>
<Button className={styles.cancelBtn} onClick={handleCancel}></Button>
<Button type="primary" className={styles.generateBtn} loading={generating} onClick={handleGenerate}>
{generating ? 'AI 生成中...' : <><RocketOutlined /> AI </>}
</Button>
</div>
</div>
<Footer />
</div>
</ConfigProvider>
);
};
export default ResumeCreate;