소스 검색

添加全屏

lex 1 년 전
부모
커밋
bde3cf80fd

+ 210 - 200
src/components/card-preview/index.tsx

@@ -1,200 +1,210 @@
-import { NModal, NSpin } from 'naive-ui';
-import { defineComponent, ref, toRef, watch } from 'vue';
-import styles from './index.module.less';
-import VideoModal from './video-modal';
-import MusicModal from './music-modal';
-import SongModal from './song-modal';
-import TheEmpty from '../TheEmpty';
-import RhythmModal from './rhythm-modal';
-import InstruemntDetail from '/src/views/prepare-lessons/model/source-instrument/detail';
-import TheoryDetail from '/src/views/prepare-lessons/model/source-knowledge/detail';
-import MusicDetail from '/src/views/prepare-lessons/model/source-music/detail';
-import ListenModal from './listen-modal';
-import useDrag from '@/hooks/useDrag';
-import Dragbom from '@/hooks/useDrag/dragbom';
-import { useUserStore } from '@/store/modules/users';
-
-export default defineComponent({
-  name: 'card-preview',
-  props: {
-    show: {
-      type: Boolean,
-      default: false
-    },
-    item: {
-      type: Object,
-      default: () => ({})
-    },
-    size: {
-      type: String,
-      default: 'default'
-    },
-    /** 是否下载 只支持 video audio */
-    isDownload: {
-      type: Boolean,
-      default: false
-    },
-    /** 从哪里使用 */
-    from: {
-      type: String,
-      default: ''
-    }
-  },
-  emit: ['update:show'],
-  setup(props, { emit }) {
-    const show = toRef(props.show);
-    const item = toRef(props.item);
-    const pptLoading = ref(true);
-
-    watch(
-      () => props.show,
-      () => {
-        show.value = props.show;
-      }
-    );
-
-    watch(
-      () => props.item,
-      () => {
-        item.value = props.item;
-      }
-    );
-    // 拖动
-    let cardPreviewBoxDragData: any;
-    let cardPreviewBoxClass: string;
-    if (props.from === 'class') {
-      const users = useUserStore();
-      cardPreviewBoxClass = 'cardPreviewBoxClass_drag';
-      cardPreviewBoxDragData = useDrag(
-        [
-          `${cardPreviewBoxClass}>.n-card-header`,
-          `${cardPreviewBoxClass} .bom_drag`
-        ],
-        cardPreviewBoxClass,
-        show,
-        users.info.id
-      );
-    }
-    return () => (
-      <>
-        <NModal
-          style={
-            props.from === 'class' ? cardPreviewBoxDragData.styleDrag.value : {}
-          }
-          v-model:show={show.value}
-          onUpdate:show={() => {
-            emit('update:show', show.value);
-            if (!show.value) {
-              pptLoading.value = true;
-            }
-          }}
-          preset="card"
-          showIcon={false}
-          class={[
-            'modalTitle background',
-            cardPreviewBoxClass,
-            props.from === 'class' && styles.classCard,
-            styles.cardPreview,
-            item.value.type === 'PPT' && styles.maxCard,
-            props.size === 'large' && styles.cardLarge
-          ]}
-          title={item.value.type === 'MUSIC' ? '曲目预览' : item.value.title}
-          blockScroll={false}>
-          {item.value.type === 'VIDEO' && (
-            <VideoModal
-              title={item.value.title}
-              poster={item.value.url}
-              src={item.value.content}
-              isDownload={props.isDownload}
-            />
-          )}
-          {item.value.type === 'MUSIC' && (
-            <MusicModal
-              class={styles.musicPreview}
-              item={item.value}
-              from={props.from}
-            />
-          )}
-          {item.value.type === 'SONG' && (
-            <SongModal item={item.value} isDownload={props.isDownload} />
-          )}
-          {item.value.type === 'PPT' && (
-            <NSpin show={pptLoading.value}>
-              <iframe
-                class={styles.pptBox}
-                src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
-                  item.value.content
-                )}`}
-                onLoad={() => {
-                  console.log('loading end');
-                  pptLoading.value = false;
-                }}
-                width="100%"
-                height="100%"
-                frameborder="1"></iframe>
-            </NSpin>
-          )}
-          {item.value.type === 'RHYTHM' && (
-            <RhythmModal class={styles.musicPreview} item={item.value} />
-          )}
-
-          {item.value.type === 'LISTEN' && (
-            <ListenModal class={styles.musicPreview} item={item.value} />
-          )}
-
-          {(item.value.type === 'INSTRUMENT' ||
-            item.value.type === 'MUSICIAN') && (
-            <div class={styles.instrumentGroup}>
-              <InstruemntDetail
-                type="modal"
-                contentType={item.value.type}
-                id={item.value.content}
-              />
-            </div>
-          )}
-          {item.value.type === 'MUSIC_WIKI' && (
-            <div class={styles.instrumentGroup}>
-              <MusicDetail
-                type="modal"
-                contentType={item.value.type}
-                id={item.value.content}
-              />
-            </div>
-          )}
-
-          {item.value.type === 'THEORY' && (
-            <div>
-              <TheoryDetail type="modal" id={item.value.content} />
-            </div>
-          )}
-
-          {/* LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家) */}
-          {/*  VIDEO("视频"),
-    MUSIC("曲目"),
-    IMG("图片"),
-    SONG("音频"),
-    PPT("ppt"),
-    LISTEN("听音练习"),
-    RHYTHM("节奏练习"),
-    THEORY("乐理知识"),
-    MUSIC_WIKI("名曲鉴赏"),
-    INSTRUMENT("乐器"),
-    MUSICIAN("音乐家"), */}
-          {![
-            'VIDEO',
-            'MUSIC',
-            'SONG',
-            'PPT',
-            'RHYTHM',
-            'INSTRUMENT',
-            'THEORY',
-            'MUSICIAN',
-            'MUSIC_WIKI',
-            'LISTEN'
-          ].includes(item.value.type) && <TheEmpty />}
-          {props.from === 'class' && <Dragbom></Dragbom>}
-        </NModal>
-      </>
-    );
-  }
-});
+import { NModal, NSpin } from 'naive-ui';
+import { defineComponent, ref, toRef, watch } from 'vue';
+import styles from './index.module.less';
+import VideoModal from './video-modal';
+import MusicModal from './music-modal';
+import SongModal from './song-modal';
+import TheEmpty from '../TheEmpty';
+import RhythmModal from './rhythm-modal';
+import InstruemntDetail from '/src/views/prepare-lessons/model/source-instrument/detail';
+import TheoryDetail from '/src/views/prepare-lessons/model/source-knowledge/detail';
+import MusicDetail from '/src/views/prepare-lessons/model/source-music/detail';
+import ListenModal from './listen-modal';
+import useDrag from '@/hooks/useDrag';
+import Dragbom from '@/hooks/useDrag/dragbom';
+import { useUserStore } from '@/store/modules/users';
+
+export default defineComponent({
+  name: 'card-preview',
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    item: {
+      type: Object,
+      default: () => ({})
+    },
+    size: {
+      type: String,
+      default: 'default'
+    },
+    /** 是否下载 只支持 video audio */
+    isDownload: {
+      type: Boolean,
+      default: false
+    },
+    /** 是否下载 只支持 video audio */
+    fullscreen: {
+      type: Boolean,
+      default: false
+    },
+    /** 从哪里使用 */
+    from: {
+      type: String,
+      default: ''
+    }
+  },
+  emit: ['update:show'],
+  setup(props, { emit }) {
+    const show = toRef(props.show);
+    const item = toRef(props.item);
+    const pptLoading = ref(true);
+
+    watch(
+      () => props.show,
+      () => {
+        show.value = props.show;
+      }
+    );
+
+    watch(
+      () => props.item,
+      () => {
+        item.value = props.item;
+      }
+    );
+    // 拖动
+    let cardPreviewBoxDragData: any;
+    let cardPreviewBoxClass: string;
+    if (props.from === 'class') {
+      const users = useUserStore();
+      cardPreviewBoxClass = 'cardPreviewBoxClass_drag';
+      cardPreviewBoxDragData = useDrag(
+        [
+          `${cardPreviewBoxClass}>.n-card-header`,
+          `${cardPreviewBoxClass} .bom_drag`
+        ],
+        cardPreviewBoxClass,
+        show,
+        users.info.id
+      );
+    }
+    return () => (
+      <>
+        <NModal
+          style={
+            props.from === 'class' ? cardPreviewBoxDragData.styleDrag.value : {}
+          }
+          v-model:show={show.value}
+          onUpdate:show={() => {
+            emit('update:show', show.value);
+            if (!show.value) {
+              pptLoading.value = true;
+            }
+          }}
+          preset="card"
+          showIcon={false}
+          class={[
+            'modalTitle background',
+            cardPreviewBoxClass,
+            props.from === 'class' && styles.classCard,
+            styles.cardPreview,
+            item.value.type === 'PPT' && styles.maxCard,
+            props.size === 'large' && styles.cardLarge
+          ]}
+          title={item.value.type === 'MUSIC' ? '曲目预览' : item.value.title}
+          blockScroll={false}>
+          {item.value.type === 'VIDEO' && (
+            <VideoModal
+              title={item.value.title}
+              poster={item.value.url}
+              src={item.value.content}
+              isDownload={props.isDownload}
+              fullscreen={props.fullscreen}
+            />
+          )}
+          {item.value.type === 'MUSIC' && (
+            <MusicModal
+              class={styles.musicPreview}
+              item={item.value}
+              from={props.from}
+            />
+          )}
+          {item.value.type === 'SONG' && (
+            <SongModal
+              item={item.value}
+              isDownload={props.isDownload}
+              fullscreen={props.fullscreen}
+            />
+          )}
+          {item.value.type === 'PPT' && (
+            <NSpin show={pptLoading.value}>
+              <iframe
+                class={styles.pptBox}
+                src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
+                  item.value.content
+                )}`}
+                onLoad={() => {
+                  console.log('loading end');
+                  pptLoading.value = false;
+                }}
+                width="100%"
+                height="100%"
+                frameborder="1"></iframe>
+            </NSpin>
+          )}
+          {item.value.type === 'RHYTHM' && (
+            <RhythmModal class={styles.musicPreview} item={item.value} />
+          )}
+
+          {item.value.type === 'LISTEN' && (
+            <ListenModal class={styles.musicPreview} item={item.value} />
+          )}
+
+          {(item.value.type === 'INSTRUMENT' ||
+            item.value.type === 'MUSICIAN') && (
+            <div class={styles.instrumentGroup}>
+              <InstruemntDetail
+                type="modal"
+                contentType={item.value.type}
+                id={item.value.content}
+              />
+            </div>
+          )}
+          {item.value.type === 'MUSIC_WIKI' && (
+            <div class={styles.instrumentGroup}>
+              <MusicDetail
+                type="modal"
+                contentType={item.value.type}
+                id={item.value.content}
+              />
+            </div>
+          )}
+
+          {item.value.type === 'THEORY' && (
+            <div>
+              <TheoryDetail type="modal" id={item.value.content} />
+            </div>
+          )}
+
+          {/* LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家) */}
+          {/*  VIDEO("视频"),
+    MUSIC("曲目"),
+    IMG("图片"),
+    SONG("音频"),
+    PPT("ppt"),
+    LISTEN("听音练习"),
+    RHYTHM("节奏练习"),
+    THEORY("乐理知识"),
+    MUSIC_WIKI("名曲鉴赏"),
+    INSTRUMENT("乐器"),
+    MUSICIAN("音乐家"), */}
+          {![
+            'VIDEO',
+            'MUSIC',
+            'SONG',
+            'PPT',
+            'RHYTHM',
+            'INSTRUMENT',
+            'THEORY',
+            'MUSICIAN',
+            'MUSIC_WIKI',
+            'LISTEN'
+          ].includes(item.value.type) && <TheEmpty />}
+          {props.from === 'class' && <Dragbom></Dragbom>}
+        </NModal>
+      </>
+    );
+  }
+});

