Browse Source

更新添加预览功能

lex 1 year ago
parent
commit
07d39ff212

+ 134 - 0
src/components/card-preview/audio-modal/index.module.less

@@ -0,0 +1,134 @@
+.audioWrap {
+  width: 100%;
+  height: 518px;
+  background-color: #fff;
+}
+
+.audioContainer {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  padding: 0;
+
+  &>div {
+    flex: 1;
+  }
+
+  .audio {
+    position: absolute;
+    top: 0;
+    opacity: 0;
+  }
+
+  .tempVudio {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    padding: 0;
+  }
+
+  canvas {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.controls {
+  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 40px 0 40px !important;
+  transition: all 0.5s;
+  display: flex;
+  align-items: center;
+  transition: all .5s;
+
+  .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;
+      }
+    }
+  }
+}
+
+.actions {
+  display: flex;
+  justify-content: space-between;
+  height: 100%;
+  color: #fff;
+  font-size: 12px;
+  align-items: center;
+
+  .actionWrap {
+    display: flex;
+  }
+
+  .actionBtn {
+    display: flex;
+    width: 52px;
+    height: 52px;
+    padding: 4px 0;
+    background: transparent;
+
+    &>img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+
+  .iconReplay {
+    width: 31px;
+    height: 29px;
+    background-color: transparent;
+
+    &>img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+
+.slider {
+  width: 100%;
+  padding: 0 20px 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;
+}

+ 204 - 0
src/components/card-preview/audio-modal/index.tsx

@@ -0,0 +1,204 @@
+import { defineComponent, reactive, ref, nextTick } from 'vue';
+import styles from './index.module.less';
+import iconplay from '@views/attend-class/image/icon-pause.svg';
+import iconpause from '@views/attend-class/image/icon-play.svg';
+import iconReplay from '@views/attend-class/image/icon-replay.svg';
+import { NSlider } from 'naive-ui';
+import Vudio from 'vudio.js';
+import tickMp3 from '@views/attend-class/image/tick.mp3';
+
+export default defineComponent({
+  name: 'audio-play',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {};
+      }
+    },
+    isEmtry: {
+      type: Boolean,
+      default: false
+    }
+  },
+
+  setup(props) {
+    const audioForms = reactive({
+      paused: true,
+      currentTimeNum: 0,
+      currentTime: '00:00',
+      durationNum: 0,
+      duration: '00:00',
+      showBar: true,
+      afterMa3: true
+    });
+    const canvas: any = ref();
+    const audio: any = ref();
+    let vudio: any = null;
+
+    // 切换音频播放
+    const onToggleAudio = (e?: MouseEvent) => {
+      e?.stopPropagation();
+      if (audio.value.paused) {
+        onInit(audio.value, canvas.value);
+        audio.value.play();
+        audioForms.afterMa3 = false;
+      } else {
+        audio.value.pause();
+      }
+      audioForms.paused = audio.value.paused;
+    };
+
+    const onInit = (audio: undefined, canvas: undefined) => {
+      if (!vudio) {
+        vudio = new Vudio(audio, canvas, {
+          effect: 'waveform',
+          accuracy: 256,
+          width: 1024,
+          height: 600,
+          waveform: {
+            maxHeight: 200,
+            color: [
+              [0, '#44D1FF'],
+              [0.5, '#44D1FF'],
+              [0.5, '#198CFE'],
+              [1, '#198CFE']
+            ],
+            prettify: false
+          }
+        });
+        vudio.dance();
+      }
+    };
+
+    // 对时间进行格式化
+    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 onReplay = () => {
+      if (!audio.value) return;
+      audio.value.currentTime = 0;
+    };
+
+    let vudio1 = null;
+    const canvas1: any = ref();
+    const audio1: any = ref();
+    nextTick(() => {
+      vudio1 = new Vudio(audio1.value, canvas1.value, {
+        effect: 'waveform',
+        accuracy: 256,
+        width: 1024,
+        height: 600,
+        waveform: {
+          maxHeight: 200,
+          color: [
+            [0, '#44D1FF'],
+            [0.5, '#44D1FF'],
+            [0.5, '#198CFE'],
+            [1, '#198CFE']
+          ],
+          prettify: false
+        }
+      });
+      vudio1.dance();
+    });
+
+    return () => (
+      <div class={styles.audioWrap}>
+        <div class={styles.audioContainer}>
+          <audio
+            ref={audio}
+            crossorigin="anonymous"
+            src={props.item.content + '?time=1'}
+            onEnded={() => {
+              audioForms.paused = true;
+            }}
+            onTimeupdate={() => {
+              audioForms.currentTime = timeFormat(
+                Math.round(audio.value?.currentTime || 0)
+              );
+              audioForms.currentTimeNum = audio.value.currentTime;
+            }}
+            onLoadedmetadata={() => {
+              audioForms.duration = timeFormat(
+                Math.round(audio.value.duration)
+              );
+              audioForms.durationNum = audio.value.duration;
+            }}></audio>
+
+          <canvas ref={canvas}></canvas>
+
+          {audioForms.afterMa3 && (
+            <div class={styles.tempVudio}>
+              <audio ref={audio1} src={tickMp3} />
+              <canvas ref={canvas1}></canvas>
+            </div>
+          )}
+        </div>
+
+        <div
+          class={[
+            styles.controls,
+            audioForms.showBar ? '' : styles.sectionAnimate
+          ]}
+          onClick={(e: MouseEvent) => {
+            e.stopPropagation();
+          }}>
+          <div class={styles.actions}>
+            <div class={styles.actionWrap}>
+              <button class={styles.actionBtn} onClick={onToggleAudio}>
+                {audioForms.paused ? (
+                  <img class={styles.playIcon} src={iconplay} />
+                ) : (
+                  <img class={styles.playIcon} src={iconpause} />
+                )}
+              </button>
+            </div>
+            <div class={styles.time}>
+              <div
+                class="plyr__time plyr__time--current"
+                aria-label="Current time">
+                {audioForms.currentTime}
+              </div>
+              <span class={styles.line}>/</span>
+              <div
+                class="plyr__time plyr__time--duration"
+                aria-label="Duration">
+                {audioForms.duration}
+              </div>
+            </div>
+          </div>
+
+          <div class={styles.slider}>
+            <NSlider
+              value={audioForms.currentTimeNum}
+              step={0.01}
+              max={audioForms.durationNum}
+              tooltip={false}
+              onUpdate:value={(val: number) => {
+                audio.value.currentTime = val;
+                audioForms.currentTimeNum = val;
+                audioForms.currentTime = timeFormat(Math.round(val || 0));
+              }}
+            />
+          </div>
+
+          <div class={styles.actions}>
+            <div class={styles.actionWrap}>
+              <button class={styles.iconReplay} onClick={onReplay}>
+                <img src={iconReplay} />
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 17 - 0
src/components/card-preview/index.module.less

@@ -0,0 +1,17 @@
+.cardPreview {
+  width: 920px;
+
+  :global {
+    .n-card__content {
+      height: 518px;
+    }
+
+    .n-card-header__main {
+      max-width: 60%;
+      margin: 0 auto;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}

+ 59 - 0
src/components/card-preview/index.tsx

@@ -0,0 +1,59 @@
+import { NModal } from 'naive-ui';
+import { defineComponent, toRef, watch } from 'vue';
+import styles from './index.module.less';
+import VideoModal from './video-modal';
+import SongModal from './song-modal';
+import AudioModal from './audio-modal';
+
+export default defineComponent({
+  name: 'card-preview',
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    item: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  emit: ['update:show'],
+  setup(props, { emit }) {
+    const show = toRef(props.show);
+    const item = toRef(props.item);
+
+    watch(
+      () => props.show,
+      () => {
+        show.value = props.show;
+      }
+    );
+
+    watch(
+      () => props.item,
+      () => {
+        item.value = props.item;
+      }
+    );
+    return () => (
+      <>
+        <NModal
+          v-model:show={show.value}
+          onUpdate:show={() => {
+            emit('update:show', show.value);
+          }}
+          preset="card"
+          showIcon={false}
+          class={['modalTitle background', styles.cardPreview]}
+          title={item.value.title}
+          blockScroll={false}>
+          {item.value.type === 'VIDEO' && (
+            <VideoModal poster={item.value.url} src={item.value.content} />
+          )}
+          {item.value.type === 'SONG' && <SongModal item={item.value} />}
+          {item.value.type === 'AUDIO' && <AudioModal item={item.value} />}
+        </NModal>
+      </>
+    );
+  }
+});

+ 15 - 0
src/components/card-preview/song-modal/index.module.less

@@ -0,0 +1,15 @@
+.musicScore {
+  width: 100%;
+  height: 518px;
+
+  iframe {
+    width: inherit;
+    height: inherit;
+
+    :global {
+      .headTopBackBtn {
+        display: none;
+      }
+    }
+  }
+}

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

@@ -0,0 +1,33 @@
+import { defineComponent, ref, watch } from 'vue';
+import styles from './index.module.less';
+
+export default defineComponent({
+  name: 'song-modal',
+  props: {
+    item: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const iframeRef = ref();
+    const isLoaded = ref(false);
+    const origin = /(localhost|192)/.test(location.host)
+      ? 'https://dev.kt.colexiu.com'
+      : location.origin;
+    const src = `${origin}/instrument?id=${props.item.content}&modelType=practise`;
+    return () => (
+      <div class={styles.musicScore}>
+        <iframe
+          ref={iframeRef}
+          onLoad={() => {
+            // emit('setIframe', iframeRef.value);
+            isLoaded.value = true;
+          }}
+          class={[styles.container, 'musicIframe']}
+          frameborder="0"
+          src={src}></iframe>
+      </div>
+    );
+  }
+});

+ 90 - 0
src/components/card-preview/video-modal/index.module.less

@@ -0,0 +1,90 @@
+.video-container {
+  position: relative;
+  width: 100%;
+  --plyr-color-main: #198CFE;
+
+  video {
+    width: 100%;
+    // object-fit: cover;
+  }
+
+  :global {
+    .video-back {
+      position: absolute;
+      left: 20px;
+      top: 20px;
+      color: #fff;
+      z-index: 99;
+      font-size: 24px;
+      width: 30px;
+      height: 30px;
+      background-color: rgba(0, 0, 0, 0.5);
+      border-radius: 50%;
+      padding: 4px 5px 4px 3px;
+    }
+
+    .plyr__poster {
+      background-size: cover;
+    }
+
+    .plyr__control--overlaid {
+      border: 1px solid #fff;
+      background-color: rgba(0, 0, 0, 0.2) !important;
+    }
+
+    .plyr--video .plyr__control:hover {
+      background-color: transparent !important;
+    }
+  }
+
+  .video {
+    position: relative;
+  }
+}
+
+.loadingVideo {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.9);
+  z-index: 10;
+}
+
+.playOver {
+  background: rgba(0, 0, 0, 0.5);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+
+  .tips {
+    font-size: 15px;
+    color: #ffffff;
+  }
+
+  .btn {
+    margin: 10px 0;
+    min-width: 94px;
+    font-size: 14px;
+    height: 28px;
+    line-height: 28px;
+  }
+
+  .replay {
+    padding-top: 12px;
+  }
+}
+
+.freeTxt {
+  font-size: 15px;
+  color: #ffffff;
+  line-height: 21px;
+  padding-top: 10px;
+}
+
+.freeRate {
+  color: #32ffd8;
+}

+ 150 - 0
src/components/card-preview/video-modal/index.tsx

@@ -0,0 +1,150 @@
+import { defineComponent, PropType } from 'vue';
+import styles from './index.module.less';
+import Plyr from 'plyr';
+import 'plyr/dist/plyr.css';
+import { browser } from '@/helpers/utils';
+export default defineComponent({
+  name: 'o-video',
+  props: {
+    setting: {
+      type: Object,
+      default: () => ({})
+    },
+    controls: Boolean,
+    height: String,
+    src: {
+      type: String,
+      default: ''
+    },
+    poster: {
+      type: String,
+      default: ''
+    },
+    styleValue: {
+      type: Object,
+      default: () => ({})
+    },
+    preload: {
+      type: String as PropType<'auto' | 'metadata' | 'none'>,
+      default: 'auto'
+    },
+    currentTime: {
+      type: Boolean,
+      default: true
+    },
+    playsinline: {
+      type: Boolean,
+      default: true
+    },
+    onPlay: {
+      type: Function,
+      default: () => ({})
+    }
+  },
+  emits: ['exitfullscreen'],
+  data() {
+    return {
+      player: null as any,
+      loading: true // 首次进入加载中
+    };
+  },
+  mounted() {
+    this._init();
+  },
+  methods: {
+    _init() {
+      // controls: [
+      //   'play-large' ,  // 中间的大播放按钮
+      //   'restart' ,  // 重新开始播放
+      //   'rewind' ,  // 按寻道时间倒带(默认 10 秒)
+      //   'play' ,  // 播放/暂停播放
+      //   'fast-forward' ,  // 快进查找时间(默认 10 秒)
+      //   'progress' ,  // 播放和缓冲的进度条和滑动条
+      //   'current-time' ,  // 播放的当前时间
+      //   ' duration' ,  // 媒体的完整持续时间
+      //   'mute' ,  // 切换静音
+      //   'volume', // 音量控制
+      //   'captions' ,  // 切换字幕
+      //   'settings' ,  // 设置菜单
+      //   'pip' ,  // 画中画(当前仅 Safari)
+      //   'airplay' ,  // Airplay(当前仅 Safari)
+      //   'download ' ,  // 显示一个下载按钮,其中包含指向当前源或您在选项中指定的自定义 URL 的链接
+      //   'fullscreen' ,  // 切换全屏
+      // ] ;
+      const controls = [
+        'play-large',
+        'play',
+        'progress',
+        'captions',
+        'fullscreen'
+      ];
+      if (this.currentTime) {
+        controls.push('current-time');
+      }
+      const params: any = {
+        controls: controls,
+        ...this.setting,
+        invertTime: false
+      };
+
+      if (browser().iPhone) {
+        params.fullscreen = {
+          enabled: true,
+          fallback: 'force',
+          iosNative: true
+        };
+      }
+
+      this.player = new Plyr((this as any).$refs.video, params);
+
+      this.player.on('play', () => {
+        this.onPlay && this.onPlay(this.player);
+      });
+
+      this.player.on('enterfullscreen', () => {
+        console.log('fullscreen');
+        const i = document.createElement('i');
+        i.id = 'fullscreen-back';
+        i.className = 'van-icon van-icon-arrow-left video-back';
+        i.addEventListener('click', () => {
+          this.player.fullscreen.exit();
+        });
+        console.log(document.getElementsByClassName('plyr'));
+        document.getElementsByClassName('plyr')[0].appendChild(i);
+      });
+
+      this.player.on('exitfullscreen', () => {
+        console.log('exitfullscreen');
+        const i = document.getElementById('fullscreen-back');
+        i && i.remove();
+        this.$emit('exitfullscreen');
+      });
+    },
+
+    onReplay() {
+      this.player.restart();
+      this.player.play();
+    },
+    onStop() {
+      this.player.stop();
+    }
+  },
+  unmounted() {
+    this.player?.destroy();
+  },
+  render() {
+    return (
+      <div class={styles['video-container']}>
+        <video
+          ref="video"
+          class={styles['video']}
+          src={this.src}
+          playsinline={this.playsinline}
+          poster={this.poster}
+          preload={this.preload}
+          style={{ ...this.styleValue }}></video>
+        {/* </div> */}
+      </div>
+    );
+  }
+});

+ 5 - 1
src/components/card-type/index.module.less

@@ -18,6 +18,10 @@
   display: inline-flex;
   transition: all .3s ease-in-out;
 
+  &.course {
+    cursor: pointer;
+  }
+
   // 鼠标经过时样式
   &:hover {
     transform: scale(1.01);
@@ -132,4 +136,4 @@
     opacity: 0;
     transition: all .3s ease-in-out;
   }
-}
+}

+ 30 - 11
src/components/card-type/index.tsx

@@ -1,6 +1,6 @@
 import { PropType, defineComponent, ref } from 'vue';
 import styles from './index.module.less';
-import { NButton, NCard, NImage } from 'naive-ui';
+import { NButton, NCard, NImage, NModal } 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';
@@ -39,6 +39,11 @@ export default defineComponent({
       type: Boolean,
       default: false
     },
+    // 是否预览
+    isPreview: {
+      type: Boolean,
+      default: true
+    },
     item: {
       type: Object as PropType<itemType>,
       default: () => ({})
@@ -84,19 +89,33 @@ export default defineComponent({
         <NCard
           class={[
             styles['card-section'],
+            props.isShowAdd ? '' : styles.course,
             props.isActive ? styles.isActive : ''
           ]}>
           {{
-            cover: () =>
-              ['IMG', 'VIDEO', 'SONG', 'AUDIO'].includes(props.item.type) && (
-                <NImage
-                  class={[styles.cover, styles.image]}
-                  lazy
-                  previewDisabled
-                  objectFit="cover"
-                  src={props.item.url}
-                />
-              ),
+            cover: () => (
+              <>
+                {props.item.type === 'IMG' && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled={!props.isPreview}
+                    objectFit="cover"
+                    src={props.item.url}
+                  />
+                )}
+
+                {['VIDEO', 'SONG', 'AUDIO'].includes(props.item.type) && (
+                  <NImage
+                    class={[styles.cover, styles.image]}
+                    lazy
+                    previewDisabled
+                    objectFit="cover"
+                    src={props.item.url}
+                  />
+                )}
+              </>
+            ),
             footer: () => (
               <div class={styles.footer}>
                 <div class={styles.title}>

+ 4 - 1
src/views/attend-class/component/musicScore.tsx

@@ -29,7 +29,10 @@ export default defineComponent({
     const isLoaded = ref(false);
     const renderError = ref(false);
     const renderSuccess = ref(false);
-    const src = `https://kt.colexiu.com/instrument?platform=pc&modelType=practise`;
+    const origin = /(localhost|192)/.test(location.host)
+      ? 'https://dev.kt.colexiu.com'
+      : location.origin;
+    const src = `${origin}/instrument?platform=pc&modelType=practise`;
     const checkView = () => {
       fetch(src)
         .then(() => {

+ 4 - 1
src/views/attend-class/model/train-type/index.tsx

@@ -45,7 +45,10 @@ export default defineComponent({
     };
 
     const onDetail = () => {
-      const src = `https://kt.colexiu.com/instrument?platform=pc&modelType=practise`;
+      const origin = /(localhost|192)/.test(location.host)
+        ? 'https://dev.kt.colexiu.com'
+        : location.origin;
+      const src = `${origin}/instrument?platform=pc&modelType=practise`;
       window.open(src, '_blank');
     };
     return () => (

+ 21 - 2
src/views/natural-resources/index.tsx

@@ -5,6 +5,7 @@ import { NTabPane, NTabs } from 'naive-ui';
 import CardType from '/src/components/card-type';
 import SearchGroupResources from './search-group-resources';
 import listData from '../xiaoku-music/data.json';
+import CardPreview from '/src/components/card-preview';
 export default defineComponent({
   name: 'student-studentList',
   setup() {
@@ -16,7 +17,9 @@ export default defineComponent({
         rows: 50,
         pageTotal: 0
       },
-      tableList: [] as any
+      tableList: [] as any,
+      show: false,
+      item: {}
     });
     const forms = reactive({
       list: [],
@@ -30,6 +33,7 @@ export default defineComponent({
           id: row.id,
           type: 'SONG',
           title: row.musicSheetName,
+          content: row.id,
           url: row.fixedTone ? row.fixedTone.split(',')[0] : '',
           isCollect: i % 3 ? false : true,
           isSelected: i % 4 ? false : true
@@ -40,6 +44,8 @@ export default defineComponent({
             id: i + 3,
             type: 'VIDEO',
             title: '其多列',
+            content:
+              'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844560120.mp4',
             url: 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687844640957.png',
             isCollect: i % 3 ? false : true,
             isSelected: i % 4 ? false : true
@@ -51,6 +57,8 @@ export default defineComponent({
             id: i + 3,
             type: 'AUDIO',
             title: '歌曲表演 大鹿',
+            content:
+              'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1686819360752.mp3',
             url: 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687916228530.png',
             isCollect: i % 3 ? false : true,
             isSelected: i % 4 ? false : true
@@ -90,7 +98,15 @@ export default defineComponent({
 
             <div class={styles.list}>
               {forms.list.map((item: any) => (
-                <CardType item={item} />
+                <CardType
+                  item={item}
+                  onClick={(val: any) => {
+                    if (val.type === 'IMG') return;
+
+                    state.show = true;
+                    state.item = val;
+                  }}
+                />
               ))}
             </div>
 
@@ -134,6 +150,9 @@ export default defineComponent({
             />
           </NTabPane>
         </NTabs>
+
+        {/* 弹窗查看 */}
+        <CardPreview v-model:show={state.show} item={state.item} />
       </div>
     );
   }

+ 5 - 3
src/views/xiaoku-music/index.tsx

@@ -210,9 +210,11 @@ export default defineComponent({
                   src={icon_goXiaoku}
                   onClick={() => {
                     handleChangeAudio('pause');
-                    window.open(
-                      `https://kt.colexiu.com/instrument/?platform=pc&id=${activeItem.value.id}`
-                    );
+                    const origin = /(localhost|192)/.test(location.host)
+                      ? 'https://dev.kt.colexiu.com'
+                      : location.origin;
+                    const src = `${origin}/instrument?platform=pc&id=${activeItem.value.id}`;
+                    window.open(src);
                   }}
                 />
                 <div class={styles.favitor} onClick={() => handleFavitor()}>