liushengqiang 2 years ago
parent
commit
0f248c8a0f
72 changed files with 979 additions and 24 deletions
  1. 3 2
      src/page-gym/App.tsx
  2. 25 0
      src/page-gym/api.ts
  3. 59 0
      src/page-gym/custom-plugins/EvaluatingWork/index.tsx
  4. BIN
      src/page-gym/custom-plugins/ExerciseStatistics/icon-time.png
  5. 64 0
      src/page-gym/custom-plugins/ExerciseStatistics/index.module.less
  6. 62 0
      src/page-gym/custom-plugins/ExerciseStatistics/index.tsx
  7. 12 0
      src/page-gym/custom-plugins/HomeWork/index.module.less
  8. 85 0
      src/page-gym/custom-plugins/HomeWork/index.tsx
  9. BIN
      src/page-gym/custom-plugins/guide-page/child/0.png
  10. BIN
      src/page-gym/custom-plugins/guide-page/child/1.png
  11. BIN
      src/page-gym/custom-plugins/guide-page/child/10.png
  12. BIN
      src/page-gym/custom-plugins/guide-page/child/11.png
  13. BIN
      src/page-gym/custom-plugins/guide-page/child/12.png
  14. BIN
      src/page-gym/custom-plugins/guide-page/child/13.png
  15. BIN
      src/page-gym/custom-plugins/guide-page/child/14.png
  16. BIN
      src/page-gym/custom-plugins/guide-page/child/15.png
  17. BIN
      src/page-gym/custom-plugins/guide-page/child/16.png
  18. BIN
      src/page-gym/custom-plugins/guide-page/child/17.png
  19. BIN
      src/page-gym/custom-plugins/guide-page/child/18.png
  20. BIN
      src/page-gym/custom-plugins/guide-page/child/19.png
  21. BIN
      src/page-gym/custom-plugins/guide-page/child/2.png
  22. BIN
      src/page-gym/custom-plugins/guide-page/child/20.png
  23. BIN
      src/page-gym/custom-plugins/guide-page/child/21.png
  24. BIN
      src/page-gym/custom-plugins/guide-page/child/22.png
  25. BIN
      src/page-gym/custom-plugins/guide-page/child/23.png
  26. BIN
      src/page-gym/custom-plugins/guide-page/child/24.png
  27. BIN
      src/page-gym/custom-plugins/guide-page/child/25.png
  28. BIN
      src/page-gym/custom-plugins/guide-page/child/26.png
  29. BIN
      src/page-gym/custom-plugins/guide-page/child/27.png
  30. BIN
      src/page-gym/custom-plugins/guide-page/child/28.png
  31. BIN
      src/page-gym/custom-plugins/guide-page/child/29.png
  32. BIN
      src/page-gym/custom-plugins/guide-page/child/3.png
  33. BIN
      src/page-gym/custom-plugins/guide-page/child/30.png
  34. BIN
      src/page-gym/custom-plugins/guide-page/child/4.png
  35. BIN
      src/page-gym/custom-plugins/guide-page/child/5.png
  36. BIN
      src/page-gym/custom-plugins/guide-page/child/6.png
  37. BIN
      src/page-gym/custom-plugins/guide-page/child/7.png
  38. BIN
      src/page-gym/custom-plugins/guide-page/child/8.png
  39. BIN
      src/page-gym/custom-plugins/guide-page/child/9.png
  40. 1 0
      src/page-gym/custom-plugins/guide-page/child/index.json
  41. 32 0
      src/page-gym/custom-plugins/guide-page/index.module.less
  42. 35 0
      src/page-gym/custom-plugins/guide-page/index.tsx
  43. BIN
      src/page-gym/custom-plugins/guide-page/mp3/0.mp3
  44. BIN
      src/page-gym/custom-plugins/guide-page/mp3/1.mp3
  45. BIN
      src/page-gym/custom-plugins/guide-page/mp3/2.mp3
  46. BIN
      src/page-gym/custom-plugins/guide-page/mp3/3.mp3
  47. BIN
      src/page-gym/custom-plugins/guide-page/mp3/4.mp3
  48. BIN
      src/page-gym/custom-plugins/guide-page/mp3/5.mp3
  49. BIN
      src/page-gym/custom-plugins/guide-page/mp3/6.mp3
  50. BIN
      src/page-gym/custom-plugins/guide-page/mp3/7.mp3
  51. BIN
      src/page-gym/custom-plugins/guide-page/mp3/8.mp3
  52. 73 0
      src/page-gym/custom-plugins/guide-page/popcontent.tsx
  53. 1 0
      src/page-gym/custom-plugins/guide-page/steps/constant.ts
  54. BIN
      src/page-gym/custom-plugins/guide-page/steps/icon.png
  55. 132 0
      src/page-gym/custom-plugins/guide-page/steps/steps.module.less
  56. 38 0
      src/page-gym/custom-plugins/guide-page/steps/text.json
  57. 103 0
      src/page-gym/custom-plugins/guide-page/steps/zero-step.tsx
  58. 50 0
      src/page-gym/custom-plugins/recording-time/index.tsx
  59. 32 0
      src/page-gym/custom-plugins/vip-verify/index.module.less
  60. 85 0
      src/page-gym/custom-plugins/vip-verify/index.tsx
  61. BIN
      src/page-gym/custom-plugins/vip-verify/tips.png
  62. 0 4
      src/page-gym/detail/index.module.less
  63. 23 5
      src/page-gym/detail/index.tsx
  64. 8 1
      src/page-gym/header-top/index.tsx
  65. 1 1
      src/page-gym/header-top/title/index.tsx
  66. 3 3
      src/page-gym/helper-model/index.tsx
  67. 4 0
      src/state.ts
  68. 2 2
      src/style.css
  69. 43 4
      src/utils/index.ts
  70. 1 1
      src/utils/queryString.ts
  71. 1 0
      src/view/music-score/index.tsx
  72. 1 1
      src/view/selection/index.tsx

+ 3 - 2
src/page-gym/App.tsx