+ 4 - 0
src/components/card-preview/song-modal/index.tsx

@@ -25,6 +25,10 @@ export default defineComponent({
     isDownload: {
       type: Boolean,
       default: false
+    },
+    fullscreen: {
+      type: Boolean,
+      default: false
     }
   },
   setup(props) {

+ 376 - 346
src/components/card-preview/video-modal/index.module.less

@@ -1,346 +1,376 @@
-.videoWrap {
-  width: 100%;
-  height: 100%;
-  height: 518px;
-
-  .controls {
-    border-radius: 0 0 16px 16px !important;
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    width: 100%;
-    background: rgba(0, 0, 0, 0.6);
-    backdrop-filter: blur(26px);
-    height: 80px;
-    padding: 0 26px 0 26px !important;
-    transition: all 0.5s;
-    display: flex;
-    align-items: center;
-    transition: all .5s;
-
-    .time {
-      display: flex;
-      justify-content: space-between;
-      color: #fff;
-      padding: 4px 0 4px 12px;
-      font-size: max(24px, 14Px);
-      font-weight: 600;
-      line-height: 33px;
-      // min-width: 150px;
-      flex-shrink: 0;
-
-      .line {
-        font-size: 20px;
-      }
-
-      :global {
-        .plyr__time {
-          font-size: max(22px, 12Px);
-        }
-
-        .plyr__time+.plyr__time:before {
-          content: '';
-          margin-right: 0;
-        }
-      }
-    }
-  }
-
-  .actions {
-    display: flex;
-    justify-content: space-between;
-    height: 100%;
-    color: #fff;
-    font-size: 12px;
-    align-items: center;
-    flex-shrink: 0;
-
-    .actionWrap {
-      display: flex;
-      align-items: center;
-    }
-
-    .actionBtn {
-      display: flex;
-      width: 40px;
-      height: 40px;
-      padding: 0;
-      background: transparent;
-      cursor: pointer;
-
-      &>img {
-        width: 100%;
-        height: 100%;
-      }
-    }
-
-
-    .actionBtnSpeed {
-      position: relative;
-      width: 40px;
-      height: 40px;
-      background-color: transparent;
-      cursor: pointer;
-
-      &>img {
-        width: 100%;
-        height: 100%;
-      }
-    }
-
-
-    .iconReplay {
-      width: 40px;
-      height: 40px;
-      background-color: transparent;
-      cursor: pointer;
-      margin: 0 22px;
-
-      &>img {
-        width: 100%;
-        height: 100%;
-      }
-    }
-
-    .iconDownload {
-      width: 40px;
-      height: 40px;
-      margin-left: 12px;
-      background-color: transparent;
-      cursor: pointer;
-
-      &>img {
-        width: 100%;
-        height: 100%;
-      }
-    }
-  }
-
-  .slider {
-    width: 100%;
-    padding: 0 0 0 12px;
-
-    :global {
-
-      .n-slider .n-slider-rail .n-slider-rail__fill,
-      .n-slider .n-slider-handles .n-slider-handle-wrapper {
-        transition: all .2s;
-      }
-    }
-  }
-
-  .sectionAnimate {
-    opacity: 0;
-    pointer-events: none;
-    transform: translateY(100%);
-    transition: all .5s;
-  }
-
-}
-
-.sliderPopup {
-  position: absolute;
-  z-index: 9999;
-  left: -13px;
-  bottom: 35px;
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  height: 252px;
-  width: 59px;
-  padding: 12Px 0 15Px;
-  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI4AAAJcCAMAAAAYSmw3AAAAaVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnbPKNAAAAI3RSTlOzAAUJiqaplqF6V0c4nSevpJN+GAZtX1EQGpqDPTQVc2QhqyTybJ0AAAKuSURBVHja7NRJVsJQAAXRRxMg9BwgGWX/2xT0KCKII5M/uHcFNaqMntm25+mm2uVf7KrN9NxuR8885ozX9Sw9mNXr8Z85p2WV3lTL0+uc1SK9Wqxe5HTz9G7e/ZbTZBDN05xJnYHUk8ec4yGDORx/5hz3GdD+eJ8zOWRQh8ldTp2B1d9zmgyuueV0KUD3lTNPAeafOasUYfWRc1qkCIvTe84yhVhec8ZVClGNLznrFGM9yvAHvKkvObMUYzbKNgXZpk1B2pxTkHOmKcg0mxRkk2ImeFVll4IUFQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAG3twIAAAAAAA5P/aCKqqqqqqqqqqqqqqqtJ+nSQ3CgVRFH2XjwCjHnWoCTXe/yJrUlEljCz7R9riD/Ks4E7yRaRzzjnnnHPOOeecc845zZWQuUZKyEhTJWSqXAnJtVFCNtopITvNlJCZKJSMAvGmZLwhJkrGBJElM4SjDEGtRNQgCJWSUAUQMFYSxoAASiWgBBCQxnFN/uWw1eC2/M+h0cAa7nNCqUGVoZPDaqEBLVZ0c9iXGky552MOodFAmkA/B64axBUe5jA56OUOE+6IjvFCL7UY0yG6Ql3oZYo68DwH1qfmXS/w3pzWfCQeyGb1Ja8K/ZKiyi/1LOMB8UTWFzcH4+wBnhCRsmNEDbFErPW3e3ZEE9FCrm+p6THkWHu29BlyjD1XIthyCEt9YUMEaw7tFz0XIthzaMvnz3YUew7tQZ86ZsSx53Ce6hP5mqcsOfE9eSCePYdVpQeWhhqEwX6hnrLFQFjcej2HFgthchupY3rGRNjcCt2pVtgIo1lxV7PHSFjtl/orP2Ml7E7HuTRvTtiJH9G2/AiRlD84lRqqt/0KYQAAAABJRU5ErkJggg==') no-repeat top center;
-  background-size: contain;
-
-  .iconAdd,
-  .iconCut {
-    display: inline-block;
-    width: 24Px;
-    height: 24Px;
-    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAABMlBMVEUAAAAkqP8ckv8zu/8op/81v/8ajv8elP82v/8rrv8elP4npv8yuf8elP8elf81v/8sr/8elf8vuv8dlv8srv8jn/8zu/8wtf82wP8oqP8npf8Zjf8mpP8Zjf8hmv8bj/40vP8wtP81wP8Zjf41vv8sr/8jnf8aj/0zuf8dlf81vv8rq/4kof4vsf8imP8aj/////8usv8qrP4jn/4inP4dlP4yuf8el/4srv8pqv4gmf4bkf40vf8ssP4wtP8lov4koP4xt/8npv4mpP4zu/8hmv4ajv4ckv4wtv4op/58x/5+y/6Cxv5/zf57xP6Dx/6BxP7u+P/t9/6a1f6Bz/56w/5iuP5zwP5Bpv4yoP6x3/6w3f6d2/6Wzv5tyf5pwv5nvv5svP5Rsv5Pr/5Mq/43p/49/opPAAAAMHRSTlMACoRW+Pf38dTUurGDWEhHR0cjI/b28fHr6+vr19fQ0Lq6sbGoqKiolZWGhoZSUlKk1yinAAAB+UlEQVQ4y43S6VbaQACG4S9E9h3c933fatISI4sWYxUjAVQQ99r2/m+hGQfGMJNEHw4M+XhPTn4AjpQNLM1MhUJTM0uBrAQ/qbXQmUNoLQUve3Nngrk9uMnHjlzF8hAkw0cewklwAr98BDBg47uvjYH72oNx1/WuHfdOFgqFu3qj8bfgiT13Pmy3DbNhtr3jcB5UTNe7DdM0f6s69dRqPemcGG13dV3t2G2737b3bW2+3gUxaxh/LHJfo+eWxLcGZxa2lKqqHcsyuyph2G8aq71rdpL/yar9tW5ZbypTJ3Fd5a0C0oiiKG9W51hhaKzwRiRkFeJfVRFiQRbbx4IaiWvivo14RUBjcY9julKlKtXXGvVA4ofexWuV/T6NyWrf9b6raxZMYuhnX8s9brFgyBE33eOmI5646PN6DBZMIHpIXJCPl0vqnjT3vYuXj9+jiB8KLkl8Je5xbB0Irt5jcd9C5utxBtKwph1oBDtprPH7sASsaFpJs5XYSeMSv68AyJR4NyS+EeYMbNFvHBrzaxTEDj8/k/iZX3fwboGby4/N5mOZGxdA5cbKnxrLoUcunxMn596nDCZx8okEHNZ/+FrHgIRfmwBHHj31MCpDkJsvntqvov0eOOdzcCNHioKIDC/p5aCzDC6n4UdKby5GxoPB8cjiZlrCoP+meld2tFTGwgAAAABJRU5ErkJggg==') no-repeat center;
-    background-size: contain;
-    flex-shrink: 0;
-    cursor: pointer;
-
-    &.disabled {
-      opacity: 0.7;
-    }
-  }
-
-  .iconCut {
-    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAABGlBMVEUAAAAkqP8zu/8op/8ajv8aj/8yuP8elP4npv4dlP8elP8yuP8elP81v/8sr/8jnf8Zjf81v/81v/8jn/8zu/8wtf8gl/82wP8oqP8npf83wP8qqv8mpP8Zjf81vf8tsv8hmv8bj/41wP8qqv8lof8Zjf41vv8sr/8jnf8aj/0zuf8dlf8bjf00vP8xt/8zvf8stv8dmf8dkv8srv8trf8qq/7///8tsP4jn/4us/8srv8gmf4lov4yuf8dlf4wt/8inf4hm/4ck/4bkf4wtP8koP4mpf4fl/4op/40vP8ajv4pqf4zuv98x/5+y/6Bxf40vv80u/6Dx/6a1f5owP5PsP4/p/5zwP4oo/4yoP7u+P7u9/5svP5it/4TfkURAAAANXRSTlMACoT49+66uoaDWFJSR0dHR/j29vHx8evr69fX19fQ0NDQsbGxsaioqKiVlYZYWCMjIyP29l0yDtwAAAGnSURBVDjLjc6HVsIwGAXg27I37r33nkilWloXCGK17OH7v4YpSKSQtPnS5v7n5J6cYIyUPt1bC/j9gbW907QEN+kjf2mE/ygNnvhWacJWHCzXoWem0DUmRAPPHIEoxkSeXETgcPJEaGSx88Rxr6ZpaqOtcUVARTWt0DBNs06yoBGTSd99NV8gXeKnwDV/hYGQqrZNoqerfCH0xVXVaNr3GqqbOGybhlG3LKunG642QaQMXW9altkmaRhk42UKwKGu6xXLquteDgFpRlGUutV8VbzMSEj1h0ZL8ZZC+FVYGLsPwnaxSudWrcJQa9HCKpbehqp3TFVaWIKPzl12uUsLPviKQx12uUMLPiwWi+XBzHsGOR9YxEqZqn0y1MrUCnYy5Uyfd+4gnBEWRlK8nIR0kxN0KwEHuVzWnrNeeQAgmRWUBLEh1t2ALXYvJIa+oEg3iIHLuRdPc5f4c/6Sz+fd/3NQct6DjBHHj66O4SC7dWWMOZv94Jg9w4SL4Ps3WeRzZvACLLH1d8Iu/Od6DDyJ/emvEdP7CbiREvL28sLU1MLytpyQ4PQLtc9vYI2HRk0AAAAASUVORK5CYII=') no-repeat center;
-    background-size: contain;
-  }
-
-  .sliderPoint {
-    background: #FFFFFF;
-    box-shadow: 0 2px 4px 0px rgba(102, 102, 102, 0.77);
-    border-radius: 14px;
-    font-size: 14Px;
-    font-weight: 500;
-    height: 22Px;
-    color: #198CFE;
-    min-width: 40px;
-    text-align: center;
-    vertical-align: text-bottom;
-
-    span {
-      font-size: 12Px;
-    }
-  }
-
-  :global {
-    .n-slider {
-      margin: 7px 0;
-      padding: 0;
-    }
-  }
-}
-
-// .videoWrap {
-//   width: 100%;
-//   height: 100%;
-//   --plyr-color-main: #198CFE;
-//   --plyr-range-track-height: 6px;
-//   --plyr-tooltip-radius: 3px;
-//   --plyr-range-thumb-height: 24px;
-//   --plyr-video-controls-background: #000;
-
-
-//   :global {
-//     .plyr--video {
-//       width: 100%;
-//       height: 100%;
-//     }
-
-//     .plyr__time {
-//       display: block !important;
-//     }
-
-//     .plyr__video-wrapper {
-//       pointer-events: none;
-//     }
-
-//   }
-// }
-
-// :global(.bottomFixed).controls {
-//   width: 100% !important;
-//   background: rgba(0, 0, 0, 0.6) !important;
-//   // backdrop-filter: blur(26px);
-//   height: 80px !important;
-//   min-height: 80px !important;
-//   padding: 0px 40px !important;
-//   z-index: 999;
-
-//   .time {
-//     display: flex;
-//     justify-content: space-between;
-//     color: #fff;
-//     padding: 4px 12px 4px;
-//     font-size: 24px;
-//     font-weight: 600;
-//     line-height: 33px;
-//     min-width: 140px;
-
-//     .line {
-//       font-size: 12px;
-//     }
-
-//     :global {
-//       .plyr__time+.plyr__time:before {
-//         content: '';
-//         margin-right: 0;
-//       }
-//     }
-//   }
-
-//   .slider {
-//     width: 100%;
-//     padding: 0 20px 0 12px;
-
-//     :global {
-//       .van-slider__button {
-//         background: var(--van-primary);
-//       }
-
-//       .van-loading {
-//         width: 100%;
-//         height: 100%;
-//       }
-//     }
-//   }
-
-//   .actions {
-//     display: flex;
-//     justify-content: space-between;
-//     color: #fff;
-//     font-size: 12px;
-//     align-items: center;
-
-//     .actionWrap {
-//       display: flex;
-//     }
-
-//     .actionBtn {
-//       display: flex;
-
-//       padding: 0;
-//       width: 52px;
-//       height: 52px;
-//       background: transparent;
-//     }
-
-//     .actionBtn>img {
-//       width: 100%;
-//       height: 100%;
-//     }
-
-//     :global {
-//       .van-loading__circular {
-//         width: 100%;
-//         height: 100%;
-//       }
-//     }
-
-//     .playIcon {
-//       display: none;
-//     }
-
-//     .btnPlay img:nth-child(2) {
-//       display: block;
-//     }
-
-//     .btnPause img:nth-child(3) {
-//       display: block;
-//     }
-
-//     .btnPlay,
-//     .btnPause {
-//       :global {
-//         .van-loading {
-//           display: none;
-//         }
-//       }
-//     }
-
-//     .loopBtn {
-//       background-color: transparent;
-//       width: 31px !important;
-//       height: 29px !important;
-//       padding: 0;
-//       cursor: pointer;
-
-//       :global {
-//         .loop {
-//           display: block;
-//         }
-
-//         .loopActive {
-//           display: none;
-//         }
-//       }
-//     }
-//   }
-// }
+/* 全屏模式下的样式 */
+:fullscreen .videoContent,
+:-webkit-full-screen .videoContent,
+:-moz-full-screen .videoContent,
+:-ms-fullscreen .videoContent {
+  width: 100% !important;
+  height: 100% !important;
+
+}
+
+.videoWrap {
+  width: 100%;
+  height: 100%;
+  height: 518px;
+
+  // &:-webkit-full-screen {
+  //   width: 100% !important;
+  //   height: 100% !important;
+  // }
+
+  .videoContent {
+    height: 100%;
+
+    // &.fullScreen {
+    //   width: 100% !important;
+    //   height: 100% !important;
+    // }
+
+    // &:-webkit-full-screen {
+    //   width: 100% !important;
+    //   height: 100% !important;
+    // }
+  }
+
+
+  .controls {
+    border-radius: 0 0 16px 16px !important;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    width: 100%;
+    background: rgba(0, 0, 0, 0.6);
+    backdrop-filter: blur(26px);
+    height: 80px;
+    padding: 0 26px 0 26px !important;
+    transition: all 0.5s;
+    display: flex;
+    align-items: center;
+    transition: all .5s;
+
+    .time {
+      display: flex;
+      justify-content: space-between;
+      color: #fff;
+      padding: 4px 0 4px 12px;
+      font-size: max(24px, 14Px);
+      font-weight: 600;
+      line-height: 33px;
+      // min-width: 150px;
+      flex-shrink: 0;
+
+      .line {
+        font-size: 20px;
+      }
+
+      :global {
+        .plyr__time {
+          font-size: max(22px, 12Px);
+        }
+
+        .plyr__time+.plyr__time:before {
+          content: '';
+          margin-right: 0;
+        }
+      }
+    }
+  }
+
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    height: 100%;
+    color: #fff;
+    font-size: 12px;
+    align-items: center;
+    flex-shrink: 0;
+
+    .actionWrap {
+      display: flex;
+      align-items: center;
+    }
+
+    .actionBtn {
+      display: flex;
+      width: 40px;
+      height: 40px;
+      padding: 0;
+      background: transparent;
+      cursor: pointer;
+
+      &>img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+
+    .actionBtnSpeed {
+      position: relative;
+      width: 40px;
+      height: 40px;
+      background-color: transparent;
+      cursor: pointer;
+
+      &>img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+
+    .iconReplay {
+      width: 40px;
+      height: 40px;
+      background-color: transparent;
+      cursor: pointer;
+      margin: 0 22px;
+
+      &>img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .iconDownload {
+      width: 40px;
+      height: 40px;
+      margin-left: 12px;
+      background-color: transparent;
+      cursor: pointer;
+
+      &>img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+
+  .slider {
+    width: 100%;
+    padding: 0 0 0 12px;
+
+    :global {
+
+      .n-slider .n-slider-rail .n-slider-rail__fill,
+      .n-slider .n-slider-handles .n-slider-handle-wrapper {
+        transition: all .2s;
+      }
+    }
+  }
+
+  .sectionAnimate {
+    opacity: 0;
+    pointer-events: none;
+    transform: translateY(100%);
+    transition: all .5s;
+  }
+
+}
+
+.sliderPopup {
+  position: absolute;
+  z-index: 9999;
+  left: -13px;
+  bottom: 35px;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  height: 252px;
+  width: 59px;
+  padding: 12Px 0 15Px;
+  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAI4AAAJcCAMAAAAYSmw3AAAAaVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnbPKNAAAAI3RSTlOzAAUJiqaplqF6V0c4nSevpJN+GAZtX1EQGpqDPTQVc2QhqyTybJ0AAAKuSURBVHja7NRJVsJQAAXRRxMg9BwgGWX/2xT0KCKII5M/uHcFNaqMntm25+mm2uVf7KrN9NxuR8885ozX9Sw9mNXr8Z85p2WV3lTL0+uc1SK9Wqxe5HTz9G7e/ZbTZBDN05xJnYHUk8ec4yGDORx/5hz3GdD+eJ8zOWRQh8ldTp2B1d9zmgyuueV0KUD3lTNPAeafOasUYfWRc1qkCIvTe84yhVhec8ZVClGNLznrFGM9yvAHvKkvObMUYzbKNgXZpk1B2pxTkHOmKcg0mxRkk2ImeFVll4IUFQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAG3twIAAAAAAA5P/aCKqqqqqqqqqqqqqqqtJ+nSQ3CgVRFH2XjwCjHnWoCTXe/yJrUlEljCz7R9riD/Ks4E7yRaRzzjnnnHPOOeecc845zZWQuUZKyEhTJWSqXAnJtVFCNtopITvNlJCZKJSMAvGmZLwhJkrGBJElM4SjDEGtRNQgCJWSUAUQMFYSxoAASiWgBBCQxnFN/uWw1eC2/M+h0cAa7nNCqUGVoZPDaqEBLVZ0c9iXGky552MOodFAmkA/B64axBUe5jA56OUOE+6IjvFCL7UY0yG6Ql3oZYo68DwH1qfmXS/w3pzWfCQeyGb1Ja8K/ZKiyi/1LOMB8UTWFzcH4+wBnhCRsmNEDbFErPW3e3ZEE9FCrm+p6THkWHu29BlyjD1XIthyCEt9YUMEaw7tFz0XIthzaMvnz3YUew7tQZ86ZsSx53Ce6hP5mqcsOfE9eSCePYdVpQeWhhqEwX6hnrLFQFjcej2HFgthchupY3rGRNjcCt2pVtgIo1lxV7PHSFjtl/orP2Ml7E7HuTRvTtiJH9G2/AiRlD84lRqqt/0KYQAAAABJRU5ErkJggg==') no-repeat top center;
+  background-size: contain;
+
+  .iconAdd,
+  .iconCut {
+    display: inline-block;
+    width: 24Px;
+    height: 24Px;
+    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAABMlBMVEUAAAAkqP8ckv8zu/8op/81v/8ajv8elP82v/8rrv8elP4npv8yuf8elP8elf81v/8sr/8elf8vuv8dlv8srv8jn/8zu/8wtf82wP8oqP8npf8Zjf8mpP8Zjf8hmv8bj/40vP8wtP81wP8Zjf41vv8sr/8jnf8aj/0zuf8dlf81vv8rq/4kof4vsf8imP8aj/////8usv8qrP4jn/4inP4dlP4yuf8el/4srv8pqv4gmf4bkf40vf8ssP4wtP8lov4koP4xt/8npv4mpP4zu/8hmv4ajv4ckv4wtv4op/58x/5+y/6Cxv5/zf57xP6Dx/6BxP7u+P/t9/6a1f6Bz/56w/5iuP5zwP5Bpv4yoP6x3/6w3f6d2/6Wzv5tyf5pwv5nvv5svP5Rsv5Pr/5Mq/43p/49/opPAAAAMHRSTlMACoRW+Pf38dTUurGDWEhHR0cjI/b28fHr6+vr19fQ0Lq6sbGoqKiolZWGhoZSUlKk1yinAAAB+UlEQVQ4y43S6VbaQACG4S9E9h3c933fatISI4sWYxUjAVQQ99r2/m+hGQfGMJNEHw4M+XhPTn4AjpQNLM1MhUJTM0uBrAQ/qbXQmUNoLQUve3Nngrk9uMnHjlzF8hAkw0cewklwAr98BDBg47uvjYH72oNx1/WuHfdOFgqFu3qj8bfgiT13Pmy3DbNhtr3jcB5UTNe7DdM0f6s69dRqPemcGG13dV3t2G2737b3bW2+3gUxaxh/LHJfo+eWxLcGZxa2lKqqHcsyuyph2G8aq71rdpL/yar9tW5ZbypTJ3Fd5a0C0oiiKG9W51hhaKzwRiRkFeJfVRFiQRbbx4IaiWvivo14RUBjcY9julKlKtXXGvVA4ofexWuV/T6NyWrf9b6raxZMYuhnX8s9brFgyBE33eOmI5646PN6DBZMIHpIXJCPl0vqnjT3vYuXj9+jiB8KLkl8Je5xbB0Irt5jcd9C5utxBtKwph1oBDtprPH7sASsaFpJs5XYSeMSv68AyJR4NyS+EeYMbNFvHBrzaxTEDj8/k/iZX3fwboGby4/N5mOZGxdA5cbKnxrLoUcunxMn596nDCZx8okEHNZ/+FrHgIRfmwBHHj31MCpDkJsvntqvov0eOOdzcCNHioKIDC/p5aCzDC6n4UdKby5GxoPB8cjiZlrCoP+meld2tFTGwgAAAABJRU5ErkJggg==') no-repeat center;
+    background-size: contain;
+    flex-shrink: 0;
+    cursor: pointer;
+
+    &.disabled {
+      opacity: 0.7;
+    }
+  }
+
+  .iconCut {
+    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAABGlBMVEUAAAAkqP8zu/8op/8ajv8aj/8yuP8elP4npv4dlP8elP8yuP8elP81v/8sr/8jnf8Zjf81v/81v/8jn/8zu/8wtf8gl/82wP8oqP8npf83wP8qqv8mpP8Zjf81vf8tsv8hmv8bj/41wP8qqv8lof8Zjf41vv8sr/8jnf8aj/0zuf8dlf8bjf00vP8xt/8zvf8stv8dmf8dkv8srv8trf8qq/7///8tsP4jn/4us/8srv8gmf4lov4yuf8dlf4wt/8inf4hm/4ck/4bkf4wtP8koP4mpf4fl/4op/40vP8ajv4pqf4zuv98x/5+y/6Bxf40vv80u/6Dx/6a1f5owP5PsP4/p/5zwP4oo/4yoP7u+P7u9/5svP5it/4TfkURAAAANXRSTlMACoT49+66uoaDWFJSR0dHR/j29vHx8evr69fX19fQ0NDQsbGxsaioqKiVlYZYWCMjIyP29l0yDtwAAAGnSURBVDjLjc6HVsIwGAXg27I37r33nkilWloXCGK17OH7v4YpSKSQtPnS5v7n5J6cYIyUPt1bC/j9gbW907QEN+kjf2mE/ygNnvhWacJWHCzXoWem0DUmRAPPHIEoxkSeXETgcPJEaGSx88Rxr6ZpaqOtcUVARTWt0DBNs06yoBGTSd99NV8gXeKnwDV/hYGQqrZNoqerfCH0xVXVaNr3GqqbOGybhlG3LKunG642QaQMXW9altkmaRhk42UKwKGu6xXLquteDgFpRlGUutV8VbzMSEj1h0ZL8ZZC+FVYGLsPwnaxSudWrcJQa9HCKpbehqp3TFVaWIKPzl12uUsLPviKQx12uUMLPiwWi+XBzHsGOR9YxEqZqn0y1MrUCnYy5Uyfd+4gnBEWRlK8nIR0kxN0KwEHuVzWnrNeeQAgmRWUBLEh1t2ALXYvJIa+oEg3iIHLuRdPc5f4c/6Sz+fd/3NQct6DjBHHj66O4SC7dWWMOZv94Jg9w4SL4Ps3WeRzZvACLLH1d8Iu/Od6DDyJ/emvEdP7CbiREvL28sLU1MLytpyQ4PQLtc9vYI2HRk0AAAAASUVORK5CYII=') no-repeat center;
+    background-size: contain;
+  }
+
+  .sliderPoint {
+    background: #FFFFFF;
+    box-shadow: 0 2px 4px 0px rgba(102, 102, 102, 0.77);
+    border-radius: 14px;
+    font-size: 14Px;
+    font-weight: 500;
+    height: 22Px;
+    color: #198CFE;
+    min-width: 40px;
+    text-align: center;
+    vertical-align: text-bottom;
+
+    span {
+      font-size: 12Px;
+    }
+  }
+
+  :global {
+    .n-slider {
+      margin: 7px 0;
+      padding: 0;
+    }
+  }
+}
+
+// .videoWrap {
+//   width: 100%;
+//   height: 100%;
+//   --plyr-color-main: #198CFE;
+//   --plyr-range-track-height: 6px;
+//   --plyr-tooltip-radius: 3px;
+//   --plyr-range-thumb-height: 24px;
+//   --plyr-video-controls-background: #000;
+
+
+//   :global {
+//     .plyr--video {
+//       width: 100%;
+//       height: 100%;
+//     }
+
+//     .plyr__time {
+//       display: block !important;
+//     }
+
+//     .plyr__video-wrapper {
+//       pointer-events: none;
+//     }
+
+//   }
+// }
+
+// :global(.bottomFixed).controls {
+//   width: 100% !important;
+//   background: rgba(0, 0, 0, 0.6) !important;
+//   // backdrop-filter: blur(26px);
+//   height: 80px !important;
+//   min-height: 80px !important;
+//   padding: 0px 40px !important;
+//   z-index: 999;
+
+//   .time {
+//     display: flex;
+//     justify-content: space-between;
+//     color: #fff;
+//     padding: 4px 12px 4px;
+//     font-size: 24px;
+//     font-weight: 600;
+//     line-height: 33px;
+//     min-width: 140px;
+
+//     .line {
+//       font-size: 12px;
+//     }
+
+//     :global {
+//       .plyr__time+.plyr__time:before {
+//         content: '';
+//         margin-right: 0;
+//       }
+//     }
+//   }
+
+//   .slider {
+//     width: 100%;
+//     padding: 0 20px 0 12px;
+
+//     :global {
+//       .van-slider__button {
+//         background: var(--van-primary);
+//       }
+
+//       .van-loading {
+//         width: 100%;
+//         height: 100%;
+//       }
+//     }
+//   }
+
+//   .actions {
+//     display: flex;
+//     justify-content: space-between;
+//     color: #fff;
+//     font-size: 12px;
+//     align-items: center;
+
+//     .actionWrap {
+//       display: flex;
+//     }
+
+//     .actionBtn {
+//       display: flex;
+
+//       padding: 0;
+//       width: 52px;
+//       height: 52px;
+//       background: transparent;
+//     }
+
+//     .actionBtn>img {
+//       width: 100%;
+//       height: 100%;
+//     }
+
+//     :global {
+//       .van-loading__circular {
+//         width: 100%;
+//         height: 100%;
+//       }
+//     }
+
+//     .playIcon {
+//       display: none;
+//     }
+
+//     .btnPlay img:nth-child(2) {
+//       display: block;
+//     }
+
+//     .btnPause img:nth-child(3) {
+//       display: block;
+//     }
+
+//     .btnPlay,
+//     .btnPause {
+//       :global {
+//         .van-loading {
+//           display: none;
+//         }
+//       }
+//     }
+
+//     .loopBtn {
+//       background-color: transparent;
+//       width: 31px !important;
+//       height: 29px !important;
+//       padding: 0;
+//       cursor: pointer;
+
+//       :global {
+//         .loop {
+//           display: block;
+//         }
+
+//         .loopActive {
+//           display: none;
+//         }
+//       }
+//     }
+//   }
+// }

+ 354 - 306
src/components/card-preview/video-modal/index.tsx

@@ -1,306 +1,354 @@
-import { defineComponent, nextTick, onMounted, reactive, toRefs } from 'vue';
-// import 'plyr/dist/plyr.css';
-// import Plyr from 'plyr';
-import { ref } from 'vue';
-import TCPlayer from 'tcplayer.js';
-import 'tcplayer.js/dist/tcplayer.min.css';
-import styles from './index.module.less';
-import iconplay from '@views/attend-class/image/icon-pause.png';
-import iconpause from '@views/attend-class/image/icon-play.png';
-import iconReplay from '@views/attend-class/image/icon-replay.png';
-import iconPreviewDownload from '@views/attend-class/image/icon-preivew-download.png';
-import iconSpeed from '@views/attend-class/image/icon-speed.png';
-import { NSlider, useMessage } from 'naive-ui';
-import { saveAs } from 'file-saver';
-
-export default defineComponent({
-  name: 'video-play',
-  props: {
-    src: {
-      type: String,
-      default: ''
-    },
-    title: {
-      type: String,
-      default: ''
-    },
-    poster: {
-      type: String,
-      default: ''
-    },
-    isEmtry: {
-      type: Boolean,
-      default: false
-    },
-    isDownload: {
-      type: Boolean,
-      default: false
-    }
-  },
-  emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
-  setup(props, { emit, expose }) {
-    const message = useMessage();
-    const { src, poster, isEmtry } = toRefs(props);
-    const videoFroms = reactive({
-      paused: true,
-      currentTimeNum: 0,
-      currentTime: '00:00',
-      durationNum: 0,
-      duration: '00:00',
-      showBar: true,
-      speedControl: false,
-      speedStyle: {
-        left: '1px'
-      },
-      defaultSpeed: 1 // 默认速度
-    });
-    const videoRef = ref();
-    const videoItem = ref();
-    const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100);
-
-    // 对时间进行格式化
-    const timeFormat = (num: number) => {
-      if (num > 0) {
-        const m = Math.floor(num / 60);
-        const s = num % 60;
-        return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
-      } else {
-        return '00:00';
-      }
-    };
-
-    //
-    const toggleHideControl = (isShow: false) => {
-      videoFroms.showBar = isShow;
-      videoFroms.speedControl = false;
-    };
-
-    const onReplay = () => {
-      videoFroms.speedControl = false;
-      if (!videoItem.value) return;
-      videoItem.value.currentTime(0);
-    };
-
-    // 切换音频播放
-    const onToggleVideo = (e?: MouseEvent) => {
-      videoFroms.speedControl = false;
-      e?.stopPropagation();
-      if (videoFroms.paused) {
-        videoItem.value.play();
-        videoFroms.paused = false;
-      } else {
-        videoItem.value.pause();
-        videoFroms.paused = true;
-      }
-      emit('togglePlay', videoFroms.paused);
-    };
-
-    // 下载资源
-    const onDownload = () => {
-      if (!props.src) {
-        message.error('下载失败');
-        return;
-      }
-      const fileUrl = props.src;
-      // 发起Fetch请求
-      fetch(fileUrl)
-        .then(response => response.blob())
-        .then(blob => {
-          saveAs(blob, props.title || new Date().getTime() + '');
-        })
-        .catch(() => {
-          message.error('下载失败');
-        });
-    };
-
-    const __init = () => {
-      if (videoItem.value) {
-        videoItem.value.poster(poster.value); // 封面
-        videoItem.value.src(isEmtry.value ? '' : src.value); // url 播放地址
-
-        // 初步加载时
-        videoItem.value.one('loadedmetadata', () => {
-          console.log(' Loading metadata');
-
-          // 获取时长
-          videoFroms.duration = timeFormat(
-            Math.round(videoItem.value.duration())
-          );
-          videoFroms.durationNum = videoItem.value.duration();
-
-          emit('loadedmetadata', videoItem.value);
-        });
-
-        // 视频播放时加载
-        videoItem.value.on('timeupdate', () => {
-          videoFroms.currentTime = timeFormat(
-            Math.round(videoItem.value?.currentTime() || 0)
-          );
-          videoFroms.currentTimeNum = videoItem.value.currentTime();
-        });
-
-        // 视频播放结束
-        videoItem.value.on('ended', () => {
-          videoFroms.paused = true;
-          emit('ended');
-        });
-      }
-    };
-
-    onMounted(() => {
-      videoItem.value = TCPlayer(videoID, {
-        appID: '',
-        controls: false
-      }); // player-container-id 为播放器容器 ID,必须与 html 中一致
-      __init();
-    });
-    expose({
-      // changePlayBtn,
-      toggleHideControl
-    });
-    return () => (
-      <div class={styles.videoWrap}>
-        <video
-          style={{ width: '100%', height: '100%' }}
-          src={isEmtry.value ? '' : src.value}
-          poster={poster.value}
-          ref={videoRef}
-          id={videoID}
-          preload="auto"
-          playsinline
-          webkit-playsinline></video>
-
-        <div
-          class={[
-            styles.controls,
-            videoFroms.showBar ? '' : styles.sectionAnimate
-          ]}
-          onClick={(e: MouseEvent) => {
-            e.stopPropagation();
-            emit('reset');
-          }}>
-          <div class={styles.actions}>
-            <div class={styles.actionWrap}>
-              <button class={styles.actionBtn} onClick={onToggleVideo}>
-                {videoFroms.paused ? (
-                  <img class={styles.playIcon} src={iconplay} />
-                ) : (
-                  <img class={styles.playIcon} src={iconpause} />
-                )}
-              </button>
-
-              <button class={styles.iconReplay} onClick={onReplay}>
-                <img src={iconReplay} />
-              </button>
-
-              <div
-                class={styles.actionBtnSpeed}
-                onClick={() => {
-                  videoFroms.speedControl = !videoFroms.speedControl;
-                }}>
-                <img src={iconSpeed} />
-
-                <div
-                  style={{
-                    display: videoFroms.speedControl ? 'block' : 'none'
-                  }}>
-                  <div
-                    class={styles.sliderPopup}
-                    onClick={(e: Event) => {
-                      e.stopPropagation();
-                    }}>
-                    <i
-                      class={styles.iconAdd}
-                      onClick={() => {
-                        if (videoFroms.defaultSpeed >= 1.5) {
-                          return;
-                        }
-
-                        if (videoItem.value) {
-                          videoFroms.defaultSpeed =
-                            (videoFroms.defaultSpeed * 10 + 1) / 10;
-                          videoItem.value.playbackRate(videoFroms.defaultSpeed);
-                        }
-                      }}></i>
-                    <NSlider
-                      value={videoFroms.defaultSpeed}
-                      step={0.1}
-                      max={1.5}
-                      min={0.5}
-                      vertical
-                      tooltip={false}
-                      onUpdate:value={(val: number) => {
-                        videoFroms.defaultSpeed = val;
-                        if (videoItem.value) {
-                          videoItem.value.playbackRate(videoFroms.defaultSpeed);
-                        }
-                      }}>
-                      {{
-                        thumb: () => (
-                          <div class={styles.sliderPoint}>
-                            {videoFroms.defaultSpeed}
-                            <span>x</span>
-                          </div>
-                        )
-                      }}
-                    </NSlider>
-                    <i
-                      class={[styles.iconCut]}
-                      onClick={() => {
-                        if (videoFroms.defaultSpeed <= 0.5) {
-                          return;
-                        }
-                        if (videoItem.value) {
-                          videoFroms.defaultSpeed =
-                            (videoFroms.defaultSpeed * 10 - 1) / 10;
-                          videoItem.value.playbackRate(videoFroms.defaultSpeed);
-                        }
-                      }}></i>
-                  </div>
-                </div>
-              </div>
-            </div>
-          </div>
-
-          <div class={styles.slider}>
-            <NSlider
-              value={videoFroms.currentTimeNum}
-              step={0.01}
-              max={videoFroms.durationNum}
-              tooltip={false}
-              onUpdate:value={(val: number) => {
-                videoFroms.speedControl = false;
-                videoItem.value.currentTime(val);
-                videoFroms.currentTimeNum = val;
-                videoFroms.currentTime = timeFormat(Math.round(val || 0));
-              }}
-            />
-          </div>
-
-          <div class={styles.actions}>
-            <div class={styles.time}>
-              <div
-                class="plyr__time plyr__time--current"
-                aria-label="Current time">
-                {videoFroms.currentTime}
-              </div>
-              <span class={styles.line}>/</span>
-              <div
-                class="plyr__time plyr__time--duration"
-                aria-label="Duration">
-                {videoFroms.duration}
-              </div>
-            </div>
-            <div class={styles.actionWrap}>
-              {props.isDownload && (
-                <button class={styles.iconDownload} onClick={onDownload}>
-                  <img src={iconPreviewDownload} />
-                </button>
-              )}
-            </div>
-          </div>
-        </div>
-      </div>
-    );
-  }
-});
+import { defineComponent, nextTick, onMounted, reactive, toRefs } from 'vue';
+// import 'plyr/dist/plyr.css';
+// import Plyr from 'plyr';
+import { ref } from 'vue';
+import TCPlayer from 'tcplayer.js';
+import 'tcplayer.js/dist/tcplayer.min.css';
+import styles from './index.module.less';
+import iconplay from '@views/attend-class/image/icon-pause.png';
+import iconpause from '@views/attend-class/image/icon-play.png';
+import iconReplay from '@views/attend-class/image/icon-replay.png';
+import iconPreviewDownload from '@views/attend-class/image/icon-preivew-download.png';
+import iconFullscreen from '@views/attend-class/image/icon-fullscreen.png';
+import iconSpeed from '@views/attend-class/image/icon-speed.png';
+import { NSlider, useMessage } from 'naive-ui';
+import { saveAs } from 'file-saver';
+import { exitFullscreen } from '/src/utils';
+
+export default defineComponent({
+  name: 'video-play',
+  props: {
+    src: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    poster: {
+      type: String,
+      default: ''
+    },
+    isEmtry: {
+      type: Boolean,
+      default: false
+    },
+    isDownload: {
+      type: Boolean,
+      default: false
+    },
+    fullscreen: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
+  setup(props, { emit, expose }) {
+    const message = useMessage();
+    const videoId =
+      'vFullscreen' + Date.now() + Math.floor(Math.random() * 100);
+    const { src, poster, isEmtry } = toRefs(props);
+    const videoFroms = reactive({
+      paused: true,
+      currentTimeNum: 0,
+      currentTime: '00:00',
+      durationNum: 0,
+      duration: '00:00',
+      showBar: true,
+      speedControl: false,
+      speedStyle: {
+        left: '1px'
+      },
+      defaultSpeed: 1 // 默认速度
+    });
+    const videoRef = ref();
+    const videoItem = ref();
+    const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100);
+
+    // 对时间进行格式化
+    const timeFormat = (num: number) => {
+      if (num > 0) {
+        const m = Math.floor(num / 60);
+        const s = num % 60;
+        return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
+      } else {
+        return '00:00';
+      }
+    };
+
+    //
+    const toggleHideControl = (isShow: false) => {
+      videoFroms.showBar = isShow;
+      videoFroms.speedControl = false;
+    };
+
+    const onReplay = () => {
+      videoFroms.speedControl = false;
+      if (!videoItem.value) return;
+      videoItem.value.currentTime(0);
+    };
+
+    // 切换音频播放
+    const onToggleVideo = (e?: MouseEvent) => {
+      videoFroms.speedControl = false;
+      e?.stopPropagation();
+      if (videoFroms.paused) {
+        videoItem.value.play();
+        videoFroms.paused = false;
+      } else {
+        videoItem.value.pause();
+        videoFroms.paused = true;
+      }
+      emit('togglePlay', videoFroms.paused);
+    };
+
+    // 下载资源
+    const onDownload = () => {
+      if (!props.src) {
+        message.error('下载失败');
+        return;
+      }
+      const fileUrl = props.src;
+      // 发起Fetch请求
+      fetch(fileUrl)
+        .then(response => response.blob())
+        .then(blob => {
+          saveAs(blob, props.title || new Date().getTime() + '');
+        })
+        .catch(() => {
+          message.error('下载失败');
+        });
+    };
+
+    const __init = () => {
+      if (videoItem.value) {
+        videoItem.value.poster(poster.value); // 封面
+        videoItem.value.src(isEmtry.value ? '' : src.value); // url 播放地址
+
+        // 初步加载时
+        videoItem.value.one('loadedmetadata', () => {
+          console.log(' Loading metadata');
+
+          // 获取时长
+          videoFroms.duration = timeFormat(
+            Math.round(videoItem.value.duration())
+          );
+          videoFroms.durationNum = videoItem.value.duration();
+
+          emit('loadedmetadata', videoItem.value);
+        });
+
+        // 视频播放时加载
+        videoItem.value.on('timeupdate', () => {
+          videoFroms.currentTime = timeFormat(
+            Math.round(videoItem.value?.currentTime() || 0)
+          );
+          videoFroms.currentTimeNum = videoItem.value.currentTime();
+        });
+
+        // 视频播放结束
+        videoItem.value.on('ended', () => {
+          videoFroms.paused = true;
+          emit('ended');
+        });
+      }
+    };
+
+    /** 全屏 */
+    const isElementFullscreen = (element: any) => {
+      const dom: any = document;
+      return (
+        element ===
+        (dom.fullscreenElement ||
+          dom.webkitFullscreenElement ||
+          dom.mozFullScreenElement ||
+          dom.msFullscreenElement)
+      );
+    };
+    const onFullScreen = () => {
+      const el: any = document.querySelector('#' + videoId);
+
+      //   //进入全屏
+      if (el) {
+        if (isElementFullscreen(el)) {
+          exitFullscreen();
+        } else {
+          (el.requestFullscreen && el.requestFullscreen()) ||
+            (el.mozRequestFullScreen && el.mozRequestFullScreen()) ||
+            (el.webkitRequestFullscreen && el.webkitRequestFullscreen()) ||
+            (el.msRequestFullscreen && el.msRequestFullscreen());
+        }
+      }
+    };
+
+    onMounted(() => {
+      videoItem.value = TCPlayer(videoID, {
+        appID: '',
+        controls: false
+      }); // player-container-id 为播放器容器 ID,必须与 html 中一致
+      __init();
+    });
+    expose({
+      // changePlayBtn,
+      toggleHideControl
+    });
+    return () => (
+      <div class={[styles.videoWrap]} id={videoId}>
+        <div class={[styles.videoContent]}>
+          <video
+            style={{ width: '100%', height: '100%' }}
+            src={isEmtry.value ? '' : src.value}
+            poster={poster.value}
+            ref={videoRef}
+            id={videoID}
+            preload="auto"
+            playsinline
+            webkit-playsinline></video>
+
+          <div
+            class={[
+              styles.controls,
+              videoFroms.showBar ? '' : styles.sectionAnimate
+            ]}
+            onClick={(e: MouseEvent) => {
+              e.stopPropagation();
+              emit('reset');
+            }}>
+            <div class={styles.actions}>
+              <div class={styles.actionWrap}>
+                <button class={styles.actionBtn} onClick={onToggleVideo}>
+                  {videoFroms.paused ? (
+                    <img class={styles.playIcon} src={iconplay} />
+                  ) : (
+                    <img class={styles.playIcon} src={iconpause} />
+                  )}
+                </button>
+
+                <button class={styles.iconReplay} onClick={onReplay}>
+                  <img src={iconReplay} />
+                </button>
+
+                <div
+                  class={styles.actionBtnSpeed}
+                  onClick={() => {
+                    videoFroms.speedControl = !videoFroms.speedControl;
+                  }}>
+                  <img src={iconSpeed} />
+
+                  <div
+                    style={{
+                      display: videoFroms.speedControl ? 'block' : 'none'
+                    }}>
+                    <div
+                      class={styles.sliderPopup}
+                      onClick={(e: Event) => {
+                        e.stopPropagation();
+                      }}>
+                      <i
+                        class={styles.iconAdd}
+                        onClick={() => {
+                          if (videoFroms.defaultSpeed >= 1.5) {
+                            return;
+                          }
+
+                          if (videoItem.value) {
+                            videoFroms.defaultSpeed =
+                              (videoFroms.defaultSpeed * 10 + 1) / 10;
+                            videoItem.value.playbackRate(
+                              videoFroms.defaultSpeed
+                            );
+                          }
+                        }}></i>
+                      <NSlider
+                        value={videoFroms.defaultSpeed}
+                        step={0.1}
+                        max={1.5}
+                        min={0.5}
+                        vertical
+                        tooltip={false}
+                        onUpdate:value={(val: number) => {
+                          videoFroms.defaultSpeed = val;
+                          if (videoItem.value) {
+                            videoItem.value.playbackRate(
+                              videoFroms.defaultSpeed
+                            );
+                          }
+                        }}>
+                        {{
+                          thumb: () => (
+                            <div class={styles.sliderPoint}>
+                              {videoFroms.defaultSpeed}
+                              <span>x</span>
+                            </div>
+                          )
+                        }}
+                      </NSlider>
+                      <i
+                        class={[styles.iconCut]}
+                        onClick={() => {
+                          if (videoFroms.defaultSpeed <= 0.5) {
+                            return;
+                          }
+                          if (videoItem.value) {
+                            videoFroms.defaultSpeed =
+                              (videoFroms.defaultSpeed * 10 - 1) / 10;
+                            videoItem.value.playbackRate(
+                              videoFroms.defaultSpeed
+                            );
+                          }
+                        }}></i>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div class={styles.slider}>
+              <NSlider
+                value={videoFroms.currentTimeNum}
+                step={0.01}
+                max={videoFroms.durationNum}
+                tooltip={false}
+                onUpdate:value={(val: number) => {
+                  videoFroms.speedControl = false;
+                  videoItem.value.currentTime(val);
+                  videoFroms.currentTimeNum = val;
+                  videoFroms.currentTime = timeFormat(Math.round(val || 0));
+                }}
+              />
+            </div>
+
+            <div class={styles.actions}>
+              <div class={styles.time}>
+                <div
+                  class="plyr__time plyr__time--current"
+                  aria-label="Current time">
+                  {videoFroms.currentTime}
+                </div>
+                <span class={styles.line}>/</span>
+                <div
+                  class="plyr__time plyr__time--duration"
+                  aria-label="Duration">
+                  {videoFroms.duration}
+                </div>
+              </div>
+              <div class={styles.actionWrap}>
+                {props.isDownload && (
+                  <button class={styles.iconDownload} onClick={onDownload}>
+                    <img src={iconPreviewDownload} />
+                  </button>
+                )}
+                {props.fullscreen && (
+                  <button class={styles.iconDownload} onClick={onFullScreen}>
+                    <img src={iconFullscreen} />
+                  </button>
+                )}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 536 - 517
src/components/card-type/index.tsx

@@ -1,517 +1,536 @@
-import { PropType, Transition, defineComponent, ref } from 'vue';
-import styles from './index.module.less';
-import { NButton, NCard, NImage, NModal, NSpin, useMessage } from 'naive-ui';
-import iconImage from '@common/images/icon-image.png';
-import iconVideo from '@common/images/icon-video.png';
-import iconAudio from '@common/images/icon-audio.png';
-import iconMusic from '@common/images/icon-music.png';
-import iconPPT from '@common/images/icon-ppt.png';
-import iconOther from '@common/images/icon-other.png';
-import iconCollectDefault from '@common/images/icon-collect-default.png';
-import iconCollectActive from '@common/images/icon-collect-active.png';
-import iconDownload from '@common/images/icon-download.png';
-import TheNoticeBar from '../TheNoticeBar';
-import AudioPlayer from './audio-player';
-import VideoPlayer from './video-player';
-import { PageEnum } from '/src/enums/pageEnum';
-import { api_musicSheetDetail } from '/src/api/user';
-import JSZip, { file } from 'jszip';
-import { saveAs } from 'file-saver';
-
-// LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家)
-type itemType = {
-  id: string | number;
-  type:
-    | 'IMG'
-    | 'VIDEO'
-    | 'SONG'
-    | 'MUSIC'
-    | 'PPT'
-    | 'LISTEN'
-    | 'RHYTHM'
-    | 'THEORY'
-    | 'MUSIC_WIKI'
-    | 'INSTRUMENT'
-    | 'MUSICIAN';
-  coverImg: string;
-  content?: string;
-  title: string;
-  isCollect: boolean;
-  isSelected: boolean; // 精选
-  exist?: boolean; // 是否已经选
-};
-
-export default defineComponent({
-  name: 'card-type',
-  props: {
-    // 是否是选中状态
-    isActive: {
-      type: Boolean,
-      default: false
-    },
-    /** 是否可以拖拽 */
-    draggable: {
-      type: Boolean,
-      default: false
-    },
-    // 是否可以收藏
-    isCollect: {
-      type: Boolean,
-      default: true
-    },
-    // 是否显示收藏
-    isShowCollect: {
-      type: Boolean,
-      default: true
-    },
-    // 是否显示添加按钮
-    isShowAdd: {
-      type: Boolean,
-      default: false
-    },
-    // 是否禁用添加按钮
-    isShowAddDisabled: {
-      type: Boolean,
-      default: false
-    },
-    // 鼠标移动上面的时候是否自动播放,或者可以点击
-    disabledMouseHover: {
-      type: Boolean,
-      default: true
-    },
-    // 是否预览
-    isPreview: {
-      type: Boolean,
-      default: true
-    },
-    item: {
-      type: Object as PropType<itemType>,
-      default: () => ({})
-    },
-    /** 是否下架 */
-    offShelf: {
-      type: Boolean,
-      default: false
-    },
-    /** 是否可以下载 */
-    isDownload: {
-      type: Boolean,
-      default: false
-    }
-  },
-  /**
-   * @type {string} click 点击事件
-   * @type {string} collect 收藏
-   * @type {string} add 添加
-   * @type {string} offShelf 下架
-   */
-  emits: ['click', 'collect', 'add', 'offShelf'],
-  setup(props, { emit }) {
-    const message = useMessage();
-    const isAnimation = ref(false);
-    const downloadStatus = ref(false);
-    const formatType = (type: string) => {
-      let typeImg = iconOther;
-      switch (type) {
-        case 'IMG':
-          typeImg = iconImage;
-          break;
-        case 'VIDEO':
-          typeImg = iconVideo;
-          break;
-        case 'SONG':
-          typeImg = iconAudio;
-          break;
-        case 'MUSIC':
-          typeImg = iconMusic;
-          break;
-        case 'PPT':
-          typeImg = iconPPT;
-          break;
-      }
-      return typeImg;
-    };
-
-    // 获取文件blob格式
-    const getFileBlob = (url: string) => {
-      return new Promise((resolve, reject) => {
-        const request = new XMLHttpRequest();
-        request.open('GET', url, true);
-        request.responseType = 'blob';
-        request.onload = (res: any) => {
-          if (res.target.status == 200) {
-            resolve(res.target.response);
-          } else {
-            reject(res);
-          }
-        };
-        request.send();
-      });
-    };
-
-    // 多个文件下载
-    const downLoadMultiFile = (files: any, filesName: string) => {
-      const zip = new JSZip();
-      const result = [];
-      for (const i in files) {
-        const promise = getFileBlob(files[i].url).then((res: any) => {
-          zip.file(files[i].name, res, { binary: true });
-        });
-        result.push(promise);
-      }
-      Promise.all(result)
-        .then(() => {
-          zip.generateAsync({ type: 'blob' }).then(res => {
-            saveAs(
-              res,
-              filesName
-                ? filesName + Date.now() + '.zip'
-                : `文件夹${Date.now()}.zip`
-            );
-          });
-        })
-        .catch(() => {
-          message.error('下载失败');
-        });
-
-      downloadStatus.value = false;
-    };
-
-    const downloadFile = (filename: string, fileUrl: string) => {
-      // 发起Fetch请求
-      fetch(fileUrl)
-        .then(response => response.blob())
-        .then(blob => {
-          saveAs(blob, filename);
-          setTimeout(() => {
-            downloadStatus.value = false;
-          }, 100);
-        })
-        .catch(() => {
-          message.error('下载失败');
-        });
-
-      downloadStatus.value = false;
-    };
-
-    const getFileName = (url: any) => {
-      // 使用正则表达式获取文件名
-      const tempUrl = url.split('?');
-      const fileNameRegex = /\/([^\\/]+)$/; // 匹配最后一个斜杠后的内容
-      const match = tempUrl[0].match(fileNameRegex);
-
-      if (match) {
-        return match[1];
-      } else {
-        return '';
-      }
-    };
-    const onDownload = async (e: MouseEvent) => {
-      e.stopPropagation();
-      e.preventDefault();
-      const item = props.item;
-      if (!item.content) {
-        message.error('下载失败');
-        return;
-      }
-      if (downloadStatus.value) return false;
-      downloadStatus.value = true;
-      const suffix: any = item.content?.split('.');
-      const fileName = item.title + '.' + suffix[suffix?.length - 1];
-      if (item.type === 'MUSIC') {
-        const { data } = await api_musicSheetDetail(item.content);
-        const urls = [];
-        if (data.xmlFileUrl) {
-          urls.push({
-            url: data.xmlFileUrl,
-            name: getFileName(data.xmlFileUrl)
-          });
-        }
-        if (data.background && data.background.length > 0) {
-          data.background.forEach((item: any) => {
-            urls.push({
-              url: item.audioFileUrl,
-              name: getFileName(item.audioFileUrl)
-            });
-          });
-        }
-        downLoadMultiFile(urls, item.title);
-
-        // setTimeout(() => {
-        //   downloadStatus.value = false;
-        // }, 1000);
-      } else {
-        downloadFile(fileName, item.content);
-      }
-    };
-
-    return () => (
-      <div
-        onClick={() => emit('click', props.item)}
-        key={props.item.id}
-        draggable={!props.draggable ? false : props.item.exist ? false : true}
-        class={[
-          styles['card-section'],
-          'card-section-container',
-          !props.draggable ? '' : props.item.exist ? '' : styles.cardDrag
-        ]}
-        onMouseenter={() => {
-          isAnimation.value = true;
-        }}
-        onMouseleave={() => {
-          isAnimation.value = false;
-        }}
-        onDragstart={(e: any) => {
-          console.log('dragstart', Date.now());
-          e.dataTransfer.setData('text', JSON.stringify(props.item));
-        }}>
-        {/* 判断是否下架 */}
-        {props.offShelf && (
-          <div class={styles.offShelfBg}>
-            <p class={styles.offShelfTips}>该资源已被下架</p>
-            <NButton
-              type="primary"
-              class={styles.offShelfBtn}
-              onClick={(e: MouseEvent) => {
-                e.stopPropagation();
-                emit('offShelf');
-              }}>
-              确认
-            </NButton>
-          </div>
-        )}
-        <NCard
-          class={[
-            styles['card-section-content'],
-            props.isShowAdd ? '' : styles.course,
-            props.isActive ? styles.isActive : '',
-            props.item.exist ? styles.showAddBtn : '' // 是否已添加
-          ]}
-          style={{ cursor: 'pointer' }}>
-          {{
-            cover: () => (
-              <>
-                {/* 图片 */}
-                {props.item.type === 'IMG' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={props.disabledMouseHover}
-                    objectFit="cover"
-                    src={props.item.coverImg}
-                    previewSrc={props.item.content}
-                  />
-                )}
-                {/* 乐谱 */}
-                {props.item.type === 'MUSIC' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="contain"
-                    src={props.item.coverImg}
-                  />
-                )}
-                {/* 音频 */}
-                {props.item.type === 'SONG' && (
-                  <AudioPlayer
-                    content={props.item.content}
-                    cover={props.item.coverImg}
-                    previewDisabled={props.disabledMouseHover}
-                  />
-                )}
-                {/* 视频 */}
-                {props.item.type === 'VIDEO' && (
-                  <VideoPlayer
-                    cover={props.item.coverImg}
-                    content={props.item.content}
-                    previewDisabled={props.disabledMouseHover}
-                  />
-                )}
-                {/* ppt */}
-                {props.item.type === 'PPT' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg || PageEnum.PPT_DEFAULT_COVER}
-                  />
-                )}
-                {/* 节奏练习 */}
-                {props.item.type === 'RHYTHM' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg || PageEnum.RHYTHM_DEFAULT_COVER}
-                  />
-                )}
-
-                {/* 听音练习 */}
-                {props.item.type === 'LISTEN' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg}
-                  />
-                )}
-                {/* 乐理 */}
-                {props.item.type === 'THEORY' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg || PageEnum.THEORY_DEFAULT_COVER}
-                  />
-                )}
-                {/* 名曲 */}
-                {props.item.type === 'MUSIC_WIKI' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg || PageEnum.MUSIC_DEFAULT_COVER}
-                  />
-                )}
-                {/* 乐器 */}
-                {props.item.type === 'INSTRUMENT' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={
-                      props.item.coverImg || PageEnum.INSTRUMENT_DEFAULT_COVER
-                    }
-                  />
-                )}
-                {/* 音乐家 */}
-                {props.item.type === 'MUSICIAN' && (
-                  <NImage
-                    class={[styles.cover, styles.image]}
-                    lazy
-                    previewDisabled={true}
-                    objectFit="cover"
-                    src={props.item.coverImg || PageEnum.MUSICIAN_DEFAULT_COVER}
-                  />
-                )}
-              </>
-            ),
-            footer: () => (
-              <div class={styles.footer}>
-                <div class={[styles.title, 'footerTitle']}>
-                  <NImage
-                    class={[styles.titleType]}
-                    src={formatType(props.item.type)}
-                    objectFit="cover"
-                  />
-                  <span class={[styles.titleContent, 'titleContent']}>
-                    <TheNoticeBar
-                      isAnimation={isAnimation.value}
-                      text={props.item.title}
-                    />
-                  </span>
-                </div>
-                {/* 收藏 */}
-                <div class={styles.btnGroup}>
-                  {props.isDownload && (
-                    <div class={styles.btnItem} onClick={onDownload}>
-                      <NSpin show={downloadStatus.value} size={'small'}>
-                        <img
-                          src={iconDownload}
-                          key="3"
-                          class={[styles.iconCollect]}
-                        />
-                      </NSpin>
-                    </div>
-                  )}
-                  {props.isShowCollect && (
-                    <div
-                      class={[styles.iconCollect, styles.btnItem]}
-                      onClick={(e: MouseEvent) => {
-                        e.stopPropagation();
-                        e.preventDefault();
-                        // 判断是否可以收藏
-                        if (props.isCollect) {
-                          emit('collect', props.item);
-                        }
-                      }}>
-                      <Transition name="favitor" mode="out-in">
-                        {props.item.isCollect ? (
-                          <img
-                            src={iconCollectActive}
-                            key="1"
-                            class={[
-                              styles.iconCollect,
-                              props.isCollect ? styles.isCollect : ''
-                            ]}
-                          />
-                        ) : (
-                          <img
-                            src={iconCollectDefault}
-                            key="2"
-                            class={[
-                              styles.iconCollect,
-                              props.isCollect ? styles.isCollect : ''
-                            ]}
-                          />
-                        )}
-                      </Transition>
-                    </div>
-                  )}
-                </div>
-
-                {/* 精选 */}
-                {props.item.isSelected && (
-                  <span class={styles.iconSelected}></span>
-                )}
-
-                {/* 添加按钮 */}
-                {props.isShowAdd &&
-                  (props.item.exist ? (
-                    <NButton
-                      type="primary"
-                      class={[
-                        styles.addBtn,
-                        props.item.exist ? styles.addBtnDisabled : ''
-                      ]}
-                      disabled={props.item.exist || props.isShowAddDisabled}
-                      onClick={(e: MouseEvent) => {
-                        e.stopPropagation();
-                        e.preventDefault();
-                        emit('add', props.item);
-                      }}>
-                      {props.item.exist ? '已添加' : '添加'}
-                    </NButton>
-                  ) : (
-                    !props.isShowAddDisabled && (
-                      <NButton
-                        type="primary"
-                        class={[
-                          styles.addBtn,
-                          props.item.exist ? styles.addBtnDisabled : ''
-                        ]}
-                        disabled={props.item.exist || props.isShowAddDisabled}
-                        onClick={(e: MouseEvent) => {
-                          e.stopPropagation();
-                          e.preventDefault();
-                          emit('add', props.item);
-                        }}>
-                        {props.item.exist ? '已添加' : '添加'}
-                      </NButton>
-                    )
-                  ))}
-              </div>
-            )
-          }}
-        </NCard>
-      </div>
-    );
-  }
-});
+import { PropType, Transition, defineComponent, ref } from 'vue';
+import styles from './index.module.less';
+import {
+  ImageRenderToolbarProps,
+  NButton,
+  NCard,
+  NImage,
+  NModal,
+  NSpin,
+  useMessage
+} from 'naive-ui';
+import iconImage from '@common/images/icon-image.png';
+import iconVideo from '@common/images/icon-video.png';
+import iconAudio from '@common/images/icon-audio.png';
+import iconMusic from '@common/images/icon-music.png';
+import iconPPT from '@common/images/icon-ppt.png';
+import iconOther from '@common/images/icon-other.png';
+import iconCollectDefault from '@common/images/icon-collect-default.png';
+import iconCollectActive from '@common/images/icon-collect-active.png';
+import iconDownload from '@common/images/icon-download.png';
+import TheNoticeBar from '../TheNoticeBar';
+import AudioPlayer from './audio-player';
+import VideoPlayer from './video-player';
+import { PageEnum } from '/src/enums/pageEnum';
+import { api_musicSheetDetail } from '/src/api/user';
+import JSZip, { file } from 'jszip';
+import { saveAs } from 'file-saver';
+
+// LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家)
+type itemType = {
+  id: string | number;
+  type:
+    | 'IMG'
+    | 'VIDEO'
+    | 'SONG'
+    | 'MUSIC'
+    | 'PPT'
+    | 'LISTEN'
+    | 'RHYTHM'
+    | 'THEORY'
+    | 'MUSIC_WIKI'
+    | 'INSTRUMENT'
+    | 'MUSICIAN';
+  coverImg: string;
+  content?: string;
+  title: string;
+  isCollect: boolean;
+  isSelected: boolean; // 精选
+  exist?: boolean; // 是否已经选
+};
+
+export default defineComponent({
+  name: 'card-type',
+  props: {
+    // 是否是选中状态
+    isActive: {
+      type: Boolean,
+      default: false
+    },
+    /** 是否可以拖拽 */
+    draggable: {
+      type: Boolean,
+      default: false
+    },
+    // 是否可以收藏
+    isCollect: {
+      type: Boolean,
+      default: true
+    },
+    // 是否显示收藏
+    isShowCollect: {
+      type: Boolean,
+      default: true
+    },
+    // 是否显示添加按钮
+    isShowAdd: {
+      type: Boolean,
+      default: false
+    },
+    // 是否禁用添加按钮
+    isShowAddDisabled: {
+      type: Boolean,
+      default: false
+    },
+    // 鼠标移动上面的时候是否自动播放,或者可以点击
+    disabledMouseHover: {
+      type: Boolean,
+      default: true
+    },
+    // 是否预览
+    isPreview: {
+      type: Boolean,
+      default: true
+    },
+    item: {
+      type: Object as PropType<itemType>,
+      default: () => ({})
+    },
+    /** 是否下架 */
+    offShelf: {
+      type: Boolean,
+      default: false
+    },
+    /** 是否可以下载 */
+    isDownload: {
+      type: Boolean,
+      default: false
+    }
+  },
+  /**
+   * @type {string} click 点击事件
+   * @type {string} collect 收藏
+   * @type {string} add 添加
+   * @type {string} offShelf 下架
+   */
+  emits: ['click', 'collect', 'add', 'offShelf'],
+  setup(props, { emit }) {
+    const message = useMessage();
+    const isAnimation = ref(false);
+    const downloadStatus = ref(false);
+    const formatType = (type: string) => {
+      let typeImg = iconOther;
+      switch (type) {
+        case 'IMG':
+          typeImg = iconImage;
+          break;
+        case 'VIDEO':
+          typeImg = iconVideo;
+          break;
+        case 'SONG':
+          typeImg = iconAudio;
+          break;
+        case 'MUSIC':
+          typeImg = iconMusic;
+          break;
+        case 'PPT':
+          typeImg = iconPPT;
+          break;
+      }
+      return typeImg;
+    };
+
+    // 获取文件blob格式
+    const getFileBlob = (url: string) => {
+      return new Promise((resolve, reject) => {
+        const request = new XMLHttpRequest();
+        request.open('GET', url, true);
+        request.responseType = 'blob';
+        request.onload = (res: any) => {
+          if (res.target.status == 200) {
+            resolve(res.target.response);
+          } else {
+            reject(res);
+          }
+        };
+        request.send();
+      });
+    };
+
+    // 多个文件下载
+    const downLoadMultiFile = (files: any, filesName: string) => {
+      const zip = new JSZip();
+      const result = [];
+      for (const i in files) {
+        const promise = getFileBlob(files[i].url).then((res: any) => {
+          zip.file(files[i].name, res, { binary: true });
+        });
+        result.push(promise);
+      }
+      Promise.all(result)
+        .then(() => {
+          zip.generateAsync({ type: 'blob' }).then(res => {
+            saveAs(
+              res,
+              filesName
+                ? filesName + Date.now() + '.zip'
+                : `文件夹${Date.now()}.zip`
+            );
+          });
+        })
+        .catch(() => {
+          message.error('下载失败');
+        });
+
+      downloadStatus.value = false;
+    };
+
+    const downloadFile = (filename: string, fileUrl: string) => {
+      // 发起Fetch请求
+      fetch(fileUrl)
+        .then(response => response.blob())
+        .then(blob => {
+          saveAs(blob, filename);
+          setTimeout(() => {
+            downloadStatus.value = false;
+          }, 100);
+        })
+        .catch(() => {
+          message.error('下载失败');
+        });
+
+      downloadStatus.value = false;
+    };
+
+    const getFileName = (url: any) => {
+      // 使用正则表达式获取文件名
+      const tempUrl = url.split('?');
+      const fileNameRegex = /\/([^\\/]+)$/; // 匹配最后一个斜杠后的内容
+      const match = tempUrl[0].match(fileNameRegex);
+
+      if (match) {
+        return match[1];
+      } else {
+        return '';
+      }
+    };
+    const onDownload = async (e: MouseEvent) => {
+      e.stopPropagation();
+      e.preventDefault();
+      const item = props.item;
+      if (!item.content) {
+        message.error('下载失败');
+        return;
+      }
+      if (downloadStatus.value) return false;
+      downloadStatus.value = true;
+      const suffix: any = item.content?.split('.');
+      const fileName = item.title + '.' + suffix[suffix?.length - 1];
+      if (item.type === 'MUSIC') {
+        const { data } = await api_musicSheetDetail(item.content);
+        const urls = [];
+        if (data.xmlFileUrl) {
+          urls.push({
+            url: data.xmlFileUrl,
+            name: getFileName(data.xmlFileUrl)
+          });
+        }
+        if (data.background && data.background.length > 0) {
+          data.background.forEach((item: any) => {
+            urls.push({
+              url: item.audioFileUrl,
+              name: getFileName(item.audioFileUrl)
+            });
+          });
+        }
+        downLoadMultiFile(urls, item.title);
+
+        // setTimeout(() => {
+        //   downloadStatus.value = false;
+        // }, 1000);
+      } else {
+        downloadFile(fileName, item.content);
+      }
+    };
+
+    return () => (
+      <div
+        onClick={() => emit('click', props.item)}
+        key={props.item.id}
+        draggable={!props.draggable ? false : props.item.exist ? false : true}
+        class={[
+          styles['card-section'],
+          'card-section-container',
+          !props.draggable ? '' : props.item.exist ? '' : styles.cardDrag
+        ]}
+        onMouseenter={() => {
+          isAnimation.value = true;
+        }}
+        onMouseleave={() => {
+          isAnimation.value = false;
+        }}
+        onDragstart={(e: any) => {
+          console.log('dragstart', Date.now());
+          e.dataTransfer.setData('text', JSON.stringify(props.item));
+        }}>
+        {/* 判断是否下架 */}
+        {props.offShelf && (
+          <div class={styles.offShelfBg}>
+            <p class={styles.offShelfTips}>该资源已被下架</p>
+            <NButton
+              type="primary"
+              class={styles.offShelfBtn}
+              onClick={(e: MouseEvent) => {
+                e.stopPropagation();
+                emit('offShelf');
+              }}>
+              确认
+            </NButton>
+          </div>
+        )}
+        <NCard
+          class={[
+            styles['card-section-content'],
+            props.isShowAdd ? '' : styles.course,
+            props.isActive ? styles.isActive : '',
+            props.item.exist ? styles.showAddBtn : '' // 是否已添加
+          ]}
+          style={{ cursor: 'pointer' }}>
+          {{
+            cover: () => (
+              <>
+                {/* 图片 */}
+                {props.item.type === 'IMG' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={props.disabledMouseHover}
+                    objectFit="cover"
+                    src={props.item.coverImg}
+                    previewSrc={props.item.content}
+                    renderToolbar={({ nodes }: ImageRenderToolbarProps) => {
+                      return [
+                        nodes.prev,
+                        nodes.next,
+                        nodes.rotateCounterclockwise,
+                        nodes.rotateClockwise,
+                        nodes.resizeToOriginalSize,
+                        nodes.zoomOut,
+                        nodes.close
+                      ];
+                    }}
+                  />
+                )}
+                {/* 乐谱 */}
+                {props.item.type === 'MUSIC' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="contain"
+                    src={props.item.coverImg}
+                  />
+                )}
+                {/* 音频 */}
+                {props.item.type === 'SONG' && (
+                  <AudioPlayer
+                    content={props.item.content}
+                    cover={props.item.coverImg}
+                    previewDisabled={props.disabledMouseHover}
+                  />
+                )}
+                {/* 视频 */}
+                {props.item.type === 'VIDEO' && (
+                  <VideoPlayer
+                    cover={props.item.coverImg}
+                    content={props.item.content}
+                    previewDisabled={props.disabledMouseHover}
+                  />
+                )}
+                {/* ppt */}
+                {props.item.type === 'PPT' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg || PageEnum.PPT_DEFAULT_COVER}
+                  />
+                )}
+                {/* 节奏练习 */}
+                {props.item.type === 'RHYTHM' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg || PageEnum.RHYTHM_DEFAULT_COVER}
+                  />
+                )}
+
+                {/* 听音练习 */}
+                {props.item.type === 'LISTEN' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg}
+                  />
+                )}
+                {/* 乐理 */}
+                {props.item.type === 'THEORY' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg || PageEnum.THEORY_DEFAULT_COVER}
+                  />
+                )}
+                {/* 名曲 */}
+                {props.item.type === 'MUSIC_WIKI' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg || PageEnum.MUSIC_DEFAULT_COVER}
+                  />
+                )}
+                {/* 乐器 */}
+                {props.item.type === 'INSTRUMENT' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={
+                      props.item.coverImg || PageEnum.INSTRUMENT_DEFAULT_COVER
+                    }
+                  />
+                )}
+                {/* 音乐家 */}
+                {props.item.type === 'MUSICIAN' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={true}
+                    objectFit="cover"
+                    src={props.item.coverImg || PageEnum.MUSICIAN_DEFAULT_COVER}
+                  />
+                )}
+              </>
+            ),
+            footer: () => (
+              <div class={styles.footer}>
+                <div class={[styles.title, 'footerTitle']}>
+                  <NImage
+                    class={[styles.titleType]}
+                    src={formatType(props.item.type)}
+                    objectFit="cover"
+                  />
+                  <span class={[styles.titleContent, 'titleContent']}>
+                    <TheNoticeBar
+                      isAnimation={isAnimation.value}
+                      text={props.item.title}
+                    />
+                  </span>
+                </div>
+                {/* 收藏 */}
+                <div class={styles.btnGroup}>
+                  {props.isDownload && (
+                    <div class={styles.btnItem} onClick={onDownload}>
+                      <NSpin show={downloadStatus.value} size={'small'}>
+                        <img
+                          src={iconDownload}
+                          key="3"
+                          class={[styles.iconCollect]}
+                        />
+                      </NSpin>
+                    </div>
+                  )}
+                  {props.isShowCollect && (
+                    <div
+                      class={[styles.iconCollect, styles.btnItem]}
+                      onClick={(e: MouseEvent) => {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        // 判断是否可以收藏
+                        if (props.isCollect) {
+                          emit('collect', props.item);
+                        }
+                      }}>
+                      <Transition name="favitor" mode="out-in">
+                        {props.item.isCollect ? (
+                          <img
+                            src={iconCollectActive}
+                            key="1"
+                            class={[
+                              styles.iconCollect,
+                              props.isCollect ? styles.isCollect : ''
+                            ]}
+                          />
+                        ) : (
+                          <img
+                            src={iconCollectDefault}
+                            key="2"
+                            class={[
+                              styles.iconCollect,
+                              props.isCollect ? styles.isCollect : ''
+                            ]}
+                          />
+                        )}
+                      </Transition>
+                    </div>
+                  )}
+                </div>
+
+                {/* 精选 */}
+                {props.item.isSelected && (
+                  <span class={styles.iconSelected}></span>
+                )}
+
+                {/* 添加按钮 */}
+                {props.isShowAdd &&
+                  (props.item.exist ? (
+                    <NButton
+                      type="primary"
+                      class={[
+                        styles.addBtn,
+                        props.item.exist ? styles.addBtnDisabled : ''
+                      ]}
+                      disabled={props.item.exist || props.isShowAddDisabled}
+                      onClick={(e: MouseEvent) => {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        emit('add', props.item);
+                      }}>
+                      {props.item.exist ? '已添加' : '添加'}
+                    </NButton>
+                  ) : (
+                    !props.isShowAddDisabled && (
+                      <NButton
+                        type="primary"
+                        class={[
+                          styles.addBtn,
+                          props.item.exist ? styles.addBtnDisabled : ''
+                        ]}
+                        disabled={props.item.exist || props.isShowAddDisabled}
+                        onClick={(e: MouseEvent) => {
+                          e.stopPropagation();
+                          e.preventDefault();
+                          emit('add', props.item);
+                        }}>
+                        {props.item.exist ? '已添加' : '添加'}
+                      </NButton>
+                    )
+                  ))}
+              </div>
+            )
+          }}
+        </NCard>
+      </div>
+    );
+  }
+});

