chore: 更新数据文件和组件优化
主要更新内容: - 优化UI组件(视频播放器、HR访问模态框、岗位信息展示等) - 更新数据文件(简历、岗位、项目案例等) - 添加新的图片资源(面试状态图标等) - 新增AgentPage等页面组件 - 清理旧的备份文件,提升代码库整洁度 - 优化岗位等级和面试状态的数据结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
14
src/pages/AgentPage/index.css
Normal file
14
src/pages/AgentPage/index.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.agent-page-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-page-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
zoom: 0.8;
|
||||
}
|
||||
17
src/pages/AgentPage/index.jsx
Normal file
17
src/pages/AgentPage/index.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import "./index.css";
|
||||
|
||||
const AgentPage = () => {
|
||||
return (
|
||||
<div className="agent-page-wrapper">
|
||||
<iframe
|
||||
src="http://192.168.2.33:4173"
|
||||
className="agent-page-iframe"
|
||||
title="Agent"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentPage;
|
||||
@@ -0,0 +1,299 @@
|
||||
/* 继承原有样式 */
|
||||
@import "../ResumeInfoModal/index.css";
|
||||
|
||||
/* 可编辑简历模态框特定样式 */
|
||||
.editable-resume-modal .resume-info-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.editable-resume-modal .action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 可编辑状态样式 */
|
||||
.editable-resume-modal .editable {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.editable-resume-modal .editable:hover {
|
||||
background-color: #fff;
|
||||
border-color: #4096ff;
|
||||
}
|
||||
|
||||
.editable-resume-modal .editable:focus {
|
||||
background-color: #fff;
|
||||
border-color: #4096ff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(64, 150, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 项目和技能项样式 */
|
||||
.editable-resume-modal .project-item,
|
||||
.editable-resume-modal .skill-item {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.editable-resume-modal .project-item:hover,
|
||||
.editable-resume-modal .skill-item:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 删除图标 */
|
||||
.editable-resume-modal .delete-icon {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
color: #ff4d4f;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.editable-resume-modal .project-item:hover .delete-icon,
|
||||
.editable-resume-modal .skill-item:hover .delete-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.editable-resume-modal .delete-icon:hover {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
/* 项目信息布局 */
|
||||
.editable-resume-modal .project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editable-resume-modal .project-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.editable-resume-modal .project-company {
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.editable-resume-modal .project-description {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.editable-resume-modal .separator {
|
||||
color: #d9d9d9;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 技能部分样式 */
|
||||
.editable-resume-modal .skills-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editable-resume-modal .sub-tag {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editable-resume-modal .skills-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.editable-resume-modal .skill-item {
|
||||
padding: 8px 12px;
|
||||
background-color: #f9f9f9;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 版本管理器样式 */
|
||||
.version-manager {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
background-color: #fff;
|
||||
border-color: #4096ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.version-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.version-manager .empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 标签样式增强 */
|
||||
.editable-resume-modal .tag {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 教育经历样式 */
|
||||
.editable-resume-modal .info-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.editable-resume-modal .resume-info-modal-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.editable-resume-modal .action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加按钮样式 */
|
||||
.editable-resume-modal .arco-btn-size-mini {
|
||||
padding: 0 6px;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.editable-resume-modal input[contenteditable="true"] {
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
/* 禁用状态的Radio Group */
|
||||
.editable-resume-modal .arco-radio-group[disabled] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 编辑模式提示 */
|
||||
.editable-resume-modal.editing::before {
|
||||
content: "编辑模式";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 50px;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.editable-resume-modal .version-item {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 保存提示样式 */
|
||||
.editable-resume-modal .save-hint {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Radio, Button, Input, Modal as ArcoModal, Message, Popconfirm, Select } from "@arco-design/web-react";
|
||||
import { IconEdit, IconSave, IconDelete, IconPlus, IconClose } from "@arco-design/web-react/icon";
|
||||
import Modal from "@/components/Modal";
|
||||
import "./index.css";
|
||||
|
||||
const EditableResumeModal = ({ visible, onClose, data, initialVersion = "1", onSave }) => {
|
||||
const [version, setVersion] = useState(initialVersion);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedContent, setEditedContent] = useState(null);
|
||||
const [userVersions, setUserVersions] = useState({});
|
||||
const [showVersionManager, setShowVersionManager] = useState(false);
|
||||
const [newVersionName, setNewVersionName] = useState("");
|
||||
const contentRef = useRef(null);
|
||||
|
||||
// 从localStorage加载用户版本
|
||||
useEffect(() => {
|
||||
const savedVersions = localStorage.getItem(`resume_versions_${data?.position}`);
|
||||
if (savedVersions) {
|
||||
setUserVersions(JSON.parse(savedVersions));
|
||||
}
|
||||
}, [data?.position]);
|
||||
|
||||
// 响应initialVersion变化
|
||||
useEffect(() => {
|
||||
setVersion(initialVersion);
|
||||
}, [initialVersion]);
|
||||
|
||||
// Markdown解析器
|
||||
const parseResumeMarkdown = (markdownContent) => {
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
personalInfo: { name: "岗位名称" },
|
||||
education: [{
|
||||
school: '苏州信息职业技术学院',
|
||||
major: '旅游管理',
|
||||
period: '2018.9-2021.6'
|
||||
}],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] },
|
||||
personalSummary: "",
|
||||
courses: []
|
||||
};
|
||||
|
||||
// 提取岗位名称
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 获取当前显示的内容
|
||||
const getCurrentContent = () => {
|
||||
if (version === "1") {
|
||||
return data?.content?.original;
|
||||
} else if (version === "2") {
|
||||
return data?.content?.modified;
|
||||
} else if (version.startsWith("user_")) {
|
||||
return userVersions[version]?.content;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 解析简历内容
|
||||
let resumeContent = {};
|
||||
const currentContent = getCurrentContent();
|
||||
|
||||
if (currentContent) {
|
||||
if (typeof currentContent === 'string') {
|
||||
resumeContent = parseResumeMarkdown(currentContent);
|
||||
} else if (currentContent.personalInfo) {
|
||||
resumeContent = currentContent;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑
|
||||
const handleStartEdit = () => {
|
||||
setIsEditing(true);
|
||||
setEditedContent({ ...resumeContent });
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent(null);
|
||||
};
|
||||
|
||||
// 保存编辑内容
|
||||
const handleSaveEdit = () => {
|
||||
if (!newVersionName.trim()) {
|
||||
Message.error("请输入版本名称");
|
||||
return;
|
||||
}
|
||||
|
||||
const versionId = `user_${Date.now()}`;
|
||||
const newVersion = {
|
||||
id: versionId,
|
||||
name: newVersionName,
|
||||
content: editedContent,
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedVersions = {
|
||||
...userVersions,
|
||||
[versionId]: newVersion
|
||||
};
|
||||
|
||||
setUserVersions(updatedVersions);
|
||||
localStorage.setItem(`resume_versions_${data?.position}`, JSON.stringify(updatedVersions));
|
||||
|
||||
setIsEditing(false);
|
||||
setVersion(versionId);
|
||||
setNewVersionName("");
|
||||
Message.success("保存成功");
|
||||
|
||||
// 通知父组件
|
||||
if (onSave) {
|
||||
onSave(versionId, newVersion);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除版本
|
||||
const handleDeleteVersion = (versionId) => {
|
||||
const updatedVersions = { ...userVersions };
|
||||
delete updatedVersions[versionId];
|
||||
|
||||
setUserVersions(updatedVersions);
|
||||
localStorage.setItem(`resume_versions_${data?.position}`, JSON.stringify(updatedVersions));
|
||||
|
||||
if (version === versionId) {
|
||||
setVersion("1");
|
||||
}
|
||||
|
||||
Message.success("删除成功");
|
||||
};
|
||||
|
||||
// 更新字段
|
||||
const updateField = (path, value) => {
|
||||
const newContent = { ...editedContent };
|
||||
const keys = path.split('.');
|
||||
let current = newContent;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
setEditedContent(newContent);
|
||||
};
|
||||
|
||||
// 添加项目
|
||||
const handleAddProject = () => {
|
||||
const newProjects = [...(editedContent.projects || [])];
|
||||
newProjects.push({
|
||||
name: "新项目",
|
||||
role: "角色",
|
||||
period: "时间",
|
||||
company: "单位",
|
||||
description: "描述"
|
||||
});
|
||||
updateField('projects', newProjects);
|
||||
};
|
||||
|
||||
// 删除项目
|
||||
const handleDeleteProject = (index) => {
|
||||
const newProjects = editedContent.projects.filter((_, i) => i !== index);
|
||||
updateField('projects', newProjects);
|
||||
};
|
||||
|
||||
// 添加技能
|
||||
const handleAddSkill = (type) => {
|
||||
const newSkills = [...(editedContent.skills[type] || [])];
|
||||
newSkills.push("新技能");
|
||||
updateField(`skills.${type}`, newSkills);
|
||||
};
|
||||
|
||||
// 删除技能
|
||||
const handleDeleteSkill = (type, index) => {
|
||||
const newSkills = editedContent.skills[type].filter((_, i) => i !== index);
|
||||
updateField(`skills.${type}`, newSkills);
|
||||
};
|
||||
|
||||
const onRadioChange = (value, e) => {
|
||||
e?.stopPropagation();
|
||||
setVersion(value);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isEditing) {
|
||||
ArcoModal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '您有未保存的修改,确定要关闭吗?',
|
||||
onOk: () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent(null);
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const displayContent = isEditing ? editedContent : resumeContent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="resume-info-modal editable-resume-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<i className="close-icon" onClick={handleCloseModal} />
|
||||
|
||||
{/* 版本选择和操作按钮 */}
|
||||
<div className="resume-info-modal-header">
|
||||
<Radio.Group
|
||||
type="button"
|
||||
name="position"
|
||||
className="resume-info-modal-radio-group"
|
||||
value={version}
|
||||
onChange={onRadioChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<Radio value="1">原始版</Radio>
|
||||
{data?.content?.modified && (
|
||||
<Radio value="2">官方修改版</Radio>
|
||||
)}
|
||||
{Object.values(userVersions).map(v => (
|
||||
<Radio key={v.id} value={v.id}>{v.name}</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<div className="action-buttons">
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconEdit />}
|
||||
onClick={handleStartEdit}
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowVersionManager(true)}
|
||||
size="small"
|
||||
>
|
||||
管理版本
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="版本名称"
|
||||
value={newVersionName}
|
||||
onChange={setNewVersionName}
|
||||
style={{ width: 150, marginRight: 8 }}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconSave />}
|
||||
onClick={handleSaveEdit}
|
||||
size="small"
|
||||
>
|
||||
保存为新版本
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="resume-info-modal-title">
|
||||
{data?.title || displayContent.personalInfo?.name || "职位名称"}
|
||||
</p>
|
||||
|
||||
{/* 简历内容展示/编辑 */}
|
||||
<ul className="resume-info-moda-list">
|
||||
{/* 教育经历 */}
|
||||
{displayContent.education?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">教育经历</div>
|
||||
{displayContent.education.map((edu, index) => (
|
||||
<li key={index} className="info-list-item">
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`education.${index}.school`, e.target.textContent)}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{edu.school}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`education.${index}.major`, e.target.textContent)}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{edu.major}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`education.${index}.period`, e.target.textContent)}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{edu.period}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 项目经历 */}
|
||||
{displayContent.projects?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">
|
||||
项目经历
|
||||
{isEditing && (
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
size="mini"
|
||||
onClick={handleAddProject}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{displayContent.projects.map((project, index) => (
|
||||
<li key={index} className="project-item">
|
||||
{isEditing && (
|
||||
<IconDelete
|
||||
className="delete-icon"
|
||||
onClick={() => handleDeleteProject(index)}
|
||||
/>
|
||||
)}
|
||||
<div className="project-header">
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`projects.${index}.name`, e.target.textContent)}
|
||||
className={`project-name ${isEditing ? "editable" : ""}`}
|
||||
>
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`projects.${index}.role`, e.target.textContent)}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{project.role}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`projects.${index}.period`, e.target.textContent)}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{project.period}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`projects.${index}.company`, e.target.textContent)}
|
||||
className={`project-company ${isEditing ? "editable" : ""}`}
|
||||
>
|
||||
{project.company}
|
||||
</div>
|
||||
<div
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => isEditing && updateField(`projects.${index}.description`, e.target.textContent)}
|
||||
className={`project-description ${isEditing ? "editable" : ""}`}
|
||||
>
|
||||
{project.description}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 专业技能 */}
|
||||
{(displayContent.skills?.core?.length > 0 || displayContent.skills?.additional?.length > 0) && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">专业技能</div>
|
||||
|
||||
{/* 核心能力 */}
|
||||
{displayContent.skills?.core?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">
|
||||
核心能力
|
||||
{isEditing && (
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
size="mini"
|
||||
onClick={() => handleAddSkill('core')}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul className="skills-list">
|
||||
{displayContent.skills.core.map((skill, index) => (
|
||||
<li key={index} className="skill-item">
|
||||
{isEditing && (
|
||||
<IconDelete
|
||||
className="delete-icon"
|
||||
onClick={() => handleDeleteSkill('core', index)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => {
|
||||
if (isEditing) {
|
||||
const newSkills = [...displayContent.skills.core];
|
||||
newSkills[index] = e.target.textContent;
|
||||
updateField('skills.core', newSkills);
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 复合能力 */}
|
||||
{displayContent.skills?.additional?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">
|
||||
复合能力
|
||||
{isEditing && (
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
size="mini"
|
||||
onClick={() => handleAddSkill('additional')}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul className="skills-list">
|
||||
{displayContent.skills.additional.map((skill, index) => (
|
||||
<li key={index} className="skill-item">
|
||||
{isEditing && (
|
||||
<IconDelete
|
||||
className="delete-icon"
|
||||
onClick={() => handleDeleteSkill('additional', index)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning
|
||||
onBlur={(e) => {
|
||||
if (isEditing) {
|
||||
const newSkills = [...displayContent.skills.additional];
|
||||
newSkills[index] = e.target.textContent;
|
||||
updateField('skills.additional', newSkills);
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable" : ""}
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 版本管理弹窗 */}
|
||||
<ArcoModal
|
||||
title="版本管理"
|
||||
visible={showVersionManager}
|
||||
onCancel={() => setShowVersionManager(false)}
|
||||
footer={null}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<div className="version-manager">
|
||||
<div className="version-list">
|
||||
{Object.values(userVersions).length === 0 ? (
|
||||
<div className="empty">暂无自定义版本</div>
|
||||
) : (
|
||||
Object.values(userVersions).map(v => (
|
||||
<div key={v.id} className="version-item">
|
||||
<div className="version-info">
|
||||
<div className="version-name">{v.name}</div>
|
||||
<div className="version-time">
|
||||
创建时间:{new Date(v.createTime).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="version-actions">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVersion(v.id);
|
||||
setShowVersionManager(false);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该版本吗?"
|
||||
onOk={() => handleDeleteVersion(v.id)}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ArcoModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableResumeModal;
|
||||
@@ -438,3 +438,174 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 公司图片网格布局样式 */
|
||||
.company-images-grid {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 根据图片数量调整列数 */
|
||||
.company-images-grid:has(.company-image-item:nth-child(1):last-child) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.company-images-grid:has(.company-image-item:nth-child(2):last-child) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.company-images-grid:has(.company-image-item:nth-child(3):last-child) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.company-images-grid:has(.company-image-item:nth-child(4)) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.company-image-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.company-image-item:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.company-grid-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.company-image-item:hover .company-grid-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 图片预览模态框样式 */
|
||||
.image-preview-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-preview-img {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.image-preview-close {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-counter {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.image-preview-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-btn-prev {
|
||||
left: -70px;
|
||||
}
|
||||
|
||||
.image-preview-btn-next {
|
||||
right: -70px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
.job-info-modal-content {
|
||||
max-height: 80vh;
|
||||
max-width: 860px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: #f2f3f5;
|
||||
background-image: url("@/assets/images/CompanyJobsPage/background.png");
|
||||
background-size: 100% auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #667eea;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-search {
|
||||
width: 319px;
|
||||
height: 36px;
|
||||
border: 1px solid #2c7aff;
|
||||
|
||||
span {
|
||||
background-color: #fff;
|
||||
}
|
||||
input {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
.empty-data-wrapper {
|
||||
width: 100%;
|
||||
min-height: 555px;
|
||||
display: flex;
|
||||
}
|
||||
.job-info-modal-user-resumes-list {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 每行两列 */
|
||||
gap: 20px; /* 网格间距 */
|
||||
justify-items: start; /* 项目左对龁 */
|
||||
overflow-y: visible;
|
||||
|
||||
.list-item {
|
||||
width: 390px;
|
||||
height: 100px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.list-item-info {
|
||||
height: 68px;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.file-icon {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
filter: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
width: 220px;
|
||||
height: 68px;
|
||||
> p {
|
||||
text-align: left;
|
||||
}
|
||||
.file-info-targetPosition {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
color: #09090b;
|
||||
line-height: 28px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-info-skills {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 21px;
|
||||
color: #788089;
|
||||
line-height: 21px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
overflow: hidden; /* 超出隐藏 */
|
||||
white-space: nowrap; /* 禁止换行 */
|
||||
text-overflow: ellipsis; /* 文本溢出显示省略号 */
|
||||
}
|
||||
|
||||
.version-selector {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.arco-select {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.arco-select-view-single {
|
||||
height: 28px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
width: 64px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #2c7aff;
|
||||
color: #2c7aff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background-color: #2c7aff;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(44, 122, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(44, 122, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info {
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.job-info-modal-content-position-info-position {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.job-category-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 根据岗位相关标签内容设置不同颜色 */
|
||||
.job-category-tag[data-category="专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="非专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="人才出海岗位"] {
|
||||
background: linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%);
|
||||
}
|
||||
|
||||
.job-remaining-positions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
.warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff4d4f;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-num {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-salary {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: #ff7d00;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-info-tags {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
|
||||
.job-info-modal-info-tag {
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 5px;
|
||||
padding: 4px 12px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-description,
|
||||
.job-info-modal-content-position-info-requirements,
|
||||
.job-info-modal-content-position-info-companyInfo {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #e5e6eb;
|
||||
|
||||
> p {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.description-title,
|
||||
.requirements-title,
|
||||
.companyInfo-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
color: #1d2129;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.description-content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
|
||||
.description-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
text-align: left;
|
||||
|
||||
.description-number {
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
margin-right: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.companyInfo-content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.requirements-content {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
.requirements-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
text-align: left;
|
||||
|
||||
.requirement-number {
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
margin-right: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.requirement-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-line {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-btn {
|
||||
width: 120px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
color: #fff;
|
||||
background-color: #2c7aff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
> i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
background-image: url("@/assets/images/CompanyJobsPage/btn_icon_2.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
.job-info-modal-content {
|
||||
max-height: 80vh;
|
||||
max-width: 860px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: #f2f3f5;
|
||||
background-image: url("@/assets/images/CompanyJobsPage/background.png");
|
||||
background-size: 100% auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px;
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #667eea;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-search {
|
||||
width: 319px;
|
||||
height: 36px;
|
||||
border: 1px solid #2c7aff;
|
||||
|
||||
span {
|
||||
background-color: #fff;
|
||||
}
|
||||
input {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
.empty-data-wrapper {
|
||||
width: 100%;
|
||||
min-height: 555px;
|
||||
display: flex;
|
||||
}
|
||||
.job-info-modal-user-resumes-list {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
margin-top: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 每行两列 */
|
||||
gap: 20px; /* 网格间距 */
|
||||
justify-items: start; /* 项目左对龁 */
|
||||
overflow-y: visible;
|
||||
|
||||
.list-item {
|
||||
width: 390px;
|
||||
height: 100px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.list-item-info {
|
||||
height: 68px;
|
||||
width: 300px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.file-icon {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
filter: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
width: 220px;
|
||||
height: 68px;
|
||||
> p {
|
||||
text-align: left;
|
||||
}
|
||||
.file-info-targetPosition {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
color: #09090b;
|
||||
line-height: 28px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-info-skills {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 21px;
|
||||
color: #788089;
|
||||
line-height: 21px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
overflow: hidden; /* 超出隐藏 */
|
||||
white-space: nowrap; /* 禁止换行 */
|
||||
text-overflow: ellipsis; /* 文本溢出显示省略号 */
|
||||
}
|
||||
|
||||
.version-selector {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.arco-select {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.arco-select-view-single {
|
||||
height: 28px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
width: 64px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #2c7aff;
|
||||
color: #2c7aff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #ffffff;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
background-color: #2c7aff;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(44, 122, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(44, 122, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info {
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.job-info-modal-content-position-info-position {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 30px;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.job-category-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 根据岗位相关标签内容设置不同颜色 */
|
||||
.job-category-tag[data-category="专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="非专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="人才出海岗位"] {
|
||||
background: linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%);
|
||||
}
|
||||
|
||||
.job-remaining-positions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
.warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff4d4f;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-num {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-salary {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: #ff7d00;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-info-tags {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
|
||||
.job-info-modal-info-tag {
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 5px;
|
||||
padding: 4px 12px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-description,
|
||||
.job-info-modal-content-position-info-requirements,
|
||||
.job-info-modal-content-position-info-companyInfo {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #e5e6eb;
|
||||
|
||||
> p {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.description-title,
|
||||
.requirements-title,
|
||||
.companyInfo-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
color: #1d2129;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.description-content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
|
||||
.description-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
text-align: left;
|
||||
|
||||
.description-number {
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
margin-right: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.companyInfo-content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.requirements-content {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
.requirements-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
text-align: left;
|
||||
|
||||
.requirement-number {
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
margin-right: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.requirement-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.requirement-line {
|
||||
margin-bottom: 8px;
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #4e5969;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-btn {
|
||||
width: 120px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
color: #fff;
|
||||
background-color: #2c7aff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
> i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
background-image: url("@/assets/images/CompanyJobsPage/btn_icon_2.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 公司图片轮播样式 */
|
||||
.company-images-carousel {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.carousel-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-counter {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 图片预览模态框样式 */
|
||||
.image-preview-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-preview-img {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.image-preview-close {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-counter {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: 6px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.image-preview-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview-btn-prev {
|
||||
left: -70px;
|
||||
}
|
||||
|
||||
.image-preview-btn-next {
|
||||
right: -70px;
|
||||
}
|
||||
@@ -24,6 +24,8 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
const [listHasMore, setListHasMore] = useState(true);
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [selectedVersions, setSelectedVersions] = useState({}); // 每个简历的版本选择,使用简历ID作为key
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0); // 当前显示的图片索引
|
||||
const [imageModalVisible, setImageModalVisible] = useState(false); // 图片预览模态框
|
||||
|
||||
// 处理directToResume参数变化
|
||||
useEffect(() => {
|
||||
@@ -39,9 +41,29 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
setResumeList([]); // 清空简历列表
|
||||
setListPage(1); // 重置分页
|
||||
setListHasMore(true); // 重置加载更多状态
|
||||
setCurrentImageIndex(0); // 重置图片索引
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 图片轮播相关函数
|
||||
const handlePrevImage = () => {
|
||||
const images = data?.details?.companyImages || [];
|
||||
setCurrentImageIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const handleNextImage = () => {
|
||||
const images = data?.details?.companyImages || [];
|
||||
setCurrentImageIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
const handleImageClick = () => {
|
||||
setImageModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseImageModal = () => {
|
||||
setImageModalVisible(false);
|
||||
};
|
||||
|
||||
const queryResumeList = useCallback(async () => {
|
||||
const res = await getResumesList({
|
||||
page: listPage,
|
||||
@@ -274,8 +296,12 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 岗位剩余量 - 仅未投递岗位显示 */}
|
||||
{!data?.isDelivered && data?.remainingPositions && (
|
||||
{/* 岗位剩余量 - 仅未投递、未过期且非面试状态岗位显示 */}
|
||||
{!data?.isDelivered &&
|
||||
!data?.isExpired &&
|
||||
data?.status !== 'expired' &&
|
||||
!hideDeliverButton &&
|
||||
data?.remainingPositions && (
|
||||
<span className="job-remaining-positions">
|
||||
<i className="warning-icon">!</i>
|
||||
岗位招聘数量仅剩{data?.remainingPositions}名
|
||||
@@ -349,9 +375,33 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{data?.details?.companyImages && data.details.companyImages.length > 0 && (
|
||||
<div className="company-images-grid">
|
||||
{data.details.companyImages.map((imageUrl, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="company-image-item"
|
||||
onClick={() => {
|
||||
setCurrentImageIndex(index);
|
||||
handleImageClick();
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={`公司图片 ${index + 1}`}
|
||||
className="company-grid-image"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hideDeliverButton && (
|
||||
{/* 立即投递按钮 - 仅未投递、未过期且非面试状态岗位显示 */}
|
||||
{!data?.isDelivered &&
|
||||
!data?.isExpired &&
|
||||
data?.status !== 'expired' &&
|
||||
!hideDeliverButton && (
|
||||
<div
|
||||
className="job-info-modal-btn"
|
||||
onClick={handleClickDeliverBtn}
|
||||
@@ -374,10 +424,27 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
setCurrentResumeId(null);
|
||||
}}
|
||||
/>
|
||||
<PermissionModal
|
||||
<PermissionModal
|
||||
visible={permissionModalVisible}
|
||||
onClose={() => setPermissionModalVisible(false)}
|
||||
/>
|
||||
{imageModalVisible && data?.details?.companyImages && (
|
||||
<div className="image-preview-modal" onClick={handleCloseImageModal}>
|
||||
<div className="image-preview-content" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="image-preview-close" onClick={handleCloseImageModal}>×</button>
|
||||
<img
|
||||
src={data.details.companyImages[currentImageIndex]}
|
||||
alt={`公司图片 ${currentImageIndex + 1}`}
|
||||
className="image-preview-img"
|
||||
/>
|
||||
<div className="image-preview-counter">
|
||||
{currentImageIndex + 1} / {data.details.companyImages.length}
|
||||
</div>
|
||||
<button className="image-preview-btn image-preview-btn-prev" onClick={handlePrevImage}>‹</button>
|
||||
<button className="image-preview-btn image-preview-btn-next" onClick={handleNextImage}>›</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Input, Select } from "@arco-design/web-react";
|
||||
import Modal from "@/components/Modal";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import toast from "@/components/Toast";
|
||||
import FILEICON from "@/assets/images/CompanyJobsPage/file_icon.png";
|
||||
import ResumeInfoModal from "../ResumeInfoModal";
|
||||
import PermissionModal from "../PermissionModal";
|
||||
import { getResumesList, submitResume, getPageData } from "@/services";
|
||||
import "./index.css";
|
||||
|
||||
const InputSearch = Input.Search;
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default ({ visible, onClose, data, directToResume = false, hideDeliverButton = false }) => {
|
||||
const studentInfo = useSelector((state) => state.student.studentInfo);
|
||||
const [resumeModalShow, setResumeModalShow] = useState(directToResume);
|
||||
const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false);
|
||||
const [resumeInfoData, setResumeInfoData] = useState(null);
|
||||
const [currentResumeId, setCurrentResumeId] = useState(null); // 当前查看的简历ID
|
||||
const [resumeList, setResumeList] = useState([]); // 简历列表
|
||||
const [listPage, setListPage] = useState(1);
|
||||
const [listHasMore, setListHasMore] = useState(true);
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [selectedVersions, setSelectedVersions] = useState({}); // 每个简历的版本选择,使用简历ID作为key
|
||||
|
||||
// 处理directToResume参数变化
|
||||
useEffect(() => {
|
||||
if (visible && directToResume) {
|
||||
setResumeModalShow(true);
|
||||
} else if (visible && !directToResume) {
|
||||
setResumeModalShow(false);
|
||||
}
|
||||
}, [visible, directToResume]);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setResumeModalShow(false);
|
||||
setResumeList([]); // 清空简历列表
|
||||
setListPage(1); // 重置分页
|
||||
setListHasMore(true); // 重置加载更多状态
|
||||
onClose();
|
||||
};
|
||||
|
||||
const queryResumeList = useCallback(async () => {
|
||||
const res = await getResumesList({
|
||||
page: listPage,
|
||||
pageSize: PAGE_SIZE,
|
||||
studentId: studentInfo?.id
|
||||
});
|
||||
if (res.success) {
|
||||
setResumeList((prevList) => {
|
||||
const newList = [...prevList, ...res.data];
|
||||
if (res.total === newList?.length) {
|
||||
setListHasMore(false);
|
||||
} else {
|
||||
setListPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
}, [listPage, studentInfo?.id]);
|
||||
|
||||
// 点击立即投递
|
||||
const handleClickDeliverBtn = (e) => {
|
||||
e.stopPropagation();
|
||||
setResumeModalShow(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 选择简历投递
|
||||
const userResumesClick = async (item) => {
|
||||
// 显示权限提示弹窗
|
||||
setPermissionModalVisible(true);
|
||||
|
||||
// 原投递逻辑暂时注释,实际使用时可根据用户权限判断
|
||||
/*
|
||||
try {
|
||||
// 调用投递服务
|
||||
const result = await submitResume({
|
||||
resumeId: item.id,
|
||||
jobId: data?.id,
|
||||
studentId: studentInfo?.id,
|
||||
resumeTitle: item.title,
|
||||
jobPosition: data?.position,
|
||||
company: data?.company,
|
||||
resumeVersion: selectedVersions[item.id] || "2" // 添加版本信息
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 投递成功,显示成功提示
|
||||
const versionText = (selectedVersions[item.id] || "2") === "1" ? "原始版" : "个人修改版";
|
||||
toast.success(`简历"${item.title}"(${versionText})投递成功!`);
|
||||
|
||||
// 关闭模态框
|
||||
handleCloseModal();
|
||||
|
||||
// 输出投递成功信息
|
||||
console.log('投递成功', {
|
||||
applicationId: result.data.applicationId,
|
||||
resumeId: item.id,
|
||||
jobId: data?.id,
|
||||
resumeTitle: item.title,
|
||||
jobPosition: data?.position,
|
||||
submittedAt: result.data.submittedAt
|
||||
});
|
||||
} else {
|
||||
toast.error(result.message || '投递失败,请重试');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('投递失败,请重试');
|
||||
console.error('投递失败:', error);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
// 点击简历详情
|
||||
const userResumesBtnClick = async (e, item) => {
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
// 获取岗位与面试题页面的数据
|
||||
const pageDataResponse = await getPageData();
|
||||
|
||||
if (pageDataResponse.success) {
|
||||
const pageData = pageDataResponse.data;
|
||||
|
||||
// 找到对应的行业信息
|
||||
const matchedIndustry = pageData.industries?.find(industry =>
|
||||
industry.name === item.industry
|
||||
);
|
||||
|
||||
// 从resumeTemplates中查找对应岗位的模板
|
||||
const industryTemplates = pageData.resumeTemplates?.[item.industry] || [];
|
||||
const positionTemplate = industryTemplates.find(template =>
|
||||
template.position === item.position
|
||||
);
|
||||
|
||||
// 添加调试日志
|
||||
console.log('查找简历模板:', {
|
||||
industryName: item.industry,
|
||||
positionTitle: item.position,
|
||||
templatesCount: industryTemplates.length,
|
||||
templatePositions: industryTemplates.map(t => t.position),
|
||||
templatesStructure: industryTemplates.slice(0, 2).map(t => ({
|
||||
position: t.position,
|
||||
hasContent: !!t.content,
|
||||
hasStudentInfo: !!t.studentInfo,
|
||||
keys: Object.keys(t)
|
||||
}))
|
||||
});
|
||||
|
||||
if (positionTemplate) {
|
||||
console.log('找到的模板:', {
|
||||
position: positionTemplate.position,
|
||||
hasContent: !!positionTemplate.content,
|
||||
hasContentOriginal: !!positionTemplate.content?.original,
|
||||
hasStudentInfo: !!positionTemplate.studentInfo,
|
||||
templateKeys: Object.keys(positionTemplate),
|
||||
contentKeys: positionTemplate.content ? Object.keys(positionTemplate.content) : null
|
||||
});
|
||||
} else {
|
||||
console.warn('未找到简历模板:', item.position);
|
||||
}
|
||||
|
||||
// 构造简历数据,使用与ResumeInterviewPage相同的格式
|
||||
const resumeData = {
|
||||
title: item.position, // 使用岗位名称作为标题
|
||||
content: positionTemplate?.content || null, // 这里包含原始版和修改版数据
|
||||
selectedTemplate: positionTemplate, // 添加selectedTemplate字段
|
||||
studentResume: pageData.myResume
|
||||
};
|
||||
|
||||
console.log('加载简历数据:', {
|
||||
resumeTitle: item.title,
|
||||
position: item.position,
|
||||
industry: item.industry,
|
||||
selectedVersion: selectedVersions[item.id] || "2",
|
||||
hasContent: !!positionTemplate?.content,
|
||||
hasOriginal: !!positionTemplate?.content?.original,
|
||||
hasModified: !!positionTemplate?.content?.modified
|
||||
});
|
||||
|
||||
setResumeInfoData(resumeData);
|
||||
setCurrentResumeId(item.id); // 记录当前简历ID
|
||||
setResumeInfoModalShow(true);
|
||||
} else {
|
||||
toast.error('加载简历数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取简历数据失败:', error);
|
||||
toast.error('加载简历数据失败');
|
||||
}
|
||||
};;;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="job-info-modal-content">
|
||||
{resumeModalShow ? (
|
||||
<>
|
||||
|
||||
{
|
||||
<InfiniteScroll
|
||||
loadMore={queryResumeList}
|
||||
hasMore={listHasMore}
|
||||
empty={resumeList.length === 0}
|
||||
className={`${
|
||||
resumeList.length
|
||||
? "job-info-modal-user-resumes-list"
|
||||
: "empty-data-wrapper"
|
||||
}`}
|
||||
>
|
||||
{resumeList.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="list-item"
|
||||
onClick={(e) => userResumesBtnClick(e, item)}
|
||||
>
|
||||
<div className="list-item-info">
|
||||
<img src={FILEICON} className="file-icon" />
|
||||
<div className="file-info">
|
||||
<p className="file-info-targetPosition">
|
||||
{item.title}
|
||||
</p>
|
||||
<div className="version-selector">
|
||||
<Select
|
||||
placeholder="选择版本"
|
||||
value={selectedVersions[item.id] || "2"}
|
||||
style={{ width: 120, fontSize: '12px' }}
|
||||
onChange={(value) => {
|
||||
setSelectedVersions(prev => ({
|
||||
...prev,
|
||||
[item.id]: value
|
||||
}));
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Select.Option value="1">原始版</Select.Option>
|
||||
<Select.Option value="2">个人修改版</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="info-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
userResumesClick(item);
|
||||
}}
|
||||
>
|
||||
投递
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="job-info-modal-content-position-info">
|
||||
<span className="job-info-modal-content-position-info-position">
|
||||
{data?.position}
|
||||
</span>
|
||||
{/* 岗位相关标签 */}
|
||||
{(data?.jobCategoryTag || data?.jobCategory) && (
|
||||
<span
|
||||
className="job-category-tag"
|
||||
data-category={data?.jobCategoryTag || data?.jobCategory}
|
||||
>
|
||||
{data?.jobCategoryTag || data?.jobCategory}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 岗位剩余量 - 仅未投递岗位显示 */}
|
||||
{!data?.isDelivered && data?.remainingPositions && (
|
||||
<span className="job-remaining-positions">
|
||||
<i className="warning-icon">!</i>
|
||||
岗位招聘数量仅剩{data?.remainingPositions}名
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="job-info-modal-content-position-info-salary">
|
||||
{data?.salary}
|
||||
</span>
|
||||
</div>
|
||||
{data?.tags?.length > 0 && (
|
||||
<ul className="job-info-modal-info-tags">
|
||||
{data?.tags?.map((tag, index) => (
|
||||
<li key={index} className="job-info-modal-info-tag">
|
||||
{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{data?.details?.description && (
|
||||
<div className="job-info-modal-content-position-info-description">
|
||||
<p className="description-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
岗位描述
|
||||
</p>
|
||||
<div className="description-content">
|
||||
{data?.details?.description.split(/\d+\.\s*/).filter(item => item.trim()).map((item, index) => (
|
||||
<div key={index} className="description-item">
|
||||
<span className="description-number">{index + 1}.</span>
|
||||
<span className="description-text">{item.trim()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(data?.details?.requirements?.length > 0 || data?.details?.requirementsText) && (
|
||||
<div className="job-info-modal-content-position-info-requirements">
|
||||
<p className="requirements-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
岗位要求
|
||||
</p>
|
||||
<div className="requirements-content">
|
||||
{data?.details?.requirements ? (
|
||||
data?.details?.requirements?.map((item, index) => (
|
||||
<div key={index} className="requirements-item">
|
||||
<span className="requirement-number">{index + 1}.</span>
|
||||
<span className="requirement-text">{item}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
data?.details?.requirementsText?.split(/\d+\.\s*/).filter(item => item.trim()).map((item, index) => (
|
||||
<div key={index} className="requirements-item">
|
||||
<span className="requirement-number">{index + 1}.</span>
|
||||
<span className="requirement-text">{item.trim()}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.details?.companyInfo && (
|
||||
<div className="job-info-modal-content-position-info-companyInfo">
|
||||
<p className="companyInfo-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
公司介绍
|
||||
</p>
|
||||
<div className="companyInfo-content">
|
||||
{data?.details?.companyInfo.split('\n').map((paragraph, index) => (
|
||||
<p key={index} className="company-paragraph">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!hideDeliverButton && (
|
||||
<div
|
||||
className="job-info-modal-btn"
|
||||
onClick={handleClickDeliverBtn}
|
||||
>
|
||||
<i />
|
||||
<span>立即投递</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<ResumeInfoModal
|
||||
visible={resumeInfoModalShow}
|
||||
data={resumeInfoData}
|
||||
initialVersion={selectedVersions[currentResumeId] || "2"}
|
||||
onClose={() => {
|
||||
setResumeInfoModalShow(false);
|
||||
setResumeInfoData(null);
|
||||
setCurrentResumeId(null);
|
||||
}}
|
||||
/>
|
||||
<PermissionModal
|
||||
visible={permissionModalVisible}
|
||||
onClose={() => setPermissionModalVisible(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,454 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Input, Select } from "@arco-design/web-react";
|
||||
import Modal from "@/components/Modal";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import toast from "@/components/Toast";
|
||||
import FILEICON from "@/assets/images/CompanyJobsPage/file_icon.png";
|
||||
import ResumeInfoModal from "../ResumeInfoModal";
|
||||
import PermissionModal from "../PermissionModal";
|
||||
import { getResumesList, submitResume, getPageData } from "@/services";
|
||||
import "./index.css";
|
||||
|
||||
const InputSearch = Input.Search;
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default ({ visible, onClose, data, directToResume = false, hideDeliverButton = false }) => {
|
||||
const studentInfo = useSelector((state) => state.student.studentInfo);
|
||||
const [resumeModalShow, setResumeModalShow] = useState(directToResume);
|
||||
const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false);
|
||||
const [resumeInfoData, setResumeInfoData] = useState(null);
|
||||
const [currentResumeId, setCurrentResumeId] = useState(null); // 当前查看的简历ID
|
||||
const [resumeList, setResumeList] = useState([]); // 简历列表
|
||||
const [listPage, setListPage] = useState(1);
|
||||
const [listHasMore, setListHasMore] = useState(true);
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [selectedVersions, setSelectedVersions] = useState({}); // 每个简历的版本选择,使用简历ID作为key
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0); // 当前显示的图片索引
|
||||
const [imageModalVisible, setImageModalVisible] = useState(false); // 图片预览模态框
|
||||
|
||||
// 处理directToResume参数变化
|
||||
useEffect(() => {
|
||||
if (visible && directToResume) {
|
||||
setResumeModalShow(true);
|
||||
} else if (visible && !directToResume) {
|
||||
setResumeModalShow(false);
|
||||
}
|
||||
}, [visible, directToResume]);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setResumeModalShow(false);
|
||||
setResumeList([]); // 清空简历列表
|
||||
setListPage(1); // 重置分页
|
||||
setListHasMore(true); // 重置加载更多状态
|
||||
setCurrentImageIndex(0); // 重置图片索引
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 图片轮播相关函数
|
||||
const handlePrevImage = () => {
|
||||
const images = data?.details?.companyImages || [];
|
||||
setCurrentImageIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const handleNextImage = () => {
|
||||
const images = data?.details?.companyImages || [];
|
||||
setCurrentImageIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
const handleImageClick = () => {
|
||||
setImageModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseImageModal = () => {
|
||||
setImageModalVisible(false);
|
||||
};
|
||||
|
||||
const queryResumeList = useCallback(async () => {
|
||||
const res = await getResumesList({
|
||||
page: listPage,
|
||||
pageSize: PAGE_SIZE,
|
||||
studentId: studentInfo?.id
|
||||
});
|
||||
if (res.success) {
|
||||
setResumeList((prevList) => {
|
||||
const newList = [...prevList, ...res.data];
|
||||
if (res.total === newList?.length) {
|
||||
setListHasMore(false);
|
||||
} else {
|
||||
setListPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
}, [listPage, studentInfo?.id]);
|
||||
|
||||
// 点击立即投递
|
||||
const handleClickDeliverBtn = (e) => {
|
||||
e.stopPropagation();
|
||||
setResumeModalShow(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 选择简历投递
|
||||
const userResumesClick = async (item) => {
|
||||
// 显示权限提示弹窗
|
||||
setPermissionModalVisible(true);
|
||||
|
||||
// 原投递逻辑暂时注释,实际使用时可根据用户权限判断
|
||||
/*
|
||||
try {
|
||||
// 调用投递服务
|
||||
const result = await submitResume({
|
||||
resumeId: item.id,
|
||||
jobId: data?.id,
|
||||
studentId: studentInfo?.id,
|
||||
resumeTitle: item.title,
|
||||
jobPosition: data?.position,
|
||||
company: data?.company,
|
||||
resumeVersion: selectedVersions[item.id] || "2" // 添加版本信息
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 投递成功,显示成功提示
|
||||
const versionText = (selectedVersions[item.id] || "2") === "1" ? "原始版" : "个人修改版";
|
||||
toast.success(`简历"${item.title}"(${versionText})投递成功!`);
|
||||
|
||||
// 关闭模态框
|
||||
handleCloseModal();
|
||||
|
||||
// 输出投递成功信息
|
||||
console.log('投递成功', {
|
||||
applicationId: result.data.applicationId,
|
||||
resumeId: item.id,
|
||||
jobId: data?.id,
|
||||
resumeTitle: item.title,
|
||||
jobPosition: data?.position,
|
||||
submittedAt: result.data.submittedAt
|
||||
});
|
||||
} else {
|
||||
toast.error(result.message || '投递失败,请重试');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error('投递失败,请重试');
|
||||
console.error('投递失败:', error);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
// 点击简历详情
|
||||
const userResumesBtnClick = async (e, item) => {
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
// 获取岗位与面试题页面的数据
|
||||
const pageDataResponse = await getPageData();
|
||||
|
||||
if (pageDataResponse.success) {
|
||||
const pageData = pageDataResponse.data;
|
||||
|
||||
// 找到对应的行业信息
|
||||
const matchedIndustry = pageData.industries?.find(industry =>
|
||||
industry.name === item.industry
|
||||
);
|
||||
|
||||
// 从resumeTemplates中查找对应岗位的模板
|
||||
const industryTemplates = pageData.resumeTemplates?.[item.industry] || [];
|
||||
const positionTemplate = industryTemplates.find(template =>
|
||||
template.position === item.position
|
||||
);
|
||||
|
||||
// 添加调试日志
|
||||
console.log('查找简历模板:', {
|
||||
industryName: item.industry,
|
||||
positionTitle: item.position,
|
||||
templatesCount: industryTemplates.length,
|
||||
templatePositions: industryTemplates.map(t => t.position),
|
||||
templatesStructure: industryTemplates.slice(0, 2).map(t => ({
|
||||
position: t.position,
|
||||
hasContent: !!t.content,
|
||||
hasStudentInfo: !!t.studentInfo,
|
||||
keys: Object.keys(t)
|
||||
}))
|
||||
});
|
||||
|
||||
if (positionTemplate) {
|
||||
console.log('找到的模板:', {
|
||||
position: positionTemplate.position,
|
||||
hasContent: !!positionTemplate.content,
|
||||
hasContentOriginal: !!positionTemplate.content?.original,
|
||||
hasStudentInfo: !!positionTemplate.studentInfo,
|
||||
templateKeys: Object.keys(positionTemplate),
|
||||
contentKeys: positionTemplate.content ? Object.keys(positionTemplate.content) : null
|
||||
});
|
||||
} else {
|
||||
console.warn('未找到简历模板:', item.position);
|
||||
}
|
||||
|
||||
// 构造简历数据,使用与ResumeInterviewPage相同的格式
|
||||
const resumeData = {
|
||||
title: item.position, // 使用岗位名称作为标题
|
||||
content: positionTemplate?.content || null, // 这里包含原始版和修改版数据
|
||||
selectedTemplate: positionTemplate, // 添加selectedTemplate字段
|
||||
studentResume: pageData.myResume
|
||||
};
|
||||
|
||||
console.log('加载简历数据:', {
|
||||
resumeTitle: item.title,
|
||||
position: item.position,
|
||||
industry: item.industry,
|
||||
selectedVersion: selectedVersions[item.id] || "2",
|
||||
hasContent: !!positionTemplate?.content,
|
||||
hasOriginal: !!positionTemplate?.content?.original,
|
||||
hasModified: !!positionTemplate?.content?.modified
|
||||
});
|
||||
|
||||
setResumeInfoData(resumeData);
|
||||
setCurrentResumeId(item.id); // 记录当前简历ID
|
||||
setResumeInfoModalShow(true);
|
||||
} else {
|
||||
toast.error('加载简历数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取简历数据失败:', error);
|
||||
toast.error('加载简历数据失败');
|
||||
}
|
||||
};;;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="job-info-modal-content">
|
||||
{resumeModalShow ? (
|
||||
<>
|
||||
|
||||
{
|
||||
<InfiniteScroll
|
||||
loadMore={queryResumeList}
|
||||
hasMore={listHasMore}
|
||||
empty={resumeList.length === 0}
|
||||
className={`${
|
||||
resumeList.length
|
||||
? "job-info-modal-user-resumes-list"
|
||||
: "empty-data-wrapper"
|
||||
}`}
|
||||
>
|
||||
{resumeList.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="list-item"
|
||||
onClick={(e) => userResumesBtnClick(e, item)}
|
||||
>
|
||||
<div className="list-item-info">
|
||||
<img src={FILEICON} className="file-icon" />
|
||||
<div className="file-info">
|
||||
<p className="file-info-targetPosition">
|
||||
{item.title}
|
||||
</p>
|
||||
<div className="version-selector">
|
||||
<Select
|
||||
placeholder="选择版本"
|
||||
value={selectedVersions[item.id] || "2"}
|
||||
style={{ width: 120, fontSize: '12px' }}
|
||||
onChange={(value) => {
|
||||
setSelectedVersions(prev => ({
|
||||
...prev,
|
||||
[item.id]: value
|
||||
}));
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Select.Option value="1">原始版</Select.Option>
|
||||
<Select.Option value="2">个人修改版</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="info-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
userResumesClick(item);
|
||||
}}
|
||||
>
|
||||
投递
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="job-info-modal-content-position-info">
|
||||
<span className="job-info-modal-content-position-info-position">
|
||||
{data?.position}
|
||||
</span>
|
||||
{/* 岗位相关标签 */}
|
||||
{(data?.jobCategoryTag || data?.jobCategory) && (
|
||||
<span
|
||||
className="job-category-tag"
|
||||
data-category={data?.jobCategoryTag || data?.jobCategory}
|
||||
>
|
||||
{data?.jobCategoryTag || data?.jobCategory}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 岗位剩余量 - 仅未投递岗位显示 */}
|
||||
{!data?.isDelivered && data?.remainingPositions && (
|
||||
<span className="job-remaining-positions">
|
||||
<i className="warning-icon">!</i>
|
||||
岗位招聘数量仅剩{data?.remainingPositions}名
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="job-info-modal-content-position-info-salary">
|
||||
{data?.salary}
|
||||
</span>
|
||||
</div>
|
||||
{data?.tags?.length > 0 && (
|
||||
<ul className="job-info-modal-info-tags">
|
||||
{data?.tags?.map((tag, index) => (
|
||||
<li key={index} className="job-info-modal-info-tag">
|
||||
{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{data?.details?.description && (
|
||||
<div className="job-info-modal-content-position-info-description">
|
||||
<p className="description-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
岗位描述
|
||||
</p>
|
||||
<div className="description-content">
|
||||
{data?.details?.description.split(/\d+\.\s*/).filter(item => item.trim()).map((item, index) => (
|
||||
<div key={index} className="description-item">
|
||||
<span className="description-number">{index + 1}.</span>
|
||||
<span className="description-text">{item.trim()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(data?.details?.requirements?.length > 0 || data?.details?.requirementsText) && (
|
||||
<div className="job-info-modal-content-position-info-requirements">
|
||||
<p className="requirements-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
岗位要求
|
||||
</p>
|
||||
<div className="requirements-content">
|
||||
{data?.details?.requirements ? (
|
||||
data?.details?.requirements?.map((item, index) => (
|
||||
<div key={index} className="requirements-item">
|
||||
<span className="requirement-number">{index + 1}.</span>
|
||||
<span className="requirement-text">{item}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
data?.details?.requirementsText?.split(/\d+\.\s*/).filter(item => item.trim()).map((item, index) => (
|
||||
<div key={index} className="requirements-item">
|
||||
<span className="requirement-number">{index + 1}.</span>
|
||||
<span className="requirement-text">{item.trim()}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.details?.companyInfo && (
|
||||
<div className="job-info-modal-content-position-info-companyInfo">
|
||||
<p className="companyInfo-title">
|
||||
<img className="title-icon" src="https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/recuW0XRVB1bpV.png" alt="" />
|
||||
公司介绍
|
||||
</p>
|
||||
<div className="companyInfo-content">
|
||||
{data?.details?.companyInfo.split('\n').map((paragraph, index) => (
|
||||
<p key={index} className="company-paragraph">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{(() => {
|
||||
console.log('🔍 [JobInfoModal] 图片数据调试:', {
|
||||
position: data?.position,
|
||||
hasDetails: !!data?.details,
|
||||
hasCompanyImages: !!data?.details?.companyImages,
|
||||
imagesLength: data?.details?.companyImages?.length || 0,
|
||||
images: data?.details?.companyImages
|
||||
});
|
||||
return null;
|
||||
})()}
|
||||
{data?.details?.companyImages && data.details.companyImages.length > 0 && (
|
||||
<div className="company-images-carousel">
|
||||
<div className="carousel-container">
|
||||
<button className="carousel-btn carousel-btn-prev" onClick={handlePrevImage}>
|
||||
‹
|
||||
</button>
|
||||
<div className="carousel-image-wrapper" onClick={handleImageClick}>
|
||||
<img
|
||||
src={data.details.companyImages[currentImageIndex]}
|
||||
alt={`公司图片 ${currentImageIndex + 1}`}
|
||||
className="carousel-image"
|
||||
/>
|
||||
<div className="image-counter">
|
||||
{currentImageIndex + 1} / {data.details.companyImages.length}
|
||||
</div>
|
||||
</div>
|
||||
<button className="carousel-btn carousel-btn-next" onClick={handleNextImage}>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hideDeliverButton && (
|
||||
<div
|
||||
className="job-info-modal-btn"
|
||||
onClick={handleClickDeliverBtn}
|
||||
>
|
||||
<i />
|
||||
<span>立即投递</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
<ResumeInfoModal
|
||||
visible={resumeInfoModalShow}
|
||||
data={resumeInfoData}
|
||||
initialVersion={selectedVersions[currentResumeId] || "2"}
|
||||
onClose={() => {
|
||||
setResumeInfoModalShow(false);
|
||||
setResumeInfoData(null);
|
||||
setCurrentResumeId(null);
|
||||
}}
|
||||
/>
|
||||
<PermissionModal
|
||||
visible={permissionModalVisible}
|
||||
onClose={() => setPermissionModalVisible(false)}
|
||||
/>
|
||||
{imageModalVisible && data?.details?.companyImages && (
|
||||
<div className="image-preview-modal" onClick={handleCloseImageModal}>
|
||||
<div className="image-preview-content" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="image-preview-close" onClick={handleCloseImageModal}>×</button>
|
||||
<img
|
||||
src={data.details.companyImages[currentImageIndex]}
|
||||
alt={`公司图片 ${currentImageIndex + 1}`}
|
||||
className="image-preview-img"
|
||||
/>
|
||||
<div className="image-preview-counter">
|
||||
{currentImageIndex + 1} / {data.details.companyImages.length}
|
||||
</div>
|
||||
<button className="image-preview-btn image-preview-btn-prev" onClick={handlePrevImage}>‹</button>
|
||||
<button className="image-preview-btn image-preview-btn-next" onClick={handleNextImage}>›</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -32,12 +32,13 @@ export default ({ className = "", data = [], backgroundColor }) => {
|
||||
// 将详细信息放在details对象中,以匹配Modal的期望格式
|
||||
details: {
|
||||
description: item.description || "",
|
||||
requirements: item.requirements ?
|
||||
(typeof item.requirements === 'string' ?
|
||||
item.requirements.split(/\d+\.\s*/).filter(r => r.trim()) :
|
||||
requirements: item.requirements ?
|
||||
(typeof item.requirements === 'string' ?
|
||||
item.requirements.split(/\d+\.\s*/).filter(r => r.trim()) :
|
||||
item.requirements) : [],
|
||||
requirementsText: typeof item.requirements === 'string' ? item.requirements : "",
|
||||
companyInfo: item.companyInfo || ""
|
||||
companyInfo: item.companyInfo || "",
|
||||
companyImages: item.companyImages || []
|
||||
}
|
||||
});
|
||||
setDirectToResume(false);
|
||||
|
||||
@@ -197,7 +197,7 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
||||
education: [{
|
||||
school: '苏州信息职业技术学院',
|
||||
major: '旅游管理',
|
||||
period: '2020.9-2023.6'
|
||||
period: '2018.9-2021.6'
|
||||
}],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] },
|
||||
@@ -358,7 +358,7 @@ export default ({ visible, onClose, data, initialVersion = "2" }) => {
|
||||
education: [{
|
||||
school: studentInfo.education?.university || '苏州信息职业技术学院',
|
||||
major: '旅游管理',
|
||||
period: studentInfo.education?.period || '2020.9-2023.6'
|
||||
period: studentInfo.education?.period || '2018.9-2021.6'
|
||||
}],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] },
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/* 继承原有样式 */
|
||||
@import "../ResumeInfoModal/index.css";
|
||||
|
||||
/* 增强版头部样式 */
|
||||
.enhanced-resume-modal .enhanced-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 编辑区域样式 */
|
||||
.enhanced-resume-modal .edit-area {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .edit-textarea {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
padding: 15px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
resize: vertical;
|
||||
transition: border-color 0.3s;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .edit-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4096ff;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 150, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 版本管理器样式 */
|
||||
.version-manager {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
background-color: #fff;
|
||||
border-color: #4096ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.version-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.version-manager .empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 项目和技能展示样式优化 */
|
||||
.enhanced-resume-modal .project-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .project-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .project-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .project-company {
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .project-description {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .separator {
|
||||
color: #d9d9d9;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 技能部分样式 */
|
||||
.enhanced-resume-modal .skills-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .sub-tag {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .skills-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .skill-item {
|
||||
padding: 8px 0;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .skill-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 禁用状态的Radio Group */
|
||||
.enhanced-resume-modal .arco-radio-group[disabled] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 编辑模式提示 */
|
||||
.enhanced-resume-modal.editing-mode .resume-info-modal-title {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal.editing-mode .resume-info-modal-title::after {
|
||||
content: "(编辑中)";
|
||||
color: #52c41a;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.enhanced-resume-modal .enhanced-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .arco-radio-group {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .version-item {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 输入框样式优化 */
|
||||
.enhanced-resume-modal .arco-input-wrapper {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 按钮组样式优化 */
|
||||
.enhanced-resume-modal .arco-btn-size-small {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* 提示信息样式 */
|
||||
.enhanced-resume-modal .edit-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 课程列表样式 */
|
||||
.enhanced-resume-modal .course-units-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.enhanced-resume-modal .course-units-list-item {
|
||||
padding: 6px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 信息列表项样式 */
|
||||
.enhanced-resume-modal .info-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Radio, Button, Input, Modal as ArcoModal, Message, Popconfirm } from "@arco-design/web-react";
|
||||
import { IconEdit, IconSave, IconDelete, IconClose } from "@arco-design/web-react/icon";
|
||||
import Modal from "@/components/Modal";
|
||||
import "./index.css";
|
||||
|
||||
const ResumeInfoModalEnhanced = ({ visible, onClose, data, initialVersion = "1", onSave }) => {
|
||||
const [version, setVersion] = useState(initialVersion);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editedContent, setEditedContent] = useState("");
|
||||
const [userVersions, setUserVersions] = useState({});
|
||||
const [showVersionManager, setShowVersionManager] = useState(false);
|
||||
const [newVersionName, setNewVersionName] = useState("");
|
||||
const [selectedUserVersion, setSelectedUserVersion] = useState(null);
|
||||
const editAreaRef = useRef(null);
|
||||
|
||||
// 从localStorage加载用户版本
|
||||
useEffect(() => {
|
||||
if (data?.position || data?.title) {
|
||||
const positionKey = data?.position || data?.title;
|
||||
const savedVersions = localStorage.getItem(`resume_versions_${positionKey}`);
|
||||
if (savedVersions) {
|
||||
setUserVersions(JSON.parse(savedVersions));
|
||||
}
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// 响应initialVersion变化
|
||||
useEffect(() => {
|
||||
setVersion(initialVersion);
|
||||
}, [initialVersion]);
|
||||
|
||||
// Markdown解析器 - 解析简历内容
|
||||
const parseResumeMarkdown = (markdownContent) => {
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
personalInfo: { name: "岗位名称" },
|
||||
education: [{
|
||||
school: '苏州信息职业技术学院',
|
||||
major: '旅游管理',
|
||||
period: '2018.9-2021.6'
|
||||
}],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] },
|
||||
personalSummary: "",
|
||||
courses: []
|
||||
};
|
||||
|
||||
// 提取岗位名称
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 获取当前显示的内容
|
||||
const getCurrentContent = () => {
|
||||
if (selectedUserVersion) {
|
||||
return userVersions[selectedUserVersion]?.content || "";
|
||||
}
|
||||
|
||||
if (version === "1" && data?.content?.original) {
|
||||
return data.content.original;
|
||||
} else if (version === "2" && data?.content?.modified) {
|
||||
return data.content.modified;
|
||||
} else if (version.startsWith("user_") && userVersions[version]) {
|
||||
return userVersions[version].content;
|
||||
}
|
||||
|
||||
return data?.content?.original || "";
|
||||
};
|
||||
|
||||
// 获取简历数据
|
||||
let resumeContent = {};
|
||||
|
||||
if (data?.content) {
|
||||
if (data.content.original) {
|
||||
const hasModified = !!data.content.modified;
|
||||
const selectedContent = (!hasModified || version === "1") ? data.content.original : data.content.modified;
|
||||
|
||||
// 如果正在编辑,使用编辑内容
|
||||
const contentToUse = isEditing ? editedContent :
|
||||
(selectedUserVersion ? userVersions[selectedUserVersion]?.content : selectedContent);
|
||||
|
||||
if (typeof contentToUse === 'string') {
|
||||
resumeContent = parseResumeMarkdown(contentToUse);
|
||||
} else if (contentToUse?.personalInfo) {
|
||||
resumeContent = contentToUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始编辑
|
||||
const handleStartEdit = () => {
|
||||
const currentContent = getCurrentContent();
|
||||
setEditedContent(currentContent);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent("");
|
||||
setNewVersionName("");
|
||||
};
|
||||
|
||||
// 保存编辑内容
|
||||
const handleSaveEdit = () => {
|
||||
if (!newVersionName.trim()) {
|
||||
Message.error("请输入版本名称");
|
||||
return;
|
||||
}
|
||||
|
||||
const positionKey = data?.position || data?.title || "unknown";
|
||||
const versionId = `user_${Date.now()}`;
|
||||
const newVersion = {
|
||||
id: versionId,
|
||||
name: newVersionName,
|
||||
content: editedContent,
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedVersions = {
|
||||
...userVersions,
|
||||
[versionId]: newVersion
|
||||
};
|
||||
|
||||
setUserVersions(updatedVersions);
|
||||
localStorage.setItem(`resume_versions_${positionKey}`, JSON.stringify(updatedVersions));
|
||||
|
||||
setIsEditing(false);
|
||||
setVersion(versionId);
|
||||
setSelectedUserVersion(versionId);
|
||||
setNewVersionName("");
|
||||
setEditedContent("");
|
||||
Message.success("保存成功");
|
||||
|
||||
// 通知父组件
|
||||
if (onSave) {
|
||||
onSave(versionId, newVersion);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除版本
|
||||
const handleDeleteVersion = (versionId) => {
|
||||
const positionKey = data?.position || data?.title || "unknown";
|
||||
const updatedVersions = { ...userVersions };
|
||||
delete updatedVersions[versionId];
|
||||
|
||||
setUserVersions(updatedVersions);
|
||||
localStorage.setItem(`resume_versions_${positionKey}`, JSON.stringify(updatedVersions));
|
||||
|
||||
if (version === versionId) {
|
||||
setVersion("1");
|
||||
}
|
||||
if (selectedUserVersion === versionId) {
|
||||
setSelectedUserVersion(null);
|
||||
}
|
||||
|
||||
Message.success("删除成功");
|
||||
};
|
||||
|
||||
const onRadioChange = (value, e) => {
|
||||
e?.stopPropagation();
|
||||
setVersion(value);
|
||||
if (value.startsWith("user_")) {
|
||||
setSelectedUserVersion(value);
|
||||
} else {
|
||||
setSelectedUserVersion(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isEditing) {
|
||||
ArcoModal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '您有未保存的修改,确定要关闭吗?',
|
||||
onOk: () => {
|
||||
setIsEditing(false);
|
||||
setEditedContent("");
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取显示内容
|
||||
const displayContent = isEditing ? parseResumeMarkdown(editedContent) : resumeContent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="resume-info-modal enhanced-resume-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<i className="close-icon" onClick={handleCloseModal} />
|
||||
|
||||
{/* 版本选择和操作按钮 */}
|
||||
{data?.content?.original && (
|
||||
<div className="resume-info-modal-header enhanced-header">
|
||||
<Radio.Group
|
||||
type="button"
|
||||
name="position"
|
||||
className="resume-info-modal-radio-group"
|
||||
value={selectedUserVersion || version}
|
||||
onChange={onRadioChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<Radio value="1">原始版</Radio>
|
||||
{data?.content?.modified && (
|
||||
<Radio value="2">个人修改版</Radio>
|
||||
)}
|
||||
{Object.values(userVersions).map(v => (
|
||||
<Radio key={v.id} value={v.id}>{v.name}</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<div className="action-buttons">
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconEdit />}
|
||||
onClick={handleStartEdit}
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{Object.keys(userVersions).length > 0 && (
|
||||
<Button
|
||||
onClick={() => setShowVersionManager(true)}
|
||||
size="small"
|
||||
>
|
||||
管理版本
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="版本名称"
|
||||
value={newVersionName}
|
||||
onChange={setNewVersionName}
|
||||
style={{ width: 120 }}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconSave />}
|
||||
onClick={handleSaveEdit}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="resume-info-modal-title">
|
||||
{data?.title || displayContent.personalInfo?.name || "职位名称"}
|
||||
</p>
|
||||
|
||||
{/* 编辑区域 */}
|
||||
{isEditing ? (
|
||||
<div className="edit-area">
|
||||
<textarea
|
||||
ref={editAreaRef}
|
||||
className="edit-textarea"
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder="在此编辑简历内容..."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* 原有的简历内容展示 */
|
||||
<ul className="resume-info-moda-list">
|
||||
{/* 教育经历 */}
|
||||
{displayContent.education?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">教育经历</div>
|
||||
{displayContent.education.map((edu, index) => (
|
||||
<li key={index} className="info-list-item">
|
||||
<span>{edu.school}</span>
|
||||
<span className="separator">|</span>
|
||||
<span>{edu.major}</span>
|
||||
<span className="separator">|</span>
|
||||
<span>{edu.period}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 项目经历 */}
|
||||
{displayContent.projects?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">项目经历</div>
|
||||
{displayContent.projects.map((project, index) => (
|
||||
<li key={index} className="project-item">
|
||||
<div className="project-header">
|
||||
<span className="project-name">{project.name}</span>
|
||||
<span className="separator">|</span>
|
||||
<span>{project.role}</span>
|
||||
<span className="separator">|</span>
|
||||
<span>{project.period}</span>
|
||||
</div>
|
||||
<div className="project-company">{project.company}</div>
|
||||
<div className="project-description">{project.description}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 专业技能 */}
|
||||
{(displayContent.skills?.core?.length > 0 || displayContent.skills?.additional?.length > 0) && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">专业技能</div>
|
||||
|
||||
{/* 核心能力 */}
|
||||
{displayContent.skills?.core?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">核心能力</div>
|
||||
<ul className="skills-list">
|
||||
{displayContent.skills.core.map((skill, index) => (
|
||||
<li key={index} className="skill-item">{skill}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 复合能力 */}
|
||||
{displayContent.skills?.additional?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">复合能力</div>
|
||||
<ul className="skills-list">
|
||||
{displayContent.skills.additional.map((skill, index) => (
|
||||
<li key={index} className="skill-item">{skill}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 相关课程 */}
|
||||
{displayContent.courses?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">相关课程</div>
|
||||
<ul className="course-units-list">
|
||||
{displayContent.courses.map((course, index) => (
|
||||
<li key={index} className="course-units-list-item">
|
||||
{course}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 版本管理弹窗 */}
|
||||
<ArcoModal
|
||||
title="版本管理"
|
||||
visible={showVersionManager}
|
||||
onCancel={() => setShowVersionManager(false)}
|
||||
footer={null}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<div className="version-manager">
|
||||
<div className="version-list">
|
||||
{Object.values(userVersions).length === 0 ? (
|
||||
<div className="empty">暂无自定义版本</div>
|
||||
) : (
|
||||
Object.values(userVersions).map(v => (
|
||||
<div key={v.id} className="version-item">
|
||||
<div className="version-info">
|
||||
<div className="version-name">{v.name}</div>
|
||||
<div className="version-time">
|
||||
创建时间:{new Date(v.createTime).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="version-actions">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVersion(v.id);
|
||||
setSelectedUserVersion(v.id);
|
||||
setShowVersionManager(false);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该版本吗?"
|
||||
onOk={() => handleDeleteVersion(v.id)}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ArcoModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumeInfoModalEnhanced;
|
||||
@@ -0,0 +1,256 @@
|
||||
/* 继承原有样式 */
|
||||
@import "../ResumeInfoModal/index.css";
|
||||
|
||||
/* 编辑模式相关样式 */
|
||||
.resume-info-modal.with-edit .resume-info-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* contentEditable 编辑样式 - 虚线边框 */
|
||||
.resume-info-modal.with-edit .editable-field {
|
||||
border: 1px dashed #d9d9d9 !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: text !important;
|
||||
min-height: 20px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .editable-field:hover {
|
||||
border-color: #40a9ff !important;
|
||||
background-color: #f0f8ff !important;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .editable-field:focus {
|
||||
outline: none !important;
|
||||
border-color: #1890ff !important;
|
||||
background-color: #fff !important;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 保持原有布局不变 */
|
||||
.resume-info-modal.with-edit .info-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .separator {
|
||||
color: #d9d9d9;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 项目经历样式 */
|
||||
.resume-info-modal.with-edit .project-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .project-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .project-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .project-company {
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .project-description {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 技能部分样式 */
|
||||
.resume-info-modal.with-edit .skills-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .sub-tag {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .skills-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .skill-item {
|
||||
padding: 8px 0;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .skill-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 版本管理器样式 */
|
||||
.version-manager {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.version-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e5e5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
background-color: #fff;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.version-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.version-manager .empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 禁用状态的Radio Group */
|
||||
.resume-info-modal.with-edit .arco-radio-group[disabled] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 编辑模式提示 */
|
||||
.resume-info-modal.with-edit.editing-mode::after {
|
||||
content: "编辑模式";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 50px;
|
||||
background-color: #52c41a;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.resume-info-modal.with-edit .resume-info-modal-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .arco-radio-group {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.resume-info-modal.with-edit .editable-field.saved {
|
||||
animation: highlight 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* 版本标签样式 */
|
||||
.resume-info-modal.with-edit .arco-radio-button {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 输入框样式优化 */
|
||||
.resume-info-modal.with-edit .arco-input-wrapper {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 按钮样式优化 */
|
||||
.resume-info-modal.with-edit .arco-btn-size-small {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* 编辑时的项目描述多行文本样式 */
|
||||
.resume-info-modal.with-edit .project-description.editable-field {
|
||||
min-height: 60px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Radio, Button, Input, Message, Popconfirm, Modal as ArcoModal } from "@arco-design/web-react";
|
||||
import { IconEdit, IconSave, IconClose, IconDelete } 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([]);
|
||||
const [showVersionManager, setShowVersionManager] = useState(false);
|
||||
const [newVersionName, setNewVersionName] = useState("");
|
||||
|
||||
// 响应initialVersion变化
|
||||
useEffect(() => {
|
||||
setVersion(initialVersion);
|
||||
}, [initialVersion]);
|
||||
|
||||
// 加载个人修改版本
|
||||
useEffect(() => {
|
||||
if (visible && data) {
|
||||
const positionTitle = data?.title || data?.position || '未知岗位';
|
||||
const versions = resumeManager.getVersionsByPosition(positionTitle);
|
||||
setCustomVersions(versions);
|
||||
}
|
||||
}, [visible, data]);
|
||||
|
||||
const onRadioChange = (value, e) => {
|
||||
e?.stopPropagation();
|
||||
setVersion(value);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
if (isEditing) {
|
||||
ArcoModal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '您有未保存的修改,确定要关闭吗?',
|
||||
onOk: () => {
|
||||
setIsEditing(false);
|
||||
setEditableData(null);
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Markdown解析器 - 解析简历内容
|
||||
const parseResumeMarkdown = (markdownContent) => {
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
personalInfo: { name: "岗位名称" },
|
||||
education: [{
|
||||
school: '苏州信息职业技术学院',
|
||||
major: '旅游管理',
|
||||
period: '2018.9-2021.6'
|
||||
}],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] },
|
||||
personalSummary: "",
|
||||
courses: []
|
||||
};
|
||||
|
||||
// 提取岗位名称
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 将结构化数据转换回markdown格式
|
||||
const convertToMarkdown = (data) => {
|
||||
if (!data) return '';
|
||||
|
||||
let markdown = `# 对应岗位:${data.personalInfo?.name || '未命名岗位'}\n\n`;
|
||||
|
||||
// 项目经历
|
||||
if (data.projects && data.projects.length > 0) {
|
||||
markdown += '# 一、项目经历\n\n';
|
||||
data.projects.forEach((project, index) => {
|
||||
if (index === 0) {
|
||||
markdown += `### (一)项目名称:${project.name}\n\n`;
|
||||
markdown += `### (二)实习岗位:${project.role}\n\n`;
|
||||
markdown += `### (三)实习时间:${project.period}\n\n`;
|
||||
markdown += `### (四)实习单位:${project.company}\n\n`;
|
||||
markdown += `### (五)岗位职责:\n\n${project.description}\n\n`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 专业技能
|
||||
markdown += '# 二、专业技能\n\n';
|
||||
|
||||
if (data.skills?.core && data.skills.core.length > 0) {
|
||||
markdown += '### (一)核心能力\n\n';
|
||||
data.skills.core.forEach((skill, index) => {
|
||||
markdown += `${index + 1}. ${skill};\n`;
|
||||
});
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
if (data.skills?.additional && data.skills.additional.length > 0) {
|
||||
markdown += '### (二)复合能力\n\n';
|
||||
data.skills.additional.forEach((skill, index) => {
|
||||
markdown += `${index + 1}. ${skill}。\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
// 获取当前版本的内容
|
||||
const getCurrentContent = () => {
|
||||
if (version.startsWith('custom_')) {
|
||||
const customVersion = customVersions.find(v => `custom_${v.id}` === version);
|
||||
return customVersion?.content || '';
|
||||
}
|
||||
|
||||
if (data?.content) {
|
||||
if (data.content.original) {
|
||||
const hasModified = !!data.content.modified;
|
||||
return (!hasModified || version === "1") ? data.content.original : data.content.modified;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 获取简历数据
|
||||
let resumeContent = {};
|
||||
const currentContent = getCurrentContent();
|
||||
|
||||
if (currentContent) {
|
||||
if (typeof currentContent === 'string') {
|
||||
resumeContent = parseResumeMarkdown(currentContent);
|
||||
} else if (currentContent.personalInfo) {
|
||||
resumeContent = currentContent;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果正在编辑,使用编辑数据
|
||||
if (isEditing && editableData) {
|
||||
resumeContent = editableData;
|
||||
}
|
||||
|
||||
// 数据校验
|
||||
const isValidData = resumeContent && Object.keys(resumeContent).length > 0 && resumeContent.personalInfo;
|
||||
|
||||
if (!isValidData) {
|
||||
resumeContent = {
|
||||
personalInfo: { name: '数据加载中...' },
|
||||
education: [],
|
||||
projects: [],
|
||||
skills: { core: [], additional: [] }
|
||||
};
|
||||
}
|
||||
|
||||
// 开始编辑
|
||||
const handleEditClick = () => {
|
||||
const currentContent = getCurrentContent();
|
||||
const parsed = parseResumeMarkdown(currentContent);
|
||||
setEditableData(parsed || resumeContent);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setEditableData(null);
|
||||
setNewVersionName("");
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const handleSaveEdit = async () => {
|
||||
if (!newVersionName.trim()) {
|
||||
Message.error('请输入版本名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const positionTitle = data?.title || data?.position || '未知岗位';
|
||||
|
||||
// 检查版本名称是否重复
|
||||
if (resumeManager.isVersionNameExists(newVersionName, positionTitle)) {
|
||||
Message.error('版本名称已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 将编辑后的数据转换回markdown格式
|
||||
const contentToSave = convertToMarkdown(editableData);
|
||||
|
||||
const result = await resumeManager.createCustomVersion({
|
||||
name: newVersionName,
|
||||
content: contentToSave,
|
||||
positionTitle: positionTitle
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
Message.success('简历已保存为个人修改版');
|
||||
setIsEditing(false);
|
||||
setEditableData(null);
|
||||
setNewVersionName("");
|
||||
|
||||
// 刷新版本列表
|
||||
const versions = resumeManager.getVersionsByPosition(positionTitle);
|
||||
setCustomVersions(versions);
|
||||
|
||||
// 切换到新保存的版本
|
||||
setVersion('custom_' + result.data.id);
|
||||
} else {
|
||||
Message.error(result.error || '保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除版本
|
||||
const handleDeleteVersion = (versionId) => {
|
||||
const result = resumeManager.deleteCustomVersion(versionId);
|
||||
if (result.success) {
|
||||
Message.success('版本已删除');
|
||||
const positionTitle = data?.title || data?.position || '未知岗位';
|
||||
const versions = resumeManager.getVersionsByPosition(positionTitle);
|
||||
setCustomVersions(versions);
|
||||
|
||||
// 如果删除的是当前版本,切换到原始版
|
||||
if (version === `custom_${versionId}`) {
|
||||
setVersion("1");
|
||||
}
|
||||
} else {
|
||||
Message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="resume-info-modal with-edit" onClick={(e) => e.stopPropagation()}>
|
||||
<i className="close-icon" onClick={handleCloseModal} />
|
||||
|
||||
{/* 版本选择和编辑按钮 */}
|
||||
{data?.content?.original && (
|
||||
<div className="resume-info-modal-header">
|
||||
<Radio.Group
|
||||
type="button"
|
||||
name="position"
|
||||
className="resume-info-modal-radio-group"
|
||||
value={version}
|
||||
onChange={onRadioChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<Radio value="1">原始版</Radio>
|
||||
{data?.content?.modified && (
|
||||
<Radio value="2">个人修改版</Radio>
|
||||
)}
|
||||
{customVersions.map(v => (
|
||||
<Radio key={v.id} value={`custom_${v.id}`}>
|
||||
{v.name}
|
||||
</Radio>
|
||||
))}
|
||||
</Radio.Group>
|
||||
|
||||
<div className="action-buttons">
|
||||
{!isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconEdit />}
|
||||
onClick={handleEditClick}
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
{customVersions.length > 0 && (
|
||||
<Button
|
||||
onClick={() => setShowVersionManager(true)}
|
||||
size="small"
|
||||
>
|
||||
管理版本
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="版本名称"
|
||||
value={newVersionName}
|
||||
onChange={setNewVersionName}
|
||||
style={{ width: 150 }}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconSave />}
|
||||
onClick={handleSaveEdit}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconClose />}
|
||||
onClick={handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="resume-info-modal-title">
|
||||
{data?.title || resumeContent.personalInfo?.name || "职位名称"}
|
||||
</p>
|
||||
|
||||
{/* 统一使用结构化样式展示所有岗位 */}
|
||||
<ul className="resume-info-moda-list">
|
||||
{/* 教育经历 */}
|
||||
{resumeContent.education?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">教育经历</div>
|
||||
{resumeContent.education.map((edu, index) => (
|
||||
<li key={index} className="info-list-item">
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newEducation = [...editableData.education];
|
||||
newEducation[index] = { ...newEducation[index], school: e.target.innerText };
|
||||
setEditableData({ ...editableData, education: newEducation });
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable-field" : ""}
|
||||
>
|
||||
{edu.school}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newEducation = [...editableData.education];
|
||||
newEducation[index] = { ...newEducation[index], major: e.target.innerText };
|
||||
setEditableData({ ...editableData, education: newEducation });
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable-field" : ""}
|
||||
>
|
||||
{edu.major}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable-field" : ""}
|
||||
>
|
||||
{edu.period}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 项目经历 */}
|
||||
{resumeContent.projects?.length > 0 && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">项目经历</div>
|
||||
{resumeContent.projects.map((project, index) => (
|
||||
<li key={index} className="project-item">
|
||||
<div className="project-header">
|
||||
<span
|
||||
className={`project-name ${isEditing ? "editable-field" : ""}`}
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newProjects = [...editableData.projects];
|
||||
newProjects[index] = { ...newProjects[index], role: e.target.innerText };
|
||||
setEditableData({ ...editableData, projects: newProjects });
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable-field" : ""}
|
||||
>
|
||||
{project.role}
|
||||
</span>
|
||||
<span className="separator">|</span>
|
||||
<span
|
||||
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 });
|
||||
}
|
||||
}}
|
||||
className={isEditing ? "editable-field" : ""}
|
||||
>
|
||||
{project.period}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`project-company ${isEditing ? "editable-field" : ""}`}
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newProjects = [...editableData.projects];
|
||||
newProjects[index] = { ...newProjects[index], company: e.target.innerText };
|
||||
setEditableData({ ...editableData, projects: newProjects });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{project.company}
|
||||
</div>
|
||||
<div
|
||||
className={`project-description ${isEditing ? "editable-field" : ""}`}
|
||||
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}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* 专业技能 */}
|
||||
{(resumeContent.skills?.core?.length > 0 || resumeContent.skills?.additional?.length > 0) && (
|
||||
<li className="resume-info-moda-list-li">
|
||||
<ul className="resume-info-moda-item">
|
||||
<div className="tag">专业技能</div>
|
||||
|
||||
{/* 核心能力 */}
|
||||
{resumeContent.skills?.core?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">核心能力</div>
|
||||
<ul className="skills-list">
|
||||
{resumeContent.skills.core.map((skill, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`skill-item ${isEditing ? "editable-field" : ""}`}
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newSkills = [...editableData.skills.core];
|
||||
newSkills[index] = e.target.innerText;
|
||||
setEditableData({
|
||||
...editableData,
|
||||
skills: { ...editableData.skills, core: newSkills }
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{skill}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 复合能力 */}
|
||||
{resumeContent.skills?.additional?.length > 0 && (
|
||||
<div className="skills-section">
|
||||
<div className="sub-tag">复合能力</div>
|
||||
<ul className="skills-list">
|
||||
{resumeContent.skills.additional.map((skill, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`skill-item ${isEditing ? "editable-field" : ""}`}
|
||||
contentEditable={isEditing}
|
||||
suppressContentEditableWarning={true}
|
||||
onBlur={(e) => {
|
||||
if (isEditing && editableData) {
|
||||
const newSkills = [...editableData.skills.additional];
|
||||
newSkills[index] = e.target.innerText;
|
||||
setEditableData({
|
||||
...editableData,
|
||||
skills: { ...editableData.skills, additional: newSkills }
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{skill}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 版本管理弹窗 */}
|
||||
<ArcoModal
|
||||
title="版本管理"
|
||||
visible={showVersionManager}
|
||||
onCancel={() => setShowVersionManager(false)}
|
||||
footer={null}
|
||||
style={{ width: 600 }}
|
||||
>
|
||||
<div className="version-manager">
|
||||
{customVersions.length === 0 ? (
|
||||
<div className="empty">暂无自定义版本</div>
|
||||
) : (
|
||||
<div className="version-list">
|
||||
{customVersions.map(v => (
|
||||
<div key={v.id} className="version-item">
|
||||
<div className="version-info">
|
||||
<div className="version-name">{v.name}</div>
|
||||
<div className="version-time">
|
||||
创建时间:{new Date(v.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="version-actions">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setVersion(`custom_${v.id}`);
|
||||
setShowVersionManager(false);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该版本吗?"
|
||||
onOk={() => handleDeleteVersion(v.id)}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
status="danger"
|
||||
icon={<IconDelete />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ArcoModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,20 @@
|
||||
.company-jobs-page-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.company-jobs-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
|
||||
.company-jobs-page-spin {
|
||||
margin: 200px 500px;
|
||||
@@ -42,7 +47,9 @@
|
||||
|
||||
.company-jobs-page-left {
|
||||
width: 570px;
|
||||
height: 860px;
|
||||
height: 100%;
|
||||
min-height: 860px;
|
||||
max-height: calc(100vh - 100px);
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
@@ -97,17 +104,20 @@
|
||||
|
||||
.company-jobs-page-left-list-wrapper {
|
||||
width: 100%;
|
||||
height: 760px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.company-jobs-page-interview-wrapper {
|
||||
width: 572px;
|
||||
height: 860px;
|
||||
height: 100%;
|
||||
min-height: 860px;
|
||||
max-height: calc(100vh - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
@@ -118,18 +128,20 @@
|
||||
|
||||
.company-jobs-page-interview {
|
||||
width: 100%;
|
||||
height: 860px;
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.company-jobs-page-interview-list {
|
||||
width: 540px;
|
||||
height: 760px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -85,7 +85,8 @@ const CompanyJobsPage = () => {
|
||||
requirements: jobData.requirements || "",
|
||||
description: jobData.description || "",
|
||||
welfare: jobData.welfare || [],
|
||||
companyInfo: jobData.companyInfo || ""
|
||||
companyInfo: jobData.companyInfo || "",
|
||||
companyImages: jobData.companyImages || []
|
||||
};
|
||||
}).filter(job => job.position); // 过滤掉没有岗位信息的项
|
||||
|
||||
@@ -346,6 +347,7 @@ const CompanyJobsPage = () => {
|
||||
<InterviewStatusAnimation
|
||||
statusText={item.statusText}
|
||||
isOpen={expandedItemId === item.id}
|
||||
stageDate={item.stageDate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -229,6 +229,13 @@
|
||||
object-position: center 0%;
|
||||
}
|
||||
}
|
||||
|
||||
&.teacher-赵雪宁 {
|
||||
.arco-avatar-image img {
|
||||
object-fit: cover;
|
||||
object-position: center 2%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-tasks-item-info-teacher-name {
|
||||
position: absolute;
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
}
|
||||
|
||||
.ChatApp {
|
||||
.Avatar img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
object-position: center 20%; /* 👈 控制上下位置,30% 表示往下移 */
|
||||
border-radius: 50px;
|
||||
}
|
||||
.user-avatar-wrapper {
|
||||
height: 22px;
|
||||
display: flex;
|
||||
|
||||
@@ -46,9 +46,8 @@ const init = [
|
||||
|
||||
const MyIM = React.forwardRef((props, ref) => {
|
||||
const { hanldeClickOpenModalBtn, initialMessages } = props;
|
||||
const [currentMessages, setCurrentMessages] = useState(init);
|
||||
// 使用ChatUI的消息管理hook
|
||||
const { messages, appendMsg, setMessages } = useMessages(currentMessages);
|
||||
const { messages, appendMsg, setTyping, resetList } = useMessages(init);
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
const id = useRef(undefined);
|
||||
const childRef = useRef();
|
||||
@@ -69,55 +68,63 @@ const MyIM = React.forwardRef((props, ref) => {
|
||||
// 处理对话内容变化
|
||||
useEffect(() => {
|
||||
if (initialMessages && initialMessages.length > 0) {
|
||||
// 构建新的消息数组
|
||||
const newMessages = [];
|
||||
// 先清空现有消息
|
||||
resetList();
|
||||
|
||||
// 逐个添加新消息
|
||||
initialMessages.forEach((msg) => {
|
||||
if (msg.type === "user") {
|
||||
newMessages.push({
|
||||
const userAvatar = (
|
||||
<div className="user-avatar-wrapper">
|
||||
<span className="user-avatar-time">
|
||||
{msg.time || dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
</span>
|
||||
<span className="user-avatar-name">{studentInfo?.realName || "学生"}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
appendMsg({
|
||||
type: "text",
|
||||
content: { text: msg.content },
|
||||
position: "right",
|
||||
user: {
|
||||
avatar: studentInfo?.avatar,
|
||||
name: userAvatarDom,
|
||||
name: userAvatar,
|
||||
},
|
||||
});
|
||||
} else if (msg.type === "assistant") {
|
||||
const mentorName = msg.mentor ? `${msg.mentor}` : "专家";
|
||||
const mentorName = msg.mentor ? msg.mentor : "专家";
|
||||
const isRobot = msg.mentor === "多多畅职机器人";
|
||||
const assistantAvatarDom = (
|
||||
<div className="user-avatar-wrapper">
|
||||
<span className="user-avatar-name">{mentorName}</span>
|
||||
<div className="user-avatar-tag">专家</div>
|
||||
<div className="user-avatar-tag">{isRobot ? "Agent" : "专家"}</div>
|
||||
<span className="user-avatar-time">
|
||||
{dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
{msg.time || dayjs().format("YYYY-MM-DD HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
newMessages.push({
|
||||
appendMsg({
|
||||
type: "text",
|
||||
content: { text: msg.content },
|
||||
position: "left",
|
||||
user: {
|
||||
avatar: ICONURL,
|
||||
avatar: msg.mentorAvatar || ICONURL,
|
||||
name: assistantAvatarDom,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 设置新消息
|
||||
if (setMessages) {
|
||||
setMessages(newMessages);
|
||||
} else {
|
||||
// 如果setMessages不存在,重新创建组件实例
|
||||
setCurrentMessages(newMessages);
|
||||
}
|
||||
|
||||
setIsInit(false);
|
||||
} else if (!initialMessages) {
|
||||
// 如果没有初始消息,重置为默认消息
|
||||
resetList();
|
||||
init.forEach(msg => appendMsg(msg));
|
||||
setIsInit(true);
|
||||
}
|
||||
}, [initialMessages, studentInfo, userAvatarDom]);
|
||||
}, [initialMessages, resetList, appendMsg, studentInfo]);
|
||||
|
||||
// 处理发送消息
|
||||
const handleSend = async (type, val, showBtn = false) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import SupportList from "./components/SupportList";
|
||||
import MyIM from "./components/MyIM";
|
||||
import FormModal from "./components/FormModal";
|
||||
import expertSupportData from "@/data/expertSupportData";
|
||||
import "./index.css";
|
||||
|
||||
const titleList = [
|
||||
@@ -25,6 +26,14 @@ const ExpertSupportPage = () => {
|
||||
const [formVisible, setFormVisible] = useState(false);
|
||||
const [initialMessages, setInitialMessages] = useState(null); // 设置消息
|
||||
const [selectedConversation, setSelectedConversation] = useState(null);
|
||||
|
||||
// 页面加载时默认选中第一个对话
|
||||
useEffect(() => {
|
||||
if (expertSupportData?.conversations && expertSupportData.conversations.length > 0) {
|
||||
const firstConversation = expertSupportData.conversations[0];
|
||||
handleSelectConversation(firstConversation);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setFormVisible(false);
|
||||
|
||||
@@ -131,6 +131,7 @@ const HomeworkPage = () => {
|
||||
src="https://du9uay.github.io/zhanhui/#/course-test"
|
||||
className="homework-page-iframe-content"
|
||||
title="展会策划教学"
|
||||
style={{ zoom: 0.8 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ export default ({ locked = false }) => {
|
||||
"民宿客房管家",
|
||||
"民宿运营专员",
|
||||
"品牌公关",
|
||||
"IP运营总监助理",
|
||||
"ip运营总监助理",
|
||||
"品牌公关管培生",
|
||||
"直播中控",
|
||||
"SEO专员",
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Modal from "@/components/Modal";
|
||||
import PDFICON from "@/assets/images/Common/pdf_icon.png";
|
||||
import FileIcon from "@/components/FileIcon";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import ImagePreviewModal from "../ImagePreviewModal";
|
||||
import "./index.css";
|
||||
|
||||
export default ({ visible, onClose, data }) => {
|
||||
const navigate = useNavigate();
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewIndex, setPreviewIndex] = useState(0);
|
||||
|
||||
const handleCloseModal = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 处理岗位点击,跳转到简历与面试题页面
|
||||
const handlePositionClick = (positionName) => {
|
||||
// 关闭当前模态框
|
||||
onClose();
|
||||
// 跳转到简历与面试题页面,并传递岗位名称参数
|
||||
navigate('/resume-interview', { state: { selectedPosition: positionName } });
|
||||
};
|
||||
|
||||
// 处理图片点击预览
|
||||
const handleImageClick = (index) => {
|
||||
setPreviewIndex(index);
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
// 将换行符转换为Markdown格式
|
||||
const formatMarkdownContent = (content) => {
|
||||
if (!content) return "";
|
||||
// 将 \\n 替换为实际的换行符
|
||||
return content.replace(/\\n/g, '\n');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={visible} onClose={handleCloseModal}>
|
||||
<div className="project-cases-modal">
|
||||
<i className="close-icon" onClick={handleCloseModal} />
|
||||
{data?.category && (
|
||||
<span className="project-cases-modal-category-tag">{data.category}</span>
|
||||
)}
|
||||
<p className="project-cases-modal-title">{data?.title}</p>
|
||||
<ul className="project-cases-modal-list">
|
||||
{/* 项目概述 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">项目概述</p>
|
||||
<p className="project-cases-modal-item-text">
|
||||
{data?.description || data?.overview || "暂无项目概述"}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
{/* 如果有sections数据结构,优先使用sections */}
|
||||
{data?.sections ? (
|
||||
<>
|
||||
{data.sections.map((section, index) => (
|
||||
<li key={index} className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">{section.title}</p>
|
||||
<div className="project-cases-modal-markdown-content">
|
||||
<ReactMarkdown>{formatMarkdownContent(section.content)}</ReactMarkdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{/* 项目图片展示 */}
|
||||
{data?.images && data.images.length > 0 && (
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">项目效果图</p>
|
||||
<div className="project-cases-modal-images">
|
||||
{data.images.map((image, index) => {
|
||||
const imageUrl = typeof image === 'string' ? image : image.url;
|
||||
const imageTitle = typeof image === 'string' ? `图 ${index + 1}` : image.title;
|
||||
|
||||
return (
|
||||
<div key={index} className="project-cases-modal-image-wrapper">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={imageTitle}
|
||||
className="project-cases-modal-image"
|
||||
onClick={() => handleImageClick(index)}
|
||||
/>
|
||||
<span className="project-cases-modal-image-title">
|
||||
{imageTitle}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 原有的数据结构保持不变 */}
|
||||
{/* 适用岗位 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">适用岗位</p>
|
||||
<ul className="project-cases-modal-horizontal-list">
|
||||
{data?.applicablePositions?.map((pos, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="high-count-list-item"
|
||||
onClick={() => handlePositionClick(pos.position)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span className={pos.level === '普通岗' ? 'low' : pos.level === '技术骨干岗' ? 'medium' : 'high'}>
|
||||
{pos.level}
|
||||
</span>
|
||||
<p>{pos.position}</p>
|
||||
</li>
|
||||
)) || (
|
||||
<li className="high-count-list-item">
|
||||
<span className="medium">暂无</span>
|
||||
<p>适用岗位信息</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
{/* 对应单元 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">对应单元</p>
|
||||
|
||||
{/* 复合能力课 */}
|
||||
<div className="unit-category-section">
|
||||
<p className="unit-category-subtitle">复合能力课</p>
|
||||
<ul className="project-cases-modal-horizontal-list">
|
||||
{data?.units?.filter(unit =>
|
||||
unit.includes('商业活动') ||
|
||||
unit.includes('营销') ||
|
||||
unit.includes('品牌') ||
|
||||
unit.includes('项目全周期')
|
||||
).map((unit, index) => (
|
||||
<li key={`compound-${index}`} className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>{unit}</span>
|
||||
</div>
|
||||
</li>
|
||||
)) || null}
|
||||
{(!data?.units || data?.units?.filter(unit =>
|
||||
unit.includes('商业活动') ||
|
||||
unit.includes('营销') ||
|
||||
unit.includes('品牌') ||
|
||||
unit.includes('项目全周期')
|
||||
).length === 0) && (
|
||||
<li className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>暂无复合能力课信息</span>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 垂直能力课 */}
|
||||
<div className="unit-category-section">
|
||||
<p className="unit-category-subtitle">垂直能力课</p>
|
||||
<ul className="project-cases-modal-horizontal-list">
|
||||
{data?.units?.filter(unit =>
|
||||
!unit.includes('商业活动') &&
|
||||
!unit.includes('营销') &&
|
||||
!unit.includes('品牌') &&
|
||||
!unit.includes('项目全周期')
|
||||
).map((unit, index) => (
|
||||
<li key={`vertical-${index}`} className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>{unit}</span>
|
||||
</div>
|
||||
</li>
|
||||
)) || null}
|
||||
{(!data?.units || data?.units?.filter(unit =>
|
||||
!unit.includes('商业活动') &&
|
||||
!unit.includes('营销') &&
|
||||
!unit.includes('品牌') &&
|
||||
!unit.includes('项目全周期')
|
||||
).length === 0) && (
|
||||
<li className="class-list-item">
|
||||
<div className="class-list-item-title">
|
||||
<i />
|
||||
<span>暂无垂直能力课信息</span>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{/* 项目整体流程介绍 - Markdown格式 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">项目整体流程介绍</p>
|
||||
<div className="project-cases-modal-markdown-content">
|
||||
{data?.process ? (
|
||||
<ReactMarkdown>{formatMarkdownContent(data.process)}</ReactMarkdown>
|
||||
) : (
|
||||
<p className="project-cases-modal-item-text">暂无项目流程介绍</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
{/* 项目案例关键技术点 - Markdown格式 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">项目案例关键技术点</p>
|
||||
<div className="project-cases-modal-markdown-content">
|
||||
{data?.keyPoints ? (
|
||||
<ReactMarkdown>{formatMarkdownContent(data.keyPoints)}</ReactMarkdown>
|
||||
) : (
|
||||
<p className="project-cases-modal-item-text">暂无关键技术点</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
{/* 附件 */}
|
||||
<li className="project-cases-modal-item">
|
||||
<p className="project-cases-modal-item-title">附件</p>
|
||||
<ul className="project-cases-modal-attachment-list">
|
||||
{data?.attachments?.map((attachment, index) => (
|
||||
<li key={index} className="attachment-list-item">
|
||||
{attachment.type ? (
|
||||
<FileIcon type={attachment.type} />
|
||||
) : (
|
||||
<img src={PDFICON} alt="icon" className="attachment-icon" />
|
||||
)}
|
||||
<div className="attachment-info">
|
||||
<p>{attachment.name}</p>
|
||||
<span>{attachment.size}</span>
|
||||
</div>
|
||||
</li>
|
||||
)) || (
|
||||
<li className="attachment-list-item">
|
||||
<img src={PDFICON} alt="icon" className="attachment-icon" />
|
||||
<div className="attachment-info">
|
||||
<p>暂无附件</p>
|
||||
<span>0kb</span>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 图片预览Modal */}
|
||||
{data?.images && (
|
||||
<ImagePreviewModal
|
||||
visible={previewVisible}
|
||||
onClose={() => setPreviewVisible(false)}
|
||||
images={data.images}
|
||||
initialIndex={previewIndex}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
369
src/pages/ProjectLibraryPage/index.jsx.backup_20250912_181103
Normal file
369
src/pages/ProjectLibraryPage/index.jsx.backup_20250912_181103
Normal file
@@ -0,0 +1,369 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { Tooltip } from "@arco-design/web-react";
|
||||
import toast from "@/components/Toast";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import ProjectCasesModal from "./components/ProjectCasesModal";
|
||||
import UploadModal from "./components/UploadModal";
|
||||
import { getProjectsList, getProjectsdetail } from "@/services/projectLibrary";
|
||||
import { IconUpload } from "@arco-design/web-react/icon";
|
||||
// 我的项目库数据
|
||||
const myProjectsData = [
|
||||
{
|
||||
unitName: "商业活动策略设计与创意策划",
|
||||
projects: [
|
||||
"校园特色摆摊创意策划与出摊运营项目",
|
||||
"社区水果店节日促销创意方案设计与落地执行项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "商业活动全程策划执行与运营优化",
|
||||
projects: [
|
||||
"社区便利店促销活动策划落地项目",
|
||||
"校园二手物品交易活动策划执行与运营项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "商业空间与文创产品设计",
|
||||
projects: [
|
||||
"街边小型咖啡馆主题空间布置与配套文创周边设计项目",
|
||||
"社区书店文创体验区空间规划项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "短视频与自媒体运营",
|
||||
projects: [
|
||||
"本地某餐厅生活服务新媒体账号运营项目",
|
||||
"某猫咖宠物日常类短视频账号运营实操项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "漫展与二次元活动策划与执行",
|
||||
projects: [
|
||||
"南京 Comic Festival 周边展区活动统筹项目",
|
||||
"盐城 ICGC 动漫嘉年华品牌互动区运营项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "户外音乐节主题策划与流程统筹",
|
||||
projects: [
|
||||
"青春旋律校园户外音乐节活动策划与实施项目",
|
||||
"环湖露天音乐节活动策划与组织项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "城市 IP 赛事活动整合与策划",
|
||||
projects: [
|
||||
"2025 城市电竞对抗赛整体策划与落地项目",
|
||||
"城市龙舟赛活动统筹与文化主题策划项目",
|
||||
"成都跑酷&街舞跨界赛事活动策划与组织项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "消费电子展品牌策划与执行",
|
||||
projects: [
|
||||
"智能穿戴设备消费电子展展区策划与执行项目",
|
||||
"智能生活类消费电子展策划项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "品牌招商展全案策划与招商运营",
|
||||
projects: [
|
||||
"苏州文旅文创产业品牌招商展策划与落地运营项目",
|
||||
"南京青年创客品牌招商展策划项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "商业街区打卡空间视觉呈现",
|
||||
projects: [
|
||||
"南京老门东历史街区创意打卡点策划项目",
|
||||
"苏州观前街沉浸式商业打卡体验空间设计项目",
|
||||
"无锡拈花湾文旅商业街区夜景灯光打卡点策划项目"
|
||||
]
|
||||
},
|
||||
{
|
||||
unitName: "文旅衍生文创产品设计",
|
||||
projects: [
|
||||
"南京云锦纹样衍生丝巾与服饰配件设计项目",
|
||||
"苏州园林拙政园窗棂纹样衍生文创书签与文具设计项目"
|
||||
]
|
||||
}
|
||||
];
|
||||
import "./index.css";
|
||||
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const ProjectLibraryPage = () => {
|
||||
// 处理我的项目数据
|
||||
const processMyProjects = () => {
|
||||
const projects = [];
|
||||
myProjectsData.forEach(unit => {
|
||||
unit.projects.forEach(projectName => {
|
||||
projects.push({
|
||||
id: `my-${projects.length + 1}`,
|
||||
unitName: unit.unitName,
|
||||
name: projectName,
|
||||
isMyProject: true
|
||||
});
|
||||
});
|
||||
});
|
||||
return projects;
|
||||
};
|
||||
|
||||
const myProjects = processMyProjects();
|
||||
|
||||
// 三个可点击查看的特殊项目
|
||||
const clickableProjects = [
|
||||
{
|
||||
id: "clickable-1",
|
||||
name: "宝可梦品牌青春治愈系全场景视觉与周边设计策划方案",
|
||||
unitName: "商业空间与文创产品设计",
|
||||
isClickable: true,
|
||||
content: {
|
||||
title: "宝可梦品牌青春治愈系全场景视觉与周边设计策划方案",
|
||||
description: "本套设计围绕让品牌成为可触摸的生活伙伴核心理念,打造视觉统一+场景沉浸的全链路品牌表达系统。",
|
||||
images: [
|
||||
{ url: "/images/project-library/29aab970-92d1-4a79-84d3-5a29d9e299eb.jpeg", title: "奶茶杯设计" },
|
||||
{ url: "/images/project-library/20250824-121446.jpg", title: "品牌海报设计" },
|
||||
{ url: "/images/project-library/outputs_20250824_3k4iaqnnbt.jpeg", title: "线下空间设计" },
|
||||
{ url: "/images/project-library/img_v3_02pf_f256578f-91f0-43f6-9e25-37fe423fd23g.jpg", title: "帽子周边设计" }
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "一、设计说明",
|
||||
content: "本套设计围绕让品牌成为可触摸的生活伙伴核心理念,打造视觉统一+场景沉浸的全链路品牌表达系统。整体风格融合清新潮酷与温暖治愈,以马卡龙渐变色系+软萌IP符号为核心视觉语言,串联周边产品(奶茶杯、帽子)、线下空间、视觉海报三大场景,核心方向聚焦从视觉符号到情感连接——通过产品的实用性、空间的体验感、海报的传播力,将品牌年轻、治愈、有温度的认知,转化为用户可感知、可参与、可分享的生活场景,强化品牌=青春治愈伙伴的心智联想。"
|
||||
},
|
||||
{
|
||||
title: "二、设计亮点",
|
||||
content: `1. 全场景视觉闭环:从周边产品的细节图案(如奶茶杯的渐变花纹、帽子的IP刺绣),到空间设计的元素延续(如墙面装饰呼应杯子造型),再到海报的色彩体系,所有设计均源于同一IP视觉库,实现看海报→逛空间→买周边的认知连贯,让品牌记忆点在不同场景中反复强化。
|
||||
|
||||
2. 情感化仪式感植入:奶茶杯内置温度感应隐藏图案(倒入热饮后浮现IP符号),将喝奶茶变成解锁小惊喜的仪式;空间设计预留奶茶杯打卡墙(与周边杯子1:10比例的巨型装置),满足用户拍美照、晒社交的需求;帽子采用可拆卸IP徽章,让用户通过DIY搭配,将品牌元素融入个人风格,增加互动感。
|
||||
|
||||
3. 年轻群体精准共鸣:色彩选择(浅粉、淡蓝、奶黄)契合Z世代治愈系审美,图案设计用简笔奶茶泡、软萌云朵等拟态符号,降低认知门槛;所有产品均兼顾潮流性与实用性(如帽子的软顶版型适配日常穿搭、杯子的防烫材质适合高频使用),让设计不仅好看,更好用。`
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "clickable-2",
|
||||
name: "四川大熊猫扇子文创产品设计",
|
||||
unitName: "文旅衍生文创产品设计",
|
||||
isClickable: true,
|
||||
content: {
|
||||
title: "四川大熊猫扇子文创产品设计",
|
||||
description: "以\"熊猫·蜀韵\"为核心主题,聚焦\"四川文化的当代表达\",将\"大熊猫、蜀绣、传统折扇\"三大符号融合为兼具文化意味与日常实用的文创产品。",
|
||||
images: [
|
||||
{ url: "/images/project-library/四川大熊猫扇子设计图.jpeg", title: "熊猫扇子效果图" },
|
||||
{ url: "/images/project-library/四川大熊猫扇子项目效果图2.jpg", title: "熊猫扇子展开效果" },
|
||||
{ url: "/images/project-library/四川大熊猫扇子项目效果图3.jpg", title: "熊猫扇子细节展示" },
|
||||
{ url: "/images/project-library/四川大熊猫扇子项目效果图4.jpg", title: "熊猫扇子包装设计" }
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "一、项目介绍",
|
||||
content: "本方案以\"熊猫·蜀韵\"为核心主题,聚焦\"四川文化的当代表达\",旨在将\"大熊猫、蜀绣、传统折扇\"三大符号,融合为兼具文化意味与日常实用的文创产品。整体风格定位为「熊猫蜀绣·新雅致」——既保留四川非遗的文化内核(如蜀绣的精细针法、大熊猫的国宝形象、川地传统折扇),又摒弃单纯\"符号拼贴\"的套路,转而用简洁的线条、精细的工艺和舒适的材质,让熊猫与蜀韵真正走进日常场景。"
|
||||
},
|
||||
{
|
||||
title: "二、设计策划案",
|
||||
content: "以 \"新中式非遗美学\" 为核心风格,融合四川 \"自然生态\" 与 \"非遗技艺\" 双重属性 —— 扇面以柔和雅致的 \"竹绿 + 浅金 + 淡青\" 为基调,搭配双宫绸的温润光泽与蜀绣的细腻针脚,营造 \"雅致不张扬、精致不堆砌\" 的视觉质感;扇骨采用四川楠竹碳化工艺,浅褐色自然纹理与浅青色棉麻流苏呼应,整体呈现 \"自然材质承载非遗文化\" 的和谐氛围。"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "clickable-3",
|
||||
name: "戴森吹风机:科技护发新风尚品牌运营策划案",
|
||||
unitName: "短视频与自媒体运营",
|
||||
isClickable: true,
|
||||
content: {
|
||||
title: "《戴森吹风机:科技护发新风尚》品牌运营策划案",
|
||||
description: "通过整合营销文案、宣传方案、活动策划及私域运营方案,全面提升戴森吹风机在目标消费群体中的品牌影响力与市场转化率。",
|
||||
images: [
|
||||
{ url: "/images/project-library/1.png", title: "品牌视觉设计" },
|
||||
{ url: "/images/project-library/2.png", title: "产品展示设计" },
|
||||
{ url: "/images/project-library/3.png", title: "营销物料设计" },
|
||||
{ url: "/images/project-library/4.png", title: "活动策划方案" }
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: "一、项目介绍",
|
||||
content: "本项目旨在通过整合营销文案、宣传方案、活动策划及私域运营方案,全面提升戴森吹风机在目标消费群体中的品牌影响力与市场转化率。项目以\"高端科技+精致生活\"为核心定位,既关注产品性能优势的突出,也注重用户体验的细腻塑造。通过线上线下结合的多渠道传播,项目将实现从品牌认知、兴趣激发、消费决策到复购沉淀的闭环路径。"
|
||||
},
|
||||
{
|
||||
title: "二、品牌运营策略",
|
||||
content: "以\"高端科技美学\"为核心风格,融合\"科技感\"与\"人性化\"双重属性——文案以简洁、专业、富有科技感的语言为基础,搭配生动、贴近生活的案例和情感化表达,营造\"科技不冰冷、高端亦亲和\"的语言质感;视觉设计上,以戴森标志性的蓝色为主色调,搭配简洁的线条和现代感的排版,整体呈现\"科技与人文融合\"的和谐氛围。"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
const [modalData, setModalData] = useState(undefined);
|
||||
const [projectList, setProjectList] = useState([]);
|
||||
const [projectCasesModalVisible, setProjectCasesModalVisible] =
|
||||
useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [selectedCategory, setSelectedCategory] = useState("全部");
|
||||
const [uploadModalVisible, setUploadModalVisible] = useState(false);
|
||||
|
||||
// 项目分类(基于数据中的direction字段)
|
||||
const categories = ["全部", "项目经营管理", "商业活动策划", "文化服务"];
|
||||
|
||||
// 根据分类过滤项目
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (selectedCategory === "全部") {
|
||||
return projectList;
|
||||
}
|
||||
return projectList.filter(project => project.direction === selectedCategory);
|
||||
}, [selectedCategory, projectList]);
|
||||
|
||||
const handleProjectClick = async (item) => {
|
||||
// 如果是我的普通项目,不允许点击
|
||||
if (item?.isMyProject && !item?.isClickable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是可点击的特殊项目,直接显示内容
|
||||
if (item?.isClickable && item?.content) {
|
||||
setModalData(item.content);
|
||||
setProjectCasesModalVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item?.id) {
|
||||
const res = await getProjectsdetail(item.id);
|
||||
if (res.success) {
|
||||
setModalData(res.data);
|
||||
setProjectCasesModalVisible(true);
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
} else {
|
||||
toast.error("加载数据失败,请刷新重试");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setProjectCasesModalVisible(false);
|
||||
setModalData(undefined);
|
||||
};
|
||||
|
||||
const fetchProjects = async (pageNum) => {
|
||||
try {
|
||||
const res = await getProjectsList({
|
||||
page: pageNum ?? page,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
if (res.success) {
|
||||
setProjectList((prevList) => {
|
||||
const newList = [...prevList, ...res.data];
|
||||
if (res.total === newList?.length) {
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch projects:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="project-library-page">
|
||||
<div className="project-library-wrapper">
|
||||
<p className="project-library-title">文旅班级项目库</p>
|
||||
|
||||
{/* 项目分类导航栏 */}
|
||||
<div className="project-category-nav">
|
||||
{categories.map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className={`category-item ${selectedCategory === category ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
loadMore={fetchProjects}
|
||||
hasMore={hasMore}
|
||||
empty={projectList.length === 0}
|
||||
className="project-library-list"
|
||||
>
|
||||
{filteredProjects.map((item) => (
|
||||
<li className="project-library-item" key={item.id}>
|
||||
<p className="project-library-item-title">{item.description}</p>
|
||||
<div>
|
||||
<p>{item.name}</p>
|
||||
<span onClick={() => handleProjectClick(item)}>详情 ></span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
|
||||
{/* 我的项目库板块 */}
|
||||
<div className="project-library-wrapper my-project-library">
|
||||
<div className="project-library-header">
|
||||
<p className="project-library-title">我完成的项目库</p>
|
||||
<button
|
||||
className="upload-button"
|
||||
onClick={() => setUploadModalVisible(true)}
|
||||
>
|
||||
<IconUpload />
|
||||
<span>上传</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="project-library-list">
|
||||
{/* 可点击的特殊项目 */}
|
||||
{clickableProjects.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="project-library-item clickable-project-item"
|
||||
onClick={() => handleProjectClick(item)}
|
||||
>
|
||||
<p className="project-library-item-title">{item.unitName}</p>
|
||||
<div>
|
||||
<p>{item.name}</p>
|
||||
<span>详情 ></span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
{/* 普通的灰色项目 */}
|
||||
{myProjects.map((item) => (
|
||||
<Tooltip
|
||||
key={item.id}
|
||||
content="非学员及导师无查看权限"
|
||||
position="top"
|
||||
>
|
||||
<li className="project-library-item my-project-item">
|
||||
<p className="project-library-item-title">{item.unitName}</p>
|
||||
<div>
|
||||
<p>{item.name}</p>
|
||||
<span className="disabled-detail">详情 ></span>
|
||||
</div>
|
||||
</li>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectCasesModal
|
||||
data={modalData}
|
||||
visible={projectCasesModalVisible}
|
||||
onClose={handleCloseModal}
|
||||
/>
|
||||
|
||||
<UploadModal
|
||||
visible={uploadModalVisible}
|
||||
onClose={() => setUploadModalVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectLibraryPage;
|
||||
Reference in New Issue
Block a user