first commit

This commit is contained in:
2026-03-17 14:30:02 +08:00
commit 313f5b3550
136 changed files with 57671 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
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;