BIN
src/views/attend-class/image/icon-fullscreen.png


+ 36 - 13
src/views/classList/work-item/index.tsx

@@ -1,5 +1,12 @@
 import { computed, defineComponent, ref } from 'vue';
-import { NImage, NDivider, NButton, NModal, useMessage } from 'naive-ui';
+import {
+  NImage,
+  NDivider,
+  NButton,
+  NModal,
+  useMessage,
+  ImageRenderToolbarProps
+} from 'naive-ui';
 import TheNoticeBar from '/src/components/TheNoticeBar';
 import styles from './index.module.less';
 import { PageEnum } from '/src/enums/pageEnum';
@@ -29,17 +36,18 @@ export default defineComponent({
     const reportSrc = ref('');
     const detailVisiable = ref(false);
 
-    const isDownload = computed(() => {
-      if (
-        props.item.fileList?.expireFlag &&
-        props.item.fileList?.fileType === 'EVALUATION'
-      ) {
-        return true;
-      } else {
-        return false;
-      }
-    });
+    // const isDownload = computed(() => {
+    //   if (
+    //     props.item.fileList?.expireFlag &&
+    //     props.item.fileList?.fileType === 'EVALUATION'
+    //   ) {
+    //     return true;
+    //   } else {
+    //     return false;
+    //   }
+    // });
 
+    console.log(props.item, 'fileList');
     return () => (
       <div
         class={[
@@ -64,7 +72,21 @@ export default defineComponent({
           )}
 
           {props.item.fileList?.fileType === 'IMG' && (
-            <NImage src={props.item.fileList?.filePath} objectFit="contain" />
+            <NImage
+              src={props.item.fileList?.filePath}
+              objectFit="contain"
+              renderToolbar={({ nodes }: ImageRenderToolbarProps) => {
+                return [
+                  nodes.prev,
+                  nodes.next,
+                  nodes.rotateCounterclockwise,
+                  nodes.rotateClockwise,
+                  nodes.resizeToOriginalSize,
+                  nodes.zoomOut,
+                  nodes.close
+                ];
+              }}
+            />
           )}
           {props.item.fileList?.fileType === 'SOUND' && (
             <div
@@ -188,7 +210,8 @@ export default defineComponent({
         <CardPreview
           v-model:show={previewShow.value}
           item={preivewItem.value}
-          isDownload={isDownload.value}
+          isDownload
+          fullscreen
         />
 
         <NModal