瀏覽代碼

ppt增加听音练习

黄琪勇 1 周之前
父節點
當前提交
7c996ee367
共有 24 個文件被更改,包括 697 次插入19 次删除
  1. 20 2
      src/hooks/useCreateElement.ts
  2. 23 1
      src/types/slides.ts
  3. 3 1
      src/views/Editor/Canvas/EditableElement.vue
  4. 6 2
      src/views/Editor/Canvas/hooks/useDragElement.ts
  5. 二進制
      src/views/Editor/CanvasTool/imgs/jzlx.png
  6. 二進制
      src/views/Editor/CanvasTool/imgs/kzzs.png
  7. 二進制
      src/views/Editor/CanvasTool/imgs/tylx.png
  8. 64 8
      src/views/Editor/CanvasTool/index.vue
  9. 7 0
      src/views/Editor/Toolbar/ElementStylePanel/ListeningPracticeStylePanel.vue
  10. 3 1
      src/views/Editor/Toolbar/ElementStylePanel/index.vue
  11. 1 1
      src/views/Editor/Toolbar/index.vue
  12. 3 1
      src/views/Mobile/MobileEditor/MobileEditableElement.vue
  13. 3 1
      src/views/Screen/ScreenElement.vue
  14. 3 1
      src/views/components/ThumbnailSlide/ThumbnailElement.vue
  15. 51 0
      src/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue
  16. 57 0
      src/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue
  17. 2 0
      src/views/components/element/listeningPracticeElement/index.ts
  18. 116 0
      src/views/components/element/listeningPracticeElement/listeningPracticeElement.vue
  19. 二進制
      src/views/components/element/listeningPracticeElement/listeningPracticeList/imgs/musicBg.png
  20. 二進制
      src/views/components/element/listeningPracticeElement/listeningPracticeList/imgs/subject-bg.png
  21. 2 0
      src/views/components/element/listeningPracticeElement/listeningPracticeList/index.ts
  22. 221 0
      src/views/components/element/listeningPracticeElement/listeningPracticeList/listeningPracticeList.vue
  23. 2 0
      src/views/components/element/listeningPracticeElement/listeningPracticePlayer/index.ts
  24. 110 0
      src/views/components/element/listeningPracticeElement/listeningPracticePlayer/listeningPracticePlayer.vue

+ 20 - 2
src/hooks/useCreateElement.ts

@@ -361,7 +361,24 @@ export default () => {
       top: 898
     })
   }
-
+  /**
+   * 听音练习
+   */
+  const createListeningPracticeElement = (code: string, img: string, name: string) => {
+    createElement({
+      type: "elf",
+      subtype: "elf-listening-practice",
+      code,
+      name,
+      instrumentImg: img,
+      id: nanoid(10),
+      width: viewportSize.value,
+      height: viewportSize.value * viewportRatio.value,
+      rotate: 0,
+      left: 0,
+      top: 0
+    })
+  }
   return {
     createImageElement,
     createChartElement,
@@ -373,6 +390,7 @@ export default () => {
     createVideoElement,
     createAudioElement,
     createCloudCoachElement,
-    createEnjoyElement
+    createEnjoyElement,
+    createListeningPracticeElement
   }
 }

+ 23 - 1
src/types/slides.ts

@@ -35,7 +35,8 @@ export const enum ElementSubtypeTypes {
   VIDEO = "elf-video",
   AUDIO = "elf-audio",
   SING_PLAY = "elf-sing-play",
-  ENJOY = "elf-enjoy"
+  ENJOY = "elf-enjoy",
+  LISTENING_PRACTICE = "elf-listening-practice"
 }
 
 /**
@@ -660,6 +661,26 @@ export interface PPTEnjoyElement extends PPTBaseElement {
   }[]
 }
 
+/**
+ * 听音练习
+ *
+ * type: elf
+ *
+ * subtype: elf-listening-practice
+ *
+ * code: 乐器code
+ *
+ * 乐器img
+ *
+ */
+export interface PPTListeningPracticeElement extends PPTBaseElement {
+  type: "elf"
+  subtype: "elf-listening-practice"
+  code: string
+  name: string
+  instrumentImg: string
+}
+
 export type PPTElement =
   | PPTTextElement
   | PPTImageElement
@@ -672,6 +693,7 @@ export type PPTElement =
   | PPTAudioElement
   | PPTCloudCoachElement
   | PPTEnjoyElement
