feat: 实现简历编辑功能并清理修改版内容

- 添加简历编辑功能,支持contentEditable直接编辑
- 保持原有页面样式不变,仅在编辑时显示虚线边框
- 支持保存为个人修改版,支持版本管理和删除
- 清理10个岗位修改版内容中的删除线和加粗符号
- 编辑按钮样式调整为白底蓝字带圆角
- 调整布局,编辑按钮与岗位名称在同一行

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-12 11:16:35 +08:00
parent 228abc5f4a
commit a9dc0ba94e
3 changed files with 523 additions and 46 deletions

View File

@@ -1,25 +1,166 @@
import { useState, useEffect } from "react";
import { Radio } from "@arco-design/web-react";
import { Radio, Button, Message } from "@arco-design/web-react";
import { IconEdit, IconSave, IconClose } from "@arco-design/web-react/icon";
import Modal from "@/components/Modal";
import * as resumeManager from '@/services/resumeManager';
import "./index.css";
export default ({ visible, onClose, data, initialVersion = "2" }) => {
const [version, setVersion] = useState(initialVersion); // 使用传入的初始版本
const [isEditing, setIsEditing] = useState(false);
const [editableData, setEditableData] = useState(null);
const [customVersions, setCustomVersions] = useState([]);
// 响应initialVersion变化
useEffect(() => {
setVersion(initialVersion);
}, [initialVersion]);
// 加载个人修改版本
useEffect(() => {
if (visible && data?.title) {
const versions = resumeManager.getVersionsByPosition(data.title);
setCustomVersions(versions);
}
}, [visible, data?.title]);
const onRadioChange = (value, e) => {
e?.stopPropagation();
setVersion(value);
setIsEditing(false); // 切换版本时退出编辑模式
};
const handleCloseModal = () => {
setIsEditing(false);
setEditableData(null);
onClose();
};
// 开始编辑
const handleEditClick = () => {
// 获取当前显示的内容并解析
let currentContent = '';
if (version.startsWith('custom_')) {
const customVersion = resumeManager.getVersionById(version.replace('custom_', ''));
currentContent = customVersion?.content || '';
} else if (data?.content) {
const hasModified = !!data.content.modified;
currentContent = (!hasModified || version === "1") ? data.content.original : data.content.modified;
}
const parsed = parseResumeMarkdown(currentContent);
setEditableData(parsed);
setIsEditing(true);
};
// 保存编辑
const handleSaveEdit = async () => {
if (!editableData) return;
// 将编辑后的数据转换回markdown格式
const contentToSave = convertToMarkdown(editableData);
if (!contentToSave.trim()) {
Message.error('简历内容不能为空');
return;
}
// 创建一个新的个人修改版
const versionName = `个人版_${new Date().toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-').replace(/:/g, '')}`;
const result = await resumeManager.createCustomVersion({
name: versionName,
content: contentToSave,
positionTitle: data?.title || '未命名岗位',
originalVersion: version
});
if (result.success) {
Message.success('已保存为个人修改版');
setIsEditing(false);
// 刷新个人修改版列表
const versions = resumeManager.getVersionsByPosition(data?.title);
setCustomVersions(versions);
// 切换到新保存的版本
setVersion('custom_' + result.data.id);
} else {
Message.error(result.error || '保存失败');
}
};
// 取消编辑
const handleCancelEdit = () => {
setIsEditing(false);
setEditableData(null);
};
// 删除个人版本
const handleDeleteVersion = (versionId) => {
const result = resumeManager.deleteCustomVersion(versionId);
if (result.success) {
Message.success('已删除');
const versions = resumeManager.getVersionsByPosition(data?.title);
setCustomVersions(versions);
// 如果删除的是当前版本,切换到原始版
if (version === `custom_${versionId}`) {
setVersion("1");
}
}
};
// 将结构化数据转换回markdown格式
const convertToMarkdown = (data) => {
if (!data) return '';
let markdown = `# 对应岗位:${data.personalInfo?.name || data?.title || '职位名称'}\n\n`;
// 项目经历
if (data.projects && data.projects.length > 0) {
markdown += '# 一、项目经历\n';
data.projects.forEach(proj => {
markdown += `### (一)项目名称:${proj.name}\n`;
markdown += `### (二)实习岗位:${proj.role || '参与者'}\n`;
if (proj.period) markdown += `### (三)实习时间:${proj.period}\n`;
if (proj.company) markdown += `### (四)实习单位:${proj.company}\n`;
if (proj.responsibilities && proj.responsibilities.length > 0) {
markdown += '### (五)岗位职责:\n';
proj.responsibilities.forEach((resp, idx) => {
markdown += `${idx + 1}. ${resp}\n`;
});
} else if (proj.description) {
markdown += `### (五)岗位职责:\n${proj.description}\n`;
}
markdown += '\n';
});
}
// 专业技能
if (data.skills) {
markdown += '# 二、专业技能\n';
if (data.skills.core && data.skills.core.length > 0) {
markdown += '## (一)核心技能\n';
data.skills.core.forEach((skill, idx) => {
markdown += `${idx + 1}. ${skill}\n`;
});
}
if (data.skills.additional && data.skills.additional.length > 0) {
markdown += '## (二)复合技能\n';
data.skills.additional.forEach((skill, idx) => {
markdown += `${idx + 1}. ${skill}\n`;
});
}
}
return markdown;
};
// Markdown解析器 - 解析简历内容
const parseResumeMarkdown = (markdownContent) => {
@@ -102,7 +243,13 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
// 获取简历数据 - 支持新的数据结构
let resumeContent = {};
if (data?.content) {
// 处理自定义版本
if (version.startsWith('custom_')) {
const customVersion = resumeManager.getVersionById(version.replace('custom_', ''));
if (customVersion?.content) {
resumeContent = parseResumeMarkdown(customVersion.content) || {};
}
} else if (data?.content) {
// 新的数据结构 - 来自resume-interview页面
if (data.content.original) {
// 有original字段可能有或没有modified字段
@@ -226,8 +373,8 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
<Modal visible={visible} onClose={handleCloseModal}>
<div className="resume-info-modal" onClick={(e) => e.stopPropagation()}>
<i className="close-icon" onClick={handleCloseModal} />
{data?.content?.original && (
<div className="resume-info-modal-header">
{(data?.content?.original || customVersions.length > 0) && (
<div className="resume-info-modal-header" style={{ marginBottom: '20px' }}>
<Radio.Group
type="button"
name="position"
@@ -240,12 +387,71 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
{data?.content?.modified && (
<Radio value="2">个人修改版</Radio>
)}
{customVersions.map((v) => (
<Radio key={v.id} value={`custom_${v.id}`}>
{v.name}
{isEditing === false && (
<span
onClick={(e) => {
e.stopPropagation();
handleDeleteVersion(v.id);
}}
style={{ marginLeft: '5px', color: '#ff4d4f', cursor: 'pointer' }}
>
×
</span>
)}
</Radio>
))}
</Radio.Group>
</div>
)}
<p className="resume-info-modal-title">
{data?.title || resumeContent.personalInfo?.name || "职位名称"}
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<p className="resume-info-modal-title" style={{ margin: 0 }}>
{data?.title || resumeContent.personalInfo?.name || "职位名称"}
</p>
<div style={{ display: 'flex', gap: '8px' }}>
{!isEditing ? (
<Button
size="small"
icon={<IconEdit />}
onClick={handleEditClick}
style={{
backgroundColor: '#fff',
color: '#1890ff',
border: '1px solid #1890ff',
borderRadius: '4px'
}}
>
编辑
</Button>
) : (
<>
<Button
type="primary"
size="small"
icon={<IconSave />}
onClick={handleSaveEdit}
style={{
borderRadius: '4px'
}}
>
保存
</Button>
<Button
size="small"
icon={<IconClose />}
onClick={handleCancelEdit}
style={{
borderRadius: '4px'
}}
>
取消
</Button>
</>
)}
</div>
</div>
{/* 统一使用结构化样式展示所有岗位 */}
<ul className="resume-info-moda-list">
@@ -253,12 +459,40 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
<li className="resume-info-moda-item">
<p className="resume-info-moda-item-title">教育经历</p>
<ul className="educational-experience-list">
{resumeContent.education?.map((edu, index) => (
{(isEditing && editableData ? editableData.education : resumeContent.education)?.map((edu, index) => (
<li key={index} className="educational-experience-list-item">
<p className="school-name">
<p className="school-name"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const text = e.target.innerText;
const parts = text.split(' - ');
const newEdu = { ...editableData.education[index] };
newEdu.school = parts[0] || edu.school;
newEdu.major = parts[1] || edu.major;
const newEducation = [...editableData.education];
newEducation[index] = newEdu;
setEditableData({ ...editableData, education: newEducation });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}
>
{edu.school} - {edu.major}
</p>
<p className="study-time">{edu.period}</p>
<p className="study-time"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newEducation = [...editableData.education];
newEducation[index] = { ...newEducation[index], period: e.target.innerText };
setEditableData({ ...editableData, education: newEducation });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{edu.period}
</p>
</li>
))}
</ul>
@@ -267,18 +501,72 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
<li className="resume-info-moda-item">
<p className="resume-info-moda-item-title">项目经历</p>
<ul className="project-experience-list">
{resumeContent.projects?.map((project, index) => (
{(isEditing && editableData ? editableData.projects : resumeContent.projects)?.map((project, index) => (
<li key={index} className="project-experience-list-item">
<div className="project-info-wrapper">
<div className="project-info">
<p className="project-name">{project.name}</p>
<p className="project-company">
<p className="project-name"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newProjects = [...editableData.projects];
newProjects[index] = { ...newProjects[index], name: e.target.innerText };
setEditableData({ ...editableData, projects: newProjects });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{project.name}
</p>
<p className="project-company"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const text = e.target.innerText;
const newProjects = [...editableData.projects];
if (text.includes('角色:')) {
const parts = text.split(' - 角色:');
newProjects[index] = {
...newProjects[index],
company: parts[0] || '',
role: parts[1] || project.role
};
} else {
newProjects[index] = { ...newProjects[index], role: text };
}
setEditableData({ ...editableData, projects: newProjects });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{project.company && `${project.company} - `}角色{project.role}
</p>
</div>
<p className="project-time">{project.period}</p>
<p className="project-time"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newProjects = [...editableData.projects];
newProjects[index] = { ...newProjects[index], period: e.target.innerText };
setEditableData({ ...editableData, projects: newProjects });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{project.period}
</p>
</div>
<p className="project-desc" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
<p className="project-desc"
style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6', ...(isEditing ? {border: '1px dashed #d9d9d9', padding: '6px', borderRadius: '4px', cursor: 'text', minHeight: '60px'} : {}) }}
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newProjects = [...editableData.projects];
newProjects[index] = { ...newProjects[index], description: e.target.innerText };
setEditableData({ ...editableData, projects: newProjects });
}
}}>
{project.description}
</p>
{project.highlights && (
@@ -297,24 +585,48 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
<li className="resume-info-moda-item">
<p className="resume-info-moda-item-title">专业技能</p>
<ul className="professional-skills-list">
{resumeContent.skills?.core && (
{(isEditing && editableData ? editableData.skills?.core : resumeContent.skills?.core) && (
<li className="professional-skills-list-item">
<p className="skill-name">核心能力</p>
<div className="core-capabilities-list">
{resumeContent.skills.core.map((skill, index) => (
<p key={index} className="core-capabilities-list-item">
{(isEditing && editableData ? editableData.skills.core : resumeContent.skills.core).map((skill, index) => (
<p key={index} className="core-capabilities-list-item"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newSkills = { ...editableData.skills };
const text = e.target.innerText;
// Remove numbering if present
newSkills.core[index] = text.replace(/^[0-9]+\.\s*/, '');
setEditableData({ ...editableData, skills: newSkills });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{index + 1}. {skill}
</p>
))}
</div>
</li>
)}
{resumeContent.skills?.additional && (
{(isEditing && editableData ? editableData.skills?.additional : resumeContent.skills?.additional) && (
<li className="professional-skills-list-item">
<p className="skill-name">复合技能</p>
<div className="core-capabilities-list">
{resumeContent.skills.additional.map((skill, index) => (
<p key={index} className="core-capabilities-list-item">
{(isEditing && editableData ? editableData.skills.additional : resumeContent.skills.additional).map((skill, index) => (
<p key={index} className="core-capabilities-list-item"
contentEditable={isEditing}
suppressContentEditableWarning={true}
onBlur={(e) => {
if (isEditing && editableData) {
const newSkills = { ...editableData.skills };
const text = e.target.innerText;
// Remove numbering if present
newSkills.additional[index] = text.replace(/^[0-9]+\.\s*/, '');
setEditableData({ ...editableData, skills: newSkills });
}
}}
style={isEditing ? {border: '1px dashed #d9d9d9', padding: '2px 6px', borderRadius: '4px', cursor: 'text'} : {}}>
{index + 1}. {skill}
</p>
))}