完整的教务系统前端项目 - 包含所有修复和9月份数据
56
src/App.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import Layout from "./components/Layout";
|
||||
import routes from "./routes";
|
||||
import { mockData } from "./data/mockData";
|
||||
import { setStudentInfo } from "./store/slices/studentSlice";
|
||||
|
||||
// 样式文件导入
|
||||
import "./global.css";
|
||||
import "@arco-design/web-react/dist/css/arco.css";
|
||||
|
||||
const getAllRoutes = (routes) => {
|
||||
const result = [];
|
||||
|
||||
const traverse = (routeItems) => {
|
||||
routeItems.forEach((item) => {
|
||||
if (item.routes) {
|
||||
// 如果有子路由,递归处理
|
||||
traverse(item.routes);
|
||||
} else if (item.path && item.element) {
|
||||
// 如果是单个路由项,添加到结果数组
|
||||
result.push(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
traverse(routes);
|
||||
return result;
|
||||
};
|
||||
|
||||
function App() {
|
||||
const dispatch = useDispatch();
|
||||
const allRoutes = getAllRoutes(routes);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化学生信息
|
||||
if (mockData.profileOverview?.studentInfo) {
|
||||
dispatch(setStudentInfo(mockData.profileOverview.studentInfo));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
<Routes>
|
||||
{allRoutes?.map((item) => (
|
||||
<Route {...item} key={item.path} />
|
||||
))}
|
||||
</Routes>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
BIN
src/assets/font/Unitext-BlackItalic.TTF
Normal file
BIN
src/assets/images/Common/close.png
Normal file
|
After Width: | Height: | Size: 751 B |
BIN
src/assets/images/Common/lock_bg.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
src/assets/images/Common/modal_bg.png
Normal file
|
After Width: | Height: | Size: 688 KiB |
BIN
src/assets/images/Common/pdf_icon.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/assets/images/Common/time_line_clock_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/Common/time_line_dot_icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/Common/title_dot_icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/Common/title_icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/CompanyJobsPage/btn_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/CompanyJobsPage/btn_icon_2.png
Normal file
|
After Width: | Height: | Size: 709 B |
BIN
src/assets/images/CompanyJobsPage/close_icon.png
Normal file
|
After Width: | Height: | Size: 993 B |
BIN
src/assets/images/CompanyJobsPage/expand_icon.png
Normal file
|
After Width: | Height: | Size: 1013 B |
BIN
src/assets/images/CompanyJobsPage/file_icon.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
src/assets/images/CompanyJobsPage/fulltime_icon.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/images/CompanyJobsPage/internship_icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 95 KiB |
BIN
src/assets/images/CompanyJobsPage/process1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/CompanyJobsPage/process2.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/images/CompanyJobsPage/process3.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/CompanyJobsPage/process4.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/CompanyJobsPage/process5.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/images/CompanyJobsPage/process6.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/images/CompanyJobsPage/process7.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/images/CompanyJobsPage/process_dot.png
Normal file
|
After Width: | Height: | Size: 999 B |
BIN
src/assets/images/CompanyJobsPage/process_wrapper_close_bg.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/assets/images/CoursesVideoPlayer/blank_icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/images/CoursesVideoPlayer/hat_icon.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/images/CoursesVideoPlayer/living_icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/images/Dashboard/QuickAccess/icon1.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src/assets/images/Dashboard/QuickAccess/icon2.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/images/Dashboard/QuickAccess/icon3.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/images/Dashboard/StartClass/start_class_bg.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/images/Dashboard/StudyStatus/study-status_bg.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/images/HomeworkPage/homework_page_icon1.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
src/assets/images/HomeworkPage/homework_page_icon2.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
src/assets/images/InterviewSimulationPage/slide_up_icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/InterviewSimulationPage/title_icon.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/images/JobStrategyDetailPage/bar_chart.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
src/assets/images/JobStrategyDetailPage/batch.png
Normal file
|
After Width: | Height: | Size: 585 KiB |
BIN
src/assets/images/JobStrategyDetailPage/batch1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/JobStrategyDetailPage/batch2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/JobStrategyDetailPage/batch3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/JobStrategyDetailPage/slide_icon.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/PersonalProfile/course_icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/PersonalProfile/line_icon.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/assets/images/PersonalProfile/location_icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/PersonalProfile/major_icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/images/PersonalProfile/male_icon.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
src/assets/images/PersonalProfile/personal_profile_bg.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
src/assets/images/PersonalProfile/school_icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/images/PersonalProfile/study_study_bg.png
Normal file
|
After Width: | Height: | Size: 553 KiB |
BIN
src/assets/images/ProjectLibraryPage/class_icon.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/images/ProjectLibraryPage/eyes_icon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/Rank/1.png
Normal file
|
After Width: | Height: | Size: 928 B |
BIN
src/assets/images/Rank/2.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/Rank/3.png
Normal file
|
After Width: | Height: | Size: 959 B |
BIN
src/assets/images/Rank/bg.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
src/assets/images/Rank/first_icon.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/images/Rank/icon4.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/assets/images/Rank/icon5.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/assets/images/Rank/icon6.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/images/Rank/second_icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/Rank/third_icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/images/Rank/title_icon.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/images/ResumeInterviewPage/bg_1.png
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
src/assets/images/ResumeInterviewPage/bg_2.png
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
src/assets/images/ResumeInterviewPage/change_icon.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/assets/images/ResumeInterviewPage/icon_1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/ResumeInterviewPage/icon_2.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/ResumeInterviewPage/modal_bg.png
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
src/assets/images/ResumeInterviewPage/question_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/Sidebar/btn_icon.png
Normal file
|
After Width: | Height: | Size: 988 B |
BIN
src/assets/images/Sidebar/logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src/assets/images/Sidebar/sidebar_icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/assets/images/Sidebar/sidebar_menu_item_active_bg .png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
src/assets/images/Sidebar/visitor_count_bg.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/images/StageProgress/active_bg.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/assets/images/StageProgress/step1_active.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/images/StageProgress/step4_active.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/images/StageProgress/step4_default.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/StageProgress/step_active.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
src/assets/images/StageProgress/step_default.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/TaskList/frame.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
221
src/components/ClassRank/index.css
Normal file
@@ -0,0 +1,221 @@
|
||||
.module-class-rank {
|
||||
width: 373px;
|
||||
height: 100%;
|
||||
background-color: #fff7f1;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #fff;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 180px;
|
||||
height: 110px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-image: url("@/assets/images/Rank/bg.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.module-class-rank-title {
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 30px;
|
||||
color: #262626;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-spin,
|
||||
.empty-data {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.module-class-rank-podium {
|
||||
width: 333px;
|
||||
height: 138px;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
> li {
|
||||
width: 88px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
|
||||
.module-class-rank-podium-avatar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -24px;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -10px;
|
||||
width: 57px;
|
||||
height: 16px;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-name {
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
> i {
|
||||
height: 27px;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.module-class-rank-podium-item1 {
|
||||
height: 98px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/first_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #ffc15b;
|
||||
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon1.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-class-rank-podium-item2 {
|
||||
height: 80px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/second_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #9ab9e3;
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon2.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-class-rank-podium-item3 {
|
||||
height: 70px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: -40px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/Rank/third_icon.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.module-class-rank-podium-avatar {
|
||||
border-color: #d7a770;
|
||||
&::before {
|
||||
background-image: url("@/assets/images/Rank/icon3.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-class-rank-list {
|
||||
width: 333px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.module-class-rank-list-item {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
|
||||
> em {
|
||||
margin-left: 5px;
|
||||
color: #86909c;
|
||||
font-size: 20px;
|
||||
font-family: "HarmonyOS_Sans_TC_Bold";
|
||||
}
|
||||
> p {
|
||||
font-size: 14px;
|
||||
height: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: #1d2129;
|
||||
margin-left: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
> span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909c;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/components/ClassRank/index.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Avatar, Spin, Empty } from "@arco-design/web-react";
|
||||
import IconFont from "@/components/IconFont";
|
||||
import "./index.css";
|
||||
|
||||
const positions = ["item2", "item1", "item3"];
|
||||
const icons = ["icon2", "icon1", "icon3"];
|
||||
|
||||
const Rank = ({ className, data, loading }) => {
|
||||
const rankings = data?.rankings?.slice(0, 6) || [];
|
||||
|
||||
// 安全处理领奖台学生,确保至少有3个位置
|
||||
const podiumStudents = [
|
||||
rankings[1] || null, // 第2名
|
||||
rankings[0] || null, // 第1名
|
||||
rankings[2] || null, // 第3名
|
||||
];
|
||||
|
||||
const listStudents = rankings.slice(3);
|
||||
|
||||
return (
|
||||
<div className={`module-class-rank ${className}`}>
|
||||
<p className="module-class-rank-title">
|
||||
<IconFont className="title-icon" src="recuUY5nNf7DWT" />
|
||||
<span>班级排名</span>
|
||||
</p>
|
||||
{loading ? (
|
||||
<Spin size={40} className="module-class-rank-spin" />
|
||||
) : !data || !data?.rankings || data?.rankings?.length === 0 ? (
|
||||
<Empty description="暂无排名数据" className="empty-data" />
|
||||
) : (
|
||||
<>
|
||||
<ul className="module-class-rank-podium">
|
||||
{podiumStudents.map((student, index) => {
|
||||
return student ? (
|
||||
<li
|
||||
key={student.studentId}
|
||||
className={`module-class-rank-podium-${positions[index]}`}
|
||||
>
|
||||
<Avatar className="module-class-rank-podium-avatar">
|
||||
{student.avatar ? (
|
||||
<img alt="avatar" src={student.avatar} />
|
||||
) : (
|
||||
student.studentName?.charAt(0) || "?"
|
||||
)}
|
||||
</Avatar>
|
||||
<span className="module-class-rank-podium-name">
|
||||
{student.studentName || "未知"}
|
||||
</span>
|
||||
<i className={`module-class-rank-podium-${icons[index]}`}></i>
|
||||
</li>
|
||||
) : (
|
||||
<li
|
||||
key={`empty-${index}`}
|
||||
className={`module-class-rank-podium-${positions[index]} empty`}
|
||||
>
|
||||
<div className="module-class-rank-podium-placeholder">
|
||||
<span>-</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<ul className="module-class-rank-list">
|
||||
{listStudents.map((student, index) => (
|
||||
<li
|
||||
key={student.studentId}
|
||||
className="module-class-rank-list-item"
|
||||
>
|
||||
<em className="module-class-rank-num">{index + 4}</em>
|
||||
<p>{student.studentName || "未知"}</p>
|
||||
<span>{student.score}分</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Rank;
|
||||
227
src/components/CourseList/index.css
Normal file
@@ -0,0 +1,227 @@
|
||||
.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-lock-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-image: url("@/assets/images/Common/time_line_lock_icon.png");
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.not-started {
|
||||
&::before {
|
||||
content: "未开始";
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
width: 48px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
background-color: #e8e8e8;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #f2f8ff;
|
||||
border: 1px solid #4080ff !important;
|
||||
box-shadow: 0 2px 8px rgba(64, 128, 255, 0.15);
|
||||
|
||||
p {
|
||||
color: #1d2129;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.pending {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
> p {
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.time-line-item-info {
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/components/CourseList/index.jsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Collapse, Timeline, Spin } from "@arco-design/web-react";
|
||||
import { getCourseLiveList } from "@/services/courseLive";
|
||||
import "./index.css";
|
||||
|
||||
const TimelineItem = Timeline.Item;
|
||||
const CollapseItem = Collapse.Item;
|
||||
|
||||
const CourseList = ({ className = "", onCourseClick }) => {
|
||||
const [courseLiveList, setCourseLiveList] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedCourseId, setSelectedCourseId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCourseList();
|
||||
}, []);
|
||||
|
||||
const fetchCourseList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getCourseLiveList();
|
||||
if (res.success) {
|
||||
const courseList = res.data || [];
|
||||
setCourseLiveList(courseList);
|
||||
|
||||
// 设置默认选中今天的课程(如果有)
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
let foundTodayCourse = false;
|
||||
|
||||
for (const unit of courseList) {
|
||||
const todayCourse = unit.courses.find(c => c.date === todayStr);
|
||||
if (todayCourse) {
|
||||
setSelectedCourseId(todayCourse.courseId);
|
||||
// 触发课程选择事件
|
||||
if (onCourseClick) {
|
||||
onCourseClick({
|
||||
...todayCourse,
|
||||
unitName: unit.unitName
|
||||
});
|
||||
}
|
||||
foundTodayCourse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有今天的课程,选中第一个current或upcoming的课程
|
||||
if (!foundTodayCourse) {
|
||||
for (const unit of courseList) {
|
||||
const activeCourse = unit.courses.find(c => c.current || c.upcoming);
|
||||
if (activeCourse) {
|
||||
setSelectedCourseId(activeCourse.courseId);
|
||||
if (onCourseClick) {
|
||||
onCourseClick({
|
||||
...activeCourse,
|
||||
unitName: unit.unitName
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取课程列表失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 判断课程状态
|
||||
const getCourseStatus = (course) => {
|
||||
if (course.completed) return "finish";
|
||||
if (course.current) return "active";
|
||||
|
||||
// 判断未来课程的具体状态
|
||||
if (course.upcoming) {
|
||||
const courseDate = new Date(course.date);
|
||||
const today = new Date();
|
||||
|
||||
// 重置时间部分只比较日期
|
||||
courseDate.setHours(0, 0, 0, 0);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const timeDiff = courseDate - today;
|
||||
const daysDiff = Math.floor(timeDiff / (24 * 60 * 60 * 1000));
|
||||
|
||||
// 未来7天内的课程显示为"即将开始"
|
||||
if (daysDiff > 0 && daysDiff <= 7) {
|
||||
return "coming";
|
||||
}
|
||||
// 7天后的课程显示为"未开始"
|
||||
return "not-started";
|
||||
}
|
||||
|
||||
// 默认状态
|
||||
return "pending";
|
||||
};
|
||||
|
||||
// 获取图标类型
|
||||
const getDotIcon = (course) => {
|
||||
if (course.completed) return <div className="time-line-dot-icon" />;
|
||||
if (course.current) return <div className="time-line-clock-icon" />;
|
||||
return <div className="time-line-lock-icon" />;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`${className} course-list-wrapper`}>
|
||||
<p className="course-list-title">课程列表</p>
|
||||
<div className="course-list-content">
|
||||
<Spin />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
defaultActiveKey={["1"]}
|
||||
>
|
||||
{courseLiveList.map((unit, index) => (
|
||||
<CollapseItem
|
||||
key={unit.unitId}
|
||||
header={unit.unitName}
|
||||
name={String(index + 1)}
|
||||
className="course-list-item"
|
||||
>
|
||||
<Timeline>
|
||||
{unit.courses.map((course) => (
|
||||
<TimelineItem
|
||||
key={course.courseId}
|
||||
dot={getDotIcon(course)}
|
||||
lineType="dashed"
|
||||
>
|
||||
<div
|
||||
className={`time-line-item ${getCourseStatus(course)} ${selectedCourseId === course.courseId ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setSelectedCourseId(course.courseId);
|
||||
onCourseClick && onCourseClick({ ...course, unitName: unit.unitName });
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<p>{course.courseName}</p>
|
||||
<div className="time-line-item-info">
|
||||
<span>{course.teacherName}</span>
|
||||
<span>{course.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
))}
|
||||
</Timeline>
|
||||
</CollapseItem>
|
||||
))}
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseList;
|
||||
268
src/components/CoursesVideoPlayer/index.css
Normal file
@@ -0,0 +1,268 @@
|
||||
.courses-video-player-wrapper {
|
||||
width: 836px;
|
||||
height: 798px;
|
||||
position: relative;
|
||||
|
||||
.video-lock-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.courses-video-player {
|
||||
width: 100%;
|
||||
height: 545px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
|
||||
.courses-video-player-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
> span {
|
||||
color: #2c7aff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
/* 添加以下样式来防止点击时背景变色 */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.courses-video-player-header-title {
|
||||
cursor: default;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.courses-video-player-video {
|
||||
width: 100%;
|
||||
height: 478px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
.courses-video-player-info {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
height: 234px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
background-image: linear-gradient(180deg, #daecff, #ffffff);
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.courses-video-player-audience-info {
|
||||
width: 308px;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
border-right: 1px solid #f2f3f5;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
.teacher-avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 150%;
|
||||
height: 150%;
|
||||
object-fit: cover;
|
||||
object-position: center 30%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 2%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 刘杰导师头像特殊调整 */
|
||||
.teacher-avatar.teacher-liujie {
|
||||
img {
|
||||
width: 400%; /* 大幅放大 */
|
||||
height: 400%;
|
||||
object-position: center 30%; /* 调整位置 */
|
||||
top: -100%; /* 继续大幅上移 */
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.living-icon {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -8px;
|
||||
width: 62px;
|
||||
height: 20px;
|
||||
background-image: url("@/assets/images/CoursesVideoPlayer/living_icon.png");
|
||||
background-size: 100% 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-name {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.teacher-tag {
|
||||
background-color: #f2f3f5;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 8px;
|
||||
border-radius: 2px;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.living-data {
|
||||
width: 100%;
|
||||
height: 47px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.living-data-item {
|
||||
min-width: 78px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
> span:first-child {
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
> span:last-child {
|
||||
color: #1d2129;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.courses-video-player-teacher-info {
|
||||
width: 456px;
|
||||
height: 100%;
|
||||
margin-left: 20px;
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.teacher-introduce {
|
||||
width: 100%;
|
||||
min-height: 84px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #4e5969;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f2f3f5;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c9cdd4;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.courses-video-player-teacher-introduce {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.courses-video-player-teacher-tags {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
.teacher-tags {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
margin-top: 5px;
|
||||
|
||||
> li {
|
||||
box-sizing: border-box;
|
||||
padding: 2px 8px;
|
||||
background-color: #e5f1ff;
|
||||
color: #0077ff;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-right: 10px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/components/CoursesVideoPlayer/index.jsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Avatar } from "@arco-design/web-react";
|
||||
import Locked from "@/components/Locked";
|
||||
import "./index.css";
|
||||
|
||||
export default ({ className = "", isLock = false, selectedCourse, teacherData, unitPosters }) => {
|
||||
const handleClickBtn = (item) => {
|
||||
console.log(item);
|
||||
};
|
||||
|
||||
// 获取当前课程的导师信息
|
||||
const currentTeacher = selectedCourse && teacherData
|
||||
? teacherData[selectedCourse.teacherName]
|
||||
: teacherData?.["魏立慧"] || {
|
||||
name: "魏立慧",
|
||||
introduction: "企业资深一线HR,专注于为求职者提供一对一的个性化指导。通过真实招聘视角,深入剖析个人优势与短板、传授面试技巧、规划职业定位与发展路径,帮助学生快速提升求职竞争力。求职策略以实用落地为核心,注重互动交流与角色定位,让学员在轻松氛围中获得直击痛点的求职策略。",
|
||||
specialties: ["深谙用人逻辑", "擅长挖掘优势", "沟通真诚自然", "点评直击要害"],
|
||||
avatar: "//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp",
|
||||
type: "企业资深HR"
|
||||
};
|
||||
|
||||
// 需要调整头像位置的导师
|
||||
const needsAdjustment = ["赵志强", "魏立慧", "郭建辉"].includes(currentTeacher.name);
|
||||
|
||||
// 根据导师设置不同的背景色 - 这些颜色提取自实际的PNG图片背景
|
||||
const getAvatarBackground = (name) => {
|
||||
const backgrounds = {
|
||||
"刘杰": "#E3E2E0", // 浅灰色
|
||||
"郭建辉": "#E0D9D3", // 米灰色
|
||||
"赵志强": "#E3E2E0", // 浅灰色
|
||||
"孙应战": "#E3E2E0", // 浅灰色
|
||||
"魏立慧": "#DCD8D4" // 灰褐色
|
||||
};
|
||||
return backgrounds[name] || "#E3E2E0";
|
||||
};
|
||||
|
||||
// 获取当前课程信息
|
||||
const courseName = selectedCourse?.courseName || "钢铁是怎样炼成的";
|
||||
const courseDate = selectedCourse?.date || "09.01";
|
||||
const unitName = selectedCourse?.unitName || "教育体系认知";
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (date) => {
|
||||
// 将 "2025-09-02" 格式转换为 "09.02"
|
||||
if (date && date.includes('-')) {
|
||||
const parts = date.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[1]}.${parts[2]}`;
|
||||
}
|
||||
}
|
||||
// 将 "9/1" 格式转换为 "09.01"
|
||||
if (date && date.includes('/')) {
|
||||
const parts = date.split('/');
|
||||
if (parts.length === 2) {
|
||||
const month = parts[0].padStart(2, '0');
|
||||
const day = parts[1].padStart(2, '0');
|
||||
return `${month}.${day}`;
|
||||
}
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${className} courses-video-player-wrapper`}>
|
||||
{/* 直播板块 */}
|
||||
<div className="courses-video-player">
|
||||
{isLock ? (
|
||||
<Locked
|
||||
className="video-lock-wrapper"
|
||||
text="该板块将于「垂直能力提升」阶段启动后开放届时,请留意教务系统通知,您可在该板块进行线上
|
||||
1V1 求职策略定制"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="courses-video-player-header">
|
||||
<span onClick={() => handleClickBtn(1)}>< 上一集</span>
|
||||
<span className="courses-video-player-header-title">
|
||||
{courseName}
|
||||
</span>
|
||||
<span onClick={() => handleClickBtn(2)}>下一集 ></span>
|
||||
</div>
|
||||
<div className="courses-video-player-video">
|
||||
{/* 如果没有selectedCourse(公共课直播间)或者selectedCourse.current为true,都播放视频 */}
|
||||
{!selectedCourse || selectedCourse?.current ? (
|
||||
<video src="/live.mp4" autoPlay controls></video>
|
||||
) : (
|
||||
<img
|
||||
src={unitPosters?.[unitName] || unitPosters?.["岗位体系认知"]}
|
||||
alt={unitName}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="courses-video-player-info">
|
||||
{/* 直播观众信息 */}
|
||||
<div className="courses-video-player-audience-info">
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar
|
||||
className={`teacher-avatar ${needsAdjustment ? 'avatar-adjust' : ''} ${currentTeacher.name === '刘杰' ? 'teacher-liujie' : ''}`}
|
||||
style={{ backgroundColor: getAvatarBackground(currentTeacher.name) }}
|
||||
>
|
||||
<img
|
||||
alt="avatar"
|
||||
src={currentTeacher.avatar}
|
||||
/>
|
||||
</Avatar>
|
||||
{selectedCourse?.current && <div className="living-icon" />}
|
||||
</div>
|
||||
<span className="teacher-name">{currentTeacher.name}老师</span>
|
||||
<span className="teacher-tag">{unitName}</span>
|
||||
<div className="living-data">
|
||||
<div className="living-data-item">
|
||||
<span>开始</span>
|
||||
<span>{formatDateTime(courseDate)} - 14:00</span>
|
||||
</div>
|
||||
<div className="living-data-item">
|
||||
<span>时长</span>
|
||||
<span>60分钟</span>
|
||||
</div>
|
||||
<div className="living-data-item">
|
||||
<span>观看</span>
|
||||
<span>3000人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 直播教师信息 */}
|
||||
<div className="courses-video-player-teacher-info">
|
||||
<div className="courses-video-player-teacher-introduce">
|
||||
<p className="title icon1">导师介绍</p>
|
||||
<p className="teacher-introduce">
|
||||
{currentTeacher.introduction}
|
||||
</p>
|
||||
</div>
|
||||
<div className="courses-video-player-teacher-tags">
|
||||
<p className="title icon2">教师专长</p>
|
||||
<ul className="teacher-tags">
|
||||
{currentTeacher.specialties.map((specialty, index) => (
|
||||
<li key={index}>{specialty}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
109
src/components/EchartsProgress/index.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const EchartsProgress = ({
|
||||
percent = 0,
|
||||
strokeWidth = 20,
|
||||
progressColor = new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: "#0275F2" },
|
||||
{ offset: 1, color: "#389CFA" },
|
||||
]), // 进度条颜色,默认为渐变色
|
||||
backgroundColor = "#FFF", // 背景颜色
|
||||
}) => {
|
||||
const chartRef = useRef(null);
|
||||
const chartInstance = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化图表
|
||||
if (chartRef.current && !chartInstance.current) {
|
||||
chartInstance.current = echarts.init(chartRef.current);
|
||||
}
|
||||
|
||||
// 配置图表选项 - 得分环
|
||||
const option = {
|
||||
series: [
|
||||
{
|
||||
type: "gauge",
|
||||
startAngle: 90,
|
||||
radius: "100%",
|
||||
endAngle: -270,
|
||||
pointer: {
|
||||
show: false,
|
||||
},
|
||||
progress: {
|
||||
show: true,
|
||||
overlap: false,
|
||||
roundCap: true,
|
||||
clip: false,
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
color: progressColor, // 设置进度条颜色
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: strokeWidth,
|
||||
color: [
|
||||
[1, backgroundColor], // 背景颜色
|
||||
],
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
distance: 0,
|
||||
length: 10,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
distance: 50,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: percent,
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
offsetCenter: ["0%", "0%"],
|
||||
},
|
||||
},
|
||||
],
|
||||
detail: {
|
||||
formatter: "{a|{value}%}\n{c| }\n{b|整体课程完成进度}",
|
||||
rich: {
|
||||
a: {
|
||||
color: "#0077FF",
|
||||
fontSize: 40,
|
||||
align: "center",
|
||||
},
|
||||
c: { color: "transparent", lineHeight: 20 }, // 透明占位行
|
||||
b: {
|
||||
color: "#4E5969",
|
||||
fontSize: 20,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 设置图表选项
|
||||
chartInstance.current.setOption(option);
|
||||
|
||||
// 响应窗口大小变化
|
||||
const resizeHandler = () => {
|
||||
chartInstance.current.resize();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [backgroundColor, percent, progressColor, strokeWidth]);
|
||||
|
||||
return <div ref={chartRef} style={{ width: "18vw", height: "18vw" }} />;
|
||||
};
|
||||
|
||||
export default EchartsProgress;
|
||||
74
src/components/FileIcon/index.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.file-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.file-icon-fold {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 12px 12px 0;
|
||||
border-color: transparent #fff transparent transparent;
|
||||
}
|
||||
|
||||
.file-icon-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* PDF - 红色 */
|
||||
.file-icon-pdf {
|
||||
background: linear-gradient(135deg, #ff4d4d 0%, #cc0000 100%);
|
||||
}
|
||||
|
||||
.file-icon-pdf .file-icon-fold {
|
||||
border-right-color: #ffcccc;
|
||||
}
|
||||
|
||||
/* Word文档 - 蓝色 */
|
||||
.file-icon-doc {
|
||||
background: linear-gradient(135deg, #2196f3 0%, #1565c0 100%);
|
||||
}
|
||||
|
||||
.file-icon-doc .file-icon-fold {
|
||||
border-right-color: #bbdefb;
|
||||
}
|
||||
|
||||
/* PPT - 橙色 */
|
||||
.file-icon-ppt {
|
||||
background: linear-gradient(135deg, #ff9800 0%, #e65100 100%);
|
||||
}
|
||||
|
||||
.file-icon-ppt .file-icon-fold {
|
||||
border-right-color: #ffe0b2;
|
||||
}
|
||||
|
||||
/* Excel - 绿色 */
|
||||
.file-icon-excel {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||
}
|
||||
|
||||
.file-icon-excel .file-icon-fold {
|
||||
border-right-color: #c8e6c9;
|
||||
}
|
||||
|
||||
/* 默认 - 灰色 */
|
||||
.file-icon-default {
|
||||
background: linear-gradient(135deg, #9e9e9e 0%, #616161 100%);
|
||||
}
|
||||
|
||||
.file-icon-default .file-icon-fold {
|
||||
border-right-color: #e0e0e0;
|
||||
}
|
||||
48
src/components/FileIcon/index.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import "./index.css";
|
||||
|
||||
const FileIcon = ({ type = "doc" }) => {
|
||||
const getIconClass = () => {
|
||||
switch(type) {
|
||||
case 'pdf':
|
||||
return 'file-icon-pdf';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'file-icon-doc';
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return 'file-icon-ppt';
|
||||
case 'excel':
|
||||
case 'xlsx':
|
||||
return 'file-icon-excel';
|
||||
default:
|
||||
return 'file-icon-default';
|
||||
}
|
||||
};
|
||||
|
||||
const getIconText = () => {
|
||||
switch(type) {
|
||||
case 'pdf':
|
||||
return 'PDF';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'DOC';
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return 'PPT';
|
||||
case 'excel':
|
||||
case 'xlsx':
|
||||
return 'XLS';
|
||||
default:
|
||||
return 'FILE';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`file-icon ${getIconClass()}`}>
|
||||
<div className="file-icon-fold"></div>
|
||||
<div className="file-icon-text">{getIconText()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileIcon;
|
||||
12
src/components/IconFont/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
const baseUrl =
|
||||
"https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/teach_sys_icon/";
|
||||
|
||||
const IconFont = (props) => {
|
||||
const { className, src, onClick } = props;
|
||||
|
||||
return (
|
||||
<img src={`${baseUrl}${src}.png`} className={className} onClick={onClick} />
|
||||
);
|
||||
};
|
||||
|
||||
export default IconFont;
|
||||
27
src/components/InfiniteScroll/index.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.infinite-scroll-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
color: #999;
|
||||
}
|
||||
/* 新增加载中样式 */
|
||||
.loading-container {
|
||||
width: 100%;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
103
src/components/InfiniteScroll/index.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Empty, Spin } from "@arco-design/web-react";
|
||||
import "./index.css";
|
||||
|
||||
const InfiniteScroll = ({
|
||||
loadMore,
|
||||
hasMore,
|
||||
children,
|
||||
threshold = 0.1, // 触发阈值,元素可见比例
|
||||
className = "",
|
||||
empty = false,
|
||||
}) => {
|
||||
const containerRef = useRef(null);
|
||||
const sentinelRef = useRef(null);
|
||||
const observerRef = useRef(null);
|
||||
const throttleRef = useRef(null); // 节流控制
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasInitialized, setHasInitialized] = useState(false); // 首次挂载
|
||||
|
||||
// 加载更多数据的处理函数(带节流)
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
// 节流处理:500ms内只能触发一次
|
||||
if (throttleRef.current) {
|
||||
clearTimeout(throttleRef.current);
|
||||
}
|
||||
|
||||
throttleRef.current = setTimeout(() => {
|
||||
setLoading(true);
|
||||
loadMore().finally(() => {
|
||||
setLoading(false);
|
||||
throttleRef.current = null;
|
||||
});
|
||||
}, 10);
|
||||
}, [hasMore, loadMore, loading]);
|
||||
|
||||
// 设置IntersectionObserver
|
||||
useEffect(() => {
|
||||
// 初始化观察器
|
||||
const options = {
|
||||
root: containerRef.current,
|
||||
rootMargin: "0px 0px 50px 0px", // 减少提前触发距离
|
||||
threshold,
|
||||
};
|
||||
|
||||
// 创建观察器实例
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting) {
|
||||
handleLoadMore();
|
||||
}
|
||||
}, options);
|
||||
|
||||
// 开始观察哨兵元素
|
||||
if (sentinelRef.current) {
|
||||
observerRef.current.observe(sentinelRef.current);
|
||||
}
|
||||
|
||||
// 初始加载检查 - 仅在组件首次挂载时执行
|
||||
if (hasMore && !loading && !hasInitialized) {
|
||||
setHasInitialized(true);
|
||||
handleLoadMore();
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
// 清理节流定时器
|
||||
if (throttleRef.current) {
|
||||
clearTimeout(throttleRef.current);
|
||||
throttleRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [loadMore, hasMore, threshold, loading, hasInitialized, handleLoadMore]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`infinite-scroll-container ${className}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
{/* 哨兵元素 - 用于触发加载更多 */}
|
||||
<div ref={sentinelRef} className="sentinel-element"></div>
|
||||
|
||||
{!hasMore && empty && (
|
||||
<Empty description="暂无数据" className="empty-data" />
|
||||
)}
|
||||
{/* 滚动加载指示器 */}
|
||||
{loading && hasMore && (
|
||||
<div className="loading-container">
|
||||
<Spin size={20} />
|
||||
<span className="loading-text">加载中...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfiniteScroll;
|
||||