lex před 10 měsíci
rodič
revize
635b298b9c

+ 3 - 1
src/views/attend-class/index.tsx

@@ -599,10 +599,12 @@ export default defineComponent({
           );
         }
 
+        console.log(activeItem.iframeRef, 'activeItem.iframeRef');
         if (
           activeItem.type === 'INSTRUMENT' ||
           activeItem.type === 'MUSICIAN' ||
-          activeItem.type === 'MUSIC_WIKI'
+          activeItem.type === 'MUSIC_WIKI' ||
+          activeItem.type === 'THEORY'
         ) {
           activeItem.iframeRef?.handleChangeAudio('pause');
         }

+ 13 - 0
src/views/content-information/content-instrument/detail.module.less

@@ -516,6 +516,19 @@
       --n-rail-height: 0 !important;
     }
 
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
   }
 }
 

+ 13 - 0
src/views/content-information/content-knowledge/index.module.less

@@ -366,6 +366,19 @@
       --n-rail-height: 0 !important;
     }
 
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
   }
 }
 

+ 13 - 0
src/views/content-information/content-music/detail.module.less

@@ -498,6 +498,19 @@
       --n-rail-height: 0 !important;
     }
 
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
   }
 }
 

+ 16 - 11
src/views/content-information/useSpeak.ts

@@ -1,6 +1,12 @@
 import { onMounted, onUnmounted, reactive, toRefs } from 'vue';
 
