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:
KQL
2025-10-15 15:55:25 +08:00
parent 3a054c4208
commit 1b964b3886
221 changed files with 110366 additions and 64316 deletions

View 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;
}

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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>
)}
</>
);
};

View File

@@ -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)}
/>
</>
);
};

View File

@@ -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>
)}
</>
);
};

View File

@@ -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);

View File

@@ -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: [] },

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>
</>
);
};

View File

@@ -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;

View File

@@ -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>
))}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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>
);

View File

@@ -283,7 +283,7 @@ export default ({ locked = false }) => {
"民宿客房管家",
"民宿运营专员",
"品牌公关",
"IP运营总监助理",
"ip运营总监助理",
"品牌公关管培生",
"直播中控",
"SEO专员",

View File

@@ -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>
);
};

View 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)}>详情 &gt;</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>详情 &gt;</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">详情 &gt;</span>
</div>
</li>
</Tooltip>
))}
</div>
</div>
<ProjectCasesModal
data={modalData}
visible={projectCasesModalVisible}
onClose={handleCloseModal}
/>
<UploadModal
visible={uploadModalVisible}
onClose={() => setUploadModalVisible(false)}
/>
</div>
);
};
export default ProjectLibraryPage;