feat: 优化教务系统多项功能

主要更新:
1. 项目库功能优化
   - 添加项目效果图点击预览功能,支持图片放大查看和切换
   - 新增ImagePreviewModal组件,提供完整的图片预览体验

2. 企业内推岗位页面改进
   - 右侧岗位面试状态卡片支持点击查看岗位详情
   - 从企业内推岗位库直接导入岗位数据
   - 面试状态查看的岗位详情隐藏投递按钮
   - 岗位要求显示优化,添加数字编号格式

3. 课堂作业板块完善
   - 修复垂直能力课只显示4个单元的问题,现可显示全部12个单元
   - 为"展会主题与品牌定位"课程添加"可试看"标签
   - 调整"可试看"标签位置,避免遮挡课程名称
   - 在全部视图中将"展会主题与品牌定位"课程置顶

4. 课程直播间页面优化
   - 为复合能力课添加文字虚线分割线,与垂直能力课保持一致
   - 删除页面顶部的进度条,简化界面

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-08 11:00:54 +08:00
parent 9a14678a12
commit 9198c67caf
13 changed files with 1753 additions and 35 deletions

View File

@@ -131,6 +131,15 @@ const CourseList = ({ className = "", onCourseClick }) => {
expandIconPosition="right"
defaultActiveKey={[]}
>
{/* 复合能力课分割线 */}
{compoundCourseList.length > 0 && (
<div className="course-divider">
<span className="divider-line"></span>
<span className="divider-text">复合能力课</span>
<span className="divider-line"></span>
</div>
)}
{/* 复合能力课部分 */}
{compoundCourseList.map((unit, index) => (
<CollapseItem
@@ -175,8 +184,8 @@ const CourseList = ({ className = "", onCourseClick }) => {
</CollapseItem>
))}
{/* 分割线 */}
{compoundCourseList.length > 0 && verticalCourseList.length > 0 && (
{/* 垂直能力课分割线 */}
{verticalCourseList.length > 0 && (
<div className="course-divider">
<span className="divider-line"></span>
<span className="divider-text">垂直能力课</span>

View File

@@ -412,7 +412,7 @@ const generatePublicCourseLiveList = (calendarEvents) => {
const teacherAvatars = {
"孙应战": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuUpJCc6qecx.jpg",
"李毅峰": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVPz0WRmxCK.jpeg",
"周伏波": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVU7Gi9YxSN.jpeg",
"周伏波": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVU7Gi9YxSN.jpg",
"范雪娇": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVU7JsHHDNZ.jpeg",
"李奇": "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuW8gePZvRn6.jpg"
};
@@ -661,7 +661,7 @@ export const mockData = {
name: "刘杰",
introduction: "15年民宿产业化研发与标准设计经验专注民宿产品创新、职业化培训体系搭建及乡村文旅业态升级主导多项国家级行业标准制定与落地实践。木亚文旅创始人兼董事长全面负责民宿产业研发、标准制定及全国业务拓展同时也是莫干山民宿行业协会执行会长兼秘书长以及浙江大学城市学院旅游管理专业校外导师。曾作为核心起草人主持《乡村民宿服务质量规范》《民宿管家职业技能等级评定规范》等国标编制推动民宿行业标准化发展申报并推动'民宿管家'入选国家新职业目录,建立首个国家级职业技能评定体系。",
specialties: ["民宿产业化专家","一听就懂", "思路很清晰", "课堂不枯燥", "学习氛围爆棚"],
avatar: "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuUpJBE4VCCx.png",
avatar: "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuUpJBE4VCCx.jpg",
type: "复合课导师",
verticalDirection: "项目经营管理",
courses: []
@@ -704,7 +704,7 @@ export const mockData = {
name: "周伏波",
introduction: "具备二十年以上光电子技术领域深耕经验长期致力于光电子芯片材料、光通信器件、半导体照明、激光器件及光电显示技术的研发与产业化工作是推动中国光电子产业链升级的重要技术推动者与行业实践者。其主导完成的多项核心材料与芯片工艺突破成功应用于光模块、光引擎、LED器件、激光显示模组等多个高精度、高性能领域累计获得授权专利60余项多项技术成果实现量产落地。",
specialties: ["光电显示技术探索者", "60+专利技术成果持有者", "国家级重大专项牵头人", "一听就懂", "思路很清晰", "讲解易懂"],
avatar: "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVU7Gi9YxSN.jpeg",
avatar: "https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_teacher-avatar/recuVU7Gi9YxSN.jpg",
type: "公共课导师",
courses: []
},
@@ -4917,11 +4917,14 @@ mockData.verticalCourseLiveList = generateVerticalCourseLiveList(allCalendarEven
// 动态更新垂直能力课作业列表
if (mockData.homework && mockData.homework[1]) {
const verticalHomeworkUnits = [];
const verticalHomeworkList = [];
let homeworkId = 1;
// 遍历所有垂直能力课单元
// 遍历所有垂直能力课单元生成units结构
mockData.verticalCourseLiveList.forEach(unit => {
const unitCourses = [];
unit.courses.forEach(course => {
// 判断课程状态
let level = "locked";
@@ -4946,11 +4949,21 @@ if (mockData.homework && mockData.homework[1]) {
homeworkItem.previewUrl = "https://du9uay.github.io/zhanhui/";
}
unitCourses.push(homeworkItem);
verticalHomeworkList.push(homeworkItem);
});
// 添加单元到units数组
if (unitCourses.length > 0) {
verticalHomeworkUnits.push({
name: unit.unitName,
courses: unitCourses
});
}
});
// 更新垂直能力课作业列表
// 更新垂直能力课作业的units和list
mockData.homework[1].units = verticalHomeworkUnits;
mockData.homework[1].list = verticalHomeworkList;
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ import "./index.css";
const InputSearch = Input.Search;
const PAGE_SIZE = 10;
export default ({ visible, onClose, data, directToResume = false }) => {
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);
@@ -260,10 +260,11 @@ export default ({ visible, onClose, data, directToResume = false }) => {
</div>
))
) : (
data?.details?.requirementsText?.split('\n').map((line, index) => (
<p key={index} className="requirement-line">
{line}
</p>
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>
@@ -281,6 +282,7 @@ export default ({ visible, onClose, data, directToResume = false }) => {
</div>
</div>
)}
{!hideDeliverButton && (
<div
className="job-info-modal-btn"
onClick={handleClickDeliverBtn}
@@ -288,6 +290,7 @@ export default ({ visible, onClose, data, directToResume = false }) => {
<i />
<span>立即投递</span>
</div>
)}
</>
)}
</div>

View File

@@ -7,11 +7,14 @@ import InfiniteScroll from "@/components/InfiniteScroll";
import toast from "@/components/Toast";
import JobList from "./components/JobList";
import InterviewStatusAnimation from "./components/InterviewStatusAnimation";
import JobInfoModal from "./components/JobInfoModal";
import {
getCompanyJobsPageData,
getJobsList,
getInterviewsList,
getJobsDetail,
} from "@/services";
import { getJobByPosition } from "@/services/companyJobsNew";
import "./index.css";
const PAGE_SIZE = 10;
@@ -27,6 +30,9 @@ const CompanyJobsPage = () => {
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
const [loading, setLoading] = useState(true);
const [expandedItemId, setExpandedItemId] = useState(null);
const [jobDetailVisible, setJobDetailVisible] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [isFromInterview, setIsFromInterview] = useState(false); // 标识是否从面试状态卡片点击
const navigate = useNavigate();
// 初始化页面数据 - 使用聚合接口
@@ -181,6 +187,24 @@ const CompanyJobsPage = () => {
navigate("/company-jobs-list");
};
// 处理岗位卡片点击,显示岗位详情
const handleJobCardClick = async (item) => {
// 直接从企业内推岗位库中查找对应的岗位
if (item.position) {
const jobData = getJobByPosition(item.position);
if (jobData) {
setSelectedJob(jobData);
setIsFromInterview(true); // 标记是从面试状态卡片点击的
setJobDetailVisible(true);
} else {
toast.error("未找到对应的岗位详情");
}
} else {
toast.error("无法获取岗位详情");
}
};
return (
<div className="company-jobs-page-wrapper">
<div className="company-jobs-page">
@@ -245,7 +269,11 @@ const CompanyJobsPage = () => {
>
{interviews.map((item) => (
<div key={item.id} className="interview-item-wrapper">
<li className="company-jobs-page-interview-item">
<li
className="company-jobs-page-interview-item"
onClick={() => handleJobCardClick(item)}
style={{ cursor: 'pointer' }}
>
<div className="company-jobs-page-interview-item-info">
<p className="company-jobs-page-interview-item-info-position">
{item.position}
@@ -293,6 +321,18 @@ const CompanyJobsPage = () => {
)}
</div>
{/* 岗位详情弹窗 */}
<JobInfoModal
visible={jobDetailVisible}
onClose={() => {
setJobDetailVisible(false);
setSelectedJob(null);
setIsFromInterview(false); // 重置标志
}}
data={selectedJob}
directToResume={false}
hideDeliverButton={isFromInterview} // 传递是否隐藏投递按钮的标志
/>
</div>
);
};

View File

@@ -133,6 +133,15 @@
cursor: default;
}
.homework-page-content-list-content-item-action {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
position: relative;
height: 42px;
}
.homework-page-content-list-content-item-btn {
cursor: pointer;
width: 109px;
@@ -172,6 +181,19 @@
}
}
}
.homework-page-preview-tag {
display: inline-block;
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
color: #ff8c00;
background: rgba(255, 140, 0, 0.1);
border: 1px solid rgba(255, 140, 0, 0.3);
border-radius: 12px;
white-space: nowrap;
margin-top: 2px;
}
}
}
}

View File

@@ -125,7 +125,7 @@ const HomeworkPage = () => {
<span className="homework-page-iframe-title">展会策划教学</span>
</div>
<iframe
src="https://du9uay.github.io/zhanhui/"
src="https://du9uay.github.io/zhanhui/#/course-test"
className="homework-page-iframe-content"
title="展会策划教学"
/>
@@ -144,7 +144,18 @@ const HomeworkPage = () => {
if (section.units) {
if (selectedUnit === "全部") {
// 将所有单元的课程合并
return section.units.flatMap(unit => unit.courses);
const allCourses = section.units.flatMap(unit => unit.courses);
// 如果是垂直能力课,将"展会主题与品牌定位"移到第一位
if (sectionId === 2) {
const targetCourse = allCourses.find(course => course.name === "展会主题与品牌定位");
if (targetCourse) {
const otherCourses = allCourses.filter(course => course.name !== "展会主题与品牌定位");
return [targetCourse, ...otherCourses];
}
}
return allCourses;
} else {
// 返回选中单元的课程
const unit = section.units.find(u => u.name === selectedUnit);
@@ -203,6 +214,7 @@ const HomeworkPage = () => {
{contentItem.name}
</p>
</Tooltip>
<div className="homework-page-content-list-content-item-action">
<div
className={`homework-page-content-list-content-item-btn ${
isDisabled(item.id, contentItem) ? "disabled" : "completed"
@@ -211,6 +223,10 @@ const HomeworkPage = () => {
>
已完成
</div>
{contentItem.name === "展会主题与品牌定位" && (
<span className="homework-page-preview-tag">可试看</span>
)}
</div>
</li>
))}
</ul>

View File

@@ -1,7 +1,6 @@
import { useState } from "react";
import CoursesVideoPlayer from "@/components/CoursesVideoPlayer";
import CourseList from "@/components/CourseList";
import StageProgress from "@/components/StageProgress";
import { mockData } from "@/data/mockData";
import "./index.css";
@@ -14,7 +13,6 @@ const LivePage = () => {
return (
<div className="live-page">
<StageProgress />
<div className="live-page-content">
<CoursesVideoPlayer selectedCourse={selectedCourse} teacherData={mockData.teacherData} unitPosters={mockData.unitPosters} />
<CourseList onCourseClick={handleCourseClick} />

View File

@@ -0,0 +1,165 @@
.image-preview-modal {
width: 90vw;
height: 90vh;
max-width: 1200px;
max-height: 800px;
background-color: #1a1a1a;
border-radius: 8px;
display: flex;
flex-direction: column;
position: relative;
}
.image-preview-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 8px 8px 0 0;
}
.image-preview-title {
color: #ffffff;
font-size: 16px;
font-weight: 500;
flex: 1;
}
.image-preview-counter {
color: #ffffff;
font-size: 14px;
margin-right: 20px;
opacity: 0.8;
}
.image-preview-header .close-icon {
width: 24px;
height: 24px;
background-image: url("@/assets/images/Common/close.png");
background-size: 100% 100%;
cursor: pointer;
opacity: 0.8;
transition: opacity 0.3s;
filter: brightness(0) invert(1);
}
.image-preview-header .close-icon:hover {
opacity: 1;
}
.image-preview-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.image-preview-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.image-preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
}
.image-preview-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
background-color: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #ffffff;
transition: all 0.3s;
z-index: 2;
}
.image-preview-nav:hover {
background-color: rgba(0, 0, 0, 0.9);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-50%) scale(1.1);
}
.image-preview-nav-prev {
left: 20px;
}
.image-preview-nav-next {
right: 20px;
}
.image-preview-thumbnails {
height: 80px;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 0 0 8px 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
overflow-x: auto;
}
.image-preview-thumbnails::-webkit-scrollbar {
height: 6px;
}
.image-preview-thumbnails::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.image-preview-thumbnails::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.image-preview-thumbnails::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.image-preview-thumbnail {
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.3s;
opacity: 0.6;
}
.image-preview-thumbnail:hover {
opacity: 0.9;
}
.image-preview-thumbnail.active {
border-color: #4080ff;
opacity: 1;
}
.image-preview-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}

View File

@@ -0,0 +1,109 @@
import { useState, useEffect } from "react";
import Modal from "@/components/Modal";
import "./index.css";
export default ({ visible, onClose, images, initialIndex = 0 }) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
useEffect(() => {
setCurrentIndex(initialIndex);
}, [initialIndex]);
useEffect(() => {
const handleKeyDown = (e) => {
if (!visible) return;
if (e.key === "Escape") {
onClose();
} else if (e.key === "ArrowLeft") {
handlePrevious();
} else if (e.key === "ArrowRight") {
handleNext();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [visible, currentIndex]);
const handlePrevious = () => {
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));
};
const handleNext = () => {
setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));
};
if (!images || images.length === 0) return null;
const currentImage = images[currentIndex];
const imageUrl = typeof currentImage === "string" ? currentImage : currentImage.url;
const imageTitle = typeof currentImage === "string"
? `图片 ${currentIndex + 1}`
: currentImage.title || `图片 ${currentIndex + 1}`;
return (
<Modal visible={visible} onClose={onClose}>
<div className="image-preview-modal">
<div className="image-preview-header">
<span className="image-preview-title">{imageTitle}</span>
<span className="image-preview-counter">
{currentIndex + 1} / {images.length}
</span>
<i className="close-icon" onClick={onClose} />
</div>
<div className="image-preview-container">
{images.length > 1 && (
<button
className="image-preview-nav image-preview-nav-prev"
onClick={handlePrevious}
aria-label="上一张"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
)}
<div className="image-preview-content">
<img
src={imageUrl}
alt={imageTitle}
className="image-preview-image"
/>
</div>
{images.length > 1 && (
<button
className="image-preview-nav image-preview-nav-next"
onClick={handleNext}
aria-label="下一张"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
)}
</div>
{images.length > 1 && (
<div className="image-preview-thumbnails">
{images.map((image, index) => {
const thumbUrl = typeof image === "string" ? image : image.url;
return (
<div
key={index}
className={`image-preview-thumbnail ${index === currentIndex ? "active" : ""}`}
onClick={() => setCurrentIndex(index)}
>
<img src={thumbUrl} alt={`缩略图 ${index + 1}`} />
</div>
);
})}
</div>
)}
</div>
</Modal>
);
};

View File

@@ -1,18 +1,29 @@
import { useState } from "react";
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 [previewVisible, setPreviewVisible] = useState(false);
const [previewIndex, setPreviewIndex] = useState(0);
const handleCloseModal = () => {
onClose();
};
// 处理图片点击预览
const handleImageClick = (index) => {
setPreviewIndex(index);
setPreviewVisible(true);
};
// 将换行符转换为Markdown格式
const formatMarkdownContent = (content) => {
if (!content) return "";
// 将 \n 替换为实际的换行符
// 将 \\n 替换为实际的换行符
return content.replace(/\\n/g, '\n');
};
@@ -29,9 +40,52 @@ export default ({ visible, onClose, data }) => {
<li className="project-cases-modal-item">
<p className="project-cases-modal-item-title">项目概述</p>
<p className="project-cases-modal-item-text">
{data?.overview || "暂无项目概述"}
{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>
@@ -121,8 +175,20 @@ export default ({ visible, onClose, data }) => {
)}
</ul>
</li>
</>
)}
</ul>
</div>
{/* 图片预览Modal */}
{data?.images && (
<ImagePreviewModal
visible={previewVisible}
onClose={() => setPreviewVisible(false)}
images={data.images}
initialIndex={previewIndex}
/>
)}
</Modal>
);
};

View File

@@ -109,6 +109,89 @@ const ProjectLibrary = () => {
};
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: "熊猫扇子效果图" }
],
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] =
@@ -119,8 +202,15 @@ const ProjectLibrary = () => {
const handleProjectClick = async (item) => {
// 如果是我的项目,不允许点击
if (item?.isMyProject) {
// 如果是我的普通项目,不允许点击
if (item?.isMyProject && !item?.isClickable) {
return;
}
// 如果是可点击的特殊项目,直接显示内容
if (item?.isClickable && item?.content) {
setModalData(item.content);
setProjectCasesModalVisible(true);
return;
}
@@ -190,6 +280,22 @@ const ProjectLibrary = () => {
<div className="project-library-wrapper my-project-library">
<p className="project-library-title">我完成的项目库</p>
<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}

View File

@@ -0,0 +1,47 @@
import companyJobsData from "@/mocks/companyJobsData.json";
// 将原始数据转换为前端格式
function transformJobData(rawJob, index) {
// 从招聘人数中提取数字
const recruitNumberMatch = rawJob["招聘人数"]?.match(/\d+/);
const remainingPositions = recruitNumberMatch ? recruitNumberMatch[0] : "若干";
return {
id: index + 1,
position: rawJob["内推岗位名称"],
salary: rawJob["薪资"],
location: rawJob["工作地点"],
education: rawJob["学历要求"],
recruitNumber: rawJob["招聘人数"],
remainingPositions: remainingPositions,
tags: rawJob["职位标签"] || [],
benefits: rawJob["福利标签"] || [],
deadline: rawJob["截止时间"],
jobCategory: rawJob["岗位相关标签"],
// details对象包含描述、要求和公司介绍
details: {
description: rawJob["职位描述"] || "",
requirementsText: rawJob["任职要求"] || "",
companyInfo: rawJob["公司介绍"] || ""
},
// 保留原始数据以备需要
_raw: rawJob
};
}
// 获取所有岗位数据
export function getAllCompanyJobs() {
return companyJobsData.map((job, index) => transformJobData(job, index));
}
// 根据岗位名称获取岗位详情
export function getJobByPosition(positionName) {
const allJobs = getAllCompanyJobs();
return allJobs.find(job => job.position === positionName);
}
// 根据ID获取岗位详情
export function getJobById(id) {
const allJobs = getAllCompanyJobs();
return allJobs.find(job => job.id === id);
}