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

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