-export const useSpeak = () => {
+/**
+ * 朗读API
+ * @param musicContent 阅读内容
+ * @returns
+ */
+export const useSpeak = (musicContent?: string) => {
+  const _musicContent = musicContent ? '#' + musicContent : '#musicContent';
   const state = reactive({
     showDom: false,
     synth: null as any,
@@ -17,11 +23,11 @@ export const useSpeak = () => {
   // 函数:递归处理节点
   const processNode = (node: any) => {
     const result = document.createDocumentFragment();
-    node.childNodes.forEach((child: any) => {
+    node.childNodes?.forEach((child: any) => {
       if (child.nodeType === Node.TEXT_NODE) {
         // 按标点符号分割文本
         const sentences = child.textContent.split(/(?<=[,,;;。])\s*/);
-        sentences.forEach((sentence: any) => {
+        sentences?.forEach((sentence: any) => {
           if (sentence.trim()) {
             const customTag = document.createElement('label');
             customTag.textContent = sentence.trim();
@@ -82,7 +88,7 @@ export const useSpeak = () => {
     // console.log(selection, 'selection');
     if (selection.toString().length > 0) {
       state.showDom = true;
-      const textContainer: any = document.querySelector('#musicContent');
+      const textContainer: any = document.querySelector(_musicContent);
       const sentences: any =
         textContainer?.querySelectorAll('label.speak-label');
       let startIndex = 0,
@@ -106,7 +112,7 @@ export const useSpeak = () => {
         anchorOffset = selection.focusOffset;
         focusOffset = selection.anchorOffset;
       }
-      sentences.forEach((element: any, index: number) => {
+      sentences?.forEach((element: any, index: number) => {
         if (element === firstNode) {
           startIndex = index;
           anchorOffset =
@@ -149,9 +155,8 @@ export const useSpeak = () => {
         const y = firstRect.top;
         const bottom = firstRect.bottom;
         const fHeight = firstRect.height;
-        const musicContent: any = document.querySelector('#musicContent');
+        const musicContent: any = document.querySelector(_musicContent);
         const parentRect: any = musicContent?.getBoundingClientRect();
-
         const showDom: any = document.getElementById('selectionCouser');
         const showDomRect = showDom.getBoundingClientRect();
 
@@ -236,9 +241,9 @@ export const useSpeak = () => {
     // responsiveVoice.cancel();
     state.synth?.cancel();
     state.isSpeak = false;
-    const textContainer: any = document.querySelector('#musicContent');
+    const textContainer: any = document.querySelector(_musicContent);
     const sentences: any = textContainer?.querySelectorAll('label.speak-label');
-    sentences.forEach((sentence: any, i: number) => {
+    sentences?.forEach((sentence: any, i: number) => {
       sentence.classList.toggle('highlight', i === -1);
     });
     clearSelection();
@@ -263,7 +268,7 @@ export const useSpeak = () => {
     anchorOffset?: number;
     focusOffset?: number;
   }) => {
-    const textContainer: any = document.querySelector('#musicContent');
+    const textContainer: any = document.querySelector(_musicContent);
     const sentences: any = textContainer?.querySelectorAll('label.speak-label');
 
     // console.log(options, '--endIndex');
@@ -273,7 +278,7 @@ export const useSpeak = () => {
 
     // 高亮显示
     const highlightSentence = (index: number) => {
-      sentences.forEach((sentence: any, i: number) => {
+      sentences?.forEach((sentence: any, i: number) => {
         sentence.classList.toggle('highlight', i === index);
       });
       // 滚动到高亮的部分

+ 591 - 445
src/views/prepare-lessons/model/source-instrument/detail.module.less

@@ -1,446 +1,592 @@
-.container {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-
-  &.containerPreview {
-    padding: 68px 78px 68px 68px;
-    // height: calc(100% - 136px);
-    background-color: #fff;
-
-    .wrapBottom {
-      padding-bottom: 60px !important;
-    }
-  }
-
-  &.containerModal {
-    .content {
-      border-top-left-radius: 0;
-      border-top-right-radius: 0;
-    }
-  }
-
-
-  &> :global(.n-space) {
-    height: 36px;
-    flex-shrink: 0;
-  }
-
-  .separator {
-    width: 9px;
-    height: 15px;
-    margin: 0 16px;
-  }
-}
-
-.wrap {
-  // padding-top: 15px;
-  flex: 1;
-  transition: padding .3s;
-  overflow: hidden;
-
-  &.wrapBottom {
-    padding-bottom: 128px;
-  }
-}
-
-.content {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  background: #DDF2FF;
-  border-radius: 20px;
-  // max-height: 90vh;
-}
-
-.tools {
-  padding: 20px;
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-
-  :global {
-    .n-input {
-      margin-left: auto;
-      width: 361px;
-    }
-
-    .n-input__input-el {
-      height: 100%;
-      line-height: 100%;
-    }
-  }
-}
-
-.contentWrap {
-  position: relative;
-  flex: 1;
-  display: flex;
-  padding: 20px 55px 20px 30px;
-  overflow: hidden;
-  gap: 0 20px;
-}
-
-.musicList {
-  background-color: #fff;
-  border-radius: 16px;
-
-  width: 470px;
-  min-width: 294px;
-  height: 100%;
-  overflow-x: hidden;
-  overflow-y: auto;
-  min-width: 330Px;
-
-  &::-webkit-scrollbar {
-    width: 0;
-    display: none;
-  }
-
-  .instrumentGroup {
-    padding-top: 27px;
-    padding-bottom: 20px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
-
-    .instrumentImg {
-      width: 125px;
-      height: 125px;
-      overflow: hidden;
-      border-radius: 50%;
-
-      &.otherImg {
-        width: 125px;
-        height: 143px;
-      }
-
-      img {
-        width: 100%;
-      }
-    }
-
-    .instrumentName {
-      padding: 13px 0 5px;
-      font-size: max(18px, 14Px);
-      font-weight: 600;
-      color: #131415;
-      line-height: 25px;
-      letter-spacing: 1px;
-    }
-
-    .instrumentTag {
-      font-size: max(13px, 12Px);
-      color: #777777;
-      line-height: 18px;
-    }
-  }
-
-
-
-  .wrapList {
-    width: 470px;
-    padding: 0 17px;
-    min-width: 294px;
-    min-height: 100%;
-    // background: #fff;
-    border-radius: 16px;
-
-    :global {
-      .n-empty .n-empty__description {
-        font-size: calc(14px, 12Px);
-      }
-    }
-
-    .titlec {
-      padding: 20px 0;
-      font-size: max(18px, 14Px);
-      font-weight: 600;
-      color: #000000;
-      line-height: 25px;
-      border-top: 1px solid #F2F2F2;
-      display: flex;
-      align-items: center;
-    }
-
-    .icon2 {
-      width: 23px;
-      height: 23px;
-      margin-right: 8px;
-      background: url('../../../content-information/images/icon-2.png') no-repeat center;
-      background-size: contain;
-    }
-  }
-
-  .empty {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 40vh;
-  }
-}
-
-.itemContainer {
-  width: 100%;
-  border-radius: 16px;
-  padding: 4px 8px;
-  // background-color: #fff;
-
-  &:first-child {
-    padding-top: 8px;
-  }
-
-  &:last-child {
-    // border-radius: 0 0 16px 16px;
-    padding-bottom: 8px;
-  }
-}
-
-.item {
-  position: relative;
-  display: flex;
-  align-items: center;
-  padding: 10px;
-  border-radius: 12px;
-
-  cursor: pointer;
-
-  &:hover {
-    background-color: rgba(0, 0, 0, .05);
-  }
-
-  &.active {
-    background-color: #DDF2FF;
-
-    .arrow {
-      opacity: 1;
-    }
-  }
-
-  .img {
-    position: relative;
-    width: 60px;
-    height: 60px;
-    border-radius: 18px;
-    margin-right: 12px;
-    overflow: hidden;
-    flex-shrink: 0;
-
-    :global {
-      .n-image {
-        width: 60px;
-        height: 60px;
-      }
-    }
-
-    img {
-      transition: opacity .3s;
-      opacity: 0;
-      height: 100%;
-      width: 100%;
-    }
-
-    img[data-loaded="true"] {
-      opacity: 1;
-    }
-  }
-
-  .title {
-    flex: 1;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    align-items: flex-start;
-
-    .titleName {
-      font-size: calc(17px, 12Px);
-      font-weight: 600;
-      color: #131415;
-      line-height: 28px;
-      width: 100%;
-    }
-
-    .titleDes {
-      font-size: 14px;
-      font-weight: 400;
-      color: #777777;
-      line-height: 20px;
-      max-width: 100%;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      overflow: hidden;
-    }
-  }
-
-  .btn {
-    margin-left: auto;
-    width: 84px;
-    height: 40px;
-    background: linear-gradient(to right, #44CAFF, #259DFE);
-    border: none;
-    padding: 0;
-    font-weight: bold !important;
-    flex-shrink: 0;
-    min-width: 62px;
-    min-height: 30px;
-
-    :global {
-      .n-button__content {
-        &>img {
-          margin-left: 10px;
-          width: 9px;
-          height: 12px;
-        }
-      }
-    }
-  }
-
-  .arrow {
-    position: absolute;
-    top: 50%;
-    right: 12px;
-    transform: translate(124%, -50%);
-    opacity: 0;
-  }
-
-  .showPlayLoading {
-    opacity: 0;
-  }
-
-}
-
-.loadingWrap {
-  display: flex;
-  justify-content: center;
-  min-height: 80px;
-}
-
-.musicStaff {
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  left: 0;
-  flex: 1;
-  background-color: #fff;
-  border-radius: 16px;
-  // height: 100%;
-  z-index: 1;
-  overflow: hidden;
-
-  &::-webkit-scrollbar {
-    width: 0;
-    display: none;
-  }
-
-
-  .musicTitle {
-    padding: 27px 27px 13px;
-    font-size: max(18px, 14Px);
-    font-weight: 600;
-    color: #000000;
-    line-height: 25px;
-    display: flex;
-    align-items: center;
-
-    .icon1,
-    .icon3 {
-      display: inline-block;
-      width: 23px;
-      height: 23px;
-      margin-right: 8px;
-      background: url('../../../content-information/images/icon-1.png') no-repeat center;
-      background-size: contain;
-    }
-
-    .icon3 {
-      background: url('../../../content-information/images/icon-3.png') no-repeat center;
-      background-size: contain;
-    }
-  }
-
-  .musicContent {
-    flex: 1;
-    overflow-y: auto;
-    height: 100%;
-    padding: 0 27px;
-
-    &>img {
-      width: 100%;
-    }
-
-    section,
-    &>div {
-      font-size: inherit !important;
-    }
-  }
-}
-
-.staffImgs {
-  flex: 1;
-  overflow-y: auto;
-  height: 100%;
-  padding: 0 30px;
-
-  &>img {
-    width: 100%;
-  }
-}
-
-
-:global {
-
-  .van-fade-enter-active,
-  .van-fade-leave-active {
-    transition: all 0.3s;
-  }
-
-  .van-fade-enter-from,
-  .van-fade-leave-to {
-    opacity: 0;
-  }
-}
-
-.changeSizeSection {
-  position: absolute;
-  right: 10px;
-  bottom: 50%;
-  width: 35px;
-  transform: translate(0, 50%);
-  background: #fff;
-  border-radius: 7px;
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  padding: 13px 0;
-
-  .iconT {
-    width: 15px;
-    height: 15px;
-  }
-
-  .iconAddT,
-  .iconPlusT {
-    width: 23px;
-    height: 23px;
-    cursor: pointer;
-  }
-
-  .iconAddT {
-    margin-top: 13px;
-    margin-bottom: 8px;
-  }
-
-  .iconPlusT {
-    margin-top: 8px;
-  }
-
-  :global {
-    .n-slider {
-      height: 125px;
-      --n-handle-size: 15px !important;
-      --n-rail-height: 0 !important;
-    }
-
-  }
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  &.containerPreview {
+    padding: 68px 78px 68px 68px;
+    // height: calc(100% - 136px);
+    background-color: #fff;
+
+    .wrapBottom {
+      padding-bottom: 60px !important;
+    }
+  }
+
+  &.containerModal {
+    .content {
+      border-top-left-radius: 0;
+      border-top-right-radius: 0;
+    }
+  }
+
+
+  &> :global(.n-space) {
+    height: 36px;
+    flex-shrink: 0;
+  }
+
+  .separator {
+    width: 9px;
+    height: 15px;
+    margin: 0 16px;
+  }
+
+  :global {
+    .highlight {
+      color: #0378EC;
+    }
+  }
+}
+
+.wrap {
+  // padding-top: 15px;
+  flex: 1;
+  transition: padding .3s;
+  overflow: hidden;
+
+  &.wrapBottom {
+    padding-bottom: 128px;
+  }
+}
+
+.content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #DDF2FF;
+  border-radius: 20px;
+  // max-height: 90vh;
+}
+
+.tools {
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+
+  :global {
+    .n-input {
+      margin-left: auto;
+      width: 361px;
+    }
+
+    .n-input__input-el {
+      height: 100%;
+      line-height: 100%;
+    }
+  }
+}
+
+.contentWrap {
+  position: relative;
+  flex: 1;
+  display: flex;
+  padding: 20px 55px 20px 30px;
+  overflow: hidden;
+  gap: 0 20px;
+}
+
+.musicList {
+  background-color: #fff;
+  border-radius: 16px;
+
+  width: 470px;
+  min-width: 294px;
+  height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  min-width: 330Px;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    display: none;
+  }
+
+  .instrumentGroup {
+    padding-top: 27px;
+    padding-bottom: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+
+    .instrumentImg {
+      width: 125px;
+      height: 125px;
+      overflow: hidden;
+      border-radius: 50%;
+
+      &.otherImg {
+        width: 125px;
+        height: 143px;
+      }
+
+      img {
+        width: 100%;
+      }
+    }
+
+    .instrumentName {
+      padding: 13px 0 5px;
+      font-size: max(18px, 14Px);
+      font-weight: 600;
+      color: #131415;
+      line-height: 25px;
+      letter-spacing: 1px;
+    }
+
+    .instrumentTag {
+      font-size: max(13px, 12Px);
+      color: #777777;
+      line-height: 18px;
+    }
+  }
+
+
+
+  .wrapList {
+    width: 470px;
+    padding: 0 17px;
+    min-width: 294px;
+    min-height: 100%;
+    // background: #fff;
+    border-radius: 16px;
+
+    :global {
+      .n-empty .n-empty__description {
+        font-size: calc(14px, 12Px);
+      }
+    }
+
+    .titlec {
+      padding: 20px 0;
+      font-size: max(18px, 14Px);
+      font-weight: 600;
+      color: #000000;
+      line-height: 25px;
+      border-top: 1px solid #F2F2F2;
+      display: flex;
+      align-items: center;
+    }
+
+    .icon2 {
+      width: 23px;
+      height: 23px;
+      margin-right: 8px;
+      background: url('../../../content-information/images/icon-2.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+  .empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 40vh;
+  }
+}
+
+.itemContainer {
+  width: 100%;
+  border-radius: 16px;
+  padding: 4px 8px;
+  // background-color: #fff;
+
+  &:first-child {
+    padding-top: 8px;
+  }
+
+  &:last-child {
+    // border-radius: 0 0 16px 16px;
+    padding-bottom: 8px;
+  }
+}
+
+.item {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  border-radius: 12px;
+
+  cursor: pointer;
+
+  &:hover {
+    background-color: rgba(0, 0, 0, .05);
+  }
+
+  &.active {
+    background-color: #DDF2FF;
+
+    .arrow {
+      opacity: 1;
+    }
+  }
+
+  .img {
+    position: relative;
+    width: 60px;
+    height: 60px;
+    border-radius: 18px;
+    margin-right: 12px;
+    overflow: hidden;
+    flex-shrink: 0;
+
+    :global {
+      .n-image {
+        width: 60px;
+        height: 60px;
+      }
+    }
+
+    img {
+      transition: opacity .3s;
+      opacity: 0;
+      height: 100%;
+      width: 100%;
+    }
+
+    img[data-loaded="true"] {
+      opacity: 1;
+    }
+  }
+
+  .title {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .titleName {
+      font-size: calc(17px, 12Px);
+      font-weight: 600;
+      color: #131415;
+      line-height: 28px;
+      width: 100%;
+    }
+
+    .titleDes {
+      font-size: 14px;
+      font-weight: 400;
+      color: #777777;
+      line-height: 20px;
+      max-width: 100%;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+  }
+
+  .btn {
+    margin-left: auto;
+    width: 84px;
+    height: 40px;
+    background: linear-gradient(to right, #44CAFF, #259DFE);
+    border: none;
+    padding: 0;
+    font-weight: bold !important;
+    flex-shrink: 0;
+    min-width: 62px;
+    min-height: 30px;
+
+    :global {
+      .n-button__content {
+        &>img {
+          margin-left: 10px;
+          width: 9px;
+          height: 12px;
+        }
+      }
+    }
+  }
+
+  .arrow {
+    position: absolute;
+    top: 50%;
+    right: 12px;
+    transform: translate(124%, -50%);
+    opacity: 0;
+  }
+
+  .showPlayLoading {
+    opacity: 0;
+  }
+
+}
+
+.loadingWrap {
+  display: flex;
+  justify-content: center;
+  min-height: 80px;
+}
+
+.musicStaff {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  left: 0;
+  flex: 1;
+  background-color: #fff;
+  border-radius: 16px;
+  // height: 100%;
+  z-index: 1;
+  overflow: hidden;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    display: none;
+  }
+
+
+  .musicTitle {
+    padding: 27px 27px 13px;
+    font-size: max(18px, 14Px);
+    font-weight: 600;
+    color: #000000;
+    line-height: 25px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .musicTitleLeft {
+      display: flex;
+      align-items: center;
+    }
+
+    .icon1,
+    .icon3 {
+      display: inline-block;
+      width: 23px;
+      height: 23px;
+      margin-right: 8px;
+      background: url('../../../content-information/images/icon-1.png') no-repeat center;
+      background-size: contain;
+    }
+
+    .icon3 {
+      background: url('../../../content-information/images/icon-3.png') no-repeat center;
+      background-size: contain;
+    }
+
+    .musicTitleRight {
+
+      .textRead,
+      .textClose {
+        padding: 7px 8px;
+        background: #E8F4FF;
+        border-radius: 13px;
+        font-weight: 500;
+        font-size: max(13px, 12Px);
+        color: #0378EC;
+        line-height: 18px;
+        display: flex;
+        align-items: center;
+        cursor: pointer;
+      }
+
+      .icon {
+        display: inline-block;
+        margin-right: 5px;
+        width: 15px;
+        height: 15px;
+      }
+
+      .textRead {
+        .icon {
+          background: url('../../../content-information/images/icon-speak-sound.png');
+          background-size: contain;
+        }
+      }
+
+      .textClose {
+        .icon {
+          background: url('../../../content-information/images/icon-speak-close.png');
+          background-size: contain;
+        }
+      }
+    }
+  }
+
+  // .musicTitle {
+  //   padding: 27px 27px 13px;
+  //   font-size: max(18px, 14Px);
+  //   font-weight: 600;
+  //   color: #000000;
+  //   line-height: 25px;
+  //   display: flex;
+  //   align-items: center;
+
+  //   .icon1,
+  //   .icon3 {
+  //     display: inline-block;
+  //     width: 23px;
+  //     height: 23px;
+  //     margin-right: 8px;
+  //     background: url('../../../content-information/images/icon-1.png') no-repeat center;
+  //     background-size: contain;
+  //   }
+
+  //   .icon3 {
+  //     background: url('../../../content-information/images/icon-3.png') no-repeat center;
+  //     background-size: contain;
+  //   }
+  // }
+
+  .musicContent {
+    flex: 1;
+    overflow-y: auto;
+    height: 100%;
+    padding: 0 27px;
+    user-select: text;
+    position: relative;
+
+    &::selection {
+      background-color: #dfdfdf;
+    }
+
+    &>img {
+      width: 100%;
+    }
+
+    section,
+    &>div {
+      font-size: inherit !important;
+    }
+  }
+}
+
+.staffImgs {
+  flex: 1;
+  overflow-y: auto;
+  height: 100%;
+  padding: 0 30px;
+
+  &>img {
+    width: 100%;
+  }
+}
+
+
+:global {
+
+  .van-fade-enter-active,
+  .van-fade-leave-active {
+    transition: all 0.3s;
+  }
+
+  .van-fade-enter-from,
+  .van-fade-leave-to {
+    opacity: 0;
+  }
+}
+
+.changeSizeSection {
+  position: absolute;
+  right: 10px;
+  bottom: 50%;
+  width: 35px;
+  transform: translate(0, 50%);
+  background: #fff;
+  border-radius: 7px;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  padding: 13px 0;
+
+  .iconT {
+    width: 15px;
+    height: 15px;
+  }
+
+  .iconAddT,
+  .iconPlusT {
+    width: 23px;
+    height: 23px;
+    cursor: pointer;
+  }
+
+  .iconAddT {
+    margin-top: 13px;
+    margin-bottom: 8px;
+  }
+
+  .iconPlusT {
+    margin-top: 8px;
+  }
+
+  :global {
+    .n-slider {
+      height: 125px;
+      --n-handle-size: 15px !important;
+      --n-rail-height: 0 !important;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
+  }
+}
+
+.selectionCouser {
+  display: flex;
+  align-items: center;
+  position: absolute;
+
+  &.hide {
+    opacity: 0;
+    visibility: hidden;
+  }
+
+  .textStart,
+  .textReadOnly {
+    cursor: pointer;
+    background: #1A8CFF;
+    border-radius: 13px;
+    font-weight: 600;
+    font-size: max(13px, 12Px);
+    color: #FFFFFF;
+    line-height: 18px;
+    display: inline-flex;
+    align-items: center;
+    padding: 3px 8px;
+    flex-shrink: 0;
+
+    .icon {
+      margin-left: 4px;
+      display: inline-block;
+    }
+  }
+
+  .textStart {
+    .icon {
+      width: 8px;
+      height: 10px;
+      background: url('../../../content-information/images/icon-speak-arrow.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+  .textReadOnly {
+    margin-left: 10px;
+
+    .icon {
+
+      width: 9px;
+      height: 9px;
+      background: url('../../../content-information/images/icon-speak-line.png') no-repeat center;
+      background-size: contain;
+    }
+  }
 }

+ 411 - 344
src/views/prepare-lessons/model/source-instrument/detail.tsx

@@ -1,344 +1,411 @@
-import {
-  NBreadcrumb,
-  NBreadcrumbItem,
-  NButton,
-  NImage,
-  NSlider,
-  NSpace,
-  NSpin
-} from 'naive-ui';
-import { computed, defineComponent, onMounted, reactive, watch } from 'vue';
-import styles from './detail.module.less';
-// import icon_back from '../../xiaoku-music/images/icon_back.png';
-import icon_arrow from '../../../xiaoku-music/images/icon_arrow.png';
-import icon_play from '../../../xiaoku-music/images/icon_play.png';
-import icon_pause from '../../../xiaoku-music/images/icon_pause.png';
-import icon_default from '../../../xiaoku-music/images/icon_default.png';
-// import icon_separator from '../../xiaoku-music/images/icon_separator.png';
-import iconT from '/src/views/content-information/images/icon-t.png';
-import iconAddT from '/src/views/content-information/images/icon-add-t.png';
-import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
-import { useRoute, useRouter } from 'vue-router';
-import PlayLoading from '../../../xiaoku-music/component/play-loading';
-import TheNoticeBar from '/src/components/TheNoticeBar';
-import TheEmpty from '/src/components/TheEmpty';
-import PlayItem from '../../../xiaoku-music/component/play-item';
-import { api_knowledgeWiki_detail } from '/src/views/content-information/api';
-
-export default defineComponent({
-  name: 'instrument-detail',
-  props: {
-    id: {
-      type: String,
-      default: ''
-    },
-    type: {
-      type: String,
-      default: ''
-    },
-    activeStatus: {
-      type: Boolean,
-      default: false
-    },
-    contentType: {
-      type: String,
-      default: ''
-    }
-  },
-  setup(props, { expose }) {
-    const route = useRoute();
-    const router = useRouter();
-    const forms = reactive({
-      page: 1,
-      rows: 20,
-      status: true,
-      name: '', // 关键词
-      type: props.contentType
-    });
-    const data = reactive({
-      loading: false,
-      finshed: false,
-      reshing: false,
-      details: {} as any,
-      list: [] as any,
-      listActive: 0,
-      playState: 'pause' as 'play' | 'pause',
-      showPlayer: false,
-      showPreivew: false,
-      previewUrl: '',
-      showCloseBtn: true,
-      fontSize: 18 // 默认18
-    });
-
-    /** 选中的item */
-    const activeItem = computed(() => {
-      return data.list[data.listActive] || {};
-    });
-
-    /** 播放曲目 */
-    const handlePlay = (item: any) => {
-      const index = data.list.findIndex((_item: any) => _item.id === item.id);
-      if (index > -1) {
-        if (data.listActive === index) {
-          data.playState = data.playState === 'play' ? 'pause' : 'play';
-        } else {
-          data.playState = 'play';
-        }
-        data.showPlayer = true;
-        data.listActive = index;
-      }
-    };
-
-    /** 音频控制 */
-    const handleChangeAudio = (
-      type: 'play' | 'pause' | 'pre' | 'next' | 'favitor'
-    ) => {
-      if (type === 'play') {
-        data.playState = 'play';
-      } else if (type === 'pause') {
-        data.playState = 'pause';
-      } else if (type === 'pre') {
-        if (data.list[data.listActive - 1]) {
-          handlePlay(data.list[data.listActive - 1]);
-        }
-      } else if (type === 'next') {
-        if (data.list[data.listActive + 1]) {
-          handlePlay(data.list[data.listActive + 1]);
-        }
-      }
-    };
-
-    const getDetail = async () => {
-      data.loading = true;
-      let res = {} as any;
-      try {
-        res = await api_knowledgeWiki_detail({
-          id: props.id || route.query.id
-        });
-      } catch (error) {
-        console.log(error);
-      }
-      if (data.reshing) {
-        data.list = [];
-        data.reshing = false;
-      }
-
-      data.finshed = true;
-      data.list = res.data.knowledgeWikiResources || [];
-      data.list.forEach((item: any) => {
-        item.audioFileUrl = item.url;
-        item.musicSheetName = item.name;
-      });
-      const knowledgeWikiCategories = res.data.knowledgeWikiCategories || [];
-      res.data.knowledgeName =
-        knowledgeWikiCategories.length > 0
-          ? knowledgeWikiCategories[0].knowledgeWikiCategoryTypeName
-          : '';
-      res.data.intros = res.data.intros.replace(
-        /<video/gi,
-        '<video class="video-music" style="width: 100% !important;" controlslist="nodownload"'
-      );
-      data.details = res.data;
-      data.loading = false;
-    };
-
-    const onStopAll = (type: 'play' | 'pause' | 'pre' | 'next' | 'favitor') => {
-      handleChangeAudio(type);
-
-      try {
-        // 暂停视频
-        const doms = document.querySelectorAll('.video-music');
-        if (doms && doms.length > 0) {
-          doms.forEach((dom: any) => {
-            dom.pause();
-          });
-        }
-      } catch {
-        //
-      }
-    };
-
-    onMounted(() => {
-      getDetail();
-    });
-
-    watch(
-      () => props.activeStatus,
-      () => {
-        if (!props.activeStatus) {
-          onStopAll('pause');
-        }
-      }
-    );
-
-    expose({
-      handleChangeAudio: onStopAll
-    });
-    return () => (
-      <div
-        class={[
-          styles.container,
-          props.type === 'preview' && styles.containerPreview,
-          props.type === 'modal' && styles.containerModal
-        ]}>
-        <div class={[styles.wrap, data.showPlayer ? styles.wrapBottom : '']}>
-          <div class={styles.content}>
-            <div class={styles.contentWrap}>
-              <div class={[styles.musicList, 'musicList-container']}>
-                <div class={styles.wrapList}>
-                  <div class={styles.instrumentGroup}>
-                    <NImage
-                      class={[
-                        styles.instrumentImg,
-                        forms.type === 'MUSICIAN' && styles.otherImg
-                      ]}
-                      src={data.details?.avatar}
-                      objectFit="cover"
-                    />
-
-                    <p class={styles.instrumentName}>{data.details.name}</p>
-                    <p class={styles.instrumentTag}>
-                      {data.details.knowledgeName}
-                    </p>
-                  </div>
-
-                  <div class={styles.titlec}>
-                    <i class={styles.icon2}></i>代表作
-                  </div>
-
-                  {data.list.map((item: any, index: any) => {
-                    return (
-                      <div class={styles.itemContainer}>
-                        <div
-                          class={[
-                            styles.item
-                            // data.listActive === index && styles.active
-                          ]}
-                          onClick={(e: Event) => {
-                            e.stopPropagation();
-                            handlePlay(item);
-                          }}>
-                          <div class={styles.img}>
-                            <NImage
-                              lazy
-                              objectFit="cover"
-                              previewDisabled={true}
-                              src={item.titleImg || icon_default}
-                              onLoad={e => {
-                                (e.target as any).dataset.loaded = 'true';
-                              }}
-                            />
-                            <PlayLoading
-                              class={[
-                                data.listActive === index &&
-                                data.playState === 'play'
-                                  ? ''
-                                  : styles.showPlayLoading
-                              ]}
-                            />
-                          </div>
-                          <div class={styles.title}>
-                            <div class={styles.titleName}>
-                              <TheNoticeBar
-                                text={item.name}
-                                style={{ marginRight: '12px' }}
-                              />
-                            </div>
-                          </div>
-
-                          <NButton
-                            color="#259CFE"
-                            textColor="#fff"
-                            round
-                            class={styles.btn}
-                            type="primary"
-                            onClick={(e: Event) => {
-                              e.stopPropagation();
-                              handlePlay(item);
-                            }}>
-                            播放
-                            <img
-                              src={
-                                data.listActive === index &&
-                                data.playState === 'play'
-                                  ? icon_pause
-                                  : icon_play
-                              }
-                            />
-                          </NButton>
-
-                          <img class={styles.arrow} src={icon_arrow} />
-                        </div>
-                      </div>
-                    );
-                  })}
-                  {!data.finshed && (
-                    <div class={styles.loadingWrap}>
-                      <NSpin show={true}></NSpin>
-                    </div>
-                  )}
-                  {!data.loading && data.list.length === 0 && (
-                    <div class={styles.empty}>
-                      <TheEmpty description="暂无代表作"></TheEmpty>
-                    </div>
-                  )}
-                </div>
-              </div>
-
-              <div class={styles.musicStaff}>
-                <div class={styles.musicTitle}>
-                  <i
-                    class={
-                      forms.type === 'MUSICIAN' ? styles.icon3 : styles.icon1
-                    }></i>
-                  {forms.type === 'MUSICIAN' ? '个人简介' : '乐器简介'}
-                </div>
-                <div
-                  class={styles.musicContent}
-                  v-html={data.details?.intros}
-                  style={{ fontSize: data.fontSize + 'px' }}></div>
-              </div>
-
-              <div class={styles.changeSizeSection}>
-                <img src={iconT} class={styles.iconT} />
-                <img
-                  src={iconAddT}
-                  class={styles.iconAddT}
-                  onClick={() => {
-                    if (data.fontSize >= 32) return;
-                    data.fontSize += 1;
-                  }}
-                />
-                <NSlider
-                  v-model:value={data.fontSize}
-                  vertical
-                  placement="left"
-                  min={12}
-                  max={32}
-                />
-                <img
-                  src={iconPlusT}
-                  class={styles.iconPlusT}
-                  onClick={() => {
-                    if (data.fontSize <= 12) return;
-                    data.fontSize -= 1;
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        </div>
-
-        {data.list.length !== 0 && (
-          <PlayItem
-            type={props.type}
-            show={data.showPlayer}
-            playState={data.playState}
-            item={activeItem.value}
-            onChange={value => handleChangeAudio(value)}
-          />
-        )}
-      </div>
-    );
-  }
-});
+import {
+  NBreadcrumb,
+  NBreadcrumbItem,
+  NButton,
+  NImage,
+  NSlider,
+  NSpace,
+  NSpin
+} from 'naive-ui';
+import { computed, defineComponent, onMounted, reactive, watch } from 'vue';
+import styles from './detail.module.less';
+// import icon_back from '../../xiaoku-music/images/icon_back.png';
+import icon_arrow from '../../../xiaoku-music/images/icon_arrow.png';
+import icon_play from '../../../xiaoku-music/images/icon_play.png';
+import icon_pause from '../../../xiaoku-music/images/icon_pause.png';
+import icon_default from '../../../xiaoku-music/images/icon_default.png';
+// import icon_separator from '../../xiaoku-music/images/icon_separator.png';
+import iconT from '/src/views/content-information/images/icon-t.png';
+import iconAddT from '/src/views/content-information/images/icon-add-t.png';
+import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
+import { useRoute, useRouter } from 'vue-router';
+import PlayLoading from '../../../xiaoku-music/component/play-loading';
+import TheNoticeBar from '/src/components/TheNoticeBar';
+import TheEmpty from '/src/components/TheEmpty';
+import PlayItem from '../../../xiaoku-music/component/play-item';
+import { api_knowledgeWiki_detail } from '/src/views/content-information/api';
+import { useSpeak } from '/src/views/content-information/useSpeak';
+
+export default defineComponent({
+  name: 'instrument-detail',
+  props: {
+    id: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: ''
+    },
+    activeStatus: {
+      type: Boolean,
+      default: false
+    },
+    contentType: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props, { expose }) {
+    const route = useRoute();
+    const speakMusicContent =
+      'musicContent' + new Date().getTime() + Math.floor(Math.random() * 100);
+    const speak = useSpeak(speakMusicContent);
+    const forms = reactive({
+      page: 1,
+      rows: 20,
+      status: true,
+      name: '', // 关键词
+      type: props.contentType
+    });
+    const data = reactive({
+      loading: false,
+      finshed: false,
+      reshing: false,
+      details: {} as any,
+      list: [] as any,
+      listActive: 0,
+      playState: 'pause' as 'play' | 'pause',
+      showPlayer: false,
+      showPreivew: false,
+      previewUrl: '',
+      showCloseBtn: true,
+      fontSize: 18 // 默认18
+    });
+
+    /** 选中的item */
+    const activeItem = computed(() => {
+      return data.list[data.listActive] || {};
+    });
+
+    watch(
+      () => data.playState,
+      () => {
+        if (data.playState === 'play') {
+          speak.onCloseSpeak();
+        }
+      }
+    );
+
+    /** 播放曲目 */
+    const handlePlay = (item: any) => {
+      const index = data.list.findIndex((_item: any) => _item.id === item.id);
+      if (index > -1) {
+        if (data.listActive === index) {
+          data.playState = data.playState === 'play' ? 'pause' : 'play';
+        } else {
+          data.playState = 'play';
+        }
+        data.showPlayer = true;
+        data.listActive = index;
+      }
+    };
+
+    /** 音频控制 */
+    const handleChangeAudio = (
+      type: 'play' | 'pause' | 'pre' | 'next' | 'favitor'
+    ) => {
+      if (type === 'play') {
+        data.playState = 'play';
+      } else if (type === 'pause') {
+        data.playState = 'pause';
+      } else if (type === 'pre') {
+        if (data.list[data.listActive - 1]) {
+          handlePlay(data.list[data.listActive - 1]);
+        }
+      } else if (type === 'next') {
+        if (data.list[data.listActive + 1]) {
+          handlePlay(data.list[data.listActive + 1]);
+        }
+      }
+    };
+
+    const getDetail = async () => {
+      data.loading = true;
+      let res = {} as any;
+      try {
+        res = await api_knowledgeWiki_detail({
+          id: props.id || route.query.id
+        });
+      } catch (error) {
+        console.log(error);
+      }
+      if (data.reshing) {
+        data.list = [];
+        data.reshing = false;
+      }
+
+      data.finshed = true;
+      data.list = res.data.knowledgeWikiResources || [];
+      data.list.forEach((item: any) => {
+        item.audioFileUrl = item.url;
+        item.musicSheetName = item.name;
+      });
+      const knowledgeWikiCategories = res.data.knowledgeWikiCategories || [];
+      res.data.knowledgeName =
+        knowledgeWikiCategories.length > 0
+          ? knowledgeWikiCategories[0].knowledgeWikiCategoryTypeName
+          : '';
+      res.data.intros = res.data.intros.replace(
+        /<video/gi,
+        '<video class="video-music" style="width: 100% !important;" controlslist="nodownload"'
+      );
+      // 使用 DOMParser 解析 HTML 字符串
+      const parser = new DOMParser();
+      const doc = parser.parseFromString(res.data.intros, 'text/html');
+
+      // 提取并分割 HTML 文档中的内容
+      document
+        .querySelector('#' + speakMusicContent)
+        ?.appendChild(speak.processNode(doc.body));
+
+      data.details = res.data;
+      data.loading = false;
+    };
+
+    const onStopAll = (type: 'play' | 'pause' | 'pre' | 'next' | 'favitor') => {
+      handleChangeAudio(type);
+      speak.onCloseSpeak();
+      try {
+        // 暂停视频
+        const doms = document.querySelectorAll('.video-music');
+        if (doms && doms.length > 0) {
+          doms.forEach((dom: any) => {
+            dom.pause();
+          });
+        }
+      } catch {
+        //
+      }
+    };
+
+    onMounted(() => {
+      getDetail();
+    });
+
+    watch(
+      () => props.activeStatus,
+      () => {
+        if (!props.activeStatus) {
+          onStopAll('pause');
+        }
+      }
+    );
+
+    expose({
+      handleChangeAudio: onStopAll
+    });
+    return () => (
+      <div
+        class={[
+          styles.container,
+          props.type === 'preview' && styles.containerPreview,
+          props.type === 'modal' && styles.containerModal
+        ]}>
+        <div class={[styles.wrap, data.showPlayer ? styles.wrapBottom : '']}>
+          <div class={styles.content}>
+            <div class={styles.contentWrap}>
+              <div class={[styles.musicList, 'musicList-container']}>
+                <div class={styles.wrapList}>
+                  <div class={styles.instrumentGroup}>
+                    <NImage
+                      class={[
+                        styles.instrumentImg,
+                        forms.type === 'MUSICIAN' && styles.otherImg
+                      ]}
+                      src={data.details?.avatar}
+                      objectFit="cover"
+                    />
+
+                    <p class={styles.instrumentName}>{data.details.name}</p>
+                    <p class={styles.instrumentTag}>
+                      {data.details.knowledgeName}
+                    </p>
+                  </div>
+
+                  <div class={styles.titlec}>
+                    <i class={styles.icon2}></i>代表作
+                  </div>
+
+                  {data.list.map((item: any, index: any) => {
+                    return (
+                      <div class={styles.itemContainer}>
+                        <div
+                          class={[
+                            styles.item
+                            // data.listActive === index && styles.active
+                          ]}
+                          onClick={(e: Event) => {
+                            e.stopPropagation();
+                            handlePlay(item);
+                          }}>
+                          <div class={styles.img}>
+                            <NImage
+                              lazy
+                              objectFit="cover"
+                              previewDisabled={true}
+                              src={item.titleImg || icon_default}
+                              onLoad={e => {
+                                (e.target as any).dataset.loaded = 'true';
+                              }}
+                            />
+                            <PlayLoading
+                              class={[
+                                data.listActive === index &&
+                                data.playState === 'play'
+                                  ? ''
+                                  : styles.showPlayLoading
+                              ]}
+                            />
+                          </div>
+                          <div class={styles.title}>
+                            <div class={styles.titleName}>
+                              <TheNoticeBar
+                                text={item.name}
+                                style={{ marginRight: '12px' }}
+                              />
+                            </div>
+                          </div>
+
+                          <NButton
+                            color="#259CFE"
+                            textColor="#fff"
+                            round
+                            class={styles.btn}
+                            type="primary"
+                            onClick={(e: Event) => {
+                              e.stopPropagation();
+                              handlePlay(item);
+                            }}>
+                            播放
+                            <img
+                              src={
+                                data.listActive === index &&
+                                data.playState === 'play'
+                                  ? icon_pause
+                                  : icon_play
+                              }
+                            />
+                          </NButton>
+
+                          <img class={styles.arrow} src={icon_arrow} />
+                        </div>
+                      </div>
+                    );
+                  })}
+                  {!data.finshed && (
+                    <div class={styles.loadingWrap}>
+                      <NSpin show={true}></NSpin>
+                    </div>
+                  )}
+                  {!data.loading && data.list.length === 0 && (
+                    <div class={styles.empty}>
+                      <TheEmpty description="暂无代表作"></TheEmpty>
+                    </div>
+                  )}
+                </div>
+              </div>
+
+              <div class={styles.musicStaff}>
+                <div class={styles.musicTitle}>
+                  <div class={styles.musicTitleLeft}>
+                    <i
+                      class={
+                        forms.type === 'MUSICIAN' ? styles.icon3 : styles.icon1
+                      }></i>
+                    {forms.type === 'MUSICIAN' ? '个人简介' : '乐器简介'}
+                  </div>
+
+                  <div class={styles.musicTitleRight}>
+                    {speak.isSpeak.value ? (
+                      <span
+                        class={styles.textClose}
+                        onClick={speak.onCloseSpeak}>
+                        <i class={styles.icon}></i>关闭朗读
+                      </span>
+                    ) : (
+                      <span
+                        class={styles.textRead}
+                        onClick={() => {
+                          speak.onAllSpeak();
+                          handleChangeAudio('pause');
+                        }}>
+                        <i class={styles.icon}></i>全文朗读
+                      </span>
+                    )}
+                  </div>
+                </div>
+                <div
+                  class={styles.musicContent}
+                  id={speakMusicContent}
+                  style={{ fontSize: data.fontSize + 'px' }}>
+                  {/* 选中的内容 */}
+                  <div
+                    id="selectionCouser"
+                    class={[
+                      styles.selectionCouser,
+                      !speak.showDom.value && styles.hide
+                    ]}>
+                    <span
+                      class={styles.textStart}
+                      onClick={() => {
+                        speak.onTextStart();
+                        handleChangeAudio('pause');
+                      }}>
+                      开始朗读<i class={styles.icon}></i>
+                    </span>
+                    <span
+                      class={styles.textReadOnly}
+                      onClick={() => {
+                        speak.onTextReadOnly();
+                        handleChangeAudio('pause');
+                      }}>
+                      只读这段<i class={styles.icon}></i>
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div class={styles.changeSizeSection}>
+                <img src={iconT} class={styles.iconT} />
+                <img
+                  src={iconAddT}
+                  class={styles.iconAddT}
+                  onClick={() => {
+                    if (data.fontSize >= 32) return;
+                    data.fontSize += 1;
+                  }}
+                />
+                <NSlider
+                  v-model:value={data.fontSize}
+                  vertical
+                  placement="left"
+                  min={12}
+                  max={32}
+                />
+                <img
+                  src={iconPlusT}
+                  class={styles.iconPlusT}
+                  onClick={() => {
+                    if (data.fontSize <= 12) return;
+                    data.fontSize -= 1;
+                  }}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {data.list.length !== 0 && (
+          <PlayItem
+            type={props.type}
+            show={data.showPlayer}
+            playState={data.playState}
+            item={activeItem.value}
+            onChange={value => handleChangeAudio(value)}
+          />
+        )}
+      </div>
+    );
+  }
+});

+ 203 - 133
src/views/prepare-lessons/model/source-knowledge/detail.tsx

@@ -1,133 +1,203 @@
-import { defineComponent, onMounted, reactive, ref, watch } from 'vue';
-import styles from './index.module.less';
-import {
-  NButton,
-  // NBreadcrumb,
-  // NBreadcrumbItem,
-  // NScrollbar,
-  NSlider,
-  NSpace,
-  NSpin
-} from 'naive-ui';
-import iconT from '/src/views/content-information/images/icon-t.png';
-import iconAddT from '/src/views/content-information/images/icon-add-t.png';
-import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
-import {
-  api_lessonCoursewareDetail_listKnowledge,
-  api_lessonCoursewareKnowledgeDetail
-} from '/src/views/content-information/api';
-import TheEmpty from '/src/components/TheEmpty';
-import { PageEnum } from '/src/enums/pageEnum';
-
-export default defineComponent({
-  name: 'cotnent-knowledge',
-  props: {
-    id: {
-      type: String,
-      default: ''
-    },
-    type: {
-      type: String,
-      default: ''
-    },
-    activeStatus: {
-      type: Boolean,
-      default: false
-    }
-  },
-  emits: ['close', 'confirm'],
-  setup(props, { emit }) {
-    const content = ref(false);
-    const musicContentRef = ref();
-    const state = reactive({
-      fontSize: 18,
-      tableList: [] as any,
-      selectKey: null,
-      details: {} as any
-    });
-
-    const getDetail = async () => {
-      content.value = true;
-      try {
-        const { data } = await api_lessonCoursewareKnowledgeDetail({
-          id: props.id
-        });
-
-        state.details = data;
-      } catch {
-        //
-      }
-      content.value = false;
-    };
-
-    onMounted(() => {
-      getDetail();
-    });
-
-    watch(
-      () => props.activeStatus,
-      () => {
-        // if (!props.activeStatus) {
-        //   handleChangeAudio('pause');
-        // }
-      }
-    );
-    return () => (
-      <div
-        class={[
-          styles.containerDetail,
-          props.type === 'preview' && styles.detailPreview
-        ]}>
-        {/* <div class={styles.detail2}> */}
-        <div class={styles.contentWrap}>
-          <div class={styles.musicStaff}>
-            <NSpin
-              show={content.value}
-              ref={musicContentRef}
-              class={
-                !content.value && !state.details?.desc ? styles.empty : ''
-              }>
-              {state.details?.desc ? (
-                <div
-                  class={styles.musicContent}
-                  v-html={state.details?.desc}
-                  style={{ fontSize: state.fontSize + 'px' }}></div>
-              ) : (
-                ''
-              )}
-              {!content.value && !state.details?.desc && <TheEmpty />}
-            </NSpin>
-          </div>
-
-          <div class={styles.changeSizeSection}>
-            <img src={iconT} class={styles.iconT} />
-            <img
-              src={iconAddT}
-              class={styles.iconAddT}
-              onClick={() => {
-                if (state.fontSize >= 32) return;
-                state.fontSize += 1;
-              }}
-            />
-            <NSlider
-              v-model:value={state.fontSize}
-              vertical
-              min={12}
-              placement="left"
-              max={32}
-            />
-            <img
-              src={iconPlusT}
-              class={styles.iconPlusT}
-              onClick={() => {
-                if (state.fontSize <= 12) return;
-                state.fontSize -= 1;
-              }}
-            />
-          </div>
-        </div>
-        {/* </div> */}
-      </div>
-    );
-  }
-});
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
+import styles from './index.module.less';
+import {
+  NButton,
+  // NBreadcrumb,
+  // NBreadcrumbItem,
+  // NScrollbar,
+  NSlider,
+  NSpace,
+  NSpin
+} from 'naive-ui';
+import iconT from '/src/views/content-information/images/icon-t.png';
+import iconAddT from '/src/views/content-information/images/icon-add-t.png';
+import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
+import {
+  api_lessonCoursewareDetail_listKnowledge,
+  api_lessonCoursewareKnowledgeDetail
+} from '/src/views/content-information/api';
+import TheEmpty from '/src/components/TheEmpty';
+// import { PageEnum } from '/src/enums/pageEnum';
+import { useSpeak } from '/src/views/content-information/useSpeak';
+
+export default defineComponent({
+  name: 'cotnent-knowledge',
+  props: {
+    id: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: ''
+    },
+    activeStatus: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['close', 'confirm'],
+  setup(props, { expose }) {
+    const content = ref(false);
+    const musicContentRef = ref();
+    const speakMusicContent =
+      'musicContent' + new Date().getTime() + Math.floor(Math.random() * 100);
+    const speak = useSpeak(speakMusicContent);
+    const state = reactive({
+      fontSize: 18,
+      tableList: [] as any,
+      selectKey: null,
+      details: {} as any
+    });
+
+    const getDetail = async () => {
+      content.value = true;
+      try {
+        const { data } = await api_lessonCoursewareKnowledgeDetail({
+          id: props.id
+        });
+
+        content.value = false;
+        state.details = data;
+        nextTick(() => {
+          // 使用 DOMParser 解析 HTML 字符串
+          const parser = new DOMParser();
+          const doc = parser.parseFromString(data.desc, 'text/html');
+
+          const hasChilds = document.querySelectorAll('.only-child-select');
+          if (hasChilds.length > 0) {
+            hasChilds.forEach(child => {
+              child.remove();
+            });
+          }
+
+          const childNodes = doc.body.childNodes;
+          childNodes?.forEach((node: any) => {
+            node?.classList.add('only-child-select');
+          });
+
+          // 提取并分割 HTML 文档中的内容
+          document
+            .querySelector('#' + speakMusicContent)
+            ?.appendChild(speak.processNode(doc.body));
+        });
+      } catch {
+        //
+      }
+      content.value = false;
+    };
+
+    const onStopAll = (type: 'play' | 'pause' | 'pre' | 'next' | 'favitor') => {
+      speak.onCloseSpeak();
+    };
+
+    onMounted(() => {
+      getDetail();
+    });
+
+    watch(
+      () => props.activeStatus,
+      () => {
+        if (!props.activeStatus) {
+          onStopAll('pause');
+        }
+      }
+    );
+
+    expose({
+      handleChangeAudio: onStopAll
+    });
+    return () => (
+      <div
+        class={[
+          styles.containerDetail,
+          props.type === 'preview' && styles.detailPreview
+        ]}>
+        {/* <div class={styles.detail2}> */}
+        <div class={styles.contentWrap}>
+          <div class={styles.musicStaff}>
+            <div class={styles.musicTitleRight}>
+              {speak.isSpeak.value ? (
+                <span class={styles.textClose} onClick={speak.onCloseSpeak}>
+                  <i class={styles.icon}></i>关闭朗读
+                </span>
+              ) : (
+                <span class={styles.textRead} onClick={speak.onAllSpeak}>
+                  <i class={styles.icon}></i>全文朗读
+                </span>
+              )}
+            </div>
+            <NSpin
+              show={content.value}
+              ref={musicContentRef}
+              class={
+                !content.value && !state.details?.desc ? styles.empty : ''
+              }>
+              {state.details?.desc ? (
+                <div
+                  class={styles.musicContent}
+                  id={speakMusicContent}
+                  style={{ fontSize: state.fontSize + 'px' }}>
+                  {/* 选中的内容 */}
+                  <div
+                    id="selectionCouser"
+                    class={[
+                      styles.selectionCouser,
+                      !speak.showDom.value && styles.hide
+                    ]}>
+                    <span class={styles.textStart} onClick={speak.onTextStart}>
+                      开始朗读<i class={styles.icon}></i>
+                    </span>
+                    <span
+                      class={styles.textReadOnly}
+                      onClick={speak.onTextReadOnly}>
+                      只读这段<i class={styles.icon}></i>
+                    </span>
+                  </div>
+                </div>
+              ) : (
+                ''
+              )}
+              {!content.value && !state.details?.desc && <TheEmpty />}
+            </NSpin>
+          </div>
+
+          <div class={styles.changeSizeSection}>
+            <img src={iconT} class={styles.iconT} />
+            <img
+              src={iconAddT}
+              class={styles.iconAddT}
+              onClick={() => {
+                if (state.fontSize >= 32) return;
+                state.fontSize += 1;
+              }}
+            />
+            <NSlider
+              v-model:value={state.fontSize}
+              vertical
+              min={12}
+              placement="left"
+              max={32}
+            />
+            <img
+              src={iconPlusT}
+              class={styles.iconPlusT}
+              onClick={() => {
+                if (state.fontSize <= 12) return;
+                state.fontSize -= 1;
+              }}
+            />
+          </div>
+        </div>
+        {/* </div> */}
+      </div>
+    );
+  }
+});

+ 484 - 366
src/views/prepare-lessons/model/source-knowledge/index.module.less

@@ -1,367 +1,485 @@
-.container {
-  display: flex;
-  flex-direction: column;
-  height: 750px;
-  background: #F1F5FF;
-  border-radius: 0 0 12Px 12Px;
-  padding-left: 27px;
-
-  .iconBack {
-    width: 36px;
-    height: 36px;
-  }
-
-  .separator {
-    width: 9px;
-    height: 15px;
-    margin: 0 16px;
-  }
-}
-
-.containerDetail {
-  display: flex;
-  flex-direction: column;
-  height: 75vh;
-  // background: #F1F5FF;
-  border-radius: 0 0 12Px 12Px;
-  padding-left: 27px;
-
-  &.detailPreview {
-    height: 100%;
-    background-color: #fff;
-    // max-width: 1024px;
-    margin: 0 auto;
-    border-radius: 0;
-    padding-left: 0;
-
-
-    .contentWrap {
-      width: 80%;
-      margin: 0 auto;
-      position: initial;
-    }
-
-    .changeSizeSection {
-      right: 85px;
-    }
-  }
-}
-
-.wrap {
-  padding-top: 15px;
-  flex: 1;
-  transition: padding .3s;
-  overflow: hidden;
-
-  &.wrapBottom {
-    padding-bottom: 108px;
-
-  }
-}
-
-.contentWrap {
-  position: relative;
-  flex: 1;
-  display: flex;
-  padding: 0 55px 0 0;
-  overflow: hidden;
-  gap: 0 16px;
-}
-
-.content {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  border-radius: 20px;
-  // max-height: 90vh;
-}
-
-.contentWrap {
-  :global {
-    .n-scrollbar-container {
-      max-height: 100%;
-    }
-  }
-
-  .scrollBar {
-    margin: 17px 0;
-    padding: 0 17px;
-    // max-height: calc(100% - 64px - 52px - 36px);
-
-    &.empty {
-      display: flex;
-      align-items: center;
-      justify-content: center;
-    }
-  }
-
-
-  .directoryList {
-    width: 350px;
-    background: #FFFFFF;
-    border-radius: 17px;
-    flex-shrink: 0;
-    height: 100%;
-    overflow-x: hidden;
-    overflow-y: auto;
-
-    &::-webkit-scrollbar {
-      width: 0;
-      display: none;
-    }
-  }
-
-  .treeParent {
-    transition: height 1s ease-in-out;
-  }
-
-  .treeChild {
-    line-height: 54px;
-  }
-
-  .treeItem {
-    position: relative;
-    display: flex;
-    align-items: center;
-    line-height: 54px;
-    border-radius: 10px;
-    padding: 0 0 0 15px;
-    cursor: pointer;
-    border-radius: 10px;
-    font-size: max(15px, 13Px);
-
-    // &:hover {
-    //   background: #EAEDF2;
-    // }
-
-    .title {
-      padding-left: 8px;
-      color: rgba(0, 0, 0, .5);
-      display: flex;
-      align-items: center;
-
-      .dir {
-        flex-shrink: 0;
-        display: inline-block;
-        width: 16px;
-        height: 18px;
-        background: url('../../../prepare-lessons/components/directory-main/images/icon-d.png') no-repeat center;
-        background-size: contain;
-        margin-right: 6px;
-      }
-
-      p {
-        overflow: hidden;
-        white-space: nowrap;
-        text-overflow: ellipsis;
-        max-width: 180px !important;
-      }
-
-      &.titleSelect {
-        color: var(--n-color);
-        font-weight: bold;
-
-        .dir {
-          background: url('../../../prepare-lessons/components/directory-main/images/icon-d-active.png') no-repeat center;
-          background-size: contain;
-        }
-      }
-    }
-
-    .arrow {
-      display: inline-block;
-      width: 14px;
-      height: 15px;
-      background: url('../../../prepare-lessons/components/directory-main/images/arrow-default.png') no-repeat center;
-      background-size: contain;
-
-      &.arrowSelect {
-        background: url('../../../prepare-lessons/components/directory-main/images/arrow-active.png') no-repeat center;
-        background-size: contain;
-      }
-    }
-
-    .childArrow {
-      width: 12px;
-    }
-
-    &.childItem {
-      padding-left: 30px;
-      font-size: max(13px, 12Px);
-
-      margin: 0 10px;
-
-      .title {
-        color: #131415;
-        max-width: 180px;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-        display: block;
-      }
-    }
-
-    &.childSelect {
-      background: #EAEDF2;
-
-      .title {
-        color: var(--n-color);
-        font-weight: bold;
-      }
-    }
-  }
-}
-
-.musicStaff {
-  // display: flex;
-  // flex-direction: column;
-  // position: relative;
-  // left: -8px;
-  flex: 1;
-  background-color: #fff;
-  border-radius: 16px;
-  // height: 100%;
-  z-index: 1;
-  overflow: hidden;
-  padding: 27px 0 27px 27px;
-
-  &::-webkit-scrollbar {
-    width: 0;
-    display: none;
-  }
-
-  .empty {
-    :global {
-      .n-spin-content {
-        min-height: 100%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-      }
-    }
-  }
-
-  :global {
-    .n-spin-container {
-      overflow-y: auto;
-      height: 100%;
-    }
-
-  }
-
-  .musicTitle {
-    padding: 27px 27px 13px;
-    font-size: 20px;
-    font-weight: 600;
-    color: #000000;
-    line-height: 30px;
-    text-align: center;
-  }
-
-  .musicContent {
-    flex: 1;
-    // overflow-y: auto;
-    // height: 100%;
-    // padding: 27px;
-    padding-right: 27px;
-
-    &>img {
-      width: 100%;
-    }
-
-    section,
-    &>div {
-      font-size: inherit !important;
-    }
-  }
-}
-
-.changeSizeSection {
-  position: absolute;
-  right: 10px;
-  bottom: 50%;
-  width: 35px;
-  transform: translate(0, 50%);
-  background: #fff;
-  border-radius: 7px;
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  padding: 13px 0;
-
-  .iconT {
-    width: 15px;
-    height: 15px;
-  }
-
-  .iconAddT,
-  .iconPlusT {
-    width: 23px;
-    height: 23px;
-    cursor: pointer;
-  }
-
-  .iconAddT {
-    margin-top: 13px;
-    margin-bottom: 8px;
-  }
-
-  .iconPlusT {
-    margin-top: 8px;
-  }
-
-  :global {
-    .n-slider {
-      height: 125px;
-      --n-handle-size: 15px !important;
-      --n-rail-height: 0 !important;
-    }
-
-  }
-}
-
-.btnGroup {
-  padding: 20px 0;
-
-  :global {
-    .n-button {
-      height: 47px;
-      min-width: 156px;
-    }
-  }
-}
-
-.treeParentSelected {
-  background-color: #F8F8F8;
-  border-radius: 8px;
-  padding-bottom: 10px;
-
-  .parentItem {
-    // border-bottom: 1px solid #E7E8E8;
-    position: relative;
-    margin-bottom: 10px;
-
-    &::after {
-      content: '';
-      position: absolute;
-      bottom: 0;
-      left: 0;
-      display: block;
-      width: 100%;
-      height: 1px;
-      background-color: #E7E8E8;
-    }
-  }
-}
-
-
-.checkbox {
-  position: absolute;
-  right: 20px;
-}
-
-.childItem {
-  .checkbox {
-    right: 10px;
-  }
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 750px;
+  background: #F1F5FF;
+  border-radius: 0 0 12Px 12Px;
+  padding-left: 27px;
+
+  .iconBack {
+    width: 36px;
+    height: 36px;
+  }
+
+  .separator {
+    width: 9px;
+    height: 15px;
+    margin: 0 16px;
+  }
+
+
+}
+
+.containerDetail {
+  display: flex;
+  flex-direction: column;
+  height: 75vh;
+  // background: #F1F5FF;
+  border-radius: 0 0 12Px 12Px;
+  padding-left: 27px;
+
+  &.detailPreview {
+    height: 100%;
+    background-color: #fff;
+    // max-width: 1024px;
+    margin: 0 auto;
+    border-radius: 0;
+    padding-left: 0;
+
+
+    .contentWrap {
+      width: 80%;
+      margin: 0 auto;
+      position: initial;
+    }
+
+    .changeSizeSection {
+      right: 85px;
+    }
+  }
+}
+
+.wrap {
+  padding-top: 15px;
+  flex: 1;
+  transition: padding .3s;
+  overflow: hidden;
+
+  &.wrapBottom {
+    padding-bottom: 108px;
+
+  }
+}
+
+.contentWrap {
+  position: relative;
+  flex: 1;
+  display: flex;
+  padding: 0 55px 0 0;
+  overflow: hidden;
+  gap: 0 16px;
+}
+
+.content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  border-radius: 20px;
+  // max-height: 90vh;
+}
+
+.contentWrap {
+  :global {
+    .n-scrollbar-container {
+      max-height: 100%;
+    }
+  }
+
+  .scrollBar {
+    margin: 17px 0;
+    padding: 0 17px;
+    // max-height: calc(100% - 64px - 52px - 36px);
+
+    &.empty {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+
+
+  .directoryList {
+    width: 350px;
+    background: #FFFFFF;
+    border-radius: 17px;
+    flex-shrink: 0;
+    height: 100%;
+    overflow-x: hidden;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar {
+      width: 0;
+      display: none;
+    }
+  }
+
+  .treeParent {
+    transition: height 1s ease-in-out;
+  }
+
+  .treeChild {
+    line-height: 54px;
+  }
+
+  .treeItem {
+    position: relative;
+    display: flex;
+    align-items: center;
+    line-height: 54px;
+    border-radius: 10px;
+    padding: 0 0 0 15px;
+    cursor: pointer;
+    border-radius: 10px;
+    font-size: max(15px, 13Px);
+
+    // &:hover {
+    //   background: #EAEDF2;
+    // }
+
+    .title {
+      padding-left: 8px;
+      color: rgba(0, 0, 0, .5);
+      display: flex;
+      align-items: center;
+
+      .dir {
+        flex-shrink: 0;
+        display: inline-block;
+        width: 16px;
+        height: 18px;
+        background: url('../../../prepare-lessons/components/directory-main/images/icon-d.png') no-repeat center;
+        background-size: contain;
+        margin-right: 6px;
+      }
+
+      p {
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        max-width: 180px !important;
+      }
+
+      &.titleSelect {
+        color: var(--n-color);
+        font-weight: bold;
+
+        .dir {
+          background: url('../../../prepare-lessons/components/directory-main/images/icon-d-active.png') no-repeat center;
+          background-size: contain;
+        }
+      }
+    }
+
+    .arrow {
+      display: inline-block;
+      width: 14px;
+      height: 15px;
+      background: url('../../../prepare-lessons/components/directory-main/images/arrow-default.png') no-repeat center;
+      background-size: contain;
+
+      &.arrowSelect {
+        background: url('../../../prepare-lessons/components/directory-main/images/arrow-active.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+
+    .childArrow {
+      width: 12px;
+    }
+
+    &.childItem {
+      padding-left: 30px;
+      font-size: max(13px, 12Px);
+
+      margin: 0 10px;
+
+      .title {
+        color: #131415;
+        max-width: 180px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        display: block;
+      }
+    }
+
+    &.childSelect {
+      background: #EAEDF2;
+
+      .title {
+        color: var(--n-color);
+        font-weight: bold;
+      }
+    }
+  }
+}
+
+.musicStaff {
+  // display: flex;
+  // flex-direction: column;
+  position: relative;
+  // left: -8px;
+  flex: 1;
+  background-color: #fff;
+  border-radius: 16px;
+  // height: 100%;
+  z-index: 1;
+  overflow: hidden;
+  padding: 54px 0 27px 27px;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    display: none;
+  }
+
+  .empty {
+    :global {
+      .n-spin-content {
+        min-height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+    }
+  }
+
+  :global {
+    .n-spin-container {
+      overflow-y: auto;
+      height: 100%;
+    }
+
+  }
+
+  .musicTitle {
+    padding: 27px 27px 13px;
+    font-size: 20px;
+    font-weight: 600;
+    color: #000000;
+    line-height: 30px;
+    text-align: center;
+  }
+
+  .musicContent {
+    flex: 1;
+    // overflow-y: auto;
+    // height: 100%;
+    // padding: 27px;
+    padding-right: 27px;
+    position: relative;
+    user-select: text;
+    position: relative;
+
+    :global {
+      .highlight {
+        color: #0378EC;
+      }
+    }
+
+    &>img {
+      width: 100%;
+    }
+
+    section,
+    &>div {
+      font-size: inherit !important;
+    }
+  }
+}
+
+.changeSizeSection {
+  position: absolute;
+  right: 10px;
+  bottom: 50%;
+  width: 35px;
+  transform: translate(0, 50%);
+  background: #fff;
+  border-radius: 7px;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  padding: 13px 0;
+
+  .iconT {
+    width: 15px;
+    height: 15px;
+  }
+
+  .iconAddT,
+  .iconPlusT {
+    width: 23px;
+    height: 23px;
+    cursor: pointer;
+  }
+
+  .iconAddT {
+    margin-top: 13px;
+    margin-bottom: 8px;
+  }
+
+  .iconPlusT {
+    margin-top: 8px;
+  }
+
+  :global {
+    .n-slider {
+      height: 125px;
+      --n-handle-size: 15px !important;
+      --n-rail-height: 0 !important;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
+  }
+}
+
+.btnGroup {
+  padding: 20px 0;
+
+  :global {
+    .n-button {
+      height: 47px;
+      min-width: 156px;
+    }
+  }
+}
+
+.treeParentSelected {
+  background-color: #F8F8F8;
+  border-radius: 8px;
+  padding-bottom: 10px;
+
+  .parentItem {
+    // border-bottom: 1px solid #E7E8E8;
+    position: relative;
+    margin-bottom: 10px;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      display: block;
+      width: 100%;
+      height: 1px;
+      background-color: #E7E8E8;
+    }
+  }
+}
+
+
+.checkbox {
+  position: absolute;
+  right: 20px;
+}
+
+.childItem {
+  .checkbox {
+    right: 10px;
+  }
+}
+
+.musicTitleRight {
+  position: absolute;
+  top: 15px;
+  right: 15px;
+  z-index: 9;
+
+  .textRead,
+  .textClose {
+    padding: 7px 8px;
+    background: #E8F4FF;
+    border-radius: 13px;
+    font-weight: 500;
+    font-size: max(13px, 12Px);
+    color: #0378EC;
+    line-height: 18px;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+  }
+
+  .icon {
+    display: inline-block;
+    margin-right: 5px;
+    width: 15px;
+    height: 15px;
+  }
+
+  .textRead {
+    .icon {
+      background: url('../../../content-information/images/icon-speak-sound.png');
+      background-size: contain;
+    }
+  }
+
+  .textClose {
+    .icon {
+      background: url('../../../content-information/images/icon-speak-close.png');
+      background-size: contain;
+    }
+  }
+}
+
+.selectionCouser {
+  display: flex;
+  align-items: center;
+  position: absolute;
+
+  &.hide {
+    opacity: 0;
+    visibility: hidden;
+  }
+
+  .textStart,
+  .textReadOnly {
+    cursor: pointer;
+    background: #1A8CFF;
+    border-radius: 13px;
+    font-weight: 600;
+    font-size: max(13px, 12Px);
+    color: #FFFFFF;
+    line-height: 18px;
+    display: inline-flex;
+    align-items: center;
+    padding: 3px 8px;
+    flex-shrink: 0;
+
+    .icon {
+      margin-left: 4px;
+      display: inline-block;
+    }
+  }
+
+  .textStart {
+    .icon {
+      width: 8px;
+      height: 10px;
+      background: url('../../../content-information/images/icon-speak-arrow.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+  .textReadOnly {
+    margin-left: 10px;
+
+    .icon {
+
+      width: 9px;
+      height: 9px;
+      background: url('../../../content-information/images/icon-speak-line.png') no-repeat center;
+      background-size: contain;
+    }
+  }
 }

+ 373 - 315
src/views/prepare-lessons/model/source-knowledge/index.tsx

@@ -1,315 +1,373 @@
-import { defineComponent, onMounted, reactive, ref } from 'vue';
-import styles from './index.module.less';
-import {
-  NButton,
-  NCheckbox,
-  NCheckboxGroup,
-  // NBreadcrumb,
-  // NBreadcrumbItem,
-  // NScrollbar,
-  NSlider,
-  NSpace,
-  NSpin
-} from 'naive-ui';
-import iconT from '/src/views/content-information/images/icon-t.png';
-import iconAddT from '/src/views/content-information/images/icon-add-t.png';
-import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
-import {
-  api_lessonCoursewareDetail_listKnowledge,
-  api_lessonCoursewareKnowledgeDetail
-} from '/src/views/content-information/api';
-import TheEmpty from '/src/components/TheEmpty';
-import { PageEnum } from '/src/enums/pageEnum';
-
-export default defineComponent({
-  name: 'cotnent-knowledge',
-  emits: ['close', 'confirm'],
-  setup(props, { emit }) {
-    const show = ref(false);
-    const content = ref(false);
-    const musicContentRef = ref();
-    const state = reactive({
-      fontSize: 18,
-      tableList: [] as any,
-      selectKey: null,
-      details: {} as any,
-      selectCheckboxs: [] as any
-    });
-
-    const getDetails = async () => {
-      show.value = true;
-      content.value = true;
-      try {
-        const { data } = await api_lessonCoursewareDetail_listKnowledge({
-          type: 'COURSEWARE'
-        });
-
-        state.tableList = data || [];
-        if (state.tableList.length > 0) {
-          const item =
-            state.tableList[0].lessonCoursewareDetailKnowledgeDetailList;
-          state.tableList[0].selected = true;
-          if (item && item.length) {
-            const child = item[0];
-            state.selectKey = child.id;
-            await getDetail();
-          }
-          state.tableList.forEach((item: any) => {
-            item.checked = false;
-            item.indeterminate = false;
-          });
-        }
-      } catch {
-        //
-      }
-      content.value = false;
-      show.value = false;
-    };
-
-    const getDetail = async () => {
-      content.value = true;
-      try {
-        const { data } = await api_lessonCoursewareKnowledgeDetail({
-          id: state.selectKey
-        });
-
-        state.details = data;
-      } catch {
-        //
-      }
-      content.value = false;
-    };
-
-    const onSubmit = () => {
-      const items: any[] = [];
-      for (const i in state.selectCheckboxs) {
-        const ids = state.selectCheckboxs[i];
-        const item = state.tableList[i];
-        if (Array.isArray(item.lessonCoursewareDetailKnowledgeDetailList)) {
-          item.lessonCoursewareDetailKnowledgeDetailList.forEach(
-            (child: any) => {
-              if (ids.includes(child.id)) {
-                items.push(child);
-              }
-            }
-          );
-        }
-      }
-
-      const result: any[] = [];
-      items.forEach(item => {
-        result.push({
-          coverImg: PageEnum.THEORY_DEFAULT_COVER,
-          title: '乐理知识-' + item.name,
-          materialId: item.id,
-          content: item.id
-        });
-      });
-
-      emit('confirm', result);
-    };
-
-    onMounted(() => {
-      getDetails();
-    });
-    return () => (
-      <div class={styles.container}>
-        <div class={[styles.wrap]}>
-          <div class={styles.content}>
-            <div class={styles.contentWrap}>
-              <div class={styles.directoryList}>
-                <div
-                  class={[
-                    styles.scrollBar,
-                    !show.value && state.tableList.length <= 0
-                      ? styles.empty
-                      : ''
-                  ]}
-                  style={{ height: '100%' }}>
-                  <NSpin show={show.value} style={{ height: '100%' }}>
-                    <div class={[styles.listSection]}>
-                      {state.tableList.map((item: any, index: number) => (
-                        <div
-                          class={[
-                            styles.treeParent,
-                            item.selected && styles.treeParentSelected
-                          ]}
-                          key={'parent' + index}>
-                          <div
-                            class={[styles.treeItem, styles.parentItem]}
-                            onClick={() => {
-                              state.tableList.forEach((child: any) => {
-                                if (item.id !== child.id) {
-                                  child.selected = false;
-                                }
-                              });
-                              item.selected = item.selected ? false : true;
-                            }}>
-                            {item.lessonCoursewareDetailKnowledgeDetailList &&
-                              item.lessonCoursewareDetailKnowledgeDetailList
-                                .length > 0 && (
-                                <span
-                                  class={[
-                                    styles.arrow,
-                                    item.selected ? styles.arrowSelect : ''
-                                  ]}></span>
-                              )}
-                            <p
-                              class={[
-                                styles.title,
-                                item.selected ? styles.titleSelect : ''
-                              ]}>
-                              <span
-                                class={[
-                                  styles.dir,
-                                  item.selected ? styles.dirSelect : ''
-                                ]}></span>
-                              <p>{item.name}</p>
-                            </p>
-                            <div
-                              class={styles.checkbox}
-                              onClick={(e: any) => {
-                                e.stopPropagation();
-                              }}>
-                              <NCheckbox
-                                checked={item.checked}
-                                indeterminate={item.indeterminate}
-                                onUpdate:checked={(val: boolean) => {
-                                  item.checked = val;
-
-                                  const child =
-                                    item.lessonCoursewareDetailKnowledgeDetailList ||
-                                    [];
-                                  if (val) {
-                                    const ids: any = [];
-                                    child.forEach((c: any) => {
-                                      ids.push(c.id);
-                                    });
-                                    state.selectCheckboxs[index] = ids;
-                                  } else {
-                                    state.selectCheckboxs[index] = [];
-                                  }
-                                  item.indeterminate = false;
-                                }}></NCheckbox>
-                            </div>
-                          </div>
-                          <NCheckboxGroup
-                            value={state.selectCheckboxs[index]}
-                            onUpdate:value={val => {
-                              state.selectCheckboxs[index] = val;
-
-                              const child =
-                                item.lessonCoursewareDetailKnowledgeDetailList ||
-                                [];
-                              if (val.length <= 0) {
-                                item.checked = false;
-                                item.indeterminate = false;
-                              } else if (val.length === child.length) {
-                                item.checked = true;
-                                item.indeterminate = false;
-                              } else {
-                                item.checked = false;
-                                item.indeterminate = true;
-                              }
-                            }}>
-                            {item.selected &&
-                              item.lessonCoursewareDetailKnowledgeDetailList &&
-                              item.lessonCoursewareDetailKnowledgeDetailList.map(
-                                (child: any, j: number) => (
-                                  <div
-                                    key={'child' + j}
-                                    class={[
-                                      styles.treeItem,
-                                      styles.childItem,
-                                      styles.animation,
-                                      state.selectKey === child.id
-                                        ? styles.childSelect
-                                        : ''
-                                    ]}
-                                    onClick={() => {
-                                      if (state.selectKey === child.id) return;
-                                      state.selectKey = child.id;
-                                      getDetail();
-                                      musicContentRef.value.$el.scrollTo(0, 0);
-                                    }}>
-                                    <span class={styles.childArrow}></span>
-                                    <p class={styles.title}>{child.name}</p>
-                                    <div
-                                      class={styles.checkbox}
-                                      onClick={(e: any) => e.stopPropagation()}>
-                                      <NCheckbox value={child.id}></NCheckbox>
-                                    </div>
-                                  </div>
-                                )
-                              )}
-                          </NCheckboxGroup>
-                        </div>
-                      ))}
-                    </div>
-                  </NSpin>
-                  {!show.value && state.tableList.length <= 0 && (
-                    <TheEmpty style={{ height: '100%' }} />
-                  )}
-                </div>
-              </div>
-
-              <div class={styles.musicStaff}>
-                <NSpin
-                  show={content.value}
-                  ref={musicContentRef}
-                  class={
-                    !content.value && !state.details?.desc ? styles.empty : ''
-                  }>
-                  {state.details?.desc ? (
-                    <div
-                      class={styles.musicContent}
-                      v-html={state.details?.desc}
-                      style={{ fontSize: state.fontSize + 'px' }}></div>
-                  ) : (
-                    ''
-                  )}
-                  {!content.value && !state.details?.desc && <TheEmpty />}
-                </NSpin>
-              </div>
-
-              <div class={styles.changeSizeSection}>
-                <img src={iconT} class={styles.iconT} />
-                <img
-                  src={iconAddT}
-                  class={styles.iconAddT}
-                  onClick={() => {
-                    if (state.fontSize >= 32) return;
-                    state.fontSize += 1;
-                  }}
-                />
-                <NSlider
-                  v-model:value={state.fontSize}
-                  vertical
-                  min={12}
-                  max={32}
-                />
-                <img
-                  src={iconPlusT}
-                  class={styles.iconPlusT}
-                  onClick={() => {
-                    if (state.fontSize <= 12) return;
-                    state.fontSize -= 1;
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <NSpace class={styles.btnGroup} justify="center">
-          <NButton round onClick={() => emit('close')}>
-            取消
-          </NButton>
-          <NButton round type="primary" onClick={onSubmit}>
-            确认添加
-          </NButton>
-        </NSpace>
-      </div>
-    );
-  }
-});
+import { defineComponent, nextTick, onMounted, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import {
+  NButton,
+  NCheckbox,
+  NCheckboxGroup,
+  // NBreadcrumb,
+  // NBreadcrumbItem,
+  // NScrollbar,
+  NSlider,
+  NSpace,
+  NSpin
+} from 'naive-ui';
+import iconT from '/src/views/content-information/images/icon-t.png';
+import iconAddT from '/src/views/content-information/images/icon-add-t.png';
+import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
+import {
+  api_lessonCoursewareDetail_listKnowledge,
+  api_lessonCoursewareKnowledgeDetail
+} from '/src/views/content-information/api';
+import TheEmpty from '/src/components/TheEmpty';
+import { PageEnum } from '/src/enums/pageEnum';
+import { useSpeak } from '/src/views/content-information/useSpeak';
+
+export default defineComponent({
+  name: 'cotnent-knowledge',
+  emits: ['close', 'confirm'],
+  setup(props, { emit }) {
+    const show = ref(false);
+    const content = ref(false);
+    const musicContentRef = ref();
+    const speakMusicContent =
+      'musicContent' + new Date().getTime() + Math.floor(Math.random() * 100);
+    const speak = useSpeak(speakMusicContent);
+    const state = reactive({
+      fontSize: 18,
+      tableList: [] as any,
+      selectKey: null,
+      details: {} as any,
+      selectCheckboxs: [] as any
+    });
+
+    const getDetails = async () => {
+      show.value = true;
+      content.value = true;
+      try {
+        const { data } = await api_lessonCoursewareDetail_listKnowledge({
+          type: 'COURSEWARE'
+        });
+
+        state.tableList = data || [];
+        if (state.tableList.length > 0) {
+          const item =
+            state.tableList[0].lessonCoursewareDetailKnowledgeDetailList;
+          state.tableList[0].selected = true;
+          if (item && item.length) {
+            const child = item[0];
+            state.selectKey = child.id;
+            await getDetail();
+          }
+          state.tableList.forEach((item: any) => {
+            item.checked = false;
+            item.indeterminate = false;
+          });
+        }
+      } catch {
+        //
+      }
+      content.value = false;
+      show.value = false;
+    };
+
+    const getDetail = async () => {
+      content.value = true;
+      try {
+        const { data } = await api_lessonCoursewareKnowledgeDetail({
+          id: state.selectKey
+        });
+
+        content.value = false;
+        state.details = data;
+        nextTick(() => {
+          // 使用 DOMParser 解析 HTML 字符串
+          const parser = new DOMParser();
+          const doc = parser.parseFromString(data.desc, 'text/html');
+
+          const hasChilds = document.querySelectorAll('.only-child-select');
+          if (hasChilds.length > 0) {
+            hasChilds.forEach(child => {
+              child.remove();
+            });
+          }
+
+          const childNodes = doc.body.childNodes;
+          childNodes?.forEach((node: any) => {
+            node?.classList.add('only-child-select');
+          });
+
+          // 提取并分割 HTML 文档中的内容
+          document
+            .querySelector('#' + speakMusicContent)
+            ?.appendChild(speak.processNode(doc.body));
+        });
+      } catch {
+        //
+      }
+      content.value = false;
+    };
+
+    const onSubmit = () => {
+      const items: any[] = [];
+      for (const i in state.selectCheckboxs) {
+        const ids = state.selectCheckboxs[i];
+        const item = state.tableList[i];
+        if (Array.isArray(item.lessonCoursewareDetailKnowledgeDetailList)) {
+          item.lessonCoursewareDetailKnowledgeDetailList.forEach(
+            (child: any) => {
+              if (ids.includes(child.id)) {
+                items.push(child);
+              }
+            }
+          );
+        }
+      }
+
+      const result: any[] = [];
+      items.forEach(item => {
+        result.push({
+          coverImg: PageEnum.THEORY_DEFAULT_COVER,
+          title: '乐理知识-' + item.name,
+          materialId: item.id,
+          content: item.id
+        });
+      });
+
+      emit('confirm', result);
+    };
+
+    onMounted(() => {
+      getDetails();
+    });
+    return () => (
+      <div class={styles.container}>
+        <div class={[styles.wrap]}>
+          <div class={styles.content}>
+            <div class={styles.contentWrap}>
+              <div class={styles.directoryList}>
+                <div
+                  class={[
+                    styles.scrollBar,
+                    !show.value && state.tableList.length <= 0
+                      ? styles.empty
+                      : ''
+                  ]}
+                  style={{ height: '100%' }}>
+                  <NSpin show={show.value} style={{ height: '100%' }}>
+                    <div class={[styles.listSection]}>
+                      {state.tableList.map((item: any, index: number) => (
+                        <div
+                          class={[
+                            styles.treeParent,
+                            item.selected && styles.treeParentSelected
+                          ]}
+                          key={'parent' + index}>
+                          <div
+                            class={[styles.treeItem, styles.parentItem]}
+                            onClick={() => {
+                              state.tableList.forEach((child: any) => {
+                                if (item.id !== child.id) {
+                                  child.selected = false;
+                                }
+                              });
+                              item.selected = item.selected ? false : true;
+                            }}>
+                            {item.lessonCoursewareDetailKnowledgeDetailList &&
+                              item.lessonCoursewareDetailKnowledgeDetailList
+                                .length > 0 && (
+                                <span
+                                  class={[
+                                    styles.arrow,
+                                    item.selected ? styles.arrowSelect : ''
+                                  ]}></span>
+                              )}
+                            <p
+                              class={[
+                                styles.title,
+                                item.selected ? styles.titleSelect : ''
+                              ]}>
+                              <span
+                                class={[
+                                  styles.dir,
+                                  item.selected ? styles.dirSelect : ''
+                                ]}></span>
+                              <p>{item.name}</p>
+                            </p>
+                            <div
+                              class={styles.checkbox}
+                              onClick={(e: any) => {
+                                e.stopPropagation();
+                              }}>
+                              <NCheckbox
+                                checked={item.checked}
+                                indeterminate={item.indeterminate}
+                                onUpdate:checked={(val: boolean) => {
+                                  item.checked = val;
+
+                                  const child =
+                                    item.lessonCoursewareDetailKnowledgeDetailList ||
+                                    [];
+                                  if (val) {
+                                    const ids: any = [];
+                                    child.forEach((c: any) => {
+                                      ids.push(c.id);
+                                    });
+                                    state.selectCheckboxs[index] = ids;
+                                  } else {
+                                    state.selectCheckboxs[index] = [];
+                                  }
+                                  item.indeterminate = false;
+                                }}></NCheckbox>
+                            </div>
+                          </div>
+                          <NCheckboxGroup
+                            value={state.selectCheckboxs[index]}
+                            onUpdate:value={val => {
+                              state.selectCheckboxs[index] = val;
+
+                              const child =
+                                item.lessonCoursewareDetailKnowledgeDetailList ||
+                                [];
+                              if (val.length <= 0) {
+                                item.checked = false;
+                                item.indeterminate = false;
+                              } else if (val.length === child.length) {
+                                item.checked = true;
+                                item.indeterminate = false;
+                              } else {
+                                item.checked = false;
+                                item.indeterminate = true;
+                              }
+                            }}>
+                            {item.selected &&
+                              item.lessonCoursewareDetailKnowledgeDetailList &&
+                              item.lessonCoursewareDetailKnowledgeDetailList.map(
+                                (child: any, j: number) => (
+                                  <div
+                                    key={'child' + j}
+                                    class={[
+                                      styles.treeItem,
+                                      styles.childItem,
+                                      styles.animation,
+                                      state.selectKey === child.id
+                                        ? styles.childSelect
+                                        : ''
+                                    ]}
+                                    onClick={() => {
+                                      if (state.selectKey === child.id) return;
+                                      state.selectKey = child.id;
+                                      getDetail();
+                                      speak.onCloseSpeak();
+                                      musicContentRef.value.$el.scrollTo(0, 0);
+                                    }}>
+                                    <span class={styles.childArrow}></span>
+                                    <p class={styles.title}>{child.name}</p>
+                                    <div
+                                      class={styles.checkbox}
+                                      onClick={(e: any) => e.stopPropagation()}>
+                                      <NCheckbox value={child.id}></NCheckbox>
+                                    </div>
+                                  </div>
+                                )
+                              )}
+                          </NCheckboxGroup>
+                        </div>
+                      ))}
+                    </div>
+                  </NSpin>
+                  {!show.value && state.tableList.length <= 0 && (
+                    <TheEmpty style={{ height: '100%' }} />
+                  )}
+                </div>
+              </div>
+
+              <div class={styles.musicStaff}>
+                <div class={styles.musicTitleRight}>
+                  {speak.isSpeak.value ? (
+                    <span class={styles.textClose} onClick={speak.onCloseSpeak}>
+                      <i class={styles.icon}></i>关闭朗读
+                    </span>
+                  ) : (
+                    <span class={styles.textRead} onClick={speak.onAllSpeak}>
+                      <i class={styles.icon}></i>全文朗读
+                    </span>
+                  )}
+                </div>
+                <NSpin
+                  show={content.value}
+                  ref={musicContentRef}
+                  class={
+                    !content.value && !state.details?.desc ? styles.empty : ''
+                  }>
+                  {state.details?.desc ? (
+                    <div
+                      class={styles.musicContent}
+                      id={speakMusicContent}
+                      style={{ fontSize: state.fontSize + 'px' }}>
+                      {/* 选中的内容 */}
+                      <div
+                        id="selectionCouser"
+                        class={[
+                          styles.selectionCouser,
+                          !speak.showDom.value && styles.hide
+                        ]}>
+                        <span
+                          class={styles.textStart}
+                          onClick={speak.onTextStart}>
+                          开始朗读<i class={styles.icon}></i>
+                        </span>
+                        <span
+                          class={styles.textReadOnly}
+                          onClick={speak.onTextReadOnly}>
+                          只读这段<i class={styles.icon}></i>
+                        </span>
+                      </div>
+                    </div>
+                  ) : (
+                    ''
+                  )}
+                  {!content.value && !state.details?.desc && <TheEmpty />}
+                </NSpin>
+              </div>
+
+              <div class={styles.changeSizeSection}>
+                <img src={iconT} class={styles.iconT} />
+                <img
+                  src={iconAddT}
+                  class={styles.iconAddT}
+                  onClick={() => {
+                    if (state.fontSize >= 32) return;
+                    state.fontSize += 1;
+                  }}
+                />
+                <NSlider
+                  v-model:value={state.fontSize}
+                  vertical
+                  min={12}
+                  max={32}
+                />
+                <img
+                  src={iconPlusT}
+                  class={styles.iconPlusT}
+                  onClick={() => {
+                    if (state.fontSize <= 12) return;
+                    state.fontSize -= 1;
+                  }}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <NSpace class={styles.btnGroup} justify="center">
+          <NButton round onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton round type="primary" onClick={onSubmit}>
+            确认添加
+          </NButton>
+        </NSpace>
+      </div>
+    );
+  }
+});

+ 659 - 540
src/views/prepare-lessons/model/source-music/detail.module.less

@@ -1,541 +1,660 @@
-.container {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-
-  &.containerPreview {
-    padding: 68px 78px 68px 68px;
-    // height: calc(100% - 136px);
-    background-color: #fff;
-
-    .wrapBottom {
-      padding-bottom: 60px !important;
-    }
-  }
-
-  &.containerModal {
-    .content {
-      border-top-left-radius: 0;
-      border-top-right-radius: 0;
-    }
-  }
-}
-
-.wrap {
-  flex: 1;
-  transition: padding .3s;
-  overflow: hidden;
-
-  &.wrapBottom {
-    padding-bottom: 108px;
-
-  }
-}
-
-.content {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  background: #DDF2FF;
-  border-radius: 20px;
-  // max-height: 90vh;
-}
-
-.tools {
-  padding: 20px;
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-
-  :global {
-    .n-input {
-      margin-left: auto;
-      width: 361px;
-    }
-
-    .n-input__input-el {
-      height: 100%;
-      line-height: 100%;
-    }
-  }
-}
-
-.contentWrap {
-  position: relative;
-  flex: 1;
-  display: flex;
-  padding: 20px 55px 20px 20px;
-  overflow: hidden;
-  gap: 0 32px;
-}
-
-.musicList {
-  background-color: #fff;
-  border-radius: 16px;
-
-  width: 470px;
-  min-width: 294px;
-  height: 100%;
-  overflow-x: hidden;
-  overflow-y: auto;
-  min-width: 330Px;
-
-  &::-webkit-scrollbar {
-    width: 0;
-    display: none;
-  }
-
-  .instrumentGroup {
-    padding-top: 27px;
-    padding-bottom: 20px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
-
-    .instrumentImg {
-      width: 125px;
-      height: 125px;
-      overflow: hidden;
-      border-radius: 50%;
-    }
-
-    .instrumentName {
-      padding: 13px 0 5px;
-      font-size: max(18px, 14Px);
-      font-weight: 600;
-      color: #131415;
-      line-height: 25px;
-      letter-spacing: 1px;
-    }
-
-    .instrumentTag {
-      font-size: max(13px, 12Px);
-      color: #777777;
-      line-height: 18px;
-    }
-  }
-
-
-
-  .wrapList {
-    width: 470px;
-    padding: 0 17px;
-    min-width: 294px;
-    min-height: 100%;
-    // background: #fff;
-    border-radius: 16px;
-
-    :global {
-      .n-empty .n-empty__description {
-        font-size: calc(14px, 12Px);
-      }
-    }
-
-
-    .titlec {
-      padding: 20px 0;
-      font-size: max(18px, 14Px);
-      font-weight: 600;
-      color: #000000;
-      line-height: 25px;
-      border-top: 1px solid #F2F2F2;
-      display: flex;
-      align-items: center;
-    }
-
-    .icon2 {
-      width: 23px;
-      height: 23px;
-      margin-right: 8px;
-      background: url('../../../content-information/images/icon-2.png') no-repeat center;
-      background-size: contain;
-    }
-  }
-
-  .empty {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    height: 50vh;
-    // height: 100%;
-  }
-}
-
-.itemContainer {
-  width: 100%;
-  border-radius: 16px;
-  padding: 4px 8px;
-  // background-color: #fff;
-
-  &:first-child {
-    padding-top: 8px;
-  }
-
-  &:last-child {
-    // border-radius: 0 0 16px 16px;
-    padding-bottom: 8px;
-  }
-}
-
-.item {
-  position: relative;
-  display: flex;
-  align-items: center;
-  padding: 10px;
-  border-radius: 12px;
-
-  cursor: pointer;
-
-  &:hover {
-    background-color: rgba(0, 0, 0, .05);
-  }
-
-  &.active {
-    background-color: #DDF2FF;
-
-    .arrow {
-      opacity: 1;
-    }
-  }
-
-  .img {
-    position: relative;
-    width: 60px;
-    height: 60px;
-    border-radius: 18px;
-    margin-right: 12px;
-    // box-shadow: 0 0 10px 4px rgba(27, 35, 55, .1);
-    overflow: hidden;
-    flex-shrink: 0;
-
-    :global {
-      .n-image {
-        width: 60px;
-        height: 60px;
-      }
-    }
-
-    img {
-      transition: opacity .3s;
-      opacity: 0;
-      height: 100%;
-      width: 100%;
-    }
-
-    img[data-loaded="true"] {
-      opacity: 1;
-    }
-  }
-
-  .title {
-    flex: 1;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-    align-items: flex-start;
-
-    .titleName {
-      font-size: calc(17px, 12Px);
-      font-weight: 600;
-      color: #131415;
-      line-height: 28px;
-      width: 100%;
-    }
-
-    .titleDes {
-      font-size: 14px;
-      font-weight: 400;
-      color: #777777;
-      line-height: 20px;
-      max-width: 100%;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      overflow: hidden;
-    }
-  }
-
-  .btn {
-    margin-left: auto;
-    width: 84px;
-    height: 40px;
-    background: linear-gradient(to right, #44CAFF, #259DFE);
-    border: none;
-    padding: 0;
-    font-weight: bold !important;
-    flex-shrink: 0;
-    min-width: 62px;
-    min-height: 30px;
-
-    :global {
-      .n-button__content {
-        &>img {
-          margin-left: 10px;
-          width: 9px;
-          height: 12px;
-        }
-      }
-    }
-  }
-
-  .arrow {
-    position: absolute;
-    top: 50%;
-    right: 12px;
-    transform: translate(124%, -50%);
-    opacity: 0;
-  }
-
-  .showPlayLoading {
-    opacity: 0;
-  }
-
-}
-
-.loadingWrap {
-  display: flex;
-  justify-content: center;
-  min-height: 80px;
-}
-
-.musicStaff {
-  display: flex;
-  flex-direction: column;
-  position: relative;
-  left: -8px;
-  flex: 1;
-  background-color: #fff;
-  border-radius: 16px;
-  padding-bottom: 18px;
-  z-index: 1;
-  overflow: hidden;
-
-  &::-webkit-scrollbar {
-    width: 0;
-    display: none;
-  }
-
-
-  .musicTitle {
-    padding: 27px 27px 13px;
-    font-size: max(18px, 14Px);
-    font-weight: 600;
-    color: #000000;
-    line-height: 25px;
-    display: flex;
-    align-items: center;
-
-    .icon1 {
-      display: inline-block;
-      width: 23px;
-      height: 23px;
-      margin-right: 8px;
-      background: url('../../../content-information/images/icon-1.png') no-repeat center;
-      background-size: contain;
-    }
-  }
-
-  .musicContent {
-    flex: 1;
-    overflow-y: auto;
-    height: 100%;
-    padding: 0 27px;
-
-    &>img {
-      width: 100%;
-    }
-
-    section,
-    &>div {
-      font-size: inherit !important;
-    }
-  }
-}
-
-.staffImgs {
-  flex: 1;
-  overflow-y: auto;
-  height: 100%;
-  padding: 0 30px;
-
-  &>img {
-    width: 100%;
-  }
-}
-
-
-:global {
-
-  .van-fade-enter-active,
-  .van-fade-leave-active {
-    transition: all 0.3s;
-  }
-
-  .van-fade-enter-from,
-  .van-fade-leave-to {
-    opacity: 0;
-  }
-}
-
-.changeSizeSection {
-  position: absolute;
-  right: 10px;
-  bottom: 50%;
-  width: 35px;
-  transform: translate(0, 50%);
-  background: #fff;
-  border-radius: 7px;
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  padding: 13px 0;
-
-  .iconT {
-    width: 15px;
-    height: 15px;
-  }
-
-  .iconAddT,
-  .iconPlusT {
-    width: 23px;
-    height: 23px;
-    cursor: pointer;
-  }
-
-  .iconAddT {
-    margin-top: 13px;
-    margin-bottom: 8px;
-  }
-
-  .iconPlusT {
-    margin-top: 8px;
-  }
-
-  :global {
-    .n-slider {
-      height: 125px;
-      --n-handle-size: 15px !important;
-      --n-rail-height: 0 !important;
-    }
-
-  }
-}
-
-.musicTop,
-.musicInfo {
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  padding-top: 36px;
-  padding-bottom: 22px;
-}
-
-.musicInfo {
-  flex-direction: row;
-}
-
-.musicImg {
-  position: relative;
-  width: 100px;
-  height: 100px;
-  border-radius: 2px;
-  z-index: 9;
-  margin-right: 54px;
-  margin-left: 31px;
-
-  .img {
-    position: relative;
-    z-index: 9;
-    width: 100px;
-    height: 100px;
-    border-radius: 2px;
-  }
-
-  .panSection {
-    position: absolute;
-    right: -44px;
-    top: 5px;
-    width: 95px;
-    height: 95px;
-    z-index: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-
-    .img2 {
-      position: relative;
-      z-index: 1;
-      width: 64px;
-      height: 64px;
-      border-radius: 50%;
-    }
-  }
-
-
-  .iconPan {
-    position: absolute;
-    left: 0;
-    right: 0;
-    width: 95px;
-    height: 95px;
-    z-index: 0;
-  }
-
-  &::before {
-    content: ' ';
-    position: absolute;
-    top: 0;
-    left: 0;
-    z-index: 10;
-    display: inline-block;
-    width: 5px;
-    height: 100px;
-    background: linear-gradient(270deg, rgba(0, 0, 0, 0.18) 0%, rgba(255, 255, 255, 0) 100%);
-  }
-
-  &::after {
-    content: ' ';
-    position: absolute;
-    left: -31px;
-    bottom: 0;
-    z-index: 8;
-    width: 148px;
-    height: 16px;
-    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 100%);
-    filter: blur(2.3328px);
-    border-radius: 50%;
-  }
-}
-
-.info {
-  text-align: left;
-}
-
-.musicInfo {
-  // width: 500px;
-
-  .name {
-    font-size: max(21px, 15Px);
-    font-weight: 600;
-    color: #131415;
-    line-height: 25px;
-    padding-bottom: 8px;
-    max-width: 220px;
-  }
-
-  .c {
-    font-size: max(13px, 12Px);
-    color: #777777;
-    line-height: 18px;
-
-    span {
-      flex-shrink: 0;
-    }
-
-    &>div {
-      display: flex;
-      margin-right: 20px;
-      max-width: 220px;
-    }
-  }
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  &.containerPreview {
+    padding: 68px 78px 68px 68px;
+    // height: calc(100% - 136px);
+    background-color: #fff;
+
+    .wrapBottom {
+      padding-bottom: 60px !important;
+    }
+  }
+
+  &.containerModal {
+    .content {
+      border-top-left-radius: 0;
+      border-top-right-radius: 0;
+    }
+  }
+
+  :global {
+    .highlight {
+      color: #0378EC;
+    }
+  }
+}
+
+.wrap {
+  flex: 1;
+  transition: padding .3s;
+  overflow: hidden;
+
+  &.wrapBottom {
+    padding-bottom: 108px;
+
+  }
+}
+
+.content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background: #DDF2FF;
+  border-radius: 20px;
+  // max-height: 90vh;
+}
+
+.tools {
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+
+  :global {
+    .n-input {
+      margin-left: auto;
+      width: 361px;
+    }
+
+    .n-input__input-el {
+      height: 100%;
+      line-height: 100%;
+    }
+  }
+}
+
+.contentWrap {
+  position: relative;
+  flex: 1;
+  display: flex;
+  padding: 20px 55px 20px 20px;
+  overflow: hidden;
+  gap: 0 32px;
+}
+
+.musicList {
+  background-color: #fff;
+  border-radius: 16px;
+
+  width: 470px;
+  min-width: 294px;
+  height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  min-width: 330Px;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    display: none;
+  }
+
+  .instrumentGroup {
+    padding-top: 27px;
+    padding-bottom: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+
+    .instrumentImg {
+      width: 125px;
+      height: 125px;
+      overflow: hidden;
+      border-radius: 50%;
+    }
+
+    .instrumentName {
+      padding: 13px 0 5px;
+      font-size: max(18px, 14Px);
+      font-weight: 600;
+      color: #131415;
+      line-height: 25px;
+      letter-spacing: 1px;
+    }
+
+    .instrumentTag {
+      font-size: max(13px, 12Px);
+      color: #777777;
+      line-height: 18px;
+    }
+  }
+
+
+
+  .wrapList {
+    width: 470px;
+    padding: 0 17px;
+    min-width: 294px;
+    min-height: 100%;
+    // background: #fff;
+    border-radius: 16px;
+
+    :global {
+      .n-empty .n-empty__description {
+        font-size: calc(14px, 12Px);
+      }
+    }
+
+
+    .titlec {
+      padding: 20px 0;
+      font-size: max(18px, 14Px);
+      font-weight: 600;
+      color: #000000;
+      line-height: 25px;
+      border-top: 1px solid #F2F2F2;
+      display: flex;
+      align-items: center;
+    }
+
+    .icon2 {
+      width: 23px;
+      height: 23px;
+      margin-right: 8px;
+      background: url('../../../content-information/images/icon-2.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+  .empty {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 50vh;
+    // height: 100%;
+  }
+}
+
+.itemContainer {
+  width: 100%;
+  border-radius: 16px;
+  padding: 4px 8px;
+  // background-color: #fff;
+
+  &:first-child {
+    padding-top: 8px;
+  }
+
+  &:last-child {
+    // border-radius: 0 0 16px 16px;
+    padding-bottom: 8px;
+  }
+}
+
+.item {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  border-radius: 12px;
+
+  cursor: pointer;
+
+  &:hover {
+    background-color: rgba(0, 0, 0, .05);
+  }
+
+  &.active {
+    background-color: #DDF2FF;
+
+    .arrow {
+      opacity: 1;
+    }
+  }
+
+  .img {
+    position: relative;
+    width: 60px;
+    height: 60px;
+    border-radius: 18px;
+    margin-right: 12px;
+    // box-shadow: 0 0 10px 4px rgba(27, 35, 55, .1);
+    overflow: hidden;
+    flex-shrink: 0;
+
+    :global {
+      .n-image {
+        width: 60px;
+        height: 60px;
+      }
+    }
+
+    img {
+      transition: opacity .3s;
+      opacity: 0;
+      height: 100%;
+      width: 100%;
+    }
+
+    img[data-loaded="true"] {
+      opacity: 1;
+    }
+  }
+
+  .title {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .titleName {
+      font-size: calc(17px, 12Px);
+      font-weight: 600;
+      color: #131415;
+      line-height: 28px;
+      width: 100%;
+    }
+
+    .titleDes {
+      font-size: 14px;
+      font-weight: 400;
+      color: #777777;
+      line-height: 20px;
+      max-width: 100%;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
+    }
+  }
+
+  .btn {
+    margin-left: auto;
+    width: 84px;
+    height: 40px;
+    background: linear-gradient(to right, #44CAFF, #259DFE);
+    border: none;
+    padding: 0;
+    font-weight: bold !important;
+    flex-shrink: 0;
+    min-width: 62px;
+    min-height: 30px;
+
+    :global {
+      .n-button__content {
+        &>img {
+          margin-left: 10px;
+          width: 9px;
+          height: 12px;
+        }
+      }
+    }
+  }
+
+  .arrow {
+    position: absolute;
+    top: 50%;
+    right: 12px;
+    transform: translate(124%, -50%);
+    opacity: 0;
+  }
+
+  .showPlayLoading {
+    opacity: 0;
+  }
+
+}
+
+.loadingWrap {
+  display: flex;
+  justify-content: center;
+  min-height: 80px;
+}
+
+.musicStaff {
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  left: -8px;
+  flex: 1;
+  background-color: #fff;
+  border-radius: 16px;
+  padding-bottom: 18px;
+  z-index: 1;
+  overflow: hidden;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    display: none;
+  }
+
+  .musicTitle {
+    padding: 27px 27px 13px;
+    font-size: max(18px, 14Px);
+    font-weight: 600;
+    color: #000000;
+    line-height: 25px;
+    display: flex;
+    align-items: center;
+
+    .icon1 {
+      display: inline-block;
+      width: 23px;
+      height: 23px;
+      margin-right: 8px;
+      background: url('../../../content-information/images/icon-1.png') no-repeat center;
+      background-size: contain;
+    }
+
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .musicTitleLeft {
+      display: flex;
+      align-items: center;
+    }
+
+    .musicTitleRight {
+
+      .textRead,
+      .textClose {
+        padding: 7px 8px;
+        background: #E8F4FF;
+        border-radius: 13px;
+        font-weight: 500;
+        font-size: max(13px, 12Px);
+        color: #0378EC;
+        line-height: 18px;
+        display: flex;
+        align-items: center;
+        cursor: pointer;
+      }
+
+      .icon {
+        display: inline-block;
+        margin-right: 5px;
+        width: 15px;
+        height: 15px;
+      }
+
+      .textRead {
+        .icon {
+          background: url('../../../content-information/images/icon-speak-sound.png');
+          background-size: contain;
+        }
+      }
+
+      .textClose {
+        .icon {
+          background: url('../../../content-information/images/icon-speak-close.png');
+          background-size: contain;
+        }
+      }
+    }
+  }
+
+  .musicContent {
+    flex: 1;
+    overflow-y: auto;
+    height: 100%;
+    padding: 0 27px;
+    user-select: text;
+    position: relative;
+
+    &>img {
+      width: 100%;
+    }
+
+    section,
+    &>div {
+      font-size: inherit !important;
+    }
+  }
+}
+
+.staffImgs {
+  flex: 1;
+  overflow-y: auto;
+  height: 100%;
+  padding: 0 30px;
+
+  &>img {
+    width: 100%;
+  }
+}
+
+
+:global {
+
+  .van-fade-enter-active,
+  .van-fade-leave-active {
+    transition: all 0.3s;
+  }
+
+  .van-fade-enter-from,
+  .van-fade-leave-to {
+    opacity: 0;
+  }
+}
+
+.changeSizeSection {
+  position: absolute;
+  right: 10px;
+  bottom: 50%;
+  width: 35px;
+  transform: translate(0, 50%);
+  background: #fff;
+  border-radius: 7px;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  padding: 13px 0;
+
+  .iconT {
+    width: 15px;
+    height: 15px;
+  }
+
+  .iconAddT,
+  .iconPlusT {
+    width: 23px;
+    height: 23px;
+    cursor: pointer;
+  }
+
+  .iconAddT {
+    margin-top: 13px;
+    margin-bottom: 8px;
+  }
+
+  .iconPlusT {
+    margin-top: 8px;
+  }
+
+  :global {
+    .n-slider {
+      height: 125px;
+      --n-handle-size: 15px !important;
+      --n-rail-height: 0 !important;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-rail {
+      border-radius: 10px;
+
+    }
+
+    .n-slider .n-slider-rail .n-slider-rail__fill {
+      border-radius: 10px;
+    }
+
+    .n-slider.n-slider--vertical .n-slider-handles {
+      top: 3px !important;
+      bottom: 3px !important;
+    }
+  }
+}
+
+.musicTop,
+.musicInfo {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  padding-top: 36px;
+  padding-bottom: 22px;
+}
+
+.musicInfo {
+  flex-direction: row;
+}
+
+.musicImg {
+  position: relative;
+  width: 100px;
+  height: 100px;
+  border-radius: 2px;
+  z-index: 9;
+  margin-right: 54px;
+  margin-left: 31px;
+
+  .img {
+    position: relative;
+    z-index: 9;
+    width: 100px;
+    height: 100px;
+    border-radius: 2px;
+  }
+
+  .panSection {
+    position: absolute;
+    right: -44px;
+    top: 5px;
+    width: 95px;
+    height: 95px;
+    z-index: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .img2 {
+      position: relative;
+      z-index: 1;
+      width: 64px;
+      height: 64px;
+      border-radius: 50%;
+    }
+  }
+
+
+  .iconPan {
+    position: absolute;
+    left: 0;
+    right: 0;
+    width: 95px;
+    height: 95px;
+    z-index: 0;
+  }
+
+  &::before {
+    content: ' ';
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 10;
+    display: inline-block;
+    width: 5px;
+    height: 100px;
+    background: linear-gradient(270deg, rgba(0, 0, 0, 0.18) 0%, rgba(255, 255, 255, 0) 100%);
+  }
+
+  &::after {
+    content: ' ';
+    position: absolute;
+    left: -31px;
+    bottom: 0;
+    z-index: 8;
+    width: 148px;
+    height: 16px;
+    background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 100%);
+    filter: blur(2.3328px);
+    border-radius: 50%;
+  }
+}
+
+.info {
+  text-align: left;
+}
+
+.musicInfo {
+  // width: 500px;
+
+  .name {
+    font-size: max(21px, 15Px);
+    font-weight: 600;
+    color: #131415;
+    line-height: 25px;
+    padding-bottom: 8px;
+    max-width: 220px;
+  }
+
+  .c {
+    font-size: max(13px, 12Px);
+    color: #777777;
+    line-height: 18px;
+
+    span {
+      flex-shrink: 0;
+    }
+
+    &>div {
+      display: flex;
+      margin-right: 20px;
+      max-width: 220px;
+    }
+  }
+}
+
+.selectionCouser {
+  display: flex;
+  align-items: center;
+  position: absolute;
+
+  &.hide {
+    opacity: 0;
+    visibility: hidden;
+  }
+
+  .textStart,
+  .textReadOnly {
+    cursor: pointer;
+    background: #1A8CFF;
+    border-radius: 13px;
+    font-weight: 600;
+    font-size: max(13px, 12Px);
+    color: #FFFFFF;
+    line-height: 18px;
+    display: inline-flex;
+    align-items: center;
+    padding: 3px 8px;
+    flex-shrink: 0;
+
+    .icon {
+      margin-left: 4px;
+      display: inline-block;
+    }
+  }
+
+  .textStart {
+    .icon {
+      width: 8px;
+      height: 10px;
+      background: url('../../../content-information//images/icon-speak-arrow.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+  .textReadOnly {
+    margin-left: 10px;
+
+    .icon {
+
+      width: 9px;
+      height: 9px;
+      background: url('../../../content-information//images/icon-speak-line.png') no-repeat center;
+      background-size: contain;
+    }
+  }
 }

+ 426 - 372
src/views/prepare-lessons/model/source-music/detail.tsx

@@ -1,372 +1,426 @@
-import {
-  NBreadcrumb,
-  NBreadcrumbItem,
-  NButton,
-  NImage,
-  NSlider,
-  NSpace,
-  NSpin
-} from 'naive-ui';
-import { computed, defineComponent, onMounted, reactive, watch } from 'vue';
-import styles from './detail.module.less';
-import icon_back from '../../../xiaoku-music/images/icon_back.png';
-import icon_arrow from '../../../xiaoku-music/images/icon_arrow.png';
-import icon_play from '../../../xiaoku-music/images/icon_play.png';
-import icon_pause from '../../../xiaoku-music/images/icon_pause.png';
-import icon_default from '../../../xiaoku-music/images/icon_default.png';
-import icon_separator from '../../../xiaoku-music/images/icon_separator.png';
-import iconT from '/src/views/content-information/images/icon-t.png';
-import iconAddT from '/src/views/content-information/images/icon-add-t.png';
-import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
-import musicBg from '../../../xiaoku-music/images/icon_default.png';
-import iconPan from '/src/views/content-information/images/icon-pan.png';
-import { useRoute, useRouter } from 'vue-router';
-import PlayLoading from '../../../xiaoku-music/component/play-loading';
-import TheNoticeBar from '/src/components/TheNoticeBar';
-import TheEmpty from '/src/components/TheEmpty';
-import PlayItem from '../../../xiaoku-music/component/play-item';
-import { api_knowledgeWiki_detail } from '/src/views/content-information/api';
-
-export default defineComponent({
-  name: 'music-detail',
-  props: {
-    id: {
-      type: String,
-      default: ''
-    },
-    type: {
-      type: String,
-      default: ''
-    },
-    activeStatus: {
-      type: Boolean,
-      default: false
-    },
-    contentType: {
-      type: String,
-      default: ''
-    }
-  },
-  setup(props, { expose }) {
-    const route = useRoute();
-    const data = reactive({
-      loading: false,
-      finshed: false,
-      reshing: false,
-      details: {} as any,
-      list: [] as any,
-      listActive: 0,
-      playState: 'pause' as 'play' | 'pause',
-      showPlayer: false,
-      showPreivew: false,
-      previewUrl: '',
-      showCloseBtn: true,
-      fontSize: 18 // 默认18
-    });
-
-    /** 选中的item */
-    const activeItem = computed(() => {
-      return data.list[data.listActive] || {};
-    });
-
-    /** 播放曲目 */
-    const handlePlay = (item: any) => {
-      const index = data.list.findIndex((_item: any) => _item.id === item.id);
-      if (index > -1) {
-        if (data.listActive === index) {
-          data.playState = data.playState === 'play' ? 'pause' : 'play';
-        } else {
-          data.playState = 'play';
-        }
-        data.showPlayer = true;
-        data.listActive = index;
-      }
-    };
-
-    /** 音频控制 */
-    const handleChangeAudio = (
-      type: 'play' | 'pause' | 'pre' | 'next' | 'favitor'
-    ) => {
-      if (type === 'play') {
-        data.playState = 'play';
-      } else if (type === 'pause') {
-        data.playState = 'pause';
-      } else if (type === 'pre') {
-        if (data.list[data.listActive - 1]) {
-          handlePlay(data.list[data.listActive - 1]);
-        }
-      } else if (type === 'next') {
-        if (data.list[data.listActive + 1]) {
-          handlePlay(data.list[data.listActive + 1]);
-        }
-      }
-    };
-
-    const getDetail = async () => {
-      data.loading = true;
-      let res = {} as any;
-      try {
-        res = await api_knowledgeWiki_detail({
-          id: props.id || route.query.id
-        });
-      } catch (error) {
-        console.log(error);
-      }
-      if (data.reshing) {
-        data.list = [];
-        data.reshing = false;
-      }
-
-      data.finshed = true;
-      try {
-        data.list = res.data?.knowledgeWikiResources || [];
-        data.list.forEach((item: any) => {
-          item.audioFileUrl = item.url;
-          item.musicSheetName = item.name;
-        });
-        const knowledgeWikiCategories = res.data?.knowledgeWikiCategories || [];
-        res.data.knowledgeName =
-          knowledgeWikiCategories.length > 0
-            ? knowledgeWikiCategories[0].name
-            : '';
-        res.data.intros = res.data.intros.replace(
-          /<video/gi,
-          '<video class="video-music" style="width: 100% !important;" controlslist="nodownload"'
-        );
-        data.details = res.data;
-      } catch {
-        //
-      }
-
-      data.loading = false;
-    };
-
-    const onStopAll = (type: 'play' | 'pause' | 'pre' | 'next' | 'favitor') => {
-      handleChangeAudio(type);
-
-      try {
-        // 暂停视频
-        const doms = document.querySelectorAll('.video-music');
-        if (doms && doms.length > 0) {
-          doms.forEach((dom: any) => {
-            dom.pause();
-          });
-        }
-      } catch {
-        //
-      }
-    };
-
-    onMounted(() => {
-      getDetail();
-    });
-
-    watch(
-      () => props.activeStatus,
-      () => {
-        if (!props.activeStatus) {
-          onStopAll('pause');
-        }
-      }
-    );
-
-    expose({
-      handleChangeAudio: onStopAll
-    });
-    return () => (
-      <div
-        class={[
-          styles.container,
-          props.type === 'preview' && styles.containerPreview,
-          props.type === 'modal' && styles.containerModal
-        ]}>
-        <div class={[styles.wrap, data.showPlayer ? styles.wrapBottom : '']}>
-          <div class={styles.content}>
-            <div class={styles.contentWrap}>
-              <div class={[styles.musicList, 'musicList-container']}>
-                <div class={styles.wrapList}>
-                  <div class={styles.musicInfo}>
-                    <div class={styles.musicImg}>
-                      <img
-                        src={data.details?.avatar || musicBg}
-                        class={styles.img}
-                      />
-                      <div class={styles.panSection}>
-                        <img src={iconPan} class={styles.iconPan} />
-                        <img
-                          src={data.details?.avatar || musicBg}
-                          class={styles.img2}
-                        />
-                      </div>
-                    </div>
-
-                    <div class={styles.info}>
-                      <div class={styles.name}>
-                        <TheNoticeBar
-                          text={data.details.name}
-                          style={{ marginRight: '0' }}
-                        />
-                        {/* {data.details.name} */}
-                      </div>
-                      <div class={styles.c}>
-                        {data.details.composers ? (
-                          <div>
-                            <span>作曲:</span>
-                            <TheNoticeBar
-                              text={data.details.composers}
-                              style={{ marginRight: '0' }}
-                            />
-                          </div>
-                        ) : (
-                          ''
-                        )}
-                        {data.details.lyricists ? (
-                          <div>
-                            <span>作词:</span>
-                            <TheNoticeBar
-                              text={data.details.lyricists}
-                              style={{ marginRight: '0' }}
-                            />
-                          </div>
-                        ) : (
-                          ''
-                        )}
-                      </div>
-                    </div>
-                  </div>
-
-                  <div class={styles.titlec}>
-                    <i class={styles.icon2}></i>名曲鉴赏
-                  </div>
-
-                  {data.list.map((item: any, index: any) => {
-                    return (
-                      <div class={styles.itemContainer}>
-                        <div
-                          class={[styles.item]}
-                          onClick={(e: Event) => {
-                            e.stopPropagation();
-                            handlePlay(item);
-                          }}>
-                          <div class={styles.img}>
-                            <NImage
-                              lazy
-                              objectFit="cover"
-                              previewDisabled={true}
-                              src={item.titleImg || icon_default}
-                              onLoad={e => {
-                                (e.target as any).dataset.loaded = 'true';
-                              }}
-                            />
-                            <PlayLoading
-                              class={[
-                                data.listActive === index &&
-                                data.playState === 'play'
-                                  ? ''
-                                  : styles.showPlayLoading
-                              ]}
-                            />
-                          </div>
-                          <div class={styles.title}>
-                            <div class={styles.titleName}>
-                              <TheNoticeBar
-                                text={item.name}
-                                style={{ marginRight: '12px' }}
-                              />
-                            </div>
-                          </div>
-
-                          <NButton
-                            color="#259CFE"
-                            textColor="#fff"
-                            round
-                            class={styles.btn}
-                            type="primary"
-                            onClick={(e: Event) => {
-                              e.stopPropagation();
-                              handlePlay(item);
-                            }}>
-                            播放
-                            <img
-                              src={
-                                data.listActive === index &&
-                                data.playState === 'play'
-                                  ? icon_pause
-                                  : icon_play
-                              }
-                            />
-                          </NButton>
-
-                          <img class={styles.arrow} src={icon_arrow} />
-                        </div>
-                      </div>
-                    );
-                  })}
-                  {!data.finshed && (
-                    <div class={styles.loadingWrap}>
-                      <NSpin show={true}></NSpin>
-                    </div>
-                  )}
-                  {!data.loading && data.list.length === 0 && (
-                    <div class={styles.empty}>
-                      <TheEmpty
-                        description="暂无名曲鉴赏"
-                        style={{ paddingTop: '0px' }}></TheEmpty>
-                    </div>
-                  )}
-                </div>
-              </div>
-
-              <div class={styles.musicStaff}>
-                <div class={styles.musicTitle}>
-                  <i class={styles.icon1}></i>名曲故事
-                </div>
-                <div
-                  class={styles.musicContent}
-                  v-html={data.details?.intros}
-                  style={{ fontSize: data.fontSize + 'px' }}></div>
-              </div>
-
-              <div class={styles.changeSizeSection}>
-                <img src={iconT} class={styles.iconT} />
-                <img
-                  src={iconAddT}
-                  class={styles.iconAddT}
-                  onClick={() => {
-                    if (data.fontSize >= 32) return;
-                    data.fontSize += 1;
-                  }}
-                />
-                <NSlider
-                  v-model:value={data.fontSize}
-                  placement="left"
-                  vertical
-                  min={12}
-                  max={32}
-                />
-                <img
-                  src={iconPlusT}
-                  class={styles.iconPlusT}
-                  onClick={() => {
-                    if (data.fontSize <= 12) return;
-                    data.fontSize -= 1;
-                  }}
-                />
-              </div>
-            </div>
-          </div>
-        </div>
-
-        {data.list.length !== 0 && (
-          <PlayItem
-            type={props.type}
-            show={data.showPlayer}
-            playState={data.playState}
-            item={activeItem.value}
-            onChange={value => handleChangeAudio(value)}
-          />
-        )}
-      </div>
-    );
-  }
-});
+import {
+  NBreadcrumb,
+  NBreadcrumbItem,
+  NButton,
+  NImage,
+  NSlider,
+  NSpace,
+  NSpin
+} from 'naive-ui';
+import { computed, defineComponent, onMounted, reactive, watch } from 'vue';
+import styles from './detail.module.less';
+import icon_back from '../../../xiaoku-music/images/icon_back.png';
+import icon_arrow from '../../../xiaoku-music/images/icon_arrow.png';
+import icon_play from '../../../xiaoku-music/images/icon_play.png';
+import icon_pause from '../../../xiaoku-music/images/icon_pause.png';
+import icon_default from '../../../xiaoku-music/images/icon_default.png';
+import icon_separator from '../../../xiaoku-music/images/icon_separator.png';
+import iconT from '/src/views/content-information/images/icon-t.png';
+import iconAddT from '/src/views/content-information/images/icon-add-t.png';
+import iconPlusT from '/src/views/content-information/images/icon-plus-t.png';
+import musicBg from '../../../xiaoku-music/images/icon_default.png';
+import iconPan from '/src/views/content-information/images/icon-pan.png';
+import { useRoute, useRouter } from 'vue-router';
+import PlayLoading from '../../../xiaoku-music/component/play-loading';
+import TheNoticeBar from '/src/components/TheNoticeBar';
+import TheEmpty from '/src/components/TheEmpty';
+import PlayItem from '../../../xiaoku-music/component/play-item';
+import { api_knowledgeWiki_detail } from '/src/views/content-information/api';
+import { useSpeak } from '/src/views/content-information/useSpeak';
+
+export default defineComponent({
+  name: 'music-detail',
+  props: {
+    id: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: ''
+    },
+    activeStatus: {
+      type: Boolean,
+      default: false
+    },
+    contentType: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props, { expose }) {
+    const route = useRoute();
+    const speakMusicContent =
+      'musicContent' + new Date().getTime() + Math.floor(Math.random() * 100);
+    const speak = useSpeak(speakMusicContent);
+    const data = reactive({
+      loading: false,
+      finshed: false,
+      reshing: false,
+      details: {} as any,
+      list: [] as any,
+      listActive: 0,
+      playState: 'pause' as 'play' | 'pause',
+      showPlayer: false,
+      showPreivew: false,
+      previewUrl: '',
+      showCloseBtn: true,
+      fontSize: 18 // 默认18
+    });
+
+    /** 选中的item */
+    const activeItem = computed(() => {
+      return data.list[data.listActive] || {};
+    });
+
+    watch(
+      () => data.playState,
+      () => {
+        if (data.playState === 'play') {
+          speak.onCloseSpeak();
+        }
+      }
+    );
+
+    /** 播放曲目 */
+    const handlePlay = (item: any) => {
+      const index = data.list.findIndex((_item: any) => _item.id === item.id);
+      if (index > -1) {
+        if (data.listActive === index) {
+          data.playState = data.playState === 'play' ? 'pause' : 'play';
+        } else {
+          data.playState = 'play';
+        }
+        data.showPlayer = true;
+        data.listActive = index;
+      }
+    };
+
+    /** 音频控制 */
+    const handleChangeAudio = (
+      type: 'play' | 'pause' | 'pre' | 'next' | 'favitor'
+    ) => {
+      if (type === 'play') {
+        data.playState = 'play';
+      } else if (type === 'pause') {
+        data.playState = 'pause';
+      } else if (type === 'pre') {
+        if (data.list[data.listActive - 1]) {
+          handlePlay(data.list[data.listActive - 1]);
+        }
+      } else if (type === 'next') {
+        if (data.list[data.listActive + 1]) {
+          handlePlay(data.list[data.listActive + 1]);
+        }
+      }
+    };
+
+    const getDetail = async () => {
+      data.loading = true;
+      let res = {} as any;
+      try {
+        res = await api_knowledgeWiki_detail({
+          id: props.id || route.query.id
+        });
+      } catch (error) {
+        console.log(error);
+      }
+      if (data.reshing) {
+        data.list = [];
+        data.reshing = false;
+      }
+
+      data.finshed = true;
+      try {
+        data.list = res.data?.knowledgeWikiResources || [];
+        data.list.forEach((item: any) => {
+          item.audioFileUrl = item.url;
+          item.musicSheetName = item.name;
+        });
+        const knowledgeWikiCategories = res.data?.knowledgeWikiCategories || [];
+        res.data.knowledgeName =
+          knowledgeWikiCategories.length > 0
+            ? knowledgeWikiCategories[0].name
+            : '';
+        res.data.intros = res.data.intros.replace(
+          /<video/gi,
+          '<video class="video-music" style="width: 100% !important;" controlslist="nodownload"'
+        );
+        // 使用 DOMParser 解析 HTML 字符串
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(res.data.intros, 'text/html');
+
+        // 提取并分割 HTML 文档中的内容
+        document
+          .querySelector('#' + speakMusicContent)
+          ?.appendChild(speak.processNode(doc.body));
+
+        data.details = res.data;
+      } catch {
+        //
+      }
+
+      data.loading = false;
+    };
+
+    const onStopAll = (type: 'play' | 'pause' | 'pre' | 'next' | 'favitor') => {
+      handleChangeAudio(type);
+      speak.onCloseSpeak();
+      try {
+        // 暂停视频
+        const doms = document.querySelectorAll('.video-music');
+        if (doms && doms.length > 0) {
+          doms.forEach((dom: any) => {
+            dom.pause();
+          });
+        }
+      } catch {
+        //
+      }
+    };
+
+    onMounted(() => {
+      getDetail();
+    });
+
+    watch(
+      () => props.activeStatus,
+      () => {
+        if (!props.activeStatus) {
+          onStopAll('pause');
+        }
+      }
+    );
+
+    expose({
+      handleChangeAudio: onStopAll
+    });
+    return () => (
+      <div
+        class={[
+          styles.container,
+          props.type === 'preview' && styles.containerPreview,
+          props.type === 'modal' && styles.containerModal
+        ]}>
+        <div class={[styles.wrap, data.showPlayer ? styles.wrapBottom : '']}>
+          <div class={styles.content}>
+            <div class={styles.contentWrap}>
+              <div class={[styles.musicList, 'musicList-container']}>
+                <div class={styles.wrapList}>
+                  <div class={styles.musicInfo}>
+                    <div class={styles.musicImg}>
+                      <img
+                        src={data.details?.avatar || musicBg}
+                        class={styles.img}
+                      />
+                      <div class={styles.panSection}>
+                        <img src={iconPan} class={styles.iconPan} />
+                        <img
+                          src={data.details?.avatar || musicBg}
+                          class={styles.img2}
+                        />
+                      </div>
+                    </div>
+
+                    <div class={styles.info}>
+                      <div class={styles.name}>
+                        <TheNoticeBar
+                          text={data.details.name}
+                          style={{ marginRight: '0' }}
+                        />
+                        {/* {data.details.name} */}
+                      </div>
+                      <div class={styles.c}>
+                        {data.details.composers ? (
+                          <div>
+                            <span>作曲:</span>
+                            <TheNoticeBar
+                              text={data.details.composers}
+                              style={{ marginRight: '0' }}
+                            />
+                          </div>
+                        ) : (
+                          ''
+                        )}
+                        {data.details.lyricists ? (
+                          <div>
+                            <span>作词:</span>
+                            <TheNoticeBar
+                              text={data.details.lyricists}
+                              style={{ marginRight: '0' }}
+                            />
+                          </div>
+                        ) : (
+                          ''
+                        )}
+                      </div>
+                    </div>
+                  </div>
+
+                  <div class={styles.titlec}>
+                    <i class={styles.icon2}></i>名曲鉴赏
+                  </div>
+
+                  {data.list.map((item: any, index: any) => {
+                    return (
+                      <div class={styles.itemContainer}>
+                        <div
+                          class={[styles.item]}
+                          onClick={(e: Event) => {
+                            e.stopPropagation();
+                            handlePlay(item);
+                          }}>
+                          <div class={styles.img}>
+                            <NImage
+                              lazy
+                              objectFit="cover"
+                              previewDisabled={true}
+                              src={item.titleImg || icon_default}
+                              onLoad={e => {
+                                (e.target as any).dataset.loaded = 'true';
+                              }}
+                            />
+                            <PlayLoading
+                              class={[
+                                data.listActive === index &&
+                                data.playState === 'play'
+                                  ? ''
+                                  : styles.showPlayLoading
+                              ]}
+                            />
+                          </div>
+                          <div class={styles.title}>
+                            <div class={styles.titleName}>
+                              <TheNoticeBar
+                                text={item.name}
+                                style={{ marginRight: '12px' }}
+                              />
+                            </div>
+                          </div>
+
+                          <NButton
+                            color="#259CFE"
+                            textColor="#fff"
+                            round
+                            class={styles.btn}
+                            type="primary"
+                            onClick={(e: Event) => {
+                              e.stopPropagation();
+                              handlePlay(item);
+                            }}>
+                            播放
+                            <img
+                              src={
+                                data.listActive === index &&
+                                data.playState === 'play'
+                                  ? icon_pause
+                                  : icon_play
+                              }
+                            />
+                          </NButton>
+
+                          <img class={styles.arrow} src={icon_arrow} />
+                        </div>
+                      </div>
+                    );
+                  })}
+                  {!data.finshed && (
+                    <div class={styles.loadingWrap}>
+                      <NSpin show={true}></NSpin>
+                    </div>
+                  )}
+                  {!data.loading && data.list.length === 0 && (
+                    <div class={styles.empty}>
+                      <TheEmpty
+                        description="暂无名曲鉴赏"
+                        style={{ paddingTop: '0px' }}></TheEmpty>
+                    </div>
+                  )}
+                </div>
+              </div>
+
+              <div class={styles.musicStaff}>
+                <div class={styles.musicTitle}>
+                  <div class={styles.musicTitleLeft}>
+                    <i class={styles.icon1}></i>名曲故事
+                  </div>
+                  <div class={styles.musicTitleRight}>
+                    {speak.isSpeak.value ? (
+                      <span
+                        class={styles.textClose}
+                        onClick={speak.onCloseSpeak}>
+                        <i class={styles.icon}></i>关闭朗读
+                      </span>
+                    ) : (
+                      <span class={styles.textRead} onClick={speak.onAllSpeak}>
+                        <i class={styles.icon}></i>全文朗读
+                      </span>
+                    )}
+                  </div>
+                </div>
+                <div
+                  class={styles.musicContent}
+                  id={speakMusicContent}
+                  style={{ fontSize: data.fontSize + 'px' }}>
+                  {/* 选中的内容 */}
+                  <div
+                    id="selectionCouser"
+                    class={[
+                      styles.selectionCouser,
+                      !speak.showDom.value && styles.hide
+                    ]}>
+                    <span class={styles.textStart} onClick={speak.onTextStart}>
+                      开始朗读<i class={styles.icon}></i>
+                    </span>
+                    <span
+                      class={styles.textReadOnly}
+                      onClick={speak.onTextReadOnly}>
+                      只读这段<i class={styles.icon}></i>
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              <div class={styles.changeSizeSection}>
+                <img src={iconT} class={styles.iconT} />
+                <img
+                  src={iconAddT}
+                  class={styles.iconAddT}
+                  onClick={() => {
+                    if (data.fontSize >= 32) return;
+                    data.fontSize += 1;
+                  }}
+                />
+                <NSlider
+                  v-model:value={data.fontSize}
+                  placement="left"
+                  vertical
+                  min={12}
+                  max={32}
+                />
+                <img
+                  src={iconPlusT}
+                  class={styles.iconPlusT}
+                  onClick={() => {
+                    if (data.fontSize <= 12) return;
+                    data.fontSize -= 1;
+                  }}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {data.list.length !== 0 && (
+          <PlayItem
+            type={props.type}
+            show={data.showPlayer}
+            playState={data.playState}
+            item={activeItem.value}
+            onChange={value => handleChangeAudio(value)}
+          />
+        )}
+      </div>
+    );
+  }
+});