@@ -3,7 +3,7 @@ import { computed, defineComponent, onBeforeMount, onMounted } from "vue";
 import { RouterView } from "vue-router";
 import TheError from "../components/The-error";
 import { setUserInfo, storeData } from "../store";
-import { getRandomKey, setToken } from "../utils";
+import { getRandomKey, setBehaviorId, setCampId, setToken } from "../utils";
 import { getQuery } from "../utils/queryString";
 import Notfind from "../view/notfind";
 import { employeeQueryUserInfo, studentQueryUserInfo, teacherQueryUserInfo } from "./api";
@@ -34,7 +34,8 @@ export default defineComponent({
 				setToken(query.Authorization);
 			}
 			setUser();
-			localStorage.setItem("behaviorId", getRandomKey());
+			setBehaviorId(getRandomKey())
+			setCampId(query.campId)
 		});
 		onMounted(() => {
 			document.getElementById("loading")!.className = "";

+ 25 - 0
src/page-gym/api.ts

@@ -43,3 +43,28 @@ export const sysMusicScoreQueryPage2 = (params: any) => {
 export const suggestionAdd = (data: any) => {
 	return request.post("/suggestion/add", { data });
 };
+
+/** 记录训练时长 */
+export const sysMusicRecordAdd = (data: any) => {
+	return request.post("/sysMusicRecord/add", { data });
+};
+/** 获取训练时长 */
+export const tempLittleArtistTrainingCampGetUserTrainingTime = () => {
+	return request.post("/tempLittleArtistTrainingCamp/getUserTrainingTime");
+};
+/** 添加作业记录 */
+export const studentCourseHomeworkAddStudentHomeworkRecord = (params: any) => {
+	return request.get("/studentCourseHomework/addStudentHomeworkRecord", { params });
+};
+/** 获取作业详情 */
+export const studentCourseHomeworkHomeworkDetail = (id: any) => {
+	return request.get(`/studentCourseHomework/homeworkDetail?id=${id}`);
+};
+/** 获取进度详情 */
+export const lessonExaminationGetDetail = (params: any) => {
+	return request.get(`/lessonExamination/getDetail`, { params });
+};
+/** 添加进度评测记录 */
+export const lessonExaminationSubmit = (data: any) => {
+	return request.get(`/lessonExamination/submit`, { data });
+};

+ 59 - 0
src/page-gym/custom-plugins/EvaluatingWork/index.tsx

@@ -0,0 +1,59 @@
+import { defineComponent, onMounted, reactive, watch } from "vue";
+import { useRoute } from "vue-router";
+import { verifyMembershipServices } from "../vip-verify";
+import { lessonExaminationGetDetail, lessonExaminationSubmit } from "../../api";
+import { IDifficulty } from "/src/state";
+import { getQuery } from "/src/utils/queryString";
+import { evaluatingData } from "/src/view/evaluating";
+
+export default defineComponent({
+	name: "EvaluatingWork",
+	setup() {
+		const query = getQuery();
+		const evaluatingWorkData = reactive({
+			difficulty: "" as IDifficulty,
+			evaluatingRecord: query.evaluatingRecord,
+		});
+		/** 隐藏评测功能 */
+		const handleHide = () => {
+			const btn = document.getElementById("tips-step-2");
+			if (btn) {
+				btn.style.display = "none";
+				btn.click();
+			}
+		};
+		/** 获取作业详情 */
+		const getWorkData = async () => {
+			try {
+				const res = await lessonExaminationGetDetail({
+					studentLessonExaminationDetailId: evaluatingWorkData.evaluatingRecord,
+				});
+				if (res?.data) {
+					evaluatingWorkData.difficulty = res.data.heardLevel;
+				}
+			} catch (error) {}
+		};
+		/** 添加记录 */
+		const addEvaluatingWorkRecored = async (data: any) => {
+			try {
+				const res = await lessonExaminationSubmit({
+					studentLessonExaminationDetailId: evaluatingWorkData.evaluatingRecord,
+					score: data?.score || 0,
+				});
+			} catch (error) {
+				console.log(error);
+			}
+		};
+		watch(() => evaluatingData.resulstMode, () => {
+			if (evaluatingData.resulstMode) {
+				addEvaluatingWorkRecored(evaluatingData.resultData)
+			}
+		})
+		onMounted(() => {
+			handleHide();
+			getWorkData();
+			verifyMembershipServices();
+		});
+		return () => <div></div>;
+	},
+});

BIN
src/page-gym/custom-plugins/ExerciseStatistics/icon-time.png


+ 64 - 0
src/page-gym/custom-plugins/ExerciseStatistics/index.module.less

@@ -0,0 +1,64 @@
+.exerciseStatistics {
+    position: fixed;
+    left: 20px;
+    bottom: 26px;
+}
+
+.btnTimeWrap {
+    position: relative;
+    display: flex;
+    align-items: center;
+    font-size: 10px;
+    border-radius: 20px;
+    --animation-time: .3s;
+    color: #fff;
+    transition: all var(--animation-time);
+    .icon {
+        position: relative;
+        display: block;
+        width: 34px;
+        height: 34px;
+        transition: all var(--animation-time);
+        filter: drop-shadow(0px 2px 4px rgba(2, 91, 86, 0.4));
+    }
+
+    .btnTietle {
+        position: absolute;
+        left: -24%;
+        bottom: -26%;
+        background: linear-gradient(180deg, #FF9941 0%, #FFC174 100%);
+        border-radius: 6px;
+        box-shadow: 0px 2px 4px 0px rgba(2, 91, 86, 0.4);
+        font-size: 9px;
+        white-space: nowrap;
+        padding: 1px 4px;
+        line-height: 12px;
+        transition: all var(--animation-time);
+        transform-origin: center center;
+    }
+    .timeTitle{
+        white-space: nowrap;
+        max-width: 0;
+        overflow: hidden;
+        transition: all var(--animation-time);
+    }
+
+    &.hide {
+        padding: 3px;
+        background: rgba(0, 73, 68, .4);
+        .icon {
+            width: 22px;
+            height: 22px;
+            filter:none;
+        }
+        .btnTietle{
+            position: absolute;
+            transform: scale(0);
+        }
+        .timeTitle{
+            max-width: 100px;
+            padding: 0 3px;
+            transition-delay: .5s;
+        }
+    }
+}

+ 62 - 0
src/page-gym/custom-plugins/ExerciseStatistics/index.tsx

@@ -0,0 +1,62 @@
+import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import styles from "./index.module.less";
+import iconTime from "./icon-time.png";
+import { tempLittleArtistTrainingCampGetUserTrainingTime } from "../../api";
+import { getSecondRPM } from "/src/utils";
+import state from "/src/state";
+
+// 练习统计
+export default defineComponent({
+	name: "ExerciseStatistics",
+	setup(props, ctx) {
+		const data = reactive({
+			isHidden: true,
+			time: 0,
+			timer: null as any,
+		});
+		const getTime = async () => {
+			try {
+				const res = await tempLittleArtistTrainingCampGetUserTrainingTime();
+				if (res?.data) {
+					data.time = res.data;
+				}
+			} catch (error) {}
+		};
+
+		const handleStart = () => {
+			data.timer = setInterval(() => {
+				data.time += 1;
+			}, 1000);
+		};
+		const handleStop = () => {
+			clearInterval(data.timer);
+		};
+		const showTime = computed(() => {
+			return getSecondRPM(data.time);
+		});
+		watch(
+			() => state.playState,
+			() => {
+				if (state.playState == "play") {
+					handleStart();
+				} else {
+					handleStop();
+				}
+			}
+		);
+		onMounted(() => {
+			getTime();
+		});
+		return () => (
+			<div class={styles.exerciseStatistics} onClick={() => (data.isHidden = !data.isHidden)}>
+				<div class={[styles.btnTimeWrap, data.isHidden ? "" : styles.hide]}>
+					<img class={styles.icon} src={iconTime} />
+					<div class={styles.btnTietle}>练习时长</div>
+					<div class={styles.timeTitle}>
+						今日练习<span style={{ fontWeight: 500 }}>{showTime.value}</span>
+					</div>
+				</div>
+			</div>
+		);
+	},
+});

+ 12 - 0
src/page-gym/custom-plugins/HomeWork/index.module.less

@@ -0,0 +1,12 @@
+.homework{
+    position: fixed;
+    left: 10px;
+    top: 70px;
+    background-color: rgba(0, 0, 0, .6);
+    border-radius: 20px;
+    font-size: 14px;
+    color: #fff;
+    padding: 5px 8px;
+    line-height: 1;
+    font-weight: 300;
+}

+ 85 - 0
src/page-gym/custom-plugins/HomeWork/index.tsx

@@ -0,0 +1,85 @@
+import { defineComponent, onMounted, reactive, watch } from "vue";
+import styles from "./index.module.less";
+import { verifyMembershipServices } from "../vip-verify";
+import { studentCourseHomeworkAddStudentHomeworkRecord, studentCourseHomeworkHomeworkDetail } from "../../api";
+import state, { handleSetSpeed } from "/src/state";
+import { getQuery } from "/src/utils/queryString";
+
+export default defineComponent({
+	name: "HomeWork",
+	setup() {
+		const training = reactive({
+			isHomeWork: false, // 是否是作业模式
+			trainingTimes: 0,
+			trainingSpeed: 0,
+			times: 0,
+			workRecord: "",
+			isAddOk: 0,
+		});
+		const query = getQuery();
+		training.workRecord = query.workRecord as any;
+		training.isHomeWork = true;
+
+		/** 隐藏评测功能 */
+		const handleHide = () => {
+			const btn = document.getElementById("tips-step-2");
+			if (btn) {
+				btn.style.display = "none";
+			}
+			const ids = ["tips-step-4", "tips-step-6", "tips-step-8", "selectionBox"];
+			for (let i = 0; i < ids.length; i++) {
+				const speedBtn = document.getElementById(ids[i]);
+				if (speedBtn) {
+					speedBtn.style.pointerEvents = "none";
+					if (ids[i] != "selectionBox") {
+						speedBtn.style.opacity = ".7";
+					}
+				}
+			}
+		};
+		/** 获取作业详情 */
+		const getWorkData = async () => {
+			const res = await studentCourseHomeworkHomeworkDetail(training.workRecord);
+			if (res?.data) {
+				training.times = res.data.times || 0;
+				training.trainingTimes = res.data.trainingTimes || 0;
+				training.trainingSpeed = res.data.trainingSpeed;
+				if (training.trainingSpeed && training.isAddOk === 0) {
+					handleSetSpeed(training.trainingSpeed);
+				}
+			}
+		};
+		watch(() => training.isAddOk, getWorkData);
+
+		/** 添加作业记录 */
+		const addHomeworkRecored = async () => {
+			if (!training.isHomeWork) return;
+			try {
+				const res = await studentCourseHomeworkAddStudentHomeworkRecord({
+					id: training.workRecord,
+				});
+				if (res?.code == 200) {
+					training.isAddOk += 1;
+				}
+			} catch (error) {}
+		};
+		watch(
+			() => state.playEnd,
+			() => {
+				if (state.playEnd) {
+					addHomeworkRecored();
+				}
+			}
+		);
+		onMounted(() => {
+			handleHide();
+			getWorkData();
+			verifyMembershipServices();
+		});
+		return () => (
+			<div class={styles.homework}>
+				{training.trainingTimes} / {training.times} 次
+			</div>
+		);
+	},
+});

BIN
src/page-gym/custom-plugins/guide-page/child/0.png


BIN
src/page-gym/custom-plugins/guide-page/child/1.png


BIN
src/page-gym/custom-plugins/guide-page/child/10.png


BIN
src/page-gym/custom-plugins/guide-page/child/11.png


BIN
src/page-gym/custom-plugins/guide-page/child/12.png


BIN
src/page-gym/custom-plugins/guide-page/child/13.png


BIN
src/page-gym/custom-plugins/guide-page/child/14.png


BIN
src/page-gym/custom-plugins/guide-page/child/15.png


BIN
src/page-gym/custom-plugins/guide-page/child/16.png


BIN
src/page-gym/custom-plugins/guide-page/child/17.png


BIN
src/page-gym/custom-plugins/guide-page/child/18.png


BIN
src/page-gym/custom-plugins/guide-page/child/19.png


BIN
src/page-gym/custom-plugins/guide-page/child/2.png


BIN
src/page-gym/custom-plugins/guide-page/child/20.png


BIN
src/page-gym/custom-plugins/guide-page/child/21.png


BIN
src/page-gym/custom-plugins/guide-page/child/22.png


BIN
src/page-gym/custom-plugins/guide-page/child/23.png


BIN
src/page-gym/custom-plugins/guide-page/child/24.png


BIN
src/page-gym/custom-plugins/guide-page/child/25.png


BIN
src/page-gym/custom-plugins/guide-page/child/26.png


BIN
src/page-gym/custom-plugins/guide-page/child/27.png


BIN
src/page-gym/custom-plugins/guide-page/child/28.png


BIN
src/page-gym/custom-plugins/guide-page/child/29.png


BIN
src/page-gym/custom-plugins/guide-page/child/3.png


BIN
src/page-gym/custom-plugins/guide-page/child/30.png


BIN
src/page-gym/custom-plugins/guide-page/child/4.png


BIN
src/page-gym/custom-plugins/guide-page/child/5.png


BIN
src/page-gym/custom-plugins/guide-page/child/6.png


BIN
src/page-gym/custom-plugins/guide-page/child/7.png


BIN
src/page-gym/custom-plugins/guide-page/child/8.png


BIN
src/page-gym/custom-plugins/guide-page/child/9.png


File diff suppressed because it is too large
+ 1 - 0
src/page-gym/custom-plugins/guide-page/child/index.json


+ 32 - 0
src/page-gym/custom-plugins/guide-page/index.module.less

@@ -0,0 +1,32 @@
+.guidePage {
+    width: 100vw;
+    height: 100vh;
+    background-color: transparent;
+    margin: 0;
+    max-width: initial;
+    overflow: hidden;
+}
+
+.bottom {
+    display: flex;
+}
+
+.content {
+    height: 100vh;
+    position: relative;
+
+    .step {
+        position: absolute;
+        bottom: 100PX;
+        right: 100PX;
+        z-index: 999;
+    }
+
+    .child {
+        width: 196PX;
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        z-index: 1000;
+    }
+}

+ 35 - 0
src/page-gym/custom-plugins/guide-page/index.tsx

@@ -0,0 +1,35 @@
+import { defineComponent } from "vue";
+import { Popup } from "vant";
+import PopContent from "./popcontent";
+import state from "/src/state";
+import styles from "./index.module.less";
+
+export default defineComponent({
+	name: "guide-page",
+	data() {
+		return {
+			show: false,
+			step: -1,
+		};
+	},
+	mounted() {
+		if (localStorage.getItem("tips-status") !== "showed" && state.modeType === "practise") {
+			this.step = 0;
+			this.show = true;
+		}
+	},
+	methods: {
+		close() {
+			this.show = false;
+			this.step = -1;
+			localStorage.setItem("tips-status", "showed");
+		},
+	},
+	render() {
+		return (
+			<Popup class={styles.guidePage} v-model:show={this.show} overlayStyle={{ background: "transparent" }} onClosed={() => (this.show = false)}>
+				<PopContent onClose={this.close} />
+			</Popup>
+		);
+	},
+});

BIN
src/page-gym/custom-plugins/guide-page/mp3/0.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/1.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/2.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/3.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/4.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/5.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/6.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/7.mp3


BIN
src/page-gym/custom-plugins/guide-page/mp3/8.mp3


+ 73 - 0
src/page-gym/custom-plugins/guide-page/popcontent.tsx

@@ -0,0 +1,73 @@
+import { defineComponent } from 'vue'
+import store from 'store'
+import Childs from './child/index.json'
+import styles from './index.module.less'
+
+import ZeroStep from './steps/zero-step'
+
+let timer: any = null
+let needStop = false
+
+export default defineComponent({
+  name: 'DetailTipsContent',
+  props: {
+    onClose: {
+      type: Function,
+      default: () => {}
+    }
+  },
+  data() {
+    return {
+      activeImgNo: 0,
+      step: -1,
+    }
+  },
+  mounted() {
+    if (store.get('tips-status') !== 'showed') {
+      this.step = 0
+    }
+  },
+  methods: {
+    startPlay() {
+      needStop = false
+      clearTimeout(timer)
+      timer = setTimeout(() => {
+        let n = this.activeImgNo + 1
+        if (n > 30) {
+          n = 0
+        }
+        if (!(needStop)) {
+          this.activeImgNo = n
+          this.startPlay()
+        } else {
+          needStop = false
+        }
+      }, 40.3333)
+    },
+    stopPlay() {
+      needStop = true
+    },
+    close() {
+      this.step = -1
+      this.onClose()
+    }
+  },
+  render() {
+    return (
+      <div class={styles.content} id="tips-step-container">
+        {this.step > -1 ? (
+          <ZeroStep
+            class={styles.step}
+            play={this.startPlay}
+            stop={this.stopPlay}
+            step={this.step}
+            onNext={(step: number) => this.step = step}
+            onClose={this.close}
+          />
+        ) : null}
+        {/* @ts-ignore */}
+        <img class={styles.child} src={Childs[this.activeImgNo]}/>
+      </div>
+    )
+  }
+})

+ 1 - 0
src/page-gym/custom-plugins/guide-page/steps/constant.ts

@@ -0,0 +1 @@
+export const steps = [0, 1, 2, 4, 5, 6, 7, 8]

BIN
src/page-gym/custom-plugins/guide-page/steps/icon.png


+ 132 - 0
src/page-gym/custom-plugins/guide-page/steps/steps.module.less

@@ -0,0 +1,132 @@
+.messagebox{
+  width: 190PX;
+  background: #FFFFFF;
+  border: 2px solid #01C1B5;
+  color: #000000;
+  font-size: 13PX;
+  padding: 10PX 16PX;
+  border-radius: 8PX;
+  position: relative;
+  line-height: 1.8;
+  margin-bottom: 20PX;
+  >h3{
+    color: #01C1B5;
+    font-size: 17PX;
+    margin: 0;
+    // margin-bottom: 10PX;
+  }
+  &::after{
+    content: '';
+    position: absolute;
+    bottom: -17PX;
+    right: 10PX;
+    background: url('./icon.png') no-repeat center;
+    width: 20PX;
+    height: 20PX;
+    background-size: contain;
+  }
+}
+@keyframes changsize {
+  0% {
+    transform: scale(.9);
+  }
+
+  50%  {
+    transform: scale(1);
+  }
+
+  100% {
+    transform: scale(.9);
+  }
+}
+.cloneParent{
+  background-color: rgb(1, 193, 181);
+  // padding: 0;
+  padding-top: 0!important;
+  border-radius: 10PX;
+  min-height: 60PX;
+  animation-duration: 1.5s;
+  animation-name: changsize;
+  animation-iteration-count:infinite;
+  display: inline-flex!important;
+  align-items: center;
+  :global{
+    .van-button{
+      border: none;
+      width: calc(20px * var(--screen));
+      height: calc(20px * var(--screen));
+      background-color: transparent;
+    }
+    .van-badge__wrapper{
+      .van-badge{
+        font-size: calc(8px * var(--screen));
+        word-break: keep-all;
+        background-color: #ECECEC;
+        color: #333;
+      }
+    }
+    .van-circle__text{
+      font-size: calc(6px * var(--screen));
+      color: #fff;
+      text-align: center;
+      padding: 0;
+    }
+  }
+  &.step-1{
+    display: flex;
+    min-width: calc(2.66667rem * var(--screen));
+    width: 200%;
+    max-width: calc(5.33333rem * var(--screen));
+    button{
+      display: none;
+    }
+    >div{
+      width: 100%;
+      >div{
+        min-width: calc(2.66667rem * var(--screen));
+        width: 200%;
+        max-width: calc(5.33333rem * var(--screen));
+        // background: rgba(1, 193, 181, 0.1);
+        display: flex;
+        align-items: center;
+        height: calc(0.64rem * var(--screen));
+        padding: 0 calc(0.10667rem * var(--screen));
+        border-radius: calc(0.4rem * var(--screen));
+        :global{
+          .van-notice-bar__content{
+            transform: translateX(0)!important;
+          }
+        }
+      }
+    }
+    :global{
+      .van-notice-bar{
+        background-color: transparent;
+      }
+    }
+  }
+  &.step-0{
+    padding-left: 10PX;
+    padding-right: 10PX;
+    min-height: 100PX;
+    img{
+      position: static;
+      margin-top: 0;
+      // height: 100PX;
+      // width: 54PX;
+    }
+    // display: inline-block;
+  }
+}
+.btn{
+  width: 100PX;
+  height: 32PX;
+}
+
+.box{
+  position: fixed;
+  box-shadow: rgba(33, 33, 33, 0.8) 0px 0px 0px 5000px;
+  transition: all .25s;
+  transform: scale(1.3);
+  border-radius: 8px;
+}

+ 38 - 0
src/page-gym/custom-plugins/guide-page/steps/text.json

@@ -0,0 +1,38 @@
+{
+  "0": {
+    "title": "投屏引导",
+    "desc": "点击这里,可以查看如何将界面投屏到电视上观看"
+  },
+  "1": {
+    "title": "曲目切换",
+    "desc": "看这里,曲目在这里切换"
+  },
+  "2": {
+    "title": "评测",
+    "desc": "点击可以打开评测模式,帮您及时发现演奏过程存在的问题哦!"
+  },
+  "3": {
+    "title": "进度",
+    "desc": "点击可以根据演奏需要,拖动进度条来选定播放点~"
+  },
+  "4": {
+    "title": "选段",
+    "desc": "这里可以选择任意小节重复播放"
+  },
+  "5": {
+    "title": "播放/暂停",
+    "desc": "点击可以让曲谱播放或者暂停~"
+  },
+  "6": {
+    "title": "原声/伴奏",
+    "desc": "点击这里可以将播放音频切换成原声或伴奏"
+  },
+  "7": {
+    "title": "重播",
+    "desc": "点击这里,可以重播曲谱~"
+  },
+  "8": {
+    "title": "调速",
+    "desc": "点击可以调整曲谱播放的速度,根据您的练习需要自由调节吧!"
+  }
+}

+ 103 - 0
src/page-gym/custom-plugins/guide-page/steps/zero-step.tsx

@@ -0,0 +1,103 @@
+import { defineComponent } from "vue";
+import { Button } from "vant";
+import { steps as IDSteps } from "./constant";
+import setpText from "./text.json";
+
+const mp3s = (import.meta as any).globEager("../mp3/*.mp3");
+
+import styles from "./steps.module.less";
+
+const loop = () => {};
+
+export default defineComponent({
+	name: "DetailTips",
+	props: {
+		play: {
+			type: Function as any,
+			default: loop,
+		},
+		stop: {
+			type: Function as any,
+			default: loop,
+		},
+		step: {
+			type: Number,
+			default: 0,
+		},
+		onNext: {
+			type: Function,
+			default: (step: number) => {},
+		},
+		onClose: {
+			type: Function,
+			default: (step: number) => {},
+		},
+	},
+	data() {
+		return {
+			audio: new Audio(),
+			box: {},
+		};
+	},
+	mounted() {
+    this.audio.muted = true
+		this.audio.addEventListener("play", this.play, false);
+		this.audio.addEventListener("pause", this.stop, false);
+		this.audio.addEventListener("ended", this.stop, false);
+
+		this.setStepContent(this.step);
+	},
+	unmounted() {
+		this.audio.removeEventListener("play", this.play, false);
+		this.audio.removeEventListener("pause", this.stop, false);
+		this.audio.removeEventListener("ended", this.stop, false);
+	},
+	methods: {
+		setStepContent(step: number) {
+			this.audio.src = mp3s[`../mp3/${IDSteps[step]}.mp3`].default;
+      console.log(step)
+      if (step === 0){
+        this.audio.muted = true
+      }
+			this.audio.play();
+			const originElement = document.getElementById("tips-step-" + IDSteps[step]);
+
+			const box: any = originElement?.getBoundingClientRect() || {};
+			this.box = {
+				left: box.x + "px",
+				top: box.y + "px",
+				width: box.width + "px",
+				height: box.height + "px",
+			};
+		},
+		next() {
+			this.setStepContent(this.step + 1);
+			this.onNext(this.step + 1);
+		},
+		close() {
+			this.audio.pause();
+			this.onClose();
+		},
+	},
+	render() {
+		return (
+			<div>
+        <audio src={mp3s[`../mp3/${IDSteps[this.step]}.mp3`].default} autoplay></audio>
+				<div style={this.box} class={styles.box}></div>
+				<div class={styles.messagebox}>
+					{/* @ts-ignore */}
+					<span>{setpText[IDSteps[this.step]].desc}</span>
+				</div>
+				{this.step < IDSteps.length - 1 ? (
+					<Button class={styles.btn} round color="#01C1B5" size="mini" onClick={this.next}>
+						下一步
+					</Button>
+				) : (
+					<Button class={styles.btn} round color="#01C1B5" size="mini" onClick={this.close}>
+						知道了
+					</Button>
+				)}
+			</div>
+		);
+	},
+});

+ 50 - 0
src/page-gym/custom-plugins/recording-time/index.tsx

@@ -0,0 +1,50 @@
+import { defineComponent, reactive, watch } from "vue";
+import state from "/src/state";
+import { sysMusicRecordAdd } from "../../api";
+import { browser, getBehaviorId, getCampId } from "/src/utils";
+
+const recordData = reactive({
+	starTime: 0,
+});
+const handleRecord = () => {
+	// 不是练习模式不记录
+	if (state.modeType !== "practise") return;
+	let total = Date.now() - recordData.starTime;
+	recordData.starTime = Date.now();
+	if (total < 0) total = 0;
+	const body = {
+		sysMusicScoreId: state.examSongId,
+		feature: "CLOUD_STUDY_TRAIN",
+		playTime: total / 1000,
+		deviceType: browser().android ? "ANDROID" : "IOS",
+		behaviorId: getBehaviorId(),
+		campId: getCampId(),
+	};
+	sysMusicRecordAdd(body);
+};
+
+export const handleNoEndExit = () => {
+    if (state.playState === 'play') {
+        handleRecord()
+    }
+}
+
+/**
+ * 记录练习时长, 仅记录练习模式的时长
+ */
+export default defineComponent({
+	name: "recordingTime",
+	setup() {
+		watch(
+			() => state.playState,
+			() => {
+				if (state.playState === "play") {
+					recordData.starTime = Date.now();
+				} else {
+					handleRecord();
+				}
+			}
+		);
+		return () => <div></div>;
+	},
+});

+ 32 - 0
src/page-gym/custom-plugins/vip-verify/index.module.less

@@ -0,0 +1,32 @@
+.vip{
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  align-items: center;
+  height: 250px;
+  border-radius: 14px;
+  background-color: #fff;
+  padding: 20PX 30PX;
+  padding-top: 30PX;
+  min-width: 230PX;
+  >img{
+    width: 161PX;
+    margin-bottom: 20PX;
+  }
+  >p{
+    margin: 0;
+    font-size: 14PX;
+    color: #808080;
+    margin-bottom: 20PX;
+  }
+  .btn{
+    font-size: 16PX;
+    height: 40PX;
+    line-height: 40PX;
+    width: 100%;
+  }
+  &+i{
+    right: 10PX;
+    top: 10PX;
+  }
+}

+ 85 - 0
src/page-gym/custom-plugins/vip-verify/index.tsx

@@ -0,0 +1,85 @@
+import { defineComponent, reactive } from "vue";
+import { Button, Popup } from "vant";
+import TipsIcon from "./tips.png";
+import styles from "./index.module.less";
+import store from "store";
+import { studentQueryUserInfo } from "../../api";
+import { setUserInfo, storeData } from "/src/store";
+import { api_back } from "/src/helpers/communication";
+import { postMessage } from "/src/utils/native-message";
+
+const vipData = reactive({
+	show: false,
+	tipsTimer: null as any,
+	isTransfer: false, // 是否被调用
+});
+/** 重新获取会员信息 */
+const reloadUserInfo = async () => {
+	const res = await studentQueryUserInfo();
+	const { student } = res?.data || {};
+	setUserInfo(student);
+};
+
+/** 验证会员服务 */
+export const verifyMembershipServices = () => {
+	vipData.isTransfer = true;
+	clearInterval(vipData.tipsTimer);
+	vipData.show = !!storeData.user?.memberRankSettingId;
+	// const tipsstatus = store.get("tips-status");
+	// if (tipsstatus) {
+	// 	return;
+	// }
+	// vipData.tipsTimer = setInterval(verifyMembershipServices, 1000);
+};
+
+export default defineComponent({
+	name: "vip-plugins",
+	data() {
+		return {
+			content: "您还不是团练宝会员,请开通服务后使用该功能",
+			hiddenProperty: "hidden" as any,
+			visibilityChangeEvent: "" as any,
+		};
+	},
+	methods: {
+		open() {
+			postMessage({
+				api: "openWebView",
+				content: {
+					url: location.origin + "/#/member",
+					orientation: 1,
+				},
+			});
+		},
+		async onVisibilityChange() {
+			if (vipData.isTransfer) {
+				await reloadUserInfo();
+				verifyMembershipServices();
+			}
+		},
+		handleBack() {
+			api_back();
+		},
+	},
+	mounted() {
+		this.hiddenProperty = "hidden" in document ? "hidden" : "webkitHidden" in document ? "webkitHidden" : "mozHidden" in document ? "mozHidden" : null;
+		this.visibilityChangeEvent = this.hiddenProperty.replace(/hidden/i, "visibilitychange");
+		document.addEventListener(this.visibilityChangeEvent, this.onVisibilityChange);
+	},
+	unmounted() {
+		document.removeEventListener(this.visibilityChangeEvent, this.onVisibilityChange);
+	},
+	render() {
+		return (
+			<Popup teleport="body" style={{ zIndex: 100000 }} class="popup-custom van-scale" show={vipData.show} transition="van-scale" closeable onClickCloseIcon={this.handleBack}>
+				<div class={styles.vip}>
+					<img src={TipsIcon} />
+					<p>{this.content}</p>
+					<Button class={styles.btn} onClick={this.open} round color="#01C1B5">
+						立即开通
+					</Button>
+				</div>
+			</Popup>
+		);
+	},
+});

BIN
src/page-gym/custom-plugins/vip-verify/tips.png


+ 0 - 4
src/page-gym/detail/index.module.less

@@ -34,10 +34,6 @@
     }
 }
 
-.plugins {
-    display: none;
-}
-
 :global {
     #cursorImg-0 {
         min-height: 58PX;

+ 23 - 5
src/page-gym/detail/index.tsx

@@ -20,6 +20,12 @@ import { mappingVoicePart, subjectFingering } from "/src/view/fingering/fingerin
 import Fingering from "/src/view/fingering";
 import store from "store";
 import HelperModel from "../helper-model";
+import RecordingTime from "../custom-plugins/recording-time";
+import ExerciseStatistics from "../custom-plugins/ExerciseStatistics";
+import HomeWork from "../custom-plugins/HomeWork";
+import EvaluatingWork from "../custom-plugins/EvaluatingWork";
+import VipModel from "../custom-plugins/vip-verify";
+import GuidePage from "../custom-plugins/guide-page";
 
 //特殊教材分类id
 const classIds = [1, 30, 97]; // [大雅金唐, 竖笛教程, 声部训练]
@@ -245,7 +251,7 @@ export default defineComponent({
 					class={[styles.container, state.setting.eyeProtection && "eyeProtection", !state.setting.displayCursor && "hideCursor"]}
 				>
 					{/* 曲谱渲染 */}
-					{!detailData.isLoading && <MusicScore onRendered={handleRendered} />}
+					{!detailData.isLoading && <MusicScore key="musicscore" onRendered={handleRendered} />}
 					{/* 播放 */}
 					{!detailData.isLoading && <AudioList />}
 					{/* 评测 */}
@@ -263,16 +269,28 @@ export default defineComponent({
 					)}
 				</div>
 
-				{/* 公用的插件 */}
-				<div class={styles.plugins}>
+				{/* 插件模块 */}
+				<div class="plugins-box">
 					{state.musicRendered && (
 						<>
 							<MeasureSpeed />
+							{/* 投屏 and 帮助 */}
+							<HelperModel />
+							{/* 统计训练时长 */}
+							<RecordingTime />
+							{/* 统计时长显示, 只有学生端显示 */}
+							{storeData.platformType === "STUDENT" && <ExerciseStatistics />}
+							{/* 课后训练作业 */}
+							{query.workRecord && storeData.platformType === "STUDENT" && <HomeWork />}
+							{/* 进度评测作业 */}
+							{query.evaluatingRecord && storeData.platformType === "STUDENT" && <EvaluatingWork />}
+							{/* 开通会员提示 */}
+							{storeData.platformType === "STUDENT" && (query.workRecord || query.evaluatingRecord) && <VipModel />}
+							{/* 引导 */}
+							<GuidePage />
 						</>
 					)}
 				</div>
-				{/* 投屏 and 帮助 */}
-				<HelperModel />
 			</div>
 		);
 	},

+ 8 - 1
src/page-gym/header-top/index.tsx

@@ -13,6 +13,8 @@ import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
 import { Popup } from "@varlet/ui";
 import Settting from "./settting";
 import MusciList from "../musci-list";
+import { handleNoEndExit } from "../custom-plugins/recording-time";
+import { api_back } from "/src/helpers/communication";
 
 export const headData = reactive({
 	speedShow: false,
@@ -30,10 +32,15 @@ export default defineComponent({
 		const toggleEvaluat = () => {
 			handleStartEvaluat();
 		};
+		/** 退出 */
+		const handleBack = () => {
+			handleNoEndExit()
+			api_back();
+		}
 		const disabledList = ["evaluating"];
 		return () => (
 			<div ref={headRef} class={styles.headerTop}>
-				<div class={styles.back}>
+				<div class={styles.back} onClick={handleBack}>
 					<img src={iconBack} />
 				</div>
 				<Title onClick={() => (headerData.listShow = true)} text={state.examSongName} />

+ 1 - 1
src/page-gym/header-top/title/index.tsx

@@ -22,7 +22,7 @@ export default defineComponent({
   },
   render() {
     return (
-      <div class={styles.container} onClick={this.$props.onClick}>
+      <div id="tips-step-1" class={styles.container} onClick={this.$props.onClick}>
         <img class={styles.icon} src={MusicIcon}/>
         <NoticeBar
           text={this.text}

+ 3 - 3
src/page-gym/helper-model/index.tsx

@@ -15,10 +15,10 @@ export default defineComponent({
 		return () => (
 			<>
 				<div class={styles.helperModel} onClick={() => (helperData.show = true)}>
-					<img src={iconRight} />
+					<img id="tips-step-0" src={iconRight} />
 				</div>
 				<Popup
-					class={["van-custom", styles.screen]}
+					class={["popup-custom", styles.screen]}
 					v-model:show={helperData.show}
 					onClose={() => {
 						helperData.show = false;
@@ -35,7 +35,7 @@ export default defineComponent({
 						}}
 					/>
 				</Popup>
-				<Popup v-model:show={helperData.recommendationShow} class="van-custom van-scale" transition="van-scale">
+				<Popup v-model:show={helperData.recommendationShow} class="popup-custom van-scale" transition="van-scale">
 					<Recommendation
 						onClose={() => {
 							helperData.recommendationShow = false;

+ 4 - 0
src/state.ts

@@ -69,6 +69,8 @@ const state = reactive({
 	isSpecialBookCategory: false,
 	/** 播放状态 */
 	playState: "paused" as "play" | "paused",
+	/** 播放结束状态 */
+	playEnd: false,
 	/** 播放那个: 原音,伴奏 */
 	playSource: "music" as "music" | "background",
 	/** 播放进度 */
@@ -164,6 +166,7 @@ const setStep = () => {
 };
 /** 开始播放 */
 export const onPlay = () => {
+	state.playEnd = false
 	setStep();
 	if (state.modeType === "evaluating") {
 		const currentTime = getAudioCurrentTime();
@@ -174,6 +177,7 @@ export const onPlay = () => {
 export const onTimeupdate = (evt: Event) => {};
 /** 播放完成事件 */
 export const onEnded = () => {
+	state.playEnd = true
 	handleStopPlay();
 	if (state.modeType === "evaluating") {
 		handleEndBegin(true);

+ 2 - 2
src/style.css

@@ -42,12 +42,12 @@
 .van-overlay{
   transition: all 0.25s;
 }
-.van-custom{
+.popup-custom{
   transition: all 0.25s;
   background: transparent;
   overflow: initial;
 }
-.van-custom.van-scale{
+.popup-custom.van-scale{
   transform-origin: center -25%;
 }
 

+ 43 - 4
src/utils/index.ts

@@ -8,11 +8,17 @@ export const browser = () => {
 		gecko: u.indexOf("Gecko") > -1 && u.indexOf("KHTML") == -1, //火狐内核
 		mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
 		ios: !!u.match(/Mac OS X/) || /(iPhone|iPad|iPod|iOS)/i.test(u), //ios终端
-		android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1,   //判断是否是 android终端
+		android: u.indexOf("Android") > -1 || u.indexOf("Adr") > -1, //判断是否是 android终端
 		iPhone: u.indexOf("ORCHESTRAAPPI") > -1, //是否为iPhone或者QQHD浏览器
-		isApp: u.includes('DAYAAPPA') || u.includes('DAYAAPPI') || u.includes('COLEXIUAPPA') || u.includes('COLEXIUAPPI') || u.includes("ORCHESTRAAPPI") || u.includes("ORCHESTRAAPPA"),
-		isTeacher: u.indexOf("ORCHESTRATEACHER") > -1 || u.includes('COLEXIUTEACHER'),
-		isStudent: u.indexOf("ORCHESTRASTUDENT") > -1 || u.includes('COLEXIUSTUDENT'),
+		isApp:
+			u.includes("DAYAAPPA") ||
+			u.includes("DAYAAPPI") ||
+			u.includes("COLEXIUAPPA") ||
+			u.includes("COLEXIUAPPI") ||
+			u.includes("ORCHESTRAAPPI") ||
+			u.includes("ORCHESTRAAPPA"),
+		isTeacher: u.indexOf("ORCHESTRATEACHER") > -1 || u.includes("COLEXIUTEACHER"),
+		isStudent: u.indexOf("ORCHESTRASTUDENT") > -1 || u.includes("COLEXIUSTUDENT"),
 		isSchool: u.indexOf("ORCHESTRASCHOOL") > -1,
 		iPad: u.indexOf("iPad") > -1, //是否iPad
 		webApp: u.indexOf("Safari") == -1, //是否web应该程序,没有头部与底部
@@ -45,3 +51,36 @@ export const setGlobalData = (_key: string, _value: any) => {
 	GYM[_key] = _value;
 	(window as any).GYM = GYM;
 };
+
+const BEHAVIORIDKEY = "BEHAVIORID";
+/** 设置 behaviorId */
+export const setBehaviorId = (value: any) => {
+	localStorage.setItem(BEHAVIORIDKEY, value);
+};
+/** 获取 behaviorId */
+export const getBehaviorId = () => {
+	return localStorage.getItem(BEHAVIORIDKEY);
+};
+
+const campIdKey = "CAMPID";
+/** 设置 训练营ID */
+export const setCampId = (value: any) => {
+	sessionStorage.setItem(campIdKey, value);
+};
+/** 获取 训练营ID */
+export const getCampId = () => {
+	return sessionStorage.getItem(campIdKey);
+};
+
+// 秒转分
+export const getSecondRPM = (second: number, type?: string) => {
+	if (isNaN(second)) return "00:00";
+	let h = Math.floor((second / 60 / 60) % 24);
+	let m = Math.floor((second / 60) % 60);
+	let s = Math.floor(second % 60);
+	if (type === "cn") {
+		return `${h > 0 ? h.toString().padStart(2, "0") + "时" : ""}${m.toString().padStart(2, "0")}分${s.toString().padStart(2, "0")}秒`;
+	} else {
+		return `${h > 0 ? h.toString().padStart(2, "0") + ":" : ""}${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
+	}
+};

+ 1 - 1
src/utils/queryString.ts

@@ -1,6 +1,6 @@
 import qs from 'query-string'
 export const getQuery = () => {
-    let search = {}
+    let search: any = {}
     try {
         search = {
             ...qs.parse(location.search),

+ 1 - 0
src/view/music-score/index.tsx

@@ -43,6 +43,7 @@ export default defineComponent({
 	name: "music-score",
 	emits: ["rendered"],
 	setup(prop, { emit }) {
+		/** 设置 曲谱模式,五线谱还是简谱 */
 		const setRenderType = () => {
 			const musicRenderType: any = sessionStorage.getItem(musicRenderTypeKey)
 			state.musicRenderType = ['staff', 'firstTone', 'fixedTone'].includes(musicRenderType) ? musicRenderType : 'staff'

+ 1 - 1
src/view/selection/index.tsx

@@ -126,7 +126,7 @@ export default defineComponent({
 			calcNoteData();
 		});
 		return () => (
-			<div class={styles.selectionContainer} onClick={(e: Event) => e.stopPropagation()}>
+			<div id="selectionBox" class={styles.selectionContainer} onClick={(e: Event) => e.stopPropagation()}>
 				{selectData.staves.map((item: any) => {
 					const scoreItem = evaluatingData.evaluatings[item.measureListIndex];
 					return (

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