diff --git a/index.html b/index.html index c081b7c..0802c73 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,15 @@ PlayEdu +
+ diff --git a/src/main.scss b/src/main.scss index 61c5d0d..e6859b0 100644 --- a/src/main.scss +++ b/src/main.scss @@ -135,3 +135,8 @@ code { overflow: hidden; text-overflow: ellipsis; } + +#meedu-player-container { + width: 100%; + height: 100%; +} diff --git a/src/pages/course/compenents/hour.tsx b/src/pages/course/compenents/hour.tsx index 3c2100b..968ba21 100644 --- a/src/pages/course/compenents/hour.tsx +++ b/src/pages/course/compenents/hour.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; import styles from "./hour.module.scss"; import { durationFormat } from "../../../utils/index"; @@ -10,6 +9,7 @@ interface PropInterface { duration: number; record: any; progress: number; + onSuccess: (cid: number, id: number) => void; } export const HourCompenent: React.FC = ({ @@ -19,14 +19,14 @@ export const HourCompenent: React.FC = ({ duration, record, progress, + onSuccess, }) => { - const navigate = useNavigate(); return ( <>
{ - navigate(`/course/${cid}/hour/${id}`); + onSuccess(cid, id); }} >
diff --git a/src/pages/course/compenents/videoHour.tsx b/src/pages/course/compenents/videoHour.tsx new file mode 100644 index 0000000..c39fab5 --- /dev/null +++ b/src/pages/course/compenents/videoHour.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import styles from "./hour.module.scss"; +import { durationFormat } from "../../../utils/index"; + +interface PropInterface { + id: number; + cid: number; + vid: number; + title: string; + duration: number; + record: any; + progress: number; + onSuccess: (cid: number, id: number) => void; +} + +export const HourCompenent: React.FC = ({ + id, + cid, + vid, + title, + duration, + record, + progress, + onSuccess, +}) => { + return ( + <> +
{ + onSuccess(cid, id); + }} + > +
+
+ + 视频 +
+ {vid === id && ( +
+ 学习中 +
+ )} + {vid !== id && progress > 0 && progress < 100 && ( +
+ + 学习到 + {durationFormat(Number(record.finished_duration || 0))} + +
+ )} + {vid !== id && progress >= 100 && ( +
+ 已学完{" "} +
+ )} +
+
+ {title}({durationFormat(Number(duration))}) +
+
+ + ); +}; diff --git a/src/pages/course/index.tsx b/src/pages/course/index.tsx index 37e989f..f4b268e 100644 --- a/src/pages/course/index.tsx +++ b/src/pages/course/index.tsx @@ -42,6 +42,10 @@ const CoursePage = () => { }); }; + const playVideo = (cid: number, id: number) => { + navigate(`/course/${cid}/hour/${id}`); + }; + return (
@@ -140,6 +144,9 @@ const CoursePage = () => { (learnHourRecord[item.id].finished_duration * 100) / learnHourRecord[item.id].total_duration } + onSuccess={(cid: number, id: number) => { + playVideo(cid, id); + }} > )} {!learnHourRecord[item.id] && ( @@ -150,6 +157,9 @@ const CoursePage = () => { record={null} duration={item.duration} progress={0} + onSuccess={(cid: number, id: number) => { + playVideo(cid, id); + }} > )}
@@ -175,6 +185,9 @@ const CoursePage = () => { (learnHourRecord[it.id].finished_duration * 100) / learnHourRecord[it.id].total_duration } + onSuccess={(cid: number, id: number) => { + playVideo(cid, id); + }} > )} {!learnHourRecord[it.id] && ( @@ -185,6 +198,9 @@ const CoursePage = () => { record={null} duration={it.duration} progress={0} + onSuccess={(cid: number, id: number) => { + playVideo(cid, id); + }} > )}
diff --git a/src/pages/course/video.module.scss b/src/pages/course/video.module.scss new file mode 100644 index 0000000..2ef7546 --- /dev/null +++ b/src/pages/course/video.module.scss @@ -0,0 +1,93 @@ +.video-body { + width: 100%; + float: left; + height: auto; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + .back-icon { + width: 30px; + height: 30px; + position: absolute; + top: 12px; + left: 20px; + z-index: 999; + } + + .video-box { + width: 100%; + float: left; + height: 211px; + position: relative; + .alert-message { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 18px; + color: white; + z-index: 100; + .alert-button { + width: 100px; + height: 36px; + background: #ff4d4f; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-bottom: 0px; + font-size: 14px; + font-weight: 500; + color: #ffffff; + } + } + } +} + +.chapters-hours-cont { + width: 100%; + height: auto; + box-sizing: border-box; + padding: 10px 20px 20px 20px; + .hours-list-box { + width: 100%; + height: auto; + display: flex; + flex-direction: column; + .chapter-it { + width: 100%; + height: auto; + margin-top: 20px; + + .chapter-name { + width: 100%; + height: 15px; + font-size: 15px; + font-weight: 500; + color: rgba(0, 0, 0, 0.88); + line-height: 15px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + .hours-it { + width: 100%; + height: auto; + margin-top: 20px; + } + } +} diff --git a/src/pages/course/video.tsx b/src/pages/course/video.tsx new file mode 100644 index 0000000..d84f9dd --- /dev/null +++ b/src/pages/course/video.tsx @@ -0,0 +1,364 @@ +import { useEffect, useRef, useState } from "react"; +import styles from "./video.module.scss"; +import { useParams, useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { course as Course } from "../../api/index"; +import { Toast, Image } from "antd-mobile"; +import backIcon from "../../assets/images/commen/icon-back-n.png"; +import { Empty } from "../../components"; +import { HourCompenent } from "./compenents/videoHour"; + +declare const window: any; + +const CoursePlayPage = () => { + const navigate = useNavigate(); + const params = useParams(); + const systemConfig = useSelector((state: any) => state.systemConfig.value); + const user = useSelector((state: any) => state.loginUser.value.user); + const [playUrl, setPlayUrl] = useState(""); + const [playDuration, setPlayDuration] = useState(0); + const [playendedStatus, setPlayendedStatus] = useState(false); + const [lastSeeValue, setLastSeeValue] = useState({}); + const [course, setCourse] = useState({}); + const [hour, setHour] = useState({}); + const [loading, setLoading] = useState(false); + const [isLastpage, setIsLastpage] = useState(false); + const [totalHours, setTotalHours] = useState([]); + const [playingTime, setPlayingTime] = useState(0); + const [watchedSeconds, setWatchedSeconds] = useState(0); + const [chapters, setChapters] = useState([]); + const [hours, setHours] = useState({}); + const [learnRecord, setLearnRecord] = useState({}); + const [learnHourRecord, setLearnHourRecord] = useState({}); + const myRef = useRef(0); + const playRef = useRef(0); + const watchRef = useRef(0); + const totalRef = useRef(0); + + useEffect(() => { + getCourse(); + getDetail(); + }, [params.courseId, params.hourId]); + + useEffect(() => { + myRef.current = playDuration; + }, [playDuration]); + + useEffect(() => { + playRef.current = playingTime; + }, [playingTime]); + + useEffect(() => { + watchRef.current = watchedSeconds; + }, [watchedSeconds]); + + useEffect(() => { + totalRef.current = hour.duration; + }, [hour]); + + const getCourse = () => { + Course.detail(Number(params.courseId)).then((res: any) => { + setChapters(res.data.chapters); + setHours(res.data.hours); + if (res.data.learn_record) { + setLearnRecord(res.data.learn_record); + } + if (res.data.learn_hour_records) { + setLearnHourRecord(res.data.learn_hour_records); + } + let totalHours: any = []; + if (res.data.chapters.length === 0) { + setTotalHours(res.data.hours[0]); + totalHours = res.data.hours[0]; + } else if (res.data.chapters.length > 0) { + const arr: any = []; + for (let key in res.data.hours) { + res.data.hours[key].map((item: any) => { + arr.push(item); + }); + } + setTotalHours(arr); + totalHours = arr; + } + const index = totalHours.findIndex( + (i: any) => i.id === Number(params.hourId) + ); + if (index === totalHours.length - 1) { + setIsLastpage(true); + } + }); + }; + + const getDetail = () => { + if (loading) { + return true; + } + setLoading(true); + Course.play(Number(params.courseId), Number(params.hourId)) + .then((res: any) => { + setCourse(res.data.course); + setHour(res.data.hour); + document.title = res.data.hour.title; + let record = res.data.user_hour_record; + let params = null; + if (record && record.finished_duration && record.is_finished === 0) { + params = { + time: 5, + pos: record.finished_duration, + }; + setLastSeeValue(params); + setWatchedSeconds(record.finished_duration); + } else if (record && record.is_finished === 1) { + setWatchedSeconds(res.data.hour.duration); + } + getVideoUrl(params); + setLoading(false); + }) + .catch((e) => { + setLoading(false); + }); + }; + + const getVideoUrl = (data: any) => { + Course.playUrl(Number(params.courseId), Number(params.hourId)).then( + (res: any) => { + setPlayUrl(res.data.url); + initDPlayer(res.data.url, 0, data); + } + ); + }; + + const initDPlayer = (playUrl: string, isTrySee: number, params: any) => { + let banDrag = + systemConfig.playerIsDisabledDrag && + watchRef.current < totalRef.current && + watchRef.current === 0; + window.player = new window.DPlayer({ + container: document.getElementById("meedu-player-container"), + autoplay: false, + video: { + url: playUrl, + pic: systemConfig.playerPoster, + }, + try: isTrySee === 1, + bulletSecret: { + enabled: systemConfig.playerIsEnabledBulletSecret, + text: systemConfig.playerBulletSecretText + .replace("{name}", user.name) + .replace("{email}", user.email) + .replace("{idCard}", user.id_card), + size: "14px", + color: systemConfig.playerBulletSecretColor || "red", + opacity: Number(systemConfig.playerBulletSecretOpacity), + }, + ban_drag: banDrag, + last_see_pos: params, + }); + // 监听播放进度更新evt + window.player.on("timeupdate", () => { + let currentTime = parseInt(window.player.video.currentTime); + if ( + systemConfig.playerIsDisabledDrag && + watchRef.current < totalRef.current && + currentTime - playRef.current >= 2 && + currentTime > watchRef.current + ) { + Toast.show("首次学习禁止快进"); + window.player.seek(watchRef.current); + } else { + setPlayingTime(currentTime); + playTimeUpdate(parseInt(window.player.video.currentTime), false); + } + }); + window.player.on("ended", () => { + if ( + systemConfig.playerIsDisabledDrag && + watchRef.current < totalRef.current && + window.player.video.duration - playRef.current >= 2 + ) { + window.player.seek(playRef.current); + return; + } + setPlayingTime(0); + setPlayendedStatus(true); + playTimeUpdate(parseInt(window.player.video.currentTime), true); + exitFullscreen(); + window.player && window.player.destroy(); + }); + setLoading(false); + }; + + const playTimeUpdate = (duration: number, isEnd: boolean) => { + if (duration - myRef.current >= 10 || isEnd === true) { + setPlayDuration(duration); + Course.record( + Number(params.courseId), + Number(params.hourId), + duration + ).then((res: any) => {}); + Course.playPing(Number(params.courseId), Number(params.hourId)).then( + (res: any) => {} + ); + } + }; + + const goNextVideo = () => { + const index = totalHours.findIndex( + (i: any) => i.id === Number(params.hourId) + ); + if (index === totalHours.length - 1) { + setIsLastpage(true); + Toast.show("已经是最后一节了!"); + } else if (index < totalHours.length - 1) { + navigate(`/course/${params.courseId}/hour/${totalHours[index + 1].id}`, { + replace: true, + }); + } + }; + + const exitFullscreen = () => { + let de: any; + de = document; + if (de.fullscreenElement !== null) { + de.exitFullscreen(); + } else if (de.mozCancelFullScreen) { + de.mozCancelFullScreen(); + } else if (de.webkitCancelFullScreen) { + de.webkitCancelFullScreen(); + } + }; + + const playVideo = (cid: number, id: number) => { + window.player && window.player.destroy(); + navigate(`/course/${cid}/hour/${id}`, { replace: true }); + }; + + return ( +
+
+ navigate(-1)} + /> +
+
+ {playendedStatus && ( +
+ {isLastpage && ( +
navigate(`/course/${params.courseId}`)} + > + 恭喜你学完最后一节 +
+ )} + {!isLastpage && ( +
{ + window.player && window.player.destroy(); + setLastSeeValue({}); + setPlayendedStatus(false); + goNextVideo(); + }} + > + 播放下一节 +
+ )} +
+ )} +
+
+
+ {chapters.length === 0 && JSON.stringify(hours) === "{}" && }{" "} + {chapters.length === 0 && JSON.stringify(hours) !== "{}" && ( +
+ {hours[0].map((item: any, index: number) => ( +
+ {learnHourRecord[item.id] && ( + { + playVideo(cid, id); + }} + > + )} + {!learnHourRecord[item.id] && ( + { + playVideo(cid, id); + }} + > + )} +
+ ))} +
+ )} + {chapters.length > 0 && JSON.stringify(hours) !== "{}" && ( +
+ {chapters.map((item: any, index: number) => ( +
+
{item.name}
+ {hours[item.id] && + hours[item.id].map((it: any, int: number) => ( +
+ {learnHourRecord[it.id] && ( + { + playVideo(cid, id); + }} + > + )} + {!learnHourRecord[it.id] && ( + { + playVideo(cid, id); + }} + > + )} +
+ ))} +
+ ))} +
+ )} +
+
+ ); +}; + +export default CoursePlayPage; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ac0416c..4be6298 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,6 +11,7 @@ import ChangePasswordPage from "../pages/change-password/index"; import ChangeDepartmentPage from "../pages/change-department/index"; import StudyPage from "../pages/study/index"; import CoursePage from "../pages/course/index"; +import CoursePlayPage from "../pages/course/video"; import PrivateRoute from "../components/private-route"; let RootPage: any = null; @@ -67,6 +68,10 @@ const routes: RouteObject[] = [ path: "/course/:courseId", element: } />, }, + { + path: "/course/:courseId/hour/:hourId", + element: } />, + }, ], }, ];