+  | PPTListeningPracticeElement
 
 export type AnimationType = "in" | "out" | "attention"
 export type AnimationTrigger = "click" | "meantime" | "auto"

+ 3 - 1
src/views/Editor/Canvas/EditableElement.vue

@@ -37,6 +37,7 @@ import VideoElement from "@/views/components/element/VideoElement/index.vue"
 import AudioElement from "@/views/components/element/AudioElement/index.vue"
 import cloudCoachElement from "@/views/components/element/cloudCoachElement"
 import enjoyElement from "@/views/components/element/enjoyElement"
+import listeningPracticeElement from "@/views/components/element/listeningPracticeElement"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -61,7 +62,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.AUDIO]: AudioElement,
     [ElementSubtypeTypes.VIDEO]: VideoElement,
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
-    [ElementSubtypeTypes.ENJOY]: enjoyElement
+    [ElementSubtypeTypes.ENJOY]: enjoyElement,
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 6 - 2
src/views/Editor/Canvas/hooks/useDragElement.ts

@@ -22,7 +22,7 @@ export default (elementList: Ref<PPTElement[]>, alignmentLines: Ref<AlignmentLin
     let isMouseDown = true
     /* 选中移动的时候 云教练模块标记移动中 */
     elementList.value.map(item => {
-      if (activeElementIdList.value.includes(item.id) && item.type === "elf" && item.subtype === "elf-sing-play") {
+      if (activeElementIdList.value.includes(item.id) && item.type === "elf" && ["elf-sing-play", "elf-listening-practice"].includes(item.subtype)) {
         item.isMove = true
       }
     })
@@ -291,7 +291,11 @@ export default (elementList: Ref<PPTElement[]>, alignmentLines: Ref<AlignmentLin
       isMouseDown = false
       /* 选中取消移动的时候 云教练模块取消标记移动中 */
       elementList.value.map(item => {
-        if (activeElementIdList.value.includes(item.id) && item.type === "elf" && item.subtype === "elf-sing-play") {
+        if (
+          activeElementIdList.value.includes(item.id) &&
+          item.type === "elf" &&
+          ["elf-sing-play", "elf-listening-practice"].includes(item.subtype)
+        ) {
           item.isMove = false
         }
       })

二進制
src/views/Editor/CanvasTool/imgs/jzlx.png


二進制
src/views/Editor/CanvasTool/imgs/kzzs.png


二進制
src/views/Editor/CanvasTool/imgs/tylx.png


+ 64 - 8
src/views/Editor/CanvasTool/index.vue

@@ -61,14 +61,40 @@
         <img class="itemImg" src="./imgs/yp.png" alt="" />
         <div class="tit">乐谱</div>
       </div>
-      <!-- <div class="handler-item">
-        <img class="itemImg" src="./imgs/jzlx.png" alt="" />
-        <div class="tit">节奏练习</div>
+      <div class="handler-item" @click="expandedKnowledgeVisible = true">
+        <img class="itemImg" src="./imgs/kzzs.png" alt="" />
+        <Popover trigger="click" v-model:value="expandedKnowledgeVisible" :offset="10" @click.stop>
+          <template #content>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  listeningPracticeVisible = true
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/tylx.png" alt="" />
+                <div class="tit">听音练习</div>
+              </div>
+            </PopoverMenuItem>
+            <PopoverMenuItem
+              @click="
+                () => {
+                  expandedKnowledgeVisible = false
+                  rhythmPracticeVisible = true
+                }
+              "
+            >
+              <div class="menuItem">
+                <img src="./imgs/jzlx.png" alt="" />
+                <div class="tit">节奏练习</div>
+              </div>
+            </PopoverMenuItem>
+          </template>
+          <div class="tit">扩展知识</div>
+        </Popover>
       </div>
-      <div class="handler-item">
-        <img class="itemImg" src="./imgs/tylx.png" alt="" />
-        <div class="tit">听音练习</div>
-      </div> -->
       <div class="handler-item" @click="resourcesListVisible = true">
         <img class="itemImg" src="./imgs/zyk.png" alt="" />
         <div class="tit">资源库</div>
@@ -267,6 +293,26 @@
         "
       />
     </Modal>
+    <Modal
+      :contentStyle="{
+        width: '742px',
+        height: '570px',
+        boxShadow: '0px 2px 10px 0px rgba(0,0,0,0.08)',
+        borderRadius: '16px',
+        border: '1px solid #DEDEDE',
+        padding: '0'
+      }"
+      v-model:visible="listeningPracticeVisible"
+    >
+      <listeningPracticeList
+        @update="handleAddListeningPractice"
+        @close="
+          () => {
+            listeningPracticeVisible = false
+          }
+        "
+      />
+    </Modal>
   </div>
 </template>
 
@@ -294,6 +340,7 @@ import PopoverMenuItem from "@/components/PopoverMenuItem.vue"
 import { ElUpload, ElMessage, type UploadRequestOptions } from "element-plus"
 import cloudCoachList from "@/views/components/element/cloudCoachElement/cloudCoachList"
 import resourcesList from "@/views/components/element/enjoyElement/resourcesList"
+import listeningPracticeList from "@/views/components/element/listeningPracticeElement/listeningPracticeList"
 import fileUpload from "@/utils/oss-file-upload"
 import usePptWork from "@/store/pptWork"
 
@@ -326,7 +373,8 @@ const {
   createVideoElement,
   createAudioElement,
   createCloudCoachElement,
-  createEnjoyElement
+  createEnjoyElement,
+  createListeningPracticeElement
 } = useCreateElement()
 
 const insertImageElement = (files: FileList) => {
@@ -345,6 +393,9 @@ const shapeMenuVisible = ref(false)
 const cloudCoachVisible = ref(false)
 const resourcesListVisible = ref(false)
 const moreToolsVisible = ref(false)
+const expandedKnowledgeVisible = ref(false)
+const listeningPracticeVisible = ref(false)
+const rhythmPracticeVisible = ref(false)
 
 // 音视频
 function handleUpload(fileData: UploadRequestOptions) {
@@ -371,6 +422,11 @@ function handleCloudCoach(id: string, name: string) {
   createCloudCoachElement(id, name)
   cloudCoachVisible.value = false
 }
+// 处理听音练习创建
+function handleAddListeningPractice(item: Record<string, any>) {
+  createListeningPracticeElement(item.code, item.img, item.name)
+  listeningPracticeVisible.value = false
+}
 // 处理资源创建
 function handleResources(item: Record<string, any>) {
   if (item.type === "SONG") {

+ 7 - 0
src/views/Editor/Toolbar/ElementStylePanel/ListeningPracticeStylePanel.vue

@@ -0,0 +1,7 @@
+<template>
+  <div class="ListeningPracticeStylePanel">听音练习配置</div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style lang="scss" scoped></style>

+ 3 - 1
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -22,6 +22,7 @@ import AudioStylePanel from "./AudioStylePanel.vue"
 import MultiStylePanel from "./MultiStylePanel.vue"
 import CloudCoachStylePanel from "./CloudCoachStylePanel.vue"
 import EnjoyStylePanel from "./EnjoyStylePanel.vue"
+import ListeningPracticeStylePanel from "./ListeningPracticeStylePanel.vue"
 
 const panelMap = {
   [ElementTypes.TEXT]: TextStylePanel,
@@ -37,7 +38,8 @@ const elementSubtypeMap = {
   [ElementSubtypeTypes.AUDIO]: AudioStylePanel,
   [ElementSubtypeTypes.VIDEO]: VideoStylePanel,
   [ElementSubtypeTypes.SING_PLAY]: CloudCoachStylePanel,
-  [ElementSubtypeTypes.ENJOY]: EnjoyStylePanel
+  [ElementSubtypeTypes.ENJOY]: EnjoyStylePanel,
+  [ElementSubtypeTypes.LISTENING_PRACTICE]: ListeningPracticeStylePanel
 }
 const { activeElementIdList, activeElementList, handleElement, activeGroupElementId } = storeToRefs(useMainStore())
 

+ 1 - 1
src/views/Editor/Toolbar/index.vue

@@ -39,7 +39,7 @@ const elementTabs = computed<ElementTabs[]>(() => {
       { label: "动画", key: ToolbarStates.EL_ANIMATION }
     ]
   }
-  if (handleElement.value?.type === "elf" && ["elf-sing-play", "elf-enjoy"].includes(handleElement.value?.subtype)) {
+  if (handleElement.value?.type === "elf" && ["elf-sing-play", "elf-enjoy", "elf-listening-practice"].includes(handleElement.value?.subtype)) {
     return [
       { label: "位置", key: ToolbarStates.EL_POSITION },
       { label: "动画", key: ToolbarStates.EL_ANIMATION }

+ 3 - 1
src/views/Mobile/MobileEditor/MobileEditableElement.vue

@@ -24,6 +24,7 @@ import VideoElement from "@/views/components/element/VideoElement/index.vue"
 import AudioElement from "@/views/components/element/AudioElement/index.vue"
 import cloudCoachElement from "@/views/components/element/cloudCoachElement"
 import enjoyElement from "@/views/components/element/enjoyElement"
+import listeningPracticeElement from "@/views/components/element/listeningPracticeElement"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -46,7 +47,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.AUDIO]: AudioElement,
     [ElementSubtypeTypes.VIDEO]: VideoElement,
     [ElementSubtypeTypes.SING_PLAY]: cloudCoachElement,
-    [ElementSubtypeTypes.ENJOY]: enjoyElement
+    [ElementSubtypeTypes.ENJOY]: enjoyElement,
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: listeningPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 3 - 1
src/views/Screen/ScreenElement.vue

@@ -34,6 +34,7 @@ import ScreenVideoElement from "@/views/components/element/VideoElement/ScreenVi
 import ScreenAudioElement from "@/views/components/element/AudioElement/ScreenAudioElement.vue"
 import ScreenCloudCoachElement from "@/views/components/element/cloudCoachElement/ScreenCloudCoachElement.vue"
 import ScreenEnjoyElement from "@/views/components/element/enjoyElement/ScreenEnjoyElement.vue"
+import ScreenListeningPracticeElement from "@/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -58,7 +59,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.AUDIO]: ScreenAudioElement,
     [ElementSubtypeTypes.VIDEO]: ScreenVideoElement,
     [ElementSubtypeTypes.SING_PLAY]: ScreenCloudCoachElement,
-    [ElementSubtypeTypes.ENJOY]: ScreenEnjoyElement
+    [ElementSubtypeTypes.ENJOY]: ScreenEnjoyElement,
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: ScreenListeningPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 3 - 1
src/views/components/ThumbnailSlide/ThumbnailElement.vue

@@ -25,6 +25,7 @@ import BaseVideoElement from "@/views/components/element/VideoElement/BaseVideoE
 import BaseAudioElement from "@/views/components/element/AudioElement/BaseAudioElement.vue"
 import BaseCloudCoachElement from "@/views/components/element/cloudCoachElement/BaseCloudCoachElement.vue"
 import BaseEnjoyElement from "@/views/components/element/enjoyElement/BaseEnjoyElement.vue"
+import BaseListeningPracticeElement from "@/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue"
 
 const props = defineProps<{
   elementInfo: PPTElement
@@ -46,7 +47,8 @@ const currentElementComponent = computed<unknown>(() => {
     [ElementSubtypeTypes.AUDIO]: BaseAudioElement,
     [ElementSubtypeTypes.VIDEO]: BaseVideoElement,
     [ElementSubtypeTypes.SING_PLAY]: BaseCloudCoachElement,
-    [ElementSubtypeTypes.ENJOY]: BaseEnjoyElement
+    [ElementSubtypeTypes.ENJOY]: BaseEnjoyElement,
+    [ElementSubtypeTypes.LISTENING_PRACTICE]: BaseListeningPracticeElement
   }
   return elementTypeMap[props.elementInfo.type] || elementSubtypeMap[props.elementInfo.subtype] || null
 })

+ 51 - 0
src/views/components/element/listeningPracticeElement/BaseListeningPracticeElement.vue

@@ -0,0 +1,51 @@
+<template>
+  <div
+    class="base-element-cloudCoach"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div class="element-content">
+        <img class="img" :src="elementInfo.instrumentImg" alt="" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import type { PPTListeningPracticeElement } from "@/types/slides"
+
+defineProps<{
+  elementInfo: PPTListeningPracticeElement
+}>()
+</script>
+
+<style lang="scss" scoped>
+.base-element-cloudCoach {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: url("./listeningPracticeList/imgs/musicBg.png") no-repeat;
+  background-size: 100% 100%;
+  position: relative;
+  .img {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+  }
+}
+</style>

+ 57 - 0
src/views/components/element/listeningPracticeElement/ScreenListeningPracticeElement.vue

@@ -0,0 +1,57 @@
+<template>
+  <div
+    class="base-element-cloudCoach screen-element-cloudCoach"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div class="element-content">
+        <listeningPracticePlayer
+          v-if="inCurrentSlide"
+          :width="elementInfo.width"
+          :height="elementInfo.height"
+          :scale="scale"
+          :code="elementInfo.code"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, inject, ref } from "vue"
+import { storeToRefs } from "pinia"
+import { useSlidesStore } from "@/store"
+import type { PPTListeningPracticeElement } from "@/types/slides"
+import { injectKeySlideId, injectKeySlideScale } from "@/types/injectKey"
+import listeningPracticePlayer from "./listeningPracticePlayer"
+
+defineProps<{
+  elementInfo: PPTListeningPracticeElement
+}>()
+
+const { currentSlide } = storeToRefs(useSlidesStore())
+
+const scale = inject(injectKeySlideScale) || ref(1)
+const slideId = inject(injectKeySlideId) || ref("")
+
+const inCurrentSlide = computed(() => currentSlide.value.id === slideId.value)
+</script>
+
+<style lang="scss" scoped>
+.screen-element-cloudCoach {
+  position: absolute;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 2 - 0
src/views/components/element/listeningPracticeElement/index.ts

@@ -0,0 +1,2 @@
+import listeningPracticeElement from "./listeningPracticeElement.vue"
+export default listeningPracticeElement

+ 116 - 0
src/views/components/element/listeningPracticeElement/listeningPracticeElement.vue

@@ -0,0 +1,116 @@
+<template>
+  <div
+    class="listeningPracticeElement"
+    :class="{ lock: elementInfo.lock }"
+    :style="{
+      top: elementInfo.top + 'px',
+      left: elementInfo.left + 'px',
+      width: elementInfo.width + 'px',
+      height: elementInfo.height + 'px'
+    }"
+  >
+    <div class="rotate-wrapper" :style="{ transform: `rotate(${elementInfo.rotate}deg)` }">
+      <div
+        class="element-content"
+        v-contextmenu="contextmenus"
+        @mousedown="$event => handleSelectElement($event)"
+        @touchstart="$event => handleSelectElement($event)"
+      >
+        <div
+          v-if="isShowMask"
+          @mousedown.stop="$event => handleSelectElement($event, false)"
+          @touchstart.stop="$event => handleSelectElement($event, false)"
+          class="mask"
+        ></div>
+        <listeningPracticePlayer :width="elementInfo.width" :height="elementInfo.height" :scale="canvasScale" :code="elementInfo.code" />
+        <div
+          :class="['handler-border', item]"
+          v-for="item in ['t', 'b', 'l', 'r']"
+          :key="item"
+          @mousedown="$event => handleSelectElement($event)"
+          @touchstart="$event => handleSelectElement($event)"
+        ></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { storeToRefs } from "pinia"
+import { useMainStore } from "@/store"
+import type { PPTListeningPracticeElement } from "@/types/slides"
+import type { ContextmenuItem } from "@/components/Contextmenu/types"
+import listeningPracticePlayer from "./listeningPracticePlayer"
+import { computed } from "vue"
+
+const props = defineProps<{
+  elementInfo: PPTListeningPracticeElement
+  selectElement: (e: MouseEvent | TouchEvent, element: PPTListeningPracticeElement, canMove?: boolean) => void
+  contextmenus: () => ContextmenuItem[] | null
+}>()
+
+const mainStore = useMainStore()
+const { canvasScale } = storeToRefs(mainStore)
+
+/* 当没有选中 或者 拖动过程中加一个遮罩,以免事件进入 iframe 无法触发*/
+const isShowMask = computed(() => {
+  return mainStore.handleElementId !== props.elementInfo.id || props.elementInfo.isMove
+})
+const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
+  if (props.elementInfo.lock) return
+  e.stopPropagation()
+
+  props.selectElement(e, props.elementInfo, canMove)
+}
+</script>
+
+<style lang="scss" scoped>
+.listeningPracticeElement {
+  position: absolute;
+  &.lock .handler-border {
+    cursor: default;
+  }
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  .mask {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+  }
+}
+.handler-border {
+  position: absolute;
+  cursor: move;
+  &.t {
+    width: 100%;
+    height: 10px;
+    top: 0;
+    left: 0;
+  }
+  &.b {
+    width: 100%;
+    height: 10px;
+    bottom: 0;
+    left: 0;
+  }
+  &.l {
+    width: 10px;
+    height: 100%;
+    left: 0;
+    top: 0;
+  }
+  &.r {
+    width: 10px;
+    height: 100%;
+    right: 0;
+    top: 0;
+  }
+}
+</style>

二進制
src/views/components/element/listeningPracticeElement/listeningPracticeList/imgs/musicBg.png


二進制
src/views/components/element/listeningPracticeElement/listeningPracticeList/imgs/subject-bg.png


+ 2 - 0
src/views/components/element/listeningPracticeElement/listeningPracticeList/index.ts

@@ -0,0 +1,2 @@
+import listeningPracticeList from "./listeningPracticeList.vue"
+export default listeningPracticeList

+ 221 - 0
src/views/components/element/listeningPracticeElement/listeningPracticeList/listeningPracticeList.vue

@@ -0,0 +1,221 @@
+<template>
+  <div class="listeningPracticeList">
+    <div class="headCon">
+      <div class="headLeft">
+        <img class="tipImg" src="@/views/Editor/CanvasTool/imgs/tylx.png" alt="" />
+        <div class="title">听音练习</div>
+      </div>
+      <div class="headright">
+        <img @click="emits('close')" class="closeBtn" src="../../cloudCoachElement/cloudCoachList/imgs/close.png" alt="" />
+      </div>
+    </div>
+    <div class="content">
+      <div class="tabTools">
+        <div class="tabCon">
+          <div class="tab" @click="handleTabChange(item)" :class="{ active: item.id === tabActive?.id }" v-for="item in tabData" :key="item.id">
+            {{ item.name }}
+          </div>
+        </div>
+      </div>
+      <div class="instrumentsContent">
+        <div class="instrument" v-for="item in tabActive?.instruments || []" :key="item.code" @click="handleAddListeningPractice(item)">
+          <div class="imgBox">
+            <img :src="item.img" />
+          </div>
+          <div class="title">{{ item.name }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getSubjectListApi } from "@/api/pptOperate"
+import { httpAjax } from "@/plugins/httpAjax"
+import { ref } from "vue"
+
+const emits = defineEmits<{
+  (event: "update", item: Record<string, any>): void
+  (event: "close"): void
+}>()
+
+function handleAddListeningPractice(item: Record<string, any>) {
+  emits("update", item)
+}
+
+type tabType = {
+  id: number
+  name: string
+  instruments: {
+    code: string
+    name: string
+    img: string
+  }[]
+}
+
+const tabData = ref<tabType[]>([])
+const tabActive = ref<tabType>()
+
+getTabList()
+function getTabList() {
+  httpAjax(getSubjectListApi).then(res => {
+    if (res.code === 200) {
+      const instruments = res.data.reduce((subTab: any[], item: any) => {
+        subTab.push(...item.instruments)
+        return subTab
+      }, [])
+      tabData.value = [
+        {
+          id: -199,
+          name: "全部声部",
+          instruments
+        },
+        ...res.data
+      ]
+      handleTabChange(tabData.value[0])
+    }
+  })
+}
+
+function handleTabChange(activeTab: tabType) {
+  tabActive.value = activeTab
+}
+</script>
+
+<style lang="scss" scoped>
+.listeningPracticeList {
+  width: 100%;
+  height: 100%;
+  .headCon {
+    width: 100%;
+    height: 64px;
+    border-bottom: 1px solid #eaeaea;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .headLeft {
+      margin-left: 30px;
+      display: flex;
+      align-items: center;
+      .tipImg {
+        width: 24px;
+        height: 24px;
+      }
+      .title {
+        font-weight: 600;
+        font-size: 18px;
+        color: #131415;
+        margin-left: 8px;
+      }
+    }
+    .headright {
+      margin-right: 30px;
+      display: flex;
+      align-items: center;
+      .closeBtn {
+        width: 24px;
+        height: 24px;
+        cursor: pointer;
+        &:hover {
+          opacity: 0.8;
+        }
+      }
+    }
+  }
+  .content {
+    width: 100%;
+    height: calc(100% - 64px);
+    .tabTools {
+      flex-shrink: 0;
+      width: 100%;
+      padding: 24px 30px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .tabCon {
+        display: flex;
+        .tab {
+          margin-right: 32px;
+          font-weight: 400;
+          font-size: 16px;
+          color: #8b8d98;
+          line-height: 22px;
+          cursor: pointer;
+          &:hover {
+            opacity: 0.8;
+          }
+          &:last-child {
+            margin-right: 0;
+          }
+          &.active {
+            font-weight: 600;
+            color: #131415;
+            position: relative;
+            &::after {
+              content: "";
+              position: absolute;
+              width: 100%;
+              height: 10px;
+              background: linear-gradient(90deg, #77bbff 0%, rgba(163, 231, 255, 0.22) 100%);
+              bottom: 0;
+              left: 0;
+              z-index: -1;
+            }
+          }
+        }
+      }
+    }
+    .instrumentsContent {
+      display: flex;
+      flex-wrap: wrap;
+      padding: 0 30px;
+      .instrument {
+        margin-right: 40px;
+        margin-bottom: 40px;
+        cursor: pointer;
+        &:nth-child(5n) {
+          margin-right: 0;
+        }
+        &:hover {
+          .imgBox {
+            border-color: #198cfe;
+            transform: scale(1.02);
+            transition: all 0.2s ease;
+          }
+        }
+        .imgBox {
+          width: 104px;
+          height: 104px;
+          border-radius: 12px;
+          border: 2px solid transparent;
+          position: relative;
+          overflow: hidden;
+          &::after {
+            content: "";
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            background: url("./imgs/subject-bg.png") no-repeat center;
+            z-index: -1;
+          }
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: contain;
+          }
+        }
+        .title {
+          margin-top: 10px;
+          font-weight: 400;
+          font-size: 14px;
+          color: #131415;
+          line-height: 20px;
+          text-align: center;
+        }
+      }
+    }
+  }
+}
+</style>

+ 2 - 0
src/views/components/element/listeningPracticeElement/listeningPracticePlayer/index.ts

@@ -0,0 +1,2 @@
+import listeningPracticePlayer from "./listeningPracticePlayer.vue"
+export default listeningPracticePlayer

+ 110 - 0
src/views/components/element/listeningPracticeElement/listeningPracticePlayer/listeningPracticePlayer.vue

@@ -0,0 +1,110 @@
+<template>
+  <div
+    class="listeningPracticePlayer"
+    :style="{
+      width: width * scale + 'px',
+      height: height * scale + 'px',
+      transform: `scale(${1 / scale})`
+    }"
+  >
+    <div v-if="loading" class="loading-overlay">
+      <div class="loadingBox">
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+        <div class="loadingItem"></div>
+      </div>
+    </div>
+    <iframe class="musicIframe" frameborder="0" :src="url" @load="handleIframeLoad"></iframe>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from "vue"
+import { getToken } from "@/libs/auth"
+import { YJL_URL_API } from "@/config/index"
+import queryParams from "@/queryParams"
+
+const props = withDefaults(
+  defineProps<{
+    width: number
+    height: number
+    scale?: number
+    code: string
+  }>(),
+  {
+    scale: 1
+  }
+)
+const url = computed(() => {
+  if (queryParams.fromType === "CLASS") {
+    return `${YJL_URL_API}/#/view-figner?Authorization=${getToken()}&code=${props.code}&type=listenMode&linkSource=class&isPreView=true`
+  } else {
+    return `${YJL_URL_API}/#/view-figner?Authorization=${getToken()}&code=${props.code}&platform=pc&type=listenMode&linkSource=class`
+  }
+})
+
+// 先关闭这个功能
+const loading = ref(true)
+function handleIframeLoad() {
+  loading.value = false
+}
+</script>
+
+<style lang="scss" scoped>
+.listeningPracticePlayer {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+  user-select: none;
+  line-height: 1;
+  transform-origin: 0 0;
+  .musicIframe {
+    width: 100%;
+    height: 100%;
+  }
+  .loading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    z-index: 10;
+    color: #fff;
+    background: url("../cloudCoachList/imgs/musicBg.png");
+    background-size: cover;
+    .loadingBox {
+      width: 30px;
+      height: 30px;
+      display: flex;
+      justify-content: space-between;
+      flex-wrap: wrap;
+      align-content: space-between;
+      animation: rotate 1.5s linear infinite;
+      .loadingItem {
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        background: #20bdff;
+        opacity: 0.5;
+        &:nth-child(2) {
+          opacity: 1;
+        }
+      }
+    }
+    @keyframes rotate {
+      from {
+        transform: rotate(0deg);
+      }
+      to {
+        transform: rotate(360deg);
+      }
+    }
+  }
+}
+</style>