فهرست منبع

学生端添加详情

lex 1 سال پیش
والد
کامیت
9343becc25

+ 7 - 0
src/components/m-header/index.module.less

@@ -0,0 +1,7 @@
+.headerSection {
+  :global {
+    .van-nav-bar__title {
+      color: inherit;
+    }
+  }
+}

+ 21 - 0
src/router/routes-common.ts

@@ -192,6 +192,27 @@ export default [
         meta: {
           title: '作品详情'
         }
+      },
+      {
+        path: '/instrumentDetail',
+        component: () => import('@/views/information/instrument-detail'),
+        meta: {
+          title: '乐器百科详情'
+        }
+      },
+      {
+        path: '/famousMusicDetail',
+        component: () => import('@/views/information/famous-music-detail'),
+        meta: {
+          title: '名曲鉴赏详情'
+        }
+      },
+      {
+        path: '/musicianDetail',
+        component: () => import('@/views/information/musician-detail'),
+        meta: {
+          title: '音乐家详情'
+        }
       }
     ]
   },

+ 202 - 0
src/views/information/components/audio-player/index.module.less

@@ -0,0 +1,202 @@
+.audioPlayer {
+
+  background-color: #fff;
+}
+
+.playerHeader {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 15px 0;
+
+
+
+  .musicInfo {
+    display: flex;
+    align-items: center;
+
+    .cover {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 42px;
+      height: 42px;
+      background: #3D465A;
+      box-shadow: 0px 2px 13px 0px rgba(0, 0, 0, 0.09);
+      border-radius: 50%;
+      transition: opacity .3s;
+      animation: rotateImg 6s linear infinite;
+
+      &.imgRotate {
+        animation-play-state: paused;
+      }
+    }
+
+
+
+    .musicBg {
+      width: 28px;
+      height: 28px;
+      border-radius: 50%;
+      overflow: hidden;
+    }
+
+    .musicName {
+      padding-left: 10px;
+      font-size: 16px;
+      font-weight: 500;
+      color: #131415;
+      line-height: 22px;
+      text-shadow: 0px 2px 13px rgba(0, 0, 0, 0.09);
+      max-width: 140px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .controls {
+    .icon {
+      font-size: 18px;
+      margin: 0 11px;
+    }
+
+
+    .iconMenu {
+      font-size: 20px;
+    }
+  }
+}
+
+.playerFooter {
+  --van-slider-bar-height: 4px;
+  --van-slider-active-background: #C8E7FF;
+  --van-slider-inactive-background: rgba(0, 0, 0, 0.09);
+  --van-slider-button-width: 10px;
+  --van-slider-button-height: 10px;
+  --van-slider-button-background: #269EFE;
+  --van-slider-button-shadow: 0px 2px 13px 0px rgba(0, 0, 0, 0.09);
+  padding: 14px 15px calc(14px + env(safe-area-inset-bottom));
+  display: flex;
+  align-items: center;
+
+
+  :global {
+    .van-slider__button-wrapper--right {
+      border: 4px solid rgba(38, 158, 254, 0.19);
+      border-radius: 50%;
+    }
+
+    .van-slider__button {
+      // border: 4px solid rgba(38, 158, 254, 0.19);
+    }
+  }
+
+  .playerTimer {
+    margin-left: 15px;
+    flex-shrink: 0;
+    font-size: 12px;
+    font-weight: 500;
+    color: #AAAAAA;
+    line-height: 17px;
+    text-shadow: 0px 2px 13px rgba(0, 0, 0, 0.09);
+  }
+}
+
+.songPopup {
+  border-radius: 12px 12px 0px 0px;
+  background: #F8F9FC;
+}
+
+.songContainer {
+  background: #FFFFFF;
+  border-radius: 16px;
+  min-height: 408px;
+  max-height: 408px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  margin: 14px 12px calc(14px + env(safe-area-inset-bottom));
+
+  .songTitle {
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    background-color: #fff;
+    display: flex;
+    align-items: center;
+    font-size: 17px;
+    font-weight: 600;
+    color: #333333;
+    line-height: 24px;
+    padding: 12px 14px;
+
+    &::before {
+      content: '';
+      width: 4px;
+      height: 14px;
+      border-radius: 2px;
+      background: linear-gradient(to bottom, #259CFE, #47CEFF);
+      margin-right: 6px;
+    }
+  }
+
+  .songCellGroup {}
+
+  .songCell {
+    margin: 25px 7px;
+    padding: 10px 7px;
+    border-radius: 12px;
+    width: auto;
+
+    &.active {
+      background: #F1F5FD;
+
+      .songName {
+        color: #1CACF1
+      }
+    }
+
+    &:first-child {
+      margin-top: 0;
+    }
+
+    :global {
+      .van-cell__value {
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+      }
+    }
+
+    .songImg {
+      width: 28px;
+      height: 27px;
+      border-radius: 10px;
+      overflow: hidden;
+      margin-right: 10px;
+      flex-shrink: 0;
+    }
+
+    .songName {
+      max-width: 180px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-size: 16px;
+      font-weight: 500;
+      color: #131415;
+      line-height: 22px;
+    }
+
+    .iconSong {
+      font-size: 20px;
+    }
+  }
+}
+
+@keyframes rotateImg {
+
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 266 - 0
src/views/information/components/audio-player/index.tsx

@@ -0,0 +1,266 @@
+import {
+  computed,
+  defineComponent,
+  reactive,
+  watch,
+  ref,
+  PropType,
+  onMounted
+} from 'vue';
+import styles from './index.module.less';
+import { Cell, Icon, Image, Popup, Slider } from 'vant';
+import musicBg from '../../images/music_bg.png';
+import iconMenu from '../../images/icon-menu.png';
+import iconPause from '../../images/icon-pause.png';
+import iconPlay from '../../images/icon-play.png';
+import songPrev from '../../images/song-prev.png';
+import songDown from '../../images/song-down.png';
+import songPause from '../../images/song-pause.png';
+import songPlay from '../../images/song-play.png';
+import { getSecondRPM } from '@/helpers/utils';
+import { nextTick } from 'process';
+import { usePageVisibility } from '@vant/use';
+
+export default defineComponent({
+  name: 'audio-player',
+  props: {
+    musicList: {
+      type: Array as PropType<any>,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const pageVisibility = usePageVisibility();
+    const state = reactive({
+      songPopup: false,
+      playState: 'pause',
+      audioObj: {} as any,
+      listActive: 0,
+      dragStatus: false // 是否开始拖动
+    });
+    let timer = null as any;
+    const audioData = reactive({
+      isFirst: true,
+      duration: 0.1,
+      currentTime: 0
+    });
+    const audioRef = ref();
+    /** 加载成功 */
+    const onLoadedmetadata = () => {
+      audioData.duration = audioRef.value.duration;
+      if (audioData.isFirst) {
+        audioData.isFirst = false;
+        return;
+      }
+      if (state.playState === 'play') {
+        audioRef.value.play();
+      }
+    };
+    /** 改变时间 */
+    const handleChangeTime = (val: number) => {
+      audioRef.value.pause();
+      audioData.currentTime = val;
+      clearTimeout(timer);
+      timer = setTimeout(() => {
+        audioRef.value.currentTime = val;
+        if (state.playState === 'play') {
+          audioRef.value.play();
+        }
+        timer = null;
+      }, 300);
+    };
+    const time = computed(() => {
+      return `${getSecondRPM(audioData.currentTime)} / ${getSecondRPM(
+        audioData.duration
+      )}`;
+    });
+
+    /** 播放曲目 */
+    const handlePlay = (item: any) => {
+      const index = props.musicList.findIndex(
+        (_item: any) => _item.id === item.id
+      );
+      if (index > -1) {
+        if (state.listActive === index) {
+          state.playState = state.playState === 'play' ? 'pause' : 'play';
+        } else {
+          state.playState = 'play';
+        }
+        state.listActive = index;
+        state.audioObj = props.musicList[index];
+      }
+    };
+
+    /** 音频控制 */
+    const handleChangeAudio = (type: 'play' | 'pause' | 'pre' | 'next') => {
+      if (type === 'play') {
+        state.playState = 'play';
+      } else if (type === 'pause') {
+        state.playState = 'pause';
+      } else if (type === 'pre') {
+        if (props.musicList[state.listActive - 1]) {
+          handlePlay(props.musicList[state.listActive - 1]);
+        }
+      } else if (type === 'next') {
+        if (props.musicList[state.listActive + 1]) {
+          handlePlay(props.musicList[state.listActive + 1]);
+        }
+      }
+    };
+
+    watch(
+      () => pageVisibility.value,
+      val => {
+        if (val === 'hidden') {
+          audioRef.value.pause();
+          state.playState = 'pause';
+        }
+      }
+    );
+    watch(
+      () => state.playState,
+      val => {
+        if (val === 'play') {
+          audioRef.value.play();
+        } else {
+          audioRef.value.pause();
+        }
+      }
+    );
+
+    onMounted(() => {
+      nextTick(() => {
+        const musicList = props.musicList;
+        if (musicList.length > 0) {
+          state.audioObj = musicList[0];
+        }
+      });
+    });
+    return () => (
+      <div class={styles.audioPlayer}>
+        <div class={styles.playerHeader}>
+          <div class={styles.musicInfo}>
+            <div
+              class={[
+                styles.cover,
+                state.playState === 'pause' && styles.imgRotate
+              ]}>
+              <Image src={musicBg} class={styles.musicBg} />
+            </div>
+            <div class={styles.musicName}>{state.audioObj?.name || '--'}</div>
+          </div>
+          <div class={styles.controls}>
+            <Icon
+              name={songPrev}
+              class={styles.icon}
+              onClick={() => {
+                handleChangeAudio('pre');
+              }}
+            />
+            <Icon
+              name={state.playState === 'play' ? iconPlay : iconPause}
+              class={[styles.icon, styles.iconPlay]}
+              onClick={() => {
+                if (state.playState === 'pause') {
+                  handleChangeAudio('play');
+                } else {
+                  handleChangeAudio('pause');
+                }
+              }}
+            />
+            <Icon
+              name={songDown}
+              class={styles.icon}
+              onClick={() => {
+                handleChangeAudio('next');
+              }}
+            />
+            <Icon
+              name={iconMenu}
+              class={[styles.icon, styles.iconMenu]}
+              onClick={() => {
+                state.songPopup = true;
+              }}
+            />
+          </div>
+        </div>
+        <div class={styles.playerFooter}>
+          <Slider
+            step={0.01}
+            class={styles.timeProgress}
+            v-model={audioData.currentTime}
+            max={audioData.duration}
+            onUpdate:modelValue={val => {
+              handleChangeTime(val);
+            }}
+            onDragStart={() => {
+              state.dragStatus = true;
+            }}
+            onDragEnd={() => {
+              state.dragStatus = false;
+            }}
+          />
+          <div class={styles.playerTimer}>{time.value}</div>
+
+          <audio
+            ref={audioRef}
+            src={state.audioObj.url}
+            onLoadedmetadata={onLoadedmetadata}
+            onEnded={() => {
+              handleChangeAudio('pause');
+            }}
+            onTimeupdate={() => {
+              if (timer) return;
+              // 开始拖动时也不能更新
+              if (state.dragStatus) return;
+              audioData.currentTime = audioRef.value?.currentTime;
+            }}></audio>
+        </div>
+
+        <Popup
+          v-model:show={state.songPopup}
+          round
+          position="bottom"
+          class={styles.songPopup}>
+          <div class={styles.songContainer}>
+            <div class={styles.songTitle}>代表作</div>
+
+            <div class={styles.songCellGroup}>
+              {props.musicList.map((item: any, index: number) => (
+                <Cell
+                  border={false}
+                  class={[
+                    styles.songCell,
+                    index === state.listActive && styles.active
+                  ]}
+                  center
+                  onClick={() => {
+                    if (index === state.listActive) return;
+                    state.audioObj = props.musicList[index];
+                    state.listActive = index;
+                    state.playState = 'play';
+                  }}>
+                  {{
+                    icon: () => <Image src={musicBg} class={styles.songImg} />,
+                    title: () => <div class={styles.songName}>{item.name}</div>,
+                    value: () => (
+                      <Icon
+                        name={
+                          index === state.listActive &&
+                          state.playState === 'play'
+                            ? songPlay
+                            : songPause
+                        }
+                        class={[styles.iconSong]}
+                      />
+                    )
+                  }}
+                </Cell>
+              ))}
+            </div>
+          </div>
+        </Popup>
+      </div>
+    );
+  }
+});

+ 132 - 0
src/views/information/famous-music-detail/index.module.less

@@ -0,0 +1,132 @@
+.detail {
+  min-height: 100vh;
+  // background: url('../images/banner1.png') no-repeat top center;
+  // background-size: contain;
+  background-color: #F8F9FC;
+  // backdrop-filter: blur(11px);
+  // filter: blur(11px);
+  position: relative;
+  z-index: 9;
+
+  .bgSection {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 315px;
+
+    .bg {
+      width: 100%;
+      height: 315px;
+      object-fit: cover;
+    }
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      background: rgba(255, 255, 255, 0.58);
+      backdrop-filter: blur(11px);
+    }
+  }
+}
+
+.musicHeader {
+  display: flex;
+  align-items: flex-start;
+  margin: 0 25px 25px;
+
+  .imgSection {
+    position: relative;
+    width: 76px;
+    height: 76px;
+    margin-right: 46px;
+
+    .img {
+      width: 76px;
+      height: 76px;
+      border-radius: 2px 8px 8px 2px;
+      overflow: hidden;
+      position: relative;
+      z-index: 9;
+    }
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      z-index: 1;
+      display: inline-block;
+      width: 5px;
+      height: 76px;
+      background: linear-gradient(270deg, rgba(0, 0, 0, 0.13) 0%, rgba(255, 255, 255, 0) 100%);
+    }
+
+    .pan {
+      content: '';
+      position: absolute;
+      top: 2px;
+      right: -23px;
+      display: inline-block;
+      width: 72px;
+      height: 72px;
+      background: url('../images/icon-pan.png') no-repeat center;
+      background-size: contain;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      img {
+        width: 40px;
+        height: 40px;
+        border-radius: 50%;
+        overflow: hidden;
+      }
+    }
+  }
+
+  .musicContent {
+    .musicTitle {
+      max-width: 200px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      font-size: 18px;
+      font-weight: 600;
+      color: #000000;
+      line-height: 24px;
+    }
+
+    .musicInto {
+      padding-top: 4px;
+      font-size: 12px;
+      color: rgba(0, 0, 0, 0.5);
+      line-height: 24px;
+      margin-right: 12px;
+    }
+  }
+}
+
+.container {
+  position: relative;
+  border-radius: 16px;
+  margin: 20px 0 16px;
+
+
+  .content {
+    padding: 20px 25px;
+    font-size: 14px;
+    line-height: 20px;
+    background: #FFFFFF;
+    border-radius: 16px;
+
+    img,
+    video {
+      width: 100% !important;
+    }
+  }
+}

+ 98 - 0
src/views/information/famous-music-detail/index.tsx

@@ -0,0 +1,98 @@
+import MHeader from '@/components/m-header';
+import MSticky from '@/components/m-sticky';
+import { defineComponent, onMounted, reactive } from 'vue';
+import { useEventListener, useWindowScroll } from '@vueuse/core';
+import styles from './index.module.less';
+import { Image } from 'vant';
+import request from '@/helpers/request';
+import { useRoute } from 'vue-router';
+import AudioPlayer from '../components/audio-player';
+import banner from '../images/banner1.png';
+import musicBg from '../images/music_bg.png';
+
+export default defineComponent({
+  name: 'instrument-detail',
+  setup() {
+    const route = useRoute();
+    const forms = reactive({
+      titleOpacity: 0,
+      background: 'transparent',
+      color: '#fff',
+      detail: {} as any,
+      musicList: [] as any
+    });
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get(
+          '/edu-app/knowledgeWiki/detail/' + route.query.id
+        );
+        data.intros = data.intros.replace(
+          /<video/gi,
+          '<video style="width: 100% !important;" controlslist="nodownload"'
+        );
+        forms.detail = data || {};
+        forms.musicList = data.knowledgeWikiResources.map((item: any) => {
+          return {
+            id: item.id,
+            name: item.name,
+            url: item.url
+          };
+        });
+      } catch {}
+    };
+    onMounted(() => {
+      useEventListener(document, 'scroll', () => {
+        const { y } = useWindowScroll();
+        forms.titleOpacity = y.value > 100 ? 1 : y.value / 100;
+      });
+
+      getDetail();
+    });
+    return () => (
+      <div class={styles.detail}>
+        <div class={styles.bgSection}>
+          <img class={styles.bg} src={forms.detail.avatar || musicBg} />
+        </div>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            background={`rgba(255,255,255, ${forms.titleOpacity})`}
+            color={`rgba(51,51,51, ${forms.titleOpacity})`}
+            title={forms.detail.name || ''}></MHeader>
+        </MSticky>
+
+        <div class={styles.container}>
+          <div class={styles.musicHeader}>
+            <div class={styles.imgSection}>
+              <Image class={styles.img} src={forms.detail.avatar || musicBg} />
+              <div class={styles.pan}>
+                <img src={forms.detail.avatar || musicBg} />
+              </div>
+            </div>
+            <div class={styles.musicContent}>
+              <div class={styles.musicTitle}>{forms.detail.name || '--'}</div>
+              <div class={styles.musicInto}>
+                {forms.detail.lyricists && (
+                  <span>作词:{forms.detail.lyricists}</span>
+                )}
+                {forms.detail.composers && (
+                  <span>作曲:{forms.detail.composers}</span>
+                )}
+              </div>
+            </div>
+          </div>
+
+          {/* <div class={styles.title}>{forms.detail.name}</div> */}
+          <div class={styles.content} v-html={forms.detail.intros}></div>
+        </div>
+
+        <MSticky position="bottom">
+          {forms.musicList.length > 0 && (
+            <AudioPlayer musicList={forms.musicList} />
+          )}
+        </MSticky>
+      </div>
+    );
+  }
+});

BIN
src/views/information/images/banner1.png


BIN
src/views/information/images/icon-menu.png


BIN
src/views/information/images/icon-pan.png


BIN
src/views/information/images/icon-pause.png


BIN
src/views/information/images/icon-play.png


BIN
src/views/information/images/music_bg.png


BIN
src/views/information/images/song-down.png


BIN
src/views/information/images/song-pause.png


BIN
src/views/information/images/song-play.png


BIN
src/views/information/images/song-prev.png


+ 53 - 0
src/views/information/instrument-detail/index.module.less

@@ -0,0 +1,53 @@
+.detail {
+  min-height: 100vh;
+  background: url('../images/banner1.png') no-repeat top center;
+  background-size: contain;
+  background-color: #F8F9FC;
+}
+
+.container {
+  position: relative;
+  background: #fff;
+  border-radius: 16px;
+  margin: 48px 13px 16px;
+  padding: 55px 14px 20px;
+
+  .instrumentLogo {
+    position: absolute;
+    left: 50%;
+    margin-left: -45px;
+    top: -3px;
+    width: 90px;
+    height: 90px;
+    background: #FFFFFF;
+    border: 2px solid #E0E0E0;
+    border-radius: 50%;
+    overflow: hidden;
+    margin-top: -45px;
+
+    .logo {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  .title {
+    font-size: 16px;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 500;
+    color: #333333;
+    line-height: 22px;
+    text-align: center;
+    padding-bottom: 12px;
+  }
+
+  .content {
+    font-size: 14px;
+    line-height: 20px;
+
+    img,
+    video {
+      width: 100% !important;
+    }
+  }
+}

+ 78 - 0
src/views/information/instrument-detail/index.tsx

@@ -0,0 +1,78 @@
+import MHeader from '@/components/m-header';
+import MSticky from '@/components/m-sticky';
+import { defineComponent, onMounted, reactive } from 'vue';
+import { useEventListener, useWindowScroll } from '@vueuse/core';
+import styles from './index.module.less';
+import { Image } from 'vant';
+import request from '@/helpers/request';
+import { useRoute } from 'vue-router';
+import AudioPlayer from '../components/audio-player';
+import { state } from '@/state';
+
+export default defineComponent({
+  name: 'instrument-detail',
+  setup() {
+    const route = useRoute();
+    const forms = reactive({
+      titleOpacity: 0,
+      background: 'transparent',
+      color: '#fff',
+      detail: {} as any,
+      musicList: [] as any
+    });
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get(
+          '/edu-app/knowledgeWiki/detail/' + route.query.id
+        );
+        data.intros = data.intros.replace(
+          /<video/gi,
+          '<video style="width: 100% !important;" controlslist="nodownload"'
+        );
+        forms.detail = data || {};
+        forms.musicList = data.knowledgeWikiResources.map((item: any) => {
+          return {
+            id: item.id,
+            name: item.name,
+            url: item.url
+          };
+        });
+      } catch {}
+    };
+    onMounted(() => {
+      useEventListener(document, 'scroll', () => {
+        const { y } = useWindowScroll();
+        forms.titleOpacity = y.value > 100 ? 1 : y.value / 100;
+      });
+
+      getDetail();
+    });
+    return () => (
+      <div class={styles.detail}>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            background={`rgba(255,255,255, ${forms.titleOpacity})`}
+            color={`rgba(51,51,51, ${forms.titleOpacity})`}
+            title={forms.detail.name || ''}></MHeader>
+        </MSticky>
+
+        <div class={styles.container}>
+          <div class={styles.instrumentLogo}>
+            <Image class={styles.logo} fit="cover" src={forms.detail.avatar} />
+          </div>
+
+          <div class={styles.title}>{forms.detail.name}</div>
+          <div class={styles.content} v-html={forms.detail.intros}></div>
+        </div>
+
+        <MSticky position="bottom">
+          {forms.musicList.length > 0 && (
+            <AudioPlayer musicList={forms.musicList} />
+          )}
+        </MSticky>
+      </div>
+    );
+  }
+});

+ 83 - 0
src/views/information/musician-detail/index.module.less

@@ -0,0 +1,83 @@
+.detail {
+  min-height: 100vh;
+  // background: url('../images/banner1.png') no-repeat top center;
+  // background-size: contain;
+  background-color: #F8F9FC;
+  // backdrop-filter: blur(11px);
+  // filter: blur(11px);
+  position: relative;
+  z-index: 9;
+
+  .bgSection {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 315px;
+
+    .bg {
+      width: 100%;
+      height: 315px;
+      object-fit: cover;
+    }
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      background: rgba(255, 255, 255, 0.58);
+      backdrop-filter: blur(11px);
+    }
+  }
+
+}
+
+.container {
+  position: relative;
+  background: #fff;
+  border-radius: 16px;
+  margin: 48px 13px 16px;
+  padding: 55px 14px 20px;
+
+  .instrumentLogo {
+    position: absolute;
+    left: 50%;
+    margin-left: -45px;
+    top: -3px;
+    width: 90px;
+    height: 112px;
+    background: #FFFFFF;
+    border: 2px solid #E0E0E0;
+    border-radius: 50%;
+    overflow: hidden;
+    margin-top: -60px;
+
+    .logo {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  .title {
+    font-size: 16px;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 500;
+    color: #333333;
+    line-height: 22px;
+    text-align: center;
+    padding-bottom: 12px;
+  }
+
+  .content {
+    font-size: 14px;
+    line-height: 20px;
+
+    img,
+    video {
+      width: 100% !important;
+    }
+  }
+}

+ 81 - 0
src/views/information/musician-detail/index.tsx

@@ -0,0 +1,81 @@
+import MHeader from '@/components/m-header';
+import MSticky from '@/components/m-sticky';
+import { defineComponent, onMounted, reactive } from 'vue';
+import { useEventListener, useWindowScroll } from '@vueuse/core';
+import styles from './index.module.less';
+import { Image } from 'vant';
+import request from '@/helpers/request';
+import { useRoute } from 'vue-router';
+import AudioPlayer from '../components/audio-player';
+import banner from '../images/banner1.png';
+
+export default defineComponent({
+  name: 'instrument-detail',
+  setup() {
+    const route = useRoute();
+    const forms = reactive({
+      titleOpacity: 0,
+      background: 'transparent',
+      color: '#fff',
+      detail: {} as any,
+      musicList: [] as any
+    });
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get(
+          '/edu-app/knowledgeWiki/detail/' + route.query.id
+        );
+        data.intros = data.intros.replace(
+          /<video/gi,
+          '<video style="width: 100% !important;" controlslist="nodownload"'
+        );
+        forms.detail = data || {};
+        forms.musicList = data.knowledgeWikiResources.map((item: any) => {
+          return {
+            id: item.id,
+            name: item.name,
+            url: item.url
+          };
+        });
+      } catch {}
+    };
+    onMounted(() => {
+      useEventListener(document, 'scroll', () => {
+        const { y } = useWindowScroll();
+        forms.titleOpacity = y.value > 100 ? 1 : y.value / 100;
+      });
+
+      getDetail();
+    });
+    return () => (
+      <div class={styles.detail}>
+        <div class={styles.bgSection}>
+          <img class={styles.bg} src={forms.detail.avatar || banner} />
+        </div>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            background={`rgba(255,255,255, ${forms.titleOpacity})`}
+            color={`rgba(51,51,51, ${forms.titleOpacity})`}
+            title={forms.detail.name || ''}></MHeader>
+        </MSticky>
+
+        <div class={styles.container}>
+          <div class={styles.instrumentLogo}>
+            <Image class={styles.logo} fit="cover" src={forms.detail.avatar} />
+          </div>
+
+          <div class={styles.title}>{forms.detail.name}</div>
+          <div class={styles.content} v-html={forms.detail.intros}></div>
+        </div>
+
+        <MSticky position="bottom">
+          {forms.musicList.length > 0 && (
+            <AudioPlayer musicList={forms.musicList} />
+          )}
+        </MSticky>
+      </div>
+    );
+  }
+});