2025-09-24 14:14:14 +08:00
|
|
|
|
import { useState, useEffect } from "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([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 调试:检查接收到的数据
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (visible && data) {
|
|
|
|
|
|
console.log('ResumeInfoModal接收到的数据:', {
|
|
|
|
|
|
title: data.title,
|
|
|
|
|
|
hasContent: !!data.content,
|
|
|
|
|
|
contentType: typeof data.content,
|
|
|
|
|
|
contentKeys: data.content ? Object.keys(data.content) : null,
|
|
|
|
|
|
hasSelectedTemplate: !!data.selectedTemplate,
|
|
|
|
|
|
selectedTemplateKeys: data.selectedTemplate ? Object.keys(data.selectedTemplate) : null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (data.content) {
|
|
|
|
|
|
console.log('content详情:', {
|
|
|
|
|
|
hasOriginal: !!data.content.original,
|
|
|
|
|
|
hasModified: !!data.content.modified,
|
|
|
|
|
|
originalType: typeof data.content.original,
|
|
|
|
|
|
originalLength: data.content.original ? data.content.original.length : 0
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [visible, data]);
|
|
|
|
|
|
|
|
|
|
|
|
// 响应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;
|
|
|
|
|
|
// 如果没有modified版本,始终使用original
|
|
|
|
|
|
currentContent = (version === "1" || !hasModified) ? 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) => {
|
|
|
|
|
|
if (!markdownContent || typeof markdownContent !== 'string') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
personalInfo: { name: "岗位名称" },
|
|
|
|
|
|
education: [{
|
2025-10-17 14:36:25 +08:00
|
|
|
|
school: '南阳医学高等专科学校',
|
2025-09-24 14:14:14 +08:00
|
|
|
|
major: '药品生产技术',
|
2025-10-17 14:36:25 +08:00
|
|
|
|
period: '2018.9-2021.6'
|
2025-09-24 14:14:14 +08:00
|
|
|
|
}],
|
|
|
|
|
|
projects: [],
|
|
|
|
|
|
skills: { core: [], additional: [] },
|
|
|
|
|
|
personalSummary: []
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提取岗位名称
|
|
|
|
|
|
const positionMatch = markdownContent.match(/# 对应岗位:(.+)/);
|
|
|
|
|
|
if (positionMatch) {
|
|
|
|
|
|
result.personalInfo.name = positionMatch[1].trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取项目经历 - 兼容"专业技能"和"掌握技能"两种标题
|
|
|
|
|
|
const projectSectionMatch = markdownContent.match(/# 一、项目经历\s+([\s\S]*?)(?=# 二、(?:专业技能|掌握技能)|# 三、|$)/);
|
|
|
|
|
|
if (projectSectionMatch) {
|
|
|
|
|
|
const projectContent = projectSectionMatch[1];
|
|
|
|
|
|
|
|
|
|
|
|
// 提取项目名称
|
|
|
|
|
|
const projectNameMatch = projectContent.match(/### (一)项目名称:(.+)/);
|
|
|
|
|
|
const roleMatch = projectContent.match(/### (二)实习岗位:(.+)/);
|
|
|
|
|
|
// 兼容不同的顺序 - 使用非捕获组(?:)
|
|
|
|
|
|
const timeMatch = projectContent.match(/### (?:(三)|(四))实习时间:(.+)/);
|
|
|
|
|
|
const companyMatch = projectContent.match(/### (?:(三)|(四))实习单位:(.+)/);
|
|
|
|
|
|
|
|
|
|
|
|
// 提取岗位职责内容 - 兼容有冒号和没有冒号的情况
|
|
|
|
|
|
const responsibilityMatch = projectContent.match(/### (五)岗位职责[::]?\s+([\s\S]*?)$/);
|
|
|
|
|
|
|
|
|
|
|
|
if (projectNameMatch) {
|
|
|
|
|
|
result.projects = [{
|
|
|
|
|
|
name: projectNameMatch[1].trim(),
|
|
|
|
|
|
role: roleMatch ? roleMatch[1].trim() : "参与者",
|
|
|
|
|
|
period: timeMatch ? timeMatch[1].trim() : "",
|
|
|
|
|
|
company: companyMatch ? companyMatch[1].trim() : "",
|
|
|
|
|
|
description: responsibilityMatch ? responsibilityMatch[1].trim() : ""
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取专业技能 - 兼容"专业技能"和"掌握技能"两种标题
|
|
|
|
|
|
const skillsSectionMatch = markdownContent.match(/# 二、(?:专业技能|掌握技能)\s+([\s\S]*?)(?=# 三、|$)/);
|
|
|
|
|
|
if (skillsSectionMatch) {
|
|
|
|
|
|
const skillsContent = skillsSectionMatch[1];
|
|
|
|
|
|
|
|
|
|
|
|
// 提取核心能力
|
|
|
|
|
|
const coreSkillsMatch = skillsContent.match(/### (一)核心能力\s+([\s\S]*?)(?=### (二)复合能力|$)/);
|
|
|
|
|
|
if (coreSkillsMatch) {
|
|
|
|
|
|
const coreSkills = coreSkillsMatch[1]
|
|
|
|
|
|
.split(/\d+\.\s+/)
|
|
|
|
|
|
.filter(skill => skill.trim())
|
|
|
|
|
|
.map(skill => skill.trim().replace(/;\s*$/, ''));
|
|
|
|
|
|
result.skills.core = coreSkills;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取复合能力
|
|
|
|
|
|
const additionalSkillsMatch = skillsContent.match(/### (二)复合能力\s+([\s\S]*?)(?=# 三、|$)/);
|
|
|
|
|
|
if (additionalSkillsMatch) {
|
|
|
|
|
|
const additionalSkills = additionalSkillsMatch[1]
|
|
|
|
|
|
.split(/\d+\.\s+/)
|
|
|
|
|
|
.filter(skill => skill.trim())
|
|
|
|
|
|
.map(skill => skill.trim().replace(/。\s*$/, ''));
|
|
|
|
|
|
result.skills.additional = additionalSkills;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提取个人评价/个人总结 - 兼容两种标题
|
|
|
|
|
|
const personalSummaryMatch = markdownContent.match(/# 三、(?:个人评价|个人总结)\s+([\s\S]*?)$/);
|
|
|
|
|
|
if (personalSummaryMatch) {
|
|
|
|
|
|
const summaryText = personalSummaryMatch[1].trim();
|
|
|
|
|
|
if (summaryText) {
|
|
|
|
|
|
result.personalSummary = [summaryText];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取简历数据 - 支持新的数据结构
|
|
|
|
|
|
let resumeContent = {};
|
|
|
|
|
|
|
|
|
|
|
|
console.log('开始处理简历内容,当前版本:', version);
|
|
|
|
|
|
console.log('data?.content存在:', !!data?.content);
|
|
|
|
|
|
|
|
|
|
|
|
// 处理自定义版本
|
|
|
|
|
|
if (version.startsWith('custom_')) {
|
|
|
|
|
|
const customVersion = resumeManager.getVersionById(version.replace('custom_', ''));
|
|
|
|
|
|
if (customVersion?.content) {
|
|
|
|
|
|
resumeContent = parseResumeMarkdown(customVersion.content) || {};
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data?.content) {
|
|
|
|
|
|
console.log('处理data.content,结构:', {
|
|
|
|
|
|
hasOriginal: !!data.content.original,
|
|
|
|
|
|
hasModified: !!data.content.modified,
|
|
|
|
|
|
originalType: typeof data.content.original
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 新的数据结构 - 来自resume-interview页面
|
|
|
|
|
|
if (data.content.original) {
|
|
|
|
|
|
// 有original字段,可能有或没有modified字段
|
|
|
|
|
|
const hasModified = !!data.content.modified;
|
|
|
|
|
|
// 如果没有modified版本,始终使用original
|
|
|
|
|
|
const selectedContent = (version === "1" || !hasModified) ? data.content.original : data.content.modified;
|
|
|
|
|
|
console.log('选择的内容长度:', selectedContent ? selectedContent.length : 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是markdown格式的字符串,使用解析器
|
|
|
|
|
|
if (typeof selectedContent === 'string') {
|
|
|
|
|
|
resumeContent = parseResumeMarkdown(selectedContent);
|
|
|
|
|
|
} else if (selectedContent.personalInfo) {
|
|
|
|
|
|
// 如果已经是结构化数据
|
|
|
|
|
|
resumeContent = selectedContent;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (data.content.personalInfo) {
|
|
|
|
|
|
// 兼容旧的数据结构(单一版本)
|
|
|
|
|
|
resumeContent = data.content;
|
|
|
|
|
|
} else if (typeof data.content === 'string') {
|
|
|
|
|
|
// 如果content直接是markdown字符串
|
|
|
|
|
|
resumeContent = parseResumeMarkdown(data.content);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log('进入else分支,检查selectedTemplate');
|
|
|
|
|
|
// 旧的数据结构 - 兼容现有的company jobs页面
|
|
|
|
|
|
const currentTemplate = data?.selectedTemplate;
|
|
|
|
|
|
const studentInfo = currentTemplate?.studentInfo;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('currentTemplate信息:', {
|
|
|
|
|
|
hasCurrentTemplate: !!currentTemplate,
|
|
|
|
|
|
hasContent: !!currentTemplate?.content,
|
|
|
|
|
|
hasContentOriginal: !!currentTemplate?.content?.original,
|
|
|
|
|
|
hasPosition: !!currentTemplate?.position,
|
|
|
|
|
|
contentKeys: currentTemplate?.content ? Object.keys(currentTemplate.content) : null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是来自resume-interview页面的数据 (有content.original字段)
|
|
|
|
|
|
if (currentTemplate && currentTemplate.content && currentTemplate.content.original) {
|
|
|
|
|
|
console.log('处理content.original结构');
|
|
|
|
|
|
// 处理有content.original的数据结构
|
|
|
|
|
|
const hasModified = !!currentTemplate.content.modified;
|
|
|
|
|
|
// 如果没有modified版本,始终使用original
|
|
|
|
|
|
const selectedContent = (version === "1" || !hasModified) ? currentTemplate.content.original : currentTemplate.content.modified;
|
|
|
|
|
|
|
|
|
|
|
|
const parsedContent = parseResumeMarkdown(selectedContent);
|
|
|
|
|
|
if (parsedContent) {
|
|
|
|
|
|
resumeContent = parsedContent;
|
|
|
|
|
|
// 添加个人总结 - 检查多个可能的位置
|
|
|
|
|
|
if (currentTemplate.personal_summary) {
|
|
|
|
|
|
resumeContent.personalSummary = [currentTemplate.personal_summary];
|
|
|
|
|
|
} else if (currentTemplate.studentInfo?.personalSummary) {
|
|
|
|
|
|
resumeContent.personalSummary = [currentTemplate.studentInfo.personalSummary];
|
|
|
|
|
|
} else if (currentTemplate.studentInfo?.personal_summary) {
|
|
|
|
|
|
resumeContent.personalSummary = [currentTemplate.studentInfo.personal_summary];
|
|
|
|
|
|
}
|
|
|
|
|
|
// 更新岗位名称
|
|
|
|
|
|
resumeContent.personalInfo.name = currentTemplate.position;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (studentInfo) {
|
|
|
|
|
|
// 构造兼容格式
|
|
|
|
|
|
resumeContent = {
|
|
|
|
|
|
personalInfo: { name: currentTemplate?.position || "岗位名称" },
|
|
|
|
|
|
education: [{
|
|
|
|
|
|
school: studentInfo.education?.university || '苏州信息职业技术学院',
|
|
|
|
|
|
major: '旅游管理',
|
|
|
|
|
|
period: studentInfo.education?.period || '2020.9-2023.6'
|
|
|
|
|
|
}],
|
|
|
|
|
|
projects: [],
|
|
|
|
|
|
skills: { core: [], additional: [] },
|
|
|
|
|
|
personalSummary: [studentInfo.personalSummary || studentInfo.personal_summary || "具有扎实的专业基础和实践经验"]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理项目经历
|
|
|
|
|
|
if (studentInfo.projectExperience) {
|
|
|
|
|
|
resumeContent.projects = [{
|
|
|
|
|
|
name: "项目经历",
|
|
|
|
|
|
role: "参与者",
|
|
|
|
|
|
period: "",
|
|
|
|
|
|
description: studentInfo.projectExperience
|
|
|
|
|
|
}];
|
|
|
|
|
|
} else if (studentInfo.project_experience) {
|
|
|
|
|
|
if (Array.isArray(studentInfo.project_experience)) {
|
|
|
|
|
|
resumeContent.projects = studentInfo.project_experience.map(proj => ({
|
|
|
|
|
|
name: proj.name || "实习项目",
|
|
|
|
|
|
role: proj.role || "参与者",
|
|
|
|
|
|
period: proj.period || "",
|
|
|
|
|
|
description: proj.description || "参与项目实施"
|
|
|
|
|
|
}));
|
|
|
|
|
|
} else if (typeof studentInfo.project_experience === 'object') {
|
|
|
|
|
|
const proj = studentInfo.project_experience;
|
|
|
|
|
|
resumeContent.projects = [{
|
|
|
|
|
|
name: proj.project_name || proj.position || "实习项目",
|
|
|
|
|
|
role: proj.role || "参与者",
|
|
|
|
|
|
period: proj.period || "",
|
|
|
|
|
|
description: proj.description || "参与项目实施"
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理专业技能
|
|
|
|
|
|
if (studentInfo.skills) {
|
|
|
|
|
|
// 新格式:skills是字符串,转换为数组
|
|
|
|
|
|
const skillsArray = studentInfo.skills.split('\n').filter(s => s.trim());
|
|
|
|
|
|
const midPoint = Math.ceil(skillsArray.length / 2);
|
|
|
|
|
|
resumeContent.skills = {
|
|
|
|
|
|
core: skillsArray.slice(0, midPoint),
|
|
|
|
|
|
additional: skillsArray.slice(midPoint)
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resumeContent.skills = {
|
|
|
|
|
|
core: studentInfo.core_skills || [],
|
|
|
|
|
|
additional: studentInfo.compound_skills || []
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数据校验:确保必要字段存在
|
|
|
|
|
|
const isValidData = resumeContent && Object.keys(resumeContent).length > 0 && resumeContent.personalInfo;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('最终resumeContent:', {
|
|
|
|
|
|
hasResumeContent: !!resumeContent,
|
|
|
|
|
|
keys: resumeContent ? Object.keys(resumeContent) : [],
|
|
|
|
|
|
hasPersonalInfo: !!resumeContent?.personalInfo,
|
|
|
|
|
|
isValidData
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果数据无效,提供默认值防止渲染异常
|
|
|
|
|
|
if (!isValidData) {
|
|
|
|
|
|
console.warn('ResumeInfoModal: Invalid resume data received', {
|
|
|
|
|
|
data,
|
|
|
|
|
|
resumeContent,
|
|
|
|
|
|
dataKeys: data ? Object.keys(data) : null,
|
|
|
|
|
|
hasContent: !!data?.content,
|
|
|
|
|
|
hasSelectedTemplate: !!data?.selectedTemplate
|
|
|
|
|
|
});
|
|
|
|
|
|
resumeContent = {
|
|
|
|
|
|
personalInfo: { name: '数据加载中...' },
|
|
|
|
|
|
education: [],
|
|
|
|
|
|
projects: [],
|
|
|
|
|
|
skills: { core: [], additional: [] }
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Modal visible={visible} onClose={handleCloseModal}>
|
|
|
|
|
|
<div className="resume-info-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<i className="close-icon" onClick={handleCloseModal} />
|
|
|
|
|
|
{(data?.content?.original || customVersions.length > 0) && (
|
|
|
|
|
|
<div className="resume-info-modal-header" style={{ marginBottom: '20px' }}>
|
|
|
|
|
|
<Radio.Group
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
name="position"
|
|
|
|
|
|
className="resume-info-modal-radio-group"
|
|
|
|
|
|
value={version}
|
|
|
|
|
|
onChange={onRadioChange}
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Radio value="1">原始版</Radio>
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<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>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="save-button-wrapper">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<IconSave />}
|
|
|
|
|
|
disabled={true}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
backgroundColor: '#d9d9d9',
|
|
|
|
|
|
borderColor: '#d9d9d9',
|
|
|
|
|
|
color: '#ffffff',
|
|
|
|
|
|
cursor: 'not-allowed',
|
|
|
|
|
|
opacity: 0.5
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '-35px',
|
|
|
|
|
|
left: '50%',
|
|
|
|
|
|
transform: 'translateX(-50%)',
|
|
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
padding: '6px 12px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
|
opacity: 0,
|
|
|
|
|
|
transition: 'opacity 0.3s',
|
|
|
|
|
|
zIndex: 1000
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="save-button-tooltip"
|
|
|
|
|
|
>
|
|
|
|
|
|
非学员与导师无修改权限
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<IconClose />}
|
|
|
|
|
|
onClick={handleCancelEdit}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: '4px'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 统一使用结构化样式展示所有岗位 */}
|
|
|
|
|
|
<ul className="resume-info-moda-list">
|
|
|
|
|
|
{/* 教育经历 */}
|
|
|
|
|
|
<li className="resume-info-moda-item">
|
|
|
|
|
|
<p className="resume-info-moda-item-title">
|
|
|
|
|
|
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" style={{ width: '28px', height: '28px', marginRight: '10px', verticalAlign: 'middle' }} />
|
|
|
|
|
|
教育经历
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="educational-experience-list">
|
|
|
|
|
|
{(isEditing && editableData ? editableData.education : resumeContent.education)?.map((edu, index) => (
|
|
|
|
|
|
<li key={index} className="educational-experience-list-item">
|
|
|
|
|
|
<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"
|
|
|
|
|
|
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>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
{/* 项目经历 */}
|
|
|
|
|
|
<li className="resume-info-moda-item">
|
|
|
|
|
|
<p className="resume-info-moda-item-title">
|
|
|
|
|
|
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" style={{ width: '28px', height: '28px', marginRight: '10px', verticalAlign: 'middle' }} />
|
|
|
|
|
|
项目经历
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="project-experience-list">
|
|
|
|
|
|
{(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"
|
|
|
|
|
|
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"
|
|
|
|
|
|
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', ...(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 && (
|
|
|
|
|
|
<ul className="job-responsibilities-list">
|
|
|
|
|
|
<p>主要成果</p>
|
|
|
|
|
|
{project.highlights.map((highlight, idx) => (
|
|
|
|
|
|
<li key={idx}>{highlight}</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
{/* 专业技能 */}
|
|
|
|
|
|
<li className="resume-info-moda-item">
|
|
|
|
|
|
<p className="resume-info-moda-item-title">
|
|
|
|
|
|
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" style={{ width: '28px', height: '28px', marginRight: '10px', verticalAlign: 'middle' }} />
|
|
|
|
|
|
专业技能
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="professional-skills-list">
|
|
|
|
|
|
{(isEditing && editableData ? editableData.skills?.core : resumeContent.skills?.core) && (
|
|
|
|
|
|
<li className="professional-skills-list-item">
|
|
|
|
|
|
<p className="skill-name">核心能力</p>
|
|
|
|
|
|
<div className="core-capabilities-list">
|
|
|
|
|
|
{(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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{(isEditing && editableData ? editableData.skills?.additional : resumeContent.skills?.additional) && (
|
|
|
|
|
|
<li className="professional-skills-list-item">
|
|
|
|
|
|
<p className="skill-name">复合技能</p>
|
|
|
|
|
|
<div className="core-capabilities-list">
|
|
|
|
|
|
{(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>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
{/* 个人总结 */}
|
|
|
|
|
|
{(resumeContent.personalSummary && resumeContent.personalSummary.length > 0) ||
|
|
|
|
|
|
(resumeContent.personalSummary && typeof resumeContent.personalSummary === 'string') ? (
|
|
|
|
|
|
<li className="resume-info-moda-item">
|
|
|
|
|
|
<p className="resume-info-moda-item-title">
|
|
|
|
|
|
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" style={{ width: '28px', height: '28px', marginRight: '10px', verticalAlign: 'middle' }} />
|
|
|
|
|
|
个人总结
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="personal-summary-content">
|
|
|
|
|
|
{Array.isArray(resumeContent.personalSummary) ? (
|
|
|
|
|
|
<ul className="personal-summary-list">
|
|
|
|
|
|
{resumeContent.personalSummary.map((summary, index) => (
|
|
|
|
|
|
<li key={index} style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
|
|
|
|
|
|
{summary}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
|
|
|
|
|
|
{resumeContent.personalSummary}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{/* 对应课程单元 - 暂时保留静态展示,后续可根据需要动态化 */}
|
|
|
|
|
|
{resumeContent.courses && (
|
|
|
|
|
|
<li className="resume-info-moda-item">
|
|
|
|
|
|
<p className="resume-info-moda-item-title">
|
|
|
|
|
|
<img src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" style={{ width: '28px', height: '28px', marginRight: '10px', verticalAlign: 'middle' }} />
|
|
|
|
|
|
对应课程单元
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul className="corresponding-course-units-list">
|
|
|
|
|
|
<li className="corresponding-course-units-list-item">
|
|
|
|
|
|
<div className="tag">相关课程</div>
|
|
|
|
|
|
<ul className="course-units-list">
|
|
|
|
|
|
{resumeContent.courses.map((course, index) => (
|
|
|
|
|
|
<li key={index} className="course-units-list-item">
|
|
|
|
|
|
{course}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|