完整的教务系统前端项目 - 包含所有修复和9月份数据

This commit is contained in:
KQL
2025-09-03 13:26:13 +08:00
commit 87b06d3176
270 changed files with 116169 additions and 0 deletions

56
src/App.jsx Normal file
View 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;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View 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;
}
}
}
}

View 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;

View 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;
}
}
}
}
}
}

View 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;

View 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;
}
}
}
}
}
}

View 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)}>&lt; 上一集</span>
<span className="courses-video-player-header-title">
{courseName}
</span>
<span onClick={() => handleClickBtn(2)}>下一集 &gt;</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>
);
};

View 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;

View 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;
}

View 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;

View 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;

View 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;
}

View 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;

Some files were not shown because too many files have changed in this diff Show More