feat: 🎸 更新了很多信息
This commit is contained in:
173
src/components/CourseList/index.css
Normal file
173
src/components/CourseList/index.css
Normal file
@@ -0,0 +1,173 @@
|
||||
.course-list-wrapper {
|
||||
width: 304px;
|
||||
height: 798px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 16px;
|
||||
overflow-y: hidden;
|
||||
|
||||
.course-list-title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
color: #1d2129;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 5px;
|
||||
width: 31px;
|
||||
height: 3px;
|
||||
background-image: url("@/assets/images/Common/title_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.course-list-content {
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
overflow-y: auto;
|
||||
|
||||
.course-list {
|
||||
width: 100%;
|
||||
|
||||
/* 自定义折叠面板元素 */
|
||||
.course-list-item {
|
||||
width: 272px;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
padding: 3px 0;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
border: none;
|
||||
|
||||
.arco-collapse-item-header {
|
||||
border-radius: 8px;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
.arco-timeline-item {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.arco-collapse-item-content-expanded {
|
||||
background-color: #fff;
|
||||
}
|
||||
.arco-collapse-item-content-box {
|
||||
margin-top: 10px;
|
||||
padding: 0 0 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 自定义时间轴元素 */
|
||||
.time-line-dot-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Common/time_line_dot_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.time-line-clock-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-image: url("@/assets/images/Common/time_line_clock_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.time-line-item {
|
||||
width: 248px;
|
||||
height: 74px;
|
||||
background-color: #f2f3f5;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
|
||||
> p {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
> .time-line-item-info {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: #4e5969;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.finish {
|
||||
&::before {
|
||||
content: "已结束";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 48px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
background-color: #c9cdd4;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #c9cdd4;
|
||||
}
|
||||
}
|
||||
.active {
|
||||
border: 1px solid #0275f2;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/CoursesVideoPlayer/living_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
.coming {
|
||||
&::before {
|
||||
content: "即将开始";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
background-color: #f2f3f5;
|
||||
color: #ff7d00;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ff7d00;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/components/CourseList/index.jsx
Normal file
56
src/components/CourseList/index.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Collapse, Timeline } from "@arco-design/web-react";
|
||||
import "./index.css";
|
||||
|
||||
const TimelineItem = Timeline.Item;
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
const CourseList = ({ className = "" }) => {
|
||||
return (
|
||||
<div className={`${className} course-list-wrapper`}>
|
||||
<p className="course-list-title">课程列表</p>
|
||||
<div className="course-list-content">
|
||||
<Collapse
|
||||
lazyload
|
||||
className="course-list"
|
||||
bordered={false}
|
||||
expandIconPosition="right"
|
||||
>
|
||||
<CollapseItem
|
||||
header="课程单元1"
|
||||
name="1"
|
||||
className="course-list-item"
|
||||
>
|
||||
<Timeline>
|
||||
<TimelineItem
|
||||
dot={<div className="time-line-dot-icon" />}
|
||||
lineType="dashed"
|
||||
>
|
||||
<div className="time-line-item finish">
|
||||
<p>终生学习系统</p>
|
||||
<div className="time-line-item-info">
|
||||
<span>张老师</span>
|
||||
<span>2023-01-01</span>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
<TimelineItem
|
||||
dot={<div className="time-line-clock-icon" />}
|
||||
lineType="dashed"
|
||||
>
|
||||
<div className="time-line-item active">
|
||||
<p>终生学习系统</p>
|
||||
<div className="time-line-item-info">
|
||||
<span>张老师</span>
|
||||
<span>2023-01-01</span>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
</CollapseItem>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseList;
|
||||
@@ -3,13 +3,10 @@
|
||||
height: 798px;
|
||||
position: relative;
|
||||
|
||||
.lock {
|
||||
position: absolute;
|
||||
top: 110px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 433px;
|
||||
height: 384px;
|
||||
.video-lock-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.courses-video-player {
|
||||
@@ -189,6 +186,7 @@
|
||||
.teacher-introduce {
|
||||
width: 100%;
|
||||
height: 84px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.courses-video-player-teacher-introduce {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { Avatar } from "@arco-design/web-react";
|
||||
import LOCKICON from "@/assets/images/Common/lock_bg.png";
|
||||
import Locked from "@/components/Locked";
|
||||
import "./index.css";
|
||||
|
||||
export default ({ className = "" }) => {
|
||||
const [isLock, setIsLock] = useState(false);
|
||||
|
||||
export default ({ className = "", isLock = false }) => {
|
||||
return (
|
||||
<div className={`${className} courses-video-player-wrapper`}>
|
||||
{/* 直播板块 */}
|
||||
<div className="courses-video-player">
|
||||
{isLock ? (
|
||||
<img className="lock" src={LOCKICON} alt="lock" />
|
||||
<Locked
|
||||
className="video-lock-wrapper"
|
||||
text="该板块将于「垂直能力提升」阶段启动后开放届时,请留意教务系统通知,您可在该板块进行线上
|
||||
1V1 求职策略定制"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="courses-video-player-header">
|
||||
@@ -27,7 +28,6 @@ export default ({ className = "" }) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 直播信息板块 */}
|
||||
<div className="courses-video-player-info">
|
||||
{/* 直播观众信息 */}
|
||||
<div className="courses-video-player-audience-info">
|
||||
|
||||
@@ -19,7 +19,10 @@ const navigation = {
|
||||
{ name: "📺 课程直播间", path: "/live" },
|
||||
{ name: "🌳 就业管家知识树", path: "/career-tree" },
|
||||
{ name: "📝 课后作业", path: "/homework" },
|
||||
{ name: "🎯 1V1定制求职策略", path: "/job-strategy" },
|
||||
{
|
||||
name: "🎯 1V1定制求职策略",
|
||||
path: ["/job-strategy", "/job-strategy-detail"],
|
||||
},
|
||||
{ name: "🎭 线下面试模拟", path: "/interview-simulation" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./index.css";
|
||||
|
||||
const LiveSummary = ({ className = "", showBtn = false, isLiving = true }) => {
|
||||
const handleClickBtn = () => {};
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClickBtn = () => {
|
||||
navigate("/job-strategy-detail");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${className} live-summary-wrapper`}>
|
||||
|
||||
32
src/components/Locked/index.css
Normal file
32
src/components/Locked/index.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.lock-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
.lock {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background-image: url("@/assets/images/Common/lock_bg.png");
|
||||
background-size: 100% 100%;
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
bottom: -50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 433px;
|
||||
height: 44px;
|
||||
text-align: center;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/components/Locked/index.jsx
Normal file
9
src/components/Locked/index.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import "./index.css";
|
||||
|
||||
export default ({ text = "", className = "" }) => {
|
||||
return (
|
||||
<div className={`lock-wrapper ${className}`}>
|
||||
<div className="lock">{text ? <span>{text}</span> : null}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,383 +0,0 @@
|
||||
/* 简历编辑弹窗样式 */
|
||||
.resume-edit-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resume-edit-modal-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
animation: modalFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗头部 */
|
||||
.resume-edit-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafbff;
|
||||
}
|
||||
|
||||
.resume-edit-modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.resume-edit-modal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #1e40af;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
.resume-edit-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.resume-edit-modal-close:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 弹窗主体 */
|
||||
.resume-edit-modal-body {
|
||||
padding: 32px;
|
||||
max-height: calc(90vh - 160px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 简历区域 */
|
||||
.resume-edit-section {
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.resume-edit-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.resume-edit-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #111111;
|
||||
margin: 0 0 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.resume-edit-section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #1e40af;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #555555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
background: #ffffff;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #1e40af;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
||||
}
|
||||
|
||||
.form-value {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
padding: 12px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 工作经历特殊样式 */
|
||||
.experience-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.responsibilities-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.responsibilities-list {
|
||||
margin: 8px 0 0 0;
|
||||
padding-left: 20px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.responsibility-item {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 技能特长样式 */
|
||||
.skills-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skills-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
background: #e0edff;
|
||||
color: #1e40af;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #b8daff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.skill-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skill-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #1e40af;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.skill-remove:hover {
|
||||
background: #1e40af;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.skill-add-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skill-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.skill-input:focus {
|
||||
outline: none;
|
||||
border-color: #1e40af;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
||||
}
|
||||
|
||||
.skill-add-btn {
|
||||
background: #10b981;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skill-add-btn:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
/* 弹窗底部 */
|
||||
.resume-edit-modal-footer {
|
||||
padding: 20px 32px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafbff;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #ffffff;
|
||||
color: #666666;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f8f9fa;
|
||||
color: #333333;
|
||||
border-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1e40af;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #1e3a8a;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.resume-edit-modal-overlay {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.resume-edit-modal-content {
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.resume-edit-modal-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.resume-edit-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skill-add-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import "./index.css";
|
||||
|
||||
const ResumeEditModal = ({
|
||||
resume,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
isEditMode = false,
|
||||
}) => {
|
||||
const [editData, setEditData] = useState({
|
||||
personalInfo: {
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
location: "",
|
||||
},
|
||||
education: {
|
||||
university: "",
|
||||
major: "",
|
||||
degree: "",
|
||||
graduationYear: "",
|
||||
},
|
||||
experience: {
|
||||
company: "",
|
||||
position: "",
|
||||
duration: "",
|
||||
responsibilities: [],
|
||||
},
|
||||
skills: [],
|
||||
});
|
||||
const [isEditing, setIsEditing] = useState(isEditMode);
|
||||
const [currentSkill, setCurrentSkill] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (resume && isOpen) {
|
||||
setEditData({
|
||||
personalInfo: {
|
||||
name: resume.personalInfo?.name || "",
|
||||
phone: resume.personalInfo?.phone || "",
|
||||
email: resume.personalInfo?.email || "",
|
||||
location: resume.personalInfo?.location || "",
|
||||
},
|
||||
education: {
|
||||
university: resume.education?.university || "",
|
||||
major: resume.education?.major || "",
|
||||
degree: resume.education?.degree || "",
|
||||
graduationYear: resume.education?.graduationYear || "",
|
||||
},
|
||||
experience: {
|
||||
company: resume.company || "",
|
||||
position: resume.name || "",
|
||||
duration: resume.experience || "",
|
||||
responsibilities: [
|
||||
"负责核心业务开发与维护",
|
||||
"参与系统架构设计",
|
||||
"协助团队制定技术规范",
|
||||
],
|
||||
},
|
||||
skills: resume.skills || [],
|
||||
});
|
||||
}
|
||||
}, [resume, isOpen]);
|
||||
|
||||
if (!isOpen || !resume) return null;
|
||||
|
||||
const handleInputChange = (section, field, value) => {
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[field]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddSkill = () => {
|
||||
if (currentSkill.trim() && !editData.skills.includes(currentSkill.trim())) {
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
skills: [...prev.skills, currentSkill.trim()],
|
||||
}));
|
||||
setCurrentSkill("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSkill = (skillToRemove) => {
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
skills: prev.skills.filter((skill) => skill !== skillToRemove),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
...resume,
|
||||
personalInfo: editData.personalInfo,
|
||||
education: editData.education,
|
||||
experience: editData.experience,
|
||||
skills: editData.skills,
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleOverlayClick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="resume-edit-modal-overlay" onClick={handleOverlayClick}>
|
||||
<div className="resume-edit-modal-content">
|
||||
<div className="resume-edit-modal-header">
|
||||
<h3 className="resume-edit-modal-title">
|
||||
{isEditing ? "编辑简历" : "简历详情"}
|
||||
</h3>
|
||||
<div className="resume-edit-modal-actions">
|
||||
{!isEditing && (
|
||||
<button className="btn-edit" onClick={() => setIsEditing(true)}>
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
<button className="resume-edit-modal-close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="resume-edit-modal-body">
|
||||
{/* 个人信息 */}
|
||||
<div className="resume-edit-section">
|
||||
<h4 className="resume-edit-section-title">个人信息</h4>
|
||||
<div className="resume-edit-content">
|
||||
<div className="form-grid">
|
||||
<div className="form-item">
|
||||
<label className="form-label">姓名:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.personalInfo.name}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"personalInfo",
|
||||
"name",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.personalInfo.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">电话:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.personalInfo.phone}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"personalInfo",
|
||||
"phone",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.personalInfo.phone}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">邮箱:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="email"
|
||||
className="form-input"
|
||||
value={editData.personalInfo.email}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"personalInfo",
|
||||
"email",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.personalInfo.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">地址:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.personalInfo.location}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"personalInfo",
|
||||
"location",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.personalInfo.location}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 教育背景 */}
|
||||
<div className="resume-edit-section">
|
||||
<h4 className="resume-edit-section-title">教育背景</h4>
|
||||
<div className="resume-edit-content">
|
||||
<div className="form-grid">
|
||||
<div className="form-item">
|
||||
<label className="form-label">院校:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.education.university}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"education",
|
||||
"university",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.education.university}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">专业:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.education.major}
|
||||
onChange={(e) =>
|
||||
handleInputChange("education", "major", e.target.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.education.major}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">学历:</label>
|
||||
{isEditing ? (
|
||||
<select
|
||||
className="form-input"
|
||||
value={editData.education.degree}
|
||||
onChange={(e) =>
|
||||
handleInputChange("education", "degree", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">请选择学历</option>
|
||||
<option value="专科">专科</option>
|
||||
<option value="本科">本科</option>
|
||||
<option value="硕士">硕士</option>
|
||||
<option value="博士">博士</option>
|
||||
</select>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.education.degree}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">毕业年份:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.education.graduationYear}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"education",
|
||||
"graduationYear",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.education.graduationYear}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工作经历 */}
|
||||
<div className="resume-edit-section">
|
||||
<h4 className="resume-edit-section-title">工作经历</h4>
|
||||
<div className="resume-edit-content">
|
||||
<div className="experience-item">
|
||||
<div className="form-grid">
|
||||
<div className="form-item">
|
||||
<label className="form-label">公司:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.experience.company}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"experience",
|
||||
"company",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.experience.company}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label className="form-label">职位:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.experience.position}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"experience",
|
||||
"position",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.experience.position}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-item form-item-full">
|
||||
<label className="form-label">工作时间:</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editData.experience.duration}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"experience",
|
||||
"duration",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="form-value">
|
||||
{editData.experience.duration}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="responsibilities-section">
|
||||
<label className="form-label">工作职责:</label>
|
||||
<ul className="responsibilities-list">
|
||||
{editData.experience.responsibilities.map((resp, index) => (
|
||||
<li key={index} className="responsibility-item">
|
||||
{resp}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 技能特长 */}
|
||||
<div className="resume-edit-section">
|
||||
<h4 className="resume-edit-section-title">技能特长</h4>
|
||||
<div className="resume-edit-content">
|
||||
<div className="skills-container">
|
||||
<div className="skills-list">
|
||||
{editData.skills.map((skill, index) => (
|
||||
<div key={index} className="skill-item">
|
||||
<span className="skill-text">{skill}</span>
|
||||
{isEditing && (
|
||||
<button
|
||||
className="skill-remove"
|
||||
onClick={() => handleRemoveSkill(skill)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{isEditing && (
|
||||
<div className="skill-add-form">
|
||||
<input
|
||||
type="text"
|
||||
className="skill-input"
|
||||
value={currentSkill}
|
||||
onChange={(e) => setCurrentSkill(e.target.value)}
|
||||
placeholder="添加技能"
|
||||
onKeyPress={(e) => e.key === "Enter" && handleAddSkill()}
|
||||
/>
|
||||
<button className="skill-add-btn" onClick={handleAddSkill}>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="resume-edit-modal-footer">
|
||||
<div className="modal-actions">
|
||||
<button className="btn-secondary" onClick={onClose}>
|
||||
取消
|
||||
</button>
|
||||
{isEditing && (
|
||||
<button className="btn-primary" onClick={handleSave}>
|
||||
保存
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumeEditModal;
|
||||
@@ -1,632 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import Portal from "@/components/common/Portal";
|
||||
|
||||
const CourseEvaluationModal = ({ isVisible, onClose, onSubmit }) => {
|
||||
// 评价状态
|
||||
const [ratings, setRatings] = useState({
|
||||
discipline: 0, // 课堂纪律
|
||||
teaching: 0, // 教学水平
|
||||
effectiveness: 0, // 课堂实效
|
||||
overall: 0, // 综合评价
|
||||
});
|
||||
|
||||
const [comment, setComment] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// 管理body的modal-open类
|
||||
React.useEffect(() => {
|
||||
if (isVisible) {
|
||||
document.body.classList.add("modal-open");
|
||||
} else {
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
document.body.classList.remove("modal-open");
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
// 评价维度配置
|
||||
const ratingDimensions = [
|
||||
{ key: "discipline", label: "课堂纪律" },
|
||||
{ key: "teaching", label: "教学水平" },
|
||||
{ key: "effectiveness", label: "课堂实效" },
|
||||
{ key: "overall", label: "课程收获" },
|
||||
];
|
||||
|
||||
// 处理星级评分
|
||||
const handleStarRating = (dimension, rating) => {
|
||||
setRatings((prev) => ({
|
||||
...prev,
|
||||
[dimension]: rating,
|
||||
}));
|
||||
};
|
||||
|
||||
// 渲染星级评分
|
||||
const renderStarRating = (dimension, currentRating) => {
|
||||
return (
|
||||
<div className="star-rating">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
className={`star ${star <= currentRating ? "filled" : ""}`}
|
||||
onClick={() => handleStarRating(dimension, star)}
|
||||
onMouseEnter={(e) => {
|
||||
// 悬停效果
|
||||
const stars =
|
||||
e.currentTarget.parentElement.querySelectorAll(".star");
|
||||
stars.forEach((s, index) => {
|
||||
s.classList.toggle("hover", index < star);
|
||||
});
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
// 清除悬停效果
|
||||
const stars =
|
||||
e.currentTarget.parentElement.querySelectorAll(".star");
|
||||
stars.forEach((s) => s.classList.remove("hover"));
|
||||
}}
|
||||
>
|
||||
★
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染评分条(模拟导师评分显示)
|
||||
const renderRatingBars = () => {
|
||||
const ratings = [
|
||||
{ stars: 5, percentage: 88.1 },
|
||||
{ stars: 4, percentage: 5.8 },
|
||||
{ stars: 3, percentage: 3.6 },
|
||||
{ stars: 2, percentage: 1.2 },
|
||||
{ stars: 1, percentage: 0.2 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rating-bars">
|
||||
{ratings.map((item) => (
|
||||
<div key={item.stars} className="rating-bar-row">
|
||||
<div className="stars-label">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`bar-star ${i < item.stars ? "filled" : ""}`}
|
||||
>
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="rating-bar">
|
||||
<div
|
||||
className="rating-fill"
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="percentage">{item.percentage}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// 模拟提交延迟
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
if (onSubmit) {
|
||||
onSubmit({ ratings, comment });
|
||||
}
|
||||
onClose();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal className="course-evaluation-portal">
|
||||
<div className="course-evaluation-overlay">
|
||||
<div className="course-evaluation-modal">
|
||||
{/* 弹窗头部 */}
|
||||
<div className="modal-header">
|
||||
<div className="header-content">
|
||||
<div className="book-icon">📖</div>
|
||||
<div className="header-text">
|
||||
<h2 className="modal-title">请对本节课进行评价</h2>
|
||||
<p className="modal-subtitle">请您客观公正的评价</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹窗内容 */}
|
||||
<div className="modal-content">
|
||||
{/* 老师信息和评分展示 */}
|
||||
<div className="teacher-evaluation-section">
|
||||
<div className="teacher-info">
|
||||
<div className="teacher-avatar">
|
||||
<img
|
||||
src="/api/placeholder/80/80"
|
||||
alt="老师头像"
|
||||
onError={(e) => {
|
||||
e.target.style.display = "none";
|
||||
e.target.nextSibling.style.display = "flex";
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="avatar-placeholder"
|
||||
style={{ display: "none" }}
|
||||
>
|
||||
顾
|
||||
</div>
|
||||
</div>
|
||||
<div className="teacher-details">
|
||||
<h3 className="teacher-name">顾华</h3>
|
||||
<p className="course-name">机械与智能制造班</p>
|
||||
<div className="teacher-rating">
|
||||
<span className="rating-score">9.5</span>
|
||||
<span className="rating-text">(863位学员评价)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评分条形图 */}
|
||||
<div className="rating-visualization">{renderRatingBars()}</div>
|
||||
</div>
|
||||
|
||||
{/* 评价维度 */}
|
||||
<div className="evaluation-dimensions">
|
||||
<div className="dimensions-grid">
|
||||
{ratingDimensions.map((dimension) => (
|
||||
<div key={dimension.key} className="dimension-item">
|
||||
<div className="dimension-label">{dimension.label}</div>
|
||||
{renderStarRating(dimension.key, ratings[dimension.key])}
|
||||
<div className="rating-text">
|
||||
({ratings[dimension.key]}/5)
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细评价 */}
|
||||
<div className="detailed-evaluation">
|
||||
<div className="evaluation-label">课程优化建议(选填)</div>
|
||||
<textarea
|
||||
className="evaluation-textarea"
|
||||
placeholder="请分享您对本次课程的改进建议(选填)..."
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
maxLength={500}
|
||||
/>
|
||||
<div className="char-count">{comment.length}/500</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹窗底部按钮 */}
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className="cancel-button"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="submit-button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "提交中..." : "提交评价"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 样式定义 */}
|
||||
<style>{`
|
||||
.course-evaluation-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.course-evaluation-modal {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: modalEnter 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes modalEnter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
||||
padding: 24px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #2196f3;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.teacher-evaluation-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.teacher-avatar {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.teacher-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.teacher-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.course-name {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.teacher-rating {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rating-score {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.rating-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.rating-visualization {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rating-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rating-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stars-label {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bar-star {
|
||||
color: #ddd;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bar-star.filled {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.rating-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rating-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ffc107 0%, #ff9800 100%);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.evaluation-dimensions {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dimensions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dimension-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.star-rating {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.star {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.star.filled,
|
||||
.star.hover {
|
||||
color: #ffc107;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.star:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.rating-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detailed-evaluation {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.evaluation-label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.evaluation-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.evaluation-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #2196f3;
|
||||
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
.char-count {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cancel-button,
|
||||
.submit-button {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background: #f8f9fa;
|
||||
color: #666;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.cancel-button:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
|
||||
color: white;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.submit-button:disabled,
|
||||
.cancel-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.course-evaluation-overlay {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dimensions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.teacher-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.teacher-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rating-score {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseEvaluationModal;
|
||||
@@ -1,690 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import CourseEvaluationModal from "./CourseEvaluationModal.jsx";
|
||||
|
||||
const VideoPlayer = ({
|
||||
title,
|
||||
courseStatus,
|
||||
startTime,
|
||||
endTime,
|
||||
onReplayRequest,
|
||||
onFullscreenChange,
|
||||
onTimeUpdate,
|
||||
}) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [volume, setVolume] = useState(1);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [showEvaluationModal, setShowEvaluationModal] = useState(false);
|
||||
const [hasVideoEnded, setHasVideoEnded] = useState(false);
|
||||
|
||||
const videoRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const controlsTimeoutRef = useRef(null);
|
||||
// 格式化课程时间显示
|
||||
const formatCourseTime = (timeString) => {
|
||||
try {
|
||||
const date = new Date(timeString);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const courseDate = new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate()
|
||||
);
|
||||
|
||||
const diffDays = Math.floor((courseDate - today) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return `今天 ${date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`;
|
||||
} else if (diffDays === 1) {
|
||||
return `明天 ${date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`;
|
||||
} else if (diffDays === -1) {
|
||||
return `昨天 ${date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`;
|
||||
} else {
|
||||
return date.toLocaleString("zh-CN", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return timeString;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理回放请求
|
||||
const handleReplayRequest = () => {
|
||||
if (onReplayRequest) {
|
||||
onReplayRequest();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理播放/暂停
|
||||
const handlePlayPause = () => {
|
||||
if (videoRef.current) {
|
||||
if (isPlaying) {
|
||||
videoRef.current.pause();
|
||||
} else {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理静音
|
||||
const handleMute = () => {
|
||||
if (videoRef.current) {
|
||||
const newMutedState = !isMuted;
|
||||
videoRef.current.muted = newMutedState;
|
||||
setIsMuted(newMutedState);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理音量调节
|
||||
const handleVolumeChange = (e) => {
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = newVolume;
|
||||
setVolume(newVolume);
|
||||
setIsMuted(newVolume === 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理全屏
|
||||
const handleFullscreen = () => {
|
||||
if (!isFullscreen) {
|
||||
if (containerRef.current.requestFullscreen) {
|
||||
containerRef.current.requestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标移动显示控制栏
|
||||
const handleMouseMove = () => {
|
||||
setShowControls(true);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
|
||||
// 3秒后隐藏控制栏(仅在播放时)
|
||||
if (isPlaying) {
|
||||
controlsTimeoutRef.current = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理课程评价弹窗关闭
|
||||
const handleEvaluationClose = () => {
|
||||
setShowEvaluationModal(false);
|
||||
};
|
||||
|
||||
// 处理课程评价提交
|
||||
const handleEvaluationSubmit = (evaluationData) => {
|
||||
setShowEvaluationModal(false);
|
||||
// 这里可以调用API提交评价数据
|
||||
};
|
||||
|
||||
// 视频事件处理
|
||||
const handleVideoEvents = {
|
||||
onPlay: () => {
|
||||
setIsPlaying(true);
|
||||
setIsLoading(false);
|
||||
setHasError(false);
|
||||
setHasVideoEnded(false);
|
||||
},
|
||||
onPause: () => {
|
||||
setIsPlaying(false);
|
||||
},
|
||||
onLoadStart: () => {
|
||||
setIsLoading(true);
|
||||
setHasError(false);
|
||||
},
|
||||
onCanPlay: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError: (e) => {
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
},
|
||||
onVolumeChange: () => {
|
||||
if (videoRef.current) {
|
||||
setVolume(videoRef.current.volume);
|
||||
setIsMuted(videoRef.current.muted);
|
||||
}
|
||||
},
|
||||
onTimeUpdate: () => {
|
||||
if (videoRef.current && onTimeUpdate) {
|
||||
const currentTime = videoRef.current.currentTime;
|
||||
const duration = videoRef.current.duration;
|
||||
onTimeUpdate({
|
||||
currentTime,
|
||||
duration,
|
||||
progress: duration > 0 ? currentTime / duration : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
onEnded: () => {
|
||||
setIsPlaying(false);
|
||||
setHasVideoEnded(true);
|
||||
// 延迟一点显示弹窗,让用户看到视频结束
|
||||
setTimeout(() => {
|
||||
setShowEvaluationModal(true);
|
||||
}, 500);
|
||||
},
|
||||
};
|
||||
|
||||
// 监听全屏状态变化
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
const fullscreen = !!document.fullscreenElement;
|
||||
setIsFullscreen(fullscreen);
|
||||
if (onFullscreenChange) {
|
||||
onFullscreenChange(fullscreen);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
};
|
||||
}, [onFullscreenChange]);
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (controlsTimeoutRef.current) {
|
||||
clearTimeout(controlsTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 统一使用固定的视频源
|
||||
const videoSrc = "/live.mp4";
|
||||
|
||||
// 根据课程状态决定是否显示视频播放器
|
||||
const shouldShowVideo = courseStatus === "live" || courseStatus === "replay";
|
||||
|
||||
// 添加容器尺寸监控日志
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
// Container size monitoring
|
||||
}
|
||||
}, [shouldShowVideo, courseStatus]);
|
||||
|
||||
// 添加视频元素尺寸监控日志
|
||||
useEffect(() => {
|
||||
if (videoRef.current && shouldShowVideo) {
|
||||
const video = videoRef.current;
|
||||
const handleLoadedMetadata = () => {};
|
||||
|
||||
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
return () => {
|
||||
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
};
|
||||
}
|
||||
}, [shouldShowVideo]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="video-player-container unified-container"
|
||||
onMouseMove={shouldShowVideo ? handleMouseMove : undefined}
|
||||
onMouseLeave={() =>
|
||||
shouldShowVideo && isPlaying && setShowControls(false)
|
||||
}
|
||||
>
|
||||
{/* 根据状态渲染不同内容 */}
|
||||
{shouldShowVideo ? (
|
||||
<>
|
||||
{/* 视频元素 */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="video-player"
|
||||
src={videoSrc}
|
||||
poster="/api/placeholder/800/450"
|
||||
autoPlay={courseStatus === "live"}
|
||||
muted={false}
|
||||
{...handleVideoEvents}
|
||||
/>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{isLoading && (
|
||||
<div className="video-overlay loading-overlay">
|
||||
<div className="loading-spinner"></div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{hasError && (
|
||||
<div className="video-overlay error-overlay">
|
||||
<div className="error-icon">⚠️</div>
|
||||
<div>视频加载失败</div>
|
||||
<button
|
||||
className="retry-button"
|
||||
onClick={() => {
|
||||
setHasError(false);
|
||||
setIsLoading(true);
|
||||
if (videoRef.current) {
|
||||
videoRef.current.load();
|
||||
}
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 播放按钮覆盖层 */}
|
||||
{!isPlaying && !isLoading && !hasError && (
|
||||
<div className="video-overlay play-overlay">
|
||||
<button className="play-button-large" onClick={handlePlayPause}>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 控制栏 */}
|
||||
<div className={`video-controls ${showControls ? "visible" : ""}`}>
|
||||
{/* 播放/暂停按钮 */}
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={handlePlayPause}
|
||||
title={isPlaying ? "暂停" : "播放"}
|
||||
>
|
||||
{isPlaying ? "⏸" : "▶"}
|
||||
</button>
|
||||
|
||||
{/* 音量控制 */}
|
||||
<div className="volume-control">
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={handleMute}
|
||||
title={isMuted ? "取消静音" : "静音"}
|
||||
>
|
||||
{isMuted || volume === 0 ? "🔇" : volume < 0.5 ? "🔉" : "🔊"}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="volume-slider"
|
||||
title="音量"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 直播状态指示器 */}
|
||||
{courseStatus === "live" && (
|
||||
<div className="live-indicator">
|
||||
<span className="live-dot"></span>
|
||||
直播中
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 回放状态指示器 */}
|
||||
{courseStatus === "replay" && (
|
||||
<div className="replay-indicator">
|
||||
<span className="replay-icon">📺</span>
|
||||
课程回放中
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 右侧控制 */}
|
||||
<div className="controls-right">
|
||||
{/* 测试评价弹窗按钮(仅开发测试用) */}
|
||||
<button
|
||||
className="control-button test-evaluation-button"
|
||||
onClick={() => {
|
||||
setShowEvaluationModal(true);
|
||||
}}
|
||||
title="测试课程评价(开发测试功能)"
|
||||
style={{
|
||||
background: "rgba(255, 193, 7, 0.8)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
📝
|
||||
</button>
|
||||
|
||||
{/* 全屏按钮 */}
|
||||
<button
|
||||
className="control-button"
|
||||
onClick={handleFullscreen}
|
||||
title={isFullscreen ? "退出全屏" : "全屏"}
|
||||
>
|
||||
{isFullscreen ? "⛶" : "⛶"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* 非播放状态的提示界面 */
|
||||
<div className="course-status-overlay">
|
||||
{courseStatus === "upcoming" && (
|
||||
<div className="upcoming-content">
|
||||
<div className="status-icon">⏰</div>
|
||||
<h3 className="status-title">此课程即将开始</h3>
|
||||
<p className="status-message">
|
||||
开始时间:{startTime ? formatCourseTime(startTime) : "待定"}
|
||||
</p>
|
||||
<div className="status-description">
|
||||
请耐心等待课程开始,届时将自动开启直播
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{courseStatus === "completed" && (
|
||||
<div className="completed-content">
|
||||
<div className="status-icon">✅</div>
|
||||
<h3 className="status-title">此课程已结束</h3>
|
||||
<p className="status-message">
|
||||
结束时间:{endTime ? formatCourseTime(endTime) : ""}
|
||||
</p>
|
||||
<button className="replay-button" onClick={handleReplayRequest}>
|
||||
查看课程回放
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 课程评价弹窗 */}
|
||||
<CourseEvaluationModal
|
||||
isVisible={showEvaluationModal}
|
||||
onClose={handleEvaluationClose}
|
||||
onSubmit={handleEvaluationSubmit}
|
||||
courseInfo={{
|
||||
title: title,
|
||||
teacher: "顾华",
|
||||
course: "机械与智能制造班",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 样式定义 */}
|
||||
<style>{`
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.error-overlay {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.play-overlay {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-button-large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.play-button-large:hover {
|
||||
background: white;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.video-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
padding: 20px 16px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.video-controls.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #ef4444;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.replay-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.replay-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.controls-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.video-player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.unified-container {
|
||||
aspect-ratio: 16/9;
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.course-status-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.upcoming-content, .completed-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-size: 18px;
|
||||
color: #e5e7eb;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-description {
|
||||
font-size: 16px;
|
||||
color: #d1d5db;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.replay-button {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.replay-button:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPlayer;
|
||||
Reference in New Issue
Block a user