feat: 优化教务系统多项功能
主要更新: 1. 项目库功能优化 - 添加项目效果图点击预览功能,支持图片放大查看和切换 - 新增ImagePreviewModal组件,提供完整的图片预览体验 2. 企业内推岗位页面改进 - 右侧岗位面试状态卡片支持点击查看岗位详情 - 从企业内推岗位库直接导入岗位数据 - 面试状态查看的岗位详情隐藏投递按钮 - 岗位要求显示优化,添加数字编号格式 3. 课堂作业板块完善 - 修复垂直能力课只显示4个单元的问题,现可显示全部12个单元 - 为"展会主题与品牌定位"课程添加"可试看"标签 - 调整"可试看"标签位置,避免遮挡课程名称 - 在全部视图中将"展会主题与品牌定位"课程置顶 4. 课程直播间页面优化 - 为复合能力课添加文字虚线分割线,与垂直能力课保持一致 - 删除页面顶部的进度条,简化界面 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user