| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785 |
- <template>
- <a-modal
- class="audio-preview-wrapper"
- v-model:visible="visible"
- :footer="null"
- :closable="false"
- :maskClosable="false"
- :width="'100%'"
- :bodyStyle="{ padding: 0 }"
- :centered="true"
- >
- <img class="audio-background" :src="musicImgUrl" alt="背景图" />
- <!-- 右上角操作 -->
- <div class="operate-box">
- <a-tooltip placement="bottom">
- <template #title>
- <div style="line-height: 2">
- 操作提示: <br />
- 1. 按 Esc 键可退出查看;<br />
- 2. 支持键盘控制:<br />
- 空格 - 暂停/播放<br />
- 左方向键 - 播放上一个<br />
- 右方向键 - 播放下一个<br />
- 上方向键 - 音量调大<br />
- 下方向键 - 音量减小<br />
- </div>
- </template>
- <question-circle-outlined class="tip-icon" />
- </a-tooltip>
- <close-outlined class="close-icon" title="关闭(Escape)" @click="handleClosePreview" />
- </div>
- <audio
- ref="audioRef"
- :src="activeFileObj.fileUrl"
- controls
- style="position: fixed; top: 0; left: 0; display: none"
- @loadedmetadata="handleLoadedmetadata"
- @timeupdate="handleTimeUpdate"
- @ended="handleChangeAudioIndex('next')"
- />
- <div class="audio-list-wrapper">
- <!-- 音频列表 -->
- <ul class="audio-list">
- <li class="audio-list-header">
- <span class="name">音频名称</span>
- <span class="audio-size">大小</span>
- <span class="path">路径</span>
- </li>
- <div class="audio-list-body">
- <li
- v-for="(item, index) in audioList"
- :key="index"
- class="audio-item"
- :class="{ active: activeIndex === index }"
- :title="isPlay ? '暂停' : '播放'"
- @click="handleChangeAudioIndex('manual', index)"
- >
- <span class="name">
- <span class="sequence" v-show="activeIndex !== index">
- {{ index + 1 }}
- </span>
- <img class="wave" :src="activePlayIcon" alt="波浪动图" v-show="activeIndex === index && isPlay" />
- <i class="no-wave el-icon-s-data" v-show="activeIndex === index && !isPlay"></i>
- <span class="text">{{ item.fileName }}.{{ item.extendName }}</span>
- </span>
- <i class="play-icon iconfont icon-icon-7" v-show="activeIndex === index && !isPlay"></i>
- <i class="pause-icon iconfont icon-icon-3" v-show="activeIndex === index && isPlay"></i>
- <a class="download" :href="$file.getDownloadFilePath(item)" target="_blank" title="下载">
- <i class="download-icon el-icon-download"></i>
- </a>
- <i
- class="share-icon el-icon-share"
- title="分享"
- @click.stop="
- $openDialog.shareFile({
- fileInfo: [
- {
- userFileId: item.userFileId
- }
- ]
- })
- "
- ></i>
- <span class="audio-size">{{ $file.calculateFileSize(item.fileSize) }}</span>
- <span class="path">{{ item.filePath }}</span>
- </li>
- </div>
- </ul>
- <!-- 歌曲图片和歌词 -->
- <div class="img-and-lyrics">
- <img class="audio-img" :src="musicImgUrl" alt="歌曲图片" />
- <div class="audio-name">{{ activeFileObj.fileName }}.{{ activeFileObj.extendName }}</div>
- <div class="album-artist" v-show="audioInfo.artist">歌手:{{ audioInfo.artist }}</div>
- <div class="album-name" v-if="audioInfo.album">专辑:{{ audioInfo.album }}</div>
- <ul class="lyrics-list" ref="lyricsListRef" :class="{ one: lyricsList.length === 1 }" v-if="lyricsList.length">
- <li
- class="lyrics-item"
- ref="lyricsLineRef"
- v-for="(item, index) in lyricsList"
- :key="index"
- :class="{
- active: currentLyricsLineIndex === index
- }"
- @click="handleChangeProgress(transferTimeToSeconds(item.time))"
- >
- {{ item.text }}
- </li>
- </ul>
- </div>
- </div>
- <!-- 底部音乐控件 -->
- <div class="control-wrapper">
- <div class="control-left">
- <step-backward-outlined
- class="operate-icon"
- title="上一个(按左方向键)"
- @click="handleChangeAudioIndex('pre')"
- />
- <play-circle-outlined
- v-if="!isPlay"
- class="operate-icon"
- title="播放(按空格键)"
- @click="handleClickPlayIcon"
- />
- <pause-circle-outlined v-else class="operate-icon" title="暂停(按空格键)" @click="handleClickPauseIcon" />
- <step-forward-outlined
- class="operate-icon"
- title="下一个(按右方向键)"
- @click="handleChangeAudioIndex('next')"
- />
- <a-slider
- class="progress-bar control-item"
- v-model:value="currentTime"
- :step="progressStep"
- :max="audioInfo.duration"
- :tooltip-visible="true"
- :tip-formatter="(val) => transferSecondsToTime(val)"
- @mousedown="isDrop = true"
- @mouseup="isDrop = false"
- @change="handleChangeProgress"
- />
- <span class="time control-item">
- {{ transferSecondsToTime(currentTime) }} / {{ transferSecondsToTime(audioInfo.duration) }}
- </span>
- </div>
- <div class="control-right">
- <i
- class="operate-icon cycle-type iconfont"
- :class="cycleTypeMap[String(cycleType)].icon"
- :title="cycleTypeMap[String(cycleType)].text"
- @click="handleChangeCycleType"
- ></i>
- <a
- class="operate-icon download-link"
- :href="$file.getDownloadFilePath(activeFileObj)"
- target="_blank"
- title="下载"
- >
- <i class="download-icon el-icon-download"></i>
- </a>
- <i
- class="operate-icon share-icon el-icon-share"
- title="分享"
- @click.stop="
- $openDialog.shareFile({
- fileInfo: [
- {
- userFileId: item.userFileId
- }
- ]
- })
- "
- ></i>
- <i
- class="operate-icon volume-icon control-item iconfont"
- :class="volume === 0 ? 'icon-jingyin01' : 'icon-yinliang101'"
- @click="handleClickVolumeIcon"
- ></i>
- <el-slider
- class="volume-bar control-item"
- v-model="volume"
- :step="0.01"
- :max="1"
- :format-tooltip="(val) => Math.floor(val * 100)"
- height="100px"
- title="可按上下方向键调节音量"
- @input="handleChangeVolumeBar"
- ></el-slider>
- </div>
- </div>
- </a-modal>
- </template>
- <script setup>
- import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
- import { message } from 'ant-design-vue'
- import {
- QuestionCircleOutlined,
- CloseOutlined,
- StepBackwardOutlined,
- StepForwardOutlined,
- PlayCircleOutlined,
- PauseCircleOutlined
- } from '@ant-design/icons-vue'
- import { getFileDetail } from '@/api/myResource/file'
- import { Base64 } from 'js-base64'
- const props = defineProps({
- visible: {
- type: Boolean,
- default: false
- },
- audioList: {
- type: Array,
- default: () => []
- },
- defaultIndex: {
- type: Number,
- default: 0
- },
- callback: {
- type: Function,
- default: () => {}
- }
- })
- const emit = defineEmits(['update:visible'])
- const visible = ref(props.visible)
- const activeIndex = ref(0)
- const isPlay = ref(false)
- const currentTime = ref(0)
- const isDrop = ref(false)
- const volume = ref(0)
- const audioInfo = ref({})
- const lyricsList = ref([])
- const currentLyricsLineIndex = ref(0)
- const activeFileObj = computed(() => {
- const res = props.audioList.length ? props.audioList[activeIndex.value] : {}
- return res
- })
- const audioElement = computed(() => {
- return refs.audioRef
- })
- const musicImgUrl = computed(() => {
- return audioInfo.value.albumImage
- ? `data:image/jpeg;base64,${audioInfo.value.albumImage}`
- : require('_a/images/file/file_music.png')
- })
- const progressStep = computed(() => {
- return audioInfo.value.duration / 100
- })
- watch(
- () => props.visible,
- (newValue) => {
- visible.value = newValue
- if (newValue) {
- activeIndex.value = props.defaultIndex
- getFileDetailData()
- document.addEventListener('keyup', handleAddKeyupEvent)
- } else {
- document.removeEventListener('keyup', handleAddKeyupEvent)
- }
- }
- )
- watch(activeIndex, () => {
- getFileDetailData()
- })
- function handleAddKeyupEvent(event) {
- switch (event.code) {
- // 关闭预览
- case 'Escape': {
- handleClosePreview()
- break
- }
- // 切换到上一个
- case 'ArrowLeft': {
- handleChangeAudioIndex('pre')
- break
- }
- // 切换到下一个
- case 'ArrowRight': {
- handleChangeAudioIndex('next')
- break
- }
- // 音量调大
- case 'ArrowUp': {
- volume.value = volume.value === 1 ? 1 : volume.value + 0.1
- volume.value = Number(volume.value.toFixed(1))
- handleChangeVolumeBar(volume.value)
- break
- }
- // 音量调小
- case 'ArrowDown': {
- volume.value = volume.value === 0 ? 0 : volume.value - 0.1
- volume.value = Number(volume.value.toFixed(1))
- handleChangeVolumeBar(volume.value)
- break
- }
- // 暂停/播放
- case 'Space': {
- handleChangeAudioIndex('manual', activeIndex.value)
- break
- }
- }
- }
- function getFileDetailData() {
- handleClickPauseIcon()
- loading.value = true
- getFileDetail({ userFileId: activeFileObj.value.userFileId })
- .then((res) => {
- loading.value = false
- if (res.success) {
- audioInfo.value = {
- ...res.data.music,
- duration: res.data.music.trackLength
- }
- // Base64 解码为 lrc 格式的歌词文件
- let lyricsStr = Base64.decode(audioInfo.value.lyrics)
- if (lyricsStr.includes('[offset:0]')) {
- // 有歌词,从标志位 [offset:0] 下一行开始截取
- lyricsStr = lyricsStr.split('[offset:0]\n')[1]
- }
- lyricsList.value = lyricsStr
- .split('\n')
- .map((item) => {
- const line = item.split('[')[1].split(']')
- return {
- time: line[0], // 当前行歌词开始播放的秒数
- text: line[1] // 当前歌词文本
- }
- })
- .filter((item) => item.text !== '')
- lyricsList.value = lyricsList.value.map((item, index) => {
- return {
- ...item,
- // 当前行歌词起始秒数
- startSeconds: transferTimeToSeconds(item.time),
- // 当前行歌词结束秒数
- endSeconds:
- index < lyricsList.value.length - 1
- ? transferTimeToSeconds(lyricsList.value[index + 1].time)
- : audioInfo.value.duration
- }
- })
- // 当切换完歌曲时,歌词重新滚动到顶部
- refs.lyricsListRef.scrollTo({
- top: 0,
- behavior: 'smooth'
- })
- currentLyricsLineIndex.value = 0
- }
- })
- .catch(() => {
- loading.value = false
- })
- }
- function handleLoadedmetadata(event) {
- const audioDom = event.target
- volume.value = audioDom.volume || 0.5
- currentTime.value = audioDom.currentTime
- handleClickPlayIcon()
- }
- function transferSecondsToTime(duration) {
- const hour = Math.floor(duration / 3600)
- const minutes = Math.floor(duration / 60)
- const seconds = Math.ceil(duration % 60)
- return `${hour < 10 ? `0${hour}` : hour}:${minutes < 10 ? `0${minutes}` : minutes}:${
- seconds < 10 ? `0${seconds}` : seconds
- }`
- }
- function transferTimeToSeconds(time) {
- const timeList = time.split('.')[0].split(':')
- return Number(timeList[1]) + Number(timeList[0]) * 60
- }
- function handleTimeUpdate(event) {
- // 如果正在拖拽进度滑块,函数结束,不计算当前时间
- if (isDrop.value) return
- currentTime.value = event.target.currentTime
- if (lyricsList.value.length) {
- // 遍历歌词,当前秒对应的歌词整行添加高亮效果
- lyricsList.value.forEach((item, index) => {
- if (
- item.startSeconds <= currentTime.value &&
- currentTime.value < item.endSeconds &&
- currentLyricsLineIndex.value !== index
- ) {
- // 确定高亮歌词行索引
- currentLyricsLineIndex.value = index
- // 使高亮歌词行永远保持在第二行
- if (currentLyricsLineIndex.value > 2) {
- // 平滑滚动
- refs.lyricsListRef.scrollTo({
- top: refs.lyricsLineRef[index].clientHeight * (index - 2),
- behavior: 'smooth'
- })
- }
- }
- })
- }
- }
- function handleChangeProgress(progress) {
- audioElement.value.currentTime = progress
- isDrop.value = false
- }
- function handleChangeCycleType() {
- if (cycleType.value === 3) {
- cycleType.value = 1
- } else if (cycleType.value >= 1) {
- cycleType.value++
- }
- }
- function handleClickPlayIcon() {
- isPlay.value = true
- audioElement.value.play()
- }
- function handleClickPauseIcon() {
- isPlay.value = false
- audioElement.value.pause()
- }
- function handleChangeAudioIndex(type, index) {
- // 如果当前手动切换
- if (type === 'manual') {
- if (activeIndex.value === index) {
- if (isPlay.value) {
- handleClickPauseIcon()
- } else {
- handleClickPlayIcon()
- }
- } else {
- activeIndex.value = index
- }
- } else {
- handleClickPauseIcon()
- // 判断当前循环播放类型
- switch (cycleType.value) {
- case 3: {
- let activeIndex = 0
- do {
- activeIndex = Math.floor(Math.random() * (props.audioList.length - 1)) + 1
- } while (activeIndex === activeIndex.value)
- activeIndex.value = activeIndex
- break
- }
- default: {
- if (type === 'pre') {
- if (activeIndex.value === 0) {
- activeIndex.value = props.audioList.length - 1
- } else {
- activeIndex.value--
- }
- } else if (type === 'next') {
- if (activeIndex.value === props.audioList.length - 1) {
- activeIndex.value = 0
- } else {
- activeIndex.value++
- }
- }
- break
- }
- }
- }
- }
- function handleClickVolumeIcon() {
- volume.value = volume.value === 0 ? 0.5 : 0
- handleChangeVolumeBar(volume.value)
- }
- function handleChangeVolumeBar(volume) {
- audioElement.value.volume = Number(volume.toFixed(1))
- }
- // 关闭音频预览
- function handleClosePreview() {
- visible.value = false
- props.callback('cancel')
- }
- onMounted(() => {
- visible.value = props.visible
- activeIndex.value = props.defaultIndex
- })
- onUnmounted(() => {
- document.removeEventListener('keyup', handleAddKeyupEvent)
- })
- defineExpose({
- visible
- })
- </script>
- <style lang="less" scoped>
- @import '@/style/myResource/varibles.less';
- @import '@/style/myResource/mixins.less';
- .audio-preview-wrapper {
- background: @PrimaryText;
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- z-index: 3;
- color: @BorderBase;
- .audio-background {
- position: fixed;
- top: -50%;
- left: 0;
- width: 100vw;
- height: auto;
- filter: blur(65px);
- opacity: 0.6;
- z-index: -1;
- }
- .operate-box {
- position: fixed;
- top: 16px;
- right: 32px;
- display: flex;
- align-items: center;
- .tip-icon,
- .close-icon {
- margin-left: 16px;
- cursor: pointer;
- &:hover {
- color: @Warning;
- }
- }
- .tip-icon {
- font-size: 26px;
- }
- .close-icon {
- font-size: 30px;
- }
- }
- .audio-list-wrapper {
- margin: 0 auto;
- width: 85%;
- height: calc(100vh - 120px);
- padding-top: 32px;
- display: flex;
- justify-content: space-between;
- .audio-list {
- flex: 1;
- list-style: none;
- .audio-list-body {
- height: calc(100% - 56px);
- overflow: auto;
- .setScrollbar(8px, transparent, rgba(0, 0, 0, 0.3));
- }
- .audio-list-header,
- .audio-item {
- border-radius: 8px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 56px;
- cursor: pointer;
- padding: 0 16px;
- &:hover {
- background: rgba(0, 0, 0, 0.1);
- }
- &.active {
- background: rgba(0, 0, 0, 0.1);
- color: @Warning;
- }
- .name {
- flex: 1;
- .sequence {
- display: inline-block;
- margin-right: 8px;
- width: 14px;
- text-align: center;
- }
- .wave {
- margin-right: 10px;
- width: 12px;
- height: 12px;
- }
- .no-wave {
- margin-right: 6px;
- font-size: 16px;
- }
- }
- .play-icon,
- .pause-icon,
- .download-icon,
- .share-icon {
- margin-right: 16px;
- font-size: 22px;
- cursor: pointer;
- &:hover {
- color: @Warning;
- }
- }
- .download {
- color: inherit;
- &:hover {
- color: @Warning;
- }
- }
- .audio-size {
- width: 120px;
- padding-right: 24px;
- text-align: right;
- }
- .path {
- min-width: 120px;
- }
- }
- .audio-list-header {
- padding-right: 24px;
- .name {
- padding-left: 18px;
- }
- }
- }
- .img-and-lyrics {
- padding: 8px 0 0 16px;
- width: 340px;
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- .audio-img {
- margin-bottom: 16px;
- width: 160px;
- height: 160px;
- }
- .audio-name {
- margin-bottom: 8px;
- font-size: 18px;
- line-height: 2;
- }
- .album-artist,
- .album-name {
- margin-bottom: 8px;
- }
- .lyrics-list {
- width: 100%;
- flex: 1;
- overflow: auto;
- .setScrollbar(6px, transparent, rgba(0, 0, 0, 0.3));
- -webkit-mask-image: linear-gradient(
- 180deg,
- hsla(0, 0%, 100%, 0) 0,
- hsla(0, 0%, 100%, 0.6) 15%,
- #fff 25%,
- #fff 75%,
- hsla(0, 0%, 100%, 0.6) 85%,
- hsla(0, 0%, 100%, 0)
- );
- &.one {
- .lyrics-item {
- margin-top: 40px;
- }
- }
- .lyrics-item {
- line-height: 40px;
- cursor: pointer;
- &:not(.active):hover {
- color: #fff;
- }
- &.active {
- color: @Warning;
- }
- }
- }
- }
- }
- .control-wrapper {
- margin: 0 auto;
- width: 85%;
- height: 120px;
- padding: 24px 0 32px 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .control-left {
- flex: 1;
- height: 100%;
- display: flex;
- align-items: center;
- text-align: center;
- padding-left: 8px;
- .operate-icon {
- margin-right: 16px;
- font-size: 40px;
- cursor: pointer;
- &:hover {
- color: @Warning;
- }
- }
- .progress-bar {
- margin-right: 16px;
- flex: 1;
- // >>> .el-slider__runway {
- // height: 2px;
- // .el-slider__button-wrapper {
- // top: -17px;
- // .el-slider__button {
- // border: none;
- // }
- // }
- // .el-slider__bar {
- // height: 100%;
- // background: @Warning;
- // }
- // }
- }
- }
- .control-right {
- width: 340px;
- font-size: 24px;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 32px;
- .operate-icon {
- margin-right: 16px;
- cursor: pointer;
- &:nth-last-of-type {
- margin-right: 0;
- }
- &:hover {
- color: @Warning;
- }
- &.download-link {
- font-size: 32px;
- color: inherit;
- &:hover {
- .download-icon {
- color: @Warning;
- }
- }
- }
- }
- .volume-icon {
- margin-right: 8px;
- }
- .volume-bar {
- width: 100px;
- // >>> .el-slider__runway {
- // height: 2px;
- // .el-slider__button-wrapper {
- // top: -19px;
- // .el-slider__button {
- // border: none;
- // }
- // }
- // .el-slider__bar {
- // height: 100%;
- // background: @Warning;
- // }
- // }
- }
- }
- }
- }
- </style>
|