feat: 实现简历编辑功能并清理修改版内容
- 添加简历编辑功能,支持contentEditable直接编辑 - 保持原有页面样式不变,仅在编辑时显示虚线边框 - 支持保存为个人修改版,支持版本管理和删除 - 清理10个岗位修改版内容中的删除线和加粗符号 - 编辑按钮样式调整为白底蓝字带圆角 - 调整布局,编辑按钮与岗位名称在同一行 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3389,34 +3389,34 @@ const resumeTemplates = {
|
|||||||
|
|
||||||
### (五)岗位职责:
|
### (五)岗位职责:
|
||||||
|
|
||||||
1. 协助提供现场咨询服务,包括研学项目、展览、节庆活动信息等,~~处理~~ **在指导下配合处理票务咨询、常见问询及游客投诉,确保游客能及时得到解答并维持顺畅的服务体验**;
|
1. 协助提供现场咨询服务,包括研学项目、展览、节庆活动信息等, 在指导下配合处理票务咨询、常见问询及游客投诉,确保游客能及时得到解答并维持顺畅的服务体验;
|
||||||
2. 收集研学人数、游客满意度、活动参与率等数据,~~通过~~ **并协助整理成热力图或统计报表,为团队提供数据依据,支持高峰区域分析和排程优化**;
|
2. 收集研学人数、游客满意度、活动参与率等数据, 并协助整理成热力图或统计报表,为团队提供数据依据,支持高峰区域分析和排程优化;
|
||||||
3. 支持花展、小自然音乐节、夜游等节庆活动的运营,包括~~流程组织、灯光布置、分区限流与应急预案配合~~ **协助流程组织、灯光布置检查、分区限流执行与应急预案落实,帮助活动顺利推进**;
|
3. 支持花展、小自然音乐节、夜游等节庆活动的运营,包括 协助流程组织、灯光布置检查、分区限流执行与应急预案落实,帮助活动顺利推进;
|
||||||
4. 协助导视与游线设置,~~配合空间动线优化,提升~~ **参与空间动线优化,帮助提升**游客行进节奏与整体文化体验感;
|
4. 协助导视与游线设置, 参与空间动线优化,帮助提升游客行进节奏与整体文化体验感;
|
||||||
5. 支持文创市集运营,包括~~产品场景布置、IP传播素材拍摄与社区联动推广~~ **协助产品场景布置、配合IP传播素材拍摄及社区联动推广,让市集氛围更完整**;
|
5. 支持文创市集运营,包括 协助产品场景布置、配合IP传播素材拍摄及社区联动推广,让市集氛围更完整;
|
||||||
6. 配合设施维护、安全巡查与环境卫生管理,~~确保~~ **协助检查并确认导视、照明、打卡设施及垃圾分类落实到位,提升现场秩序和安全性**;
|
6. 配合设施维护、安全巡查与环境卫生管理, 协助检查并确认导视、照明、打卡设施及垃圾分类落实到位,提升现场秩序和安全性;
|
||||||
7. 协助处理高峰期游客拥堵、设备故障等突发情况,~~配合实施~~ **在预案指引下配合实施疏散与应急引导,并及时反馈情况,减少突发事件影响**;
|
7. 协助处理高峰期游客拥堵、设备故障等突发情况, 在预案指引下配合实施疏散与应急引导,并及时反馈情况,减少突发事件影响;
|
||||||
8. 协助整理活动文档、流程手册与导览模板,~~为未来复用提供标准资料支撑~~ **帮助团队形成可复用的标准资料,为后续活动执行和规范化提供支持**。
|
8. 协助整理活动文档、流程手册与导览模板, 帮助团队形成可复用的标准资料,为后续活动执行和规范化提供支持。
|
||||||
|
|
||||||
# 二、专业技能
|
# 二、专业技能
|
||||||
|
|
||||||
### (一)核心能力
|
### (一)核心能力
|
||||||
|
|
||||||
1. 熟悉景区现场运营流程,能~~协助导览服务、动线管理与活动节奏控制~~ **在团队指导下协助导览服务,参与动线管理与活动节奏控制,帮助维持整体秩序**;
|
1. 熟悉景区现场运营流程,能 在团队指导下协助导览服务,参与动线管理与活动节奏控制,帮助维持整体秩序;
|
||||||
2. ~~擅长处理游客咨询与投诉,参与提升服务质量与满意度~~
|
2.
|
||||||
|
|
||||||
**能配合处理游客咨询与一般性投诉,协助提升服务质量与游客满意度**;
|
能配合处理游客咨询与一般性投诉,协助提升服务质量与游客满意度;
|
||||||
|
|
||||||
3. 具备基础的数据收集与报告能力,能~~辅助运营数据决策支持~~ **协助统计游客人数、满意度等基础数据,并整理成简要报表,为运营决策提供支持**;
|
3. 具备基础的数据收集与报告能力,能 协助统计游客人数、满意度等基础数据,并整理成简要报表,为运营决策提供支持;
|
||||||
4. 能参与节庆活动~~点位布置、流程排练及时序控制~~ **点位布置、流程排练及现场时序的执行配合**;
|
4. 能参与节庆活动 点位布置、流程排练及现场时序的执行配合;
|
||||||
5. 拥有现场应急协助经验,能~~配合处理突发状况~~ **在预案指导下配合处理游客拥堵、设备故障等突发状况**;
|
5. 拥有现场应急协助经验,能 在预案指导下配合处理游客拥堵、设备故障等突发状况;
|
||||||
6. ~~了解文创传播节奏与素材管理,可支持IP推广与现场体验~~
|
6.
|
||||||
|
|
||||||
**了解文创传播节奏与素材管理,能在现场协助IP推广与体验环节的执行**;
|
了解文创传播节奏与素材管理,能在现场协助IP推广与体验环节的执行;
|
||||||
|
|
||||||
7. 具备设施巡检与环境维护~~指导经验,确保服务环境优化~~
|
7. 具备设施巡检与环境维护
|
||||||
|
|
||||||
**协助进行设施巡检与环境维护,帮助确保导视、照明和环境卫生到位,优化服务环境**。
|
协助进行设施巡检与环境维护,帮助确保导视、照明和环境卫生到位,优化服务环境。
|
||||||
|
|
||||||
|
|
||||||
### (二)复合能力
|
### (二)复合能力
|
||||||
@@ -3433,7 +3433,7 @@ const resumeTemplates = {
|
|||||||
|
|
||||||
# 三、个人总结
|
# 三、个人总结
|
||||||
|
|
||||||
我是一名刚完成实习的大专毕业生,所学"智慧旅游技术应用"专业背景~~支撑我对文旅行业的理解~~ **让我逐步建立起对文旅行业运行模式和前沿趋势的理解**。我在"武汉植物园生态科普与文旅运营项目"中,~~深度参与现场导览支持、活动执行、安全维护及数字互动部署~~ **主要参与现场导览支持、活动执行、安全维护及数字互动等环节的协助工作,积累了较为系统的一线运营经验**。同时,我关注环保与文旅融合策略,~~具备协同共赢与可持续运营意识~~ **并在项目中体会到协同共赢与可持续运营的重要性**。未来期望在景区运营与文旅生态融合方向继续成长,成为~~一名具备执行力、数据敏感度和服务意识的专业运营人才~~ **一名具备扎实执行力、数据敏感度和服务意识的专业运营型人才**。`
|
我是一名刚完成实习的大专毕业生,所学"智慧旅游技术应用"专业背景 让我逐步建立起对文旅行业运行模式和前沿趋势的理解。我在"武汉植物园生态科普与文旅运营项目"中, 主要参与现场导览支持、活动执行、安全维护及数字互动等环节的协助工作,积累了较为系统的一线运营经验。同时,我关注环保与文旅融合策略, 并在项目中体会到协同共赢与可持续运营的重要性。未来期望在景区运营与文旅生态融合方向继续成长,成为 一名具备扎实执行力、数据敏感度和服务意识的专业运营型人才。`
|
||||||
},
|
},
|
||||||
studentInfo: {
|
studentInfo: {
|
||||||
project_experience: {
|
project_experience: {
|
||||||
|
|||||||
@@ -1,25 +1,166 @@
|
|||||||
import { useState, useEffect } from "react";
|
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 Modal from "@/components/Modal";
|
||||||
|
import * as resumeManager from '@/services/resumeManager';
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
||||||
const [version, setVersion] = useState(initialVersion); // 使用传入的初始版本
|
const [version, setVersion] = useState(initialVersion); // 使用传入的初始版本
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editableData, setEditableData] = useState(null);
|
||||||
|
const [customVersions, setCustomVersions] = useState([]);
|
||||||
|
|
||||||
// 响应initialVersion变化
|
// 响应initialVersion变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVersion(initialVersion);
|
setVersion(initialVersion);
|
||||||
}, [initialVersion]);
|
}, [initialVersion]);
|
||||||
|
|
||||||
|
// 加载个人修改版本
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && data?.title) {
|
||||||
|
const versions = resumeManager.getVersionsByPosition(data.title);
|
||||||
|
setCustomVersions(versions);
|
||||||
|
}
|
||||||
|
}, [visible, data?.title]);
|
||||||
|
|
||||||
const onRadioChange = (value, e) => {
|
const onRadioChange = (value, e) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
setVersion(value);
|
setVersion(value);
|
||||||
|
setIsEditing(false); // 切换版本时退出编辑模式
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditableData(null);
|
||||||
onClose();
|
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解析器 - 解析简历内容
|
// Markdown解析器 - 解析简历内容
|
||||||
const parseResumeMarkdown = (markdownContent) => {
|
const parseResumeMarkdown = (markdownContent) => {
|
||||||
@@ -102,7 +243,13 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
// 获取简历数据 - 支持新的数据结构
|
// 获取简历数据 - 支持新的数据结构
|
||||||
let resumeContent = {};
|
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页面
|
// 新的数据结构 - 来自resume-interview页面
|
||||||
if (data.content.original) {
|
if (data.content.original) {
|
||||||
// 有original字段,可能有或没有modified字段
|
// 有original字段,可能有或没有modified字段
|
||||||
@@ -226,8 +373,8 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
<Modal visible={visible} onClose={handleCloseModal}>
|
<Modal visible={visible} onClose={handleCloseModal}>
|
||||||
<div className="resume-info-modal" onClick={(e) => e.stopPropagation()}>
|
<div className="resume-info-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
<i className="close-icon" onClick={handleCloseModal} />
|
<i className="close-icon" onClick={handleCloseModal} />
|
||||||
{data?.content?.original && (
|
{(data?.content?.original || customVersions.length > 0) && (
|
||||||
<div className="resume-info-modal-header">
|
<div className="resume-info-modal-header" style={{ marginBottom: '20px' }}>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
type="button"
|
type="button"
|
||||||
name="position"
|
name="position"
|
||||||
@@ -240,12 +387,71 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
{data?.content?.modified && (
|
{data?.content?.modified && (
|
||||||
<Radio value="2">个人修改版</Radio>
|
<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>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="resume-info-modal-title">
|
<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 || "职位名称"}
|
{data?.title || resumeContent.personalInfo?.name || "职位名称"}
|
||||||
</p>
|
</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">
|
<ul className="resume-info-moda-list">
|
||||||
@@ -253,12 +459,40 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
<li className="resume-info-moda-item">
|
<li className="resume-info-moda-item">
|
||||||
<p className="resume-info-moda-item-title">教育经历</p>
|
<p className="resume-info-moda-item-title">教育经历</p>
|
||||||
<ul className="educational-experience-list">
|
<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">
|
<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}
|
{edu.school} - {edu.major}
|
||||||
</p>
|
</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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -267,18 +501,72 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
<li className="resume-info-moda-item">
|
<li className="resume-info-moda-item">
|
||||||
<p className="resume-info-moda-item-title">项目经历</p>
|
<p className="resume-info-moda-item-title">项目经历</p>
|
||||||
<ul className="project-experience-list">
|
<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">
|
<li key={index} className="project-experience-list-item">
|
||||||
<div className="project-info-wrapper">
|
<div className="project-info-wrapper">
|
||||||
<div className="project-info">
|
<div className="project-info">
|
||||||
<p className="project-name">{project.name}</p>
|
<p className="project-name"
|
||||||
<p className="project-company">
|
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}
|
{project.company && `${project.company} - `}角色:{project.role}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</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}
|
{project.description}
|
||||||
</p>
|
</p>
|
||||||
{project.highlights && (
|
{project.highlights && (
|
||||||
@@ -297,24 +585,48 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
|||||||
<li className="resume-info-moda-item">
|
<li className="resume-info-moda-item">
|
||||||
<p className="resume-info-moda-item-title">专业技能</p>
|
<p className="resume-info-moda-item-title">专业技能</p>
|
||||||
<ul className="professional-skills-list">
|
<ul className="professional-skills-list">
|
||||||
{resumeContent.skills?.core && (
|
{(isEditing && editableData ? editableData.skills?.core : resumeContent.skills?.core) && (
|
||||||
<li className="professional-skills-list-item">
|
<li className="professional-skills-list-item">
|
||||||
<p className="skill-name">核心能力</p>
|
<p className="skill-name">核心能力</p>
|
||||||
<div className="core-capabilities-list">
|
<div className="core-capabilities-list">
|
||||||
{resumeContent.skills.core.map((skill, index) => (
|
{(isEditing && editableData ? editableData.skills.core : resumeContent.skills.core).map((skill, index) => (
|
||||||
<p key={index} className="core-capabilities-list-item">
|
<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}
|
{index + 1}. {skill}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{resumeContent.skills?.additional && (
|
{(isEditing && editableData ? editableData.skills?.additional : resumeContent.skills?.additional) && (
|
||||||
<li className="professional-skills-list-item">
|
<li className="professional-skills-list-item">
|
||||||
<p className="skill-name">复合技能</p>
|
<p className="skill-name">复合技能</p>
|
||||||
<div className="core-capabilities-list">
|
<div className="core-capabilities-list">
|
||||||
{resumeContent.skills.additional.map((skill, index) => (
|
{(isEditing && editableData ? editableData.skills.additional : resumeContent.skills.additional).map((skill, index) => (
|
||||||
<p key={index} className="core-capabilities-list-item">
|
<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}
|
{index + 1}. {skill}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|||||||
165
src/services/resumeManager.js
Normal file
165
src/services/resumeManager.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 简历管理服务
|
||||||
|
* 处理个人修改版简历的CRUD操作
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'resume_custom_versions';
|
||||||
|
|
||||||
|
// 获取所有个人修改版
|
||||||
|
export const getCustomVersions = () => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取个人修改版失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存个人修改版列表
|
||||||
|
export const saveCustomVersions = (versions) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(versions));
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存个人修改版失败:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建新的个人修改版
|
||||||
|
export const createCustomVersion = (data) => {
|
||||||
|
try {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
const newVersion = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: data.name,
|
||||||
|
content: data.content,
|
||||||
|
positionTitle: data.positionTitle,
|
||||||
|
originalVersion: data.originalVersion || 'default',
|
||||||
|
tags: data.tags || [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedVersions = [...versions, newVersion];
|
||||||
|
const result = saveCustomVersions(updatedVersions);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: newVersion };
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建个人修改版失败:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新个人修改版
|
||||||
|
export const updateCustomVersion = (id, data) => {
|
||||||
|
try {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
const index = versions.findIndex(v => v.id === id);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return { success: false, error: '版本不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
versions[index] = {
|
||||||
|
...versions[index],
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = saveCustomVersions(versions);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: versions[index] };
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新个人修改版失败:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除个人修改版
|
||||||
|
export const deleteCustomVersion = (id) => {
|
||||||
|
try {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
const updatedVersions = versions.filter(v => v.id !== id);
|
||||||
|
|
||||||
|
const result = saveCustomVersions(updatedVersions);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除个人修改版失败:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据岗位获取版本
|
||||||
|
export const getVersionsByPosition = (positionTitle) => {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
return versions.filter(v => v.positionTitle === positionTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID获取单个版本
|
||||||
|
export const getVersionById = (id) => {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
return versions.find(v => v.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查版本名称是否重复
|
||||||
|
export const isVersionNameExists = (name, positionTitle) => {
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
return versions.some(v => v.name === name && v.positionTitle === positionTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出版本为JSON
|
||||||
|
export const exportVersion = (id) => {
|
||||||
|
const version = getVersionById(id);
|
||||||
|
if (!version) {
|
||||||
|
return { success: false, error: '版本不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataStr = JSON.stringify(version, null, 2);
|
||||||
|
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||||
|
|
||||||
|
const exportFileDefaultName = `resume_${version.positionTitle}_${version.name}.json`;
|
||||||
|
|
||||||
|
const linkElement = document.createElement('a');
|
||||||
|
linkElement.setAttribute('href', dataUri);
|
||||||
|
linkElement.setAttribute('download', exportFileDefaultName);
|
||||||
|
linkElement.click();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出版本失败:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入版本
|
||||||
|
export const importVersion = (jsonData) => {
|
||||||
|
try {
|
||||||
|
const version = JSON.parse(jsonData);
|
||||||
|
version.id = Date.now().toString(); // 生成新ID
|
||||||
|
version.importedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const versions = getCustomVersions();
|
||||||
|
const updatedVersions = [...versions, version];
|
||||||
|
const result = saveCustomVersions(updatedVersions);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, data: version };
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入版本失败:', error);
|
||||||
|
return { success: false, error: '无效的JSON格式' };
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user