fix(ai): 补全音乐播放器进度交互
- 音乐列表改用 typed provide/inject + MusicSong - audioBar 绑定真实 audio 元数据,支持进度显示、拖动 seek 与切歌重载 - 同步适配 web-antd、web-ele、web-antdv-next 对齐 Vue3 管理后台 0970806dcmigration
parent
1a3de7e97a
commit
c2707a499a
|
|
@ -1,17 +1,22 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, reactive, ref } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { computed, inject, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatPast } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Image, Slider } from 'ant-design-vue';
|
import { Image, Slider } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
const currentAudioUrl = computed(() => currentSong.value.audioUrl || undefined);
|
||||||
|
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||||
const audioProgress = ref(0);
|
const audioProgress = ref(0);
|
||||||
|
const audioDuration = ref(0);
|
||||||
const audioProps = reactive<any>({
|
const audioProps = reactive<any>({
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
paused: false,
|
paused: false,
|
||||||
|
|
@ -21,6 +26,17 @@ const audioProps = reactive<any>({
|
||||||
volume: 50,
|
volume: 50,
|
||||||
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||||
|
|
||||||
|
function formatAudioTime(seconds: number) {
|
||||||
|
if (!Number.isFinite(seconds)) {
|
||||||
|
return '00:00';
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleStatus(type: string) {
|
function toggleStatus(type: string) {
|
||||||
audioProps[type] = !audioProps[type];
|
audioProps[type] = !audioProps[type];
|
||||||
if (type === 'paused' && audioRef.value) {
|
if (type === 'paused' && audioRef.value) {
|
||||||
|
|
@ -33,9 +49,40 @@ function toggleStatus(type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新播放位置 */
|
/** 更新播放位置 */
|
||||||
function audioTimeUpdate(args: any) {
|
function audioTimeUpdate() {
|
||||||
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss');
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioProgress.value = audioRef.value.currentTime;
|
||||||
|
audioProps.currentTime = formatAudioTime(audioRef.value.currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function audioLoadedMetadata() {
|
||||||
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioDuration.value = audioRef.value.duration;
|
||||||
|
audioProps.duration = formatAudioTime(audioRef.value.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProgressChange(value: number | [number, number]) {
|
||||||
|
if (!audioRef.value || Array.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioRef.value.currentTime = value;
|
||||||
|
audioProgress.value = value;
|
||||||
|
audioProps.currentTime = formatAudioTime(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentAudioUrl, () => {
|
||||||
|
audioProgress.value = 0;
|
||||||
|
audioDuration.value = 0;
|
||||||
|
audioProps.currentTime = '00:00';
|
||||||
|
audioProps.duration = '00:00';
|
||||||
|
nextTick(() => {
|
||||||
|
audioRef.value?.load();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -49,8 +96,10 @@ function audioTimeUpdate(args: any) {
|
||||||
:width="45"
|
:width="45"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div>{{ currentSong.name }}</div>
|
<div>{{ currentSong.title || '暂无音乐' }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ currentSong.singer }}</div>
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ currentSong.singer || currentSong.desc }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频controls -->
|
<!-- 音频controls -->
|
||||||
|
|
@ -74,19 +123,26 @@ function audioTimeUpdate(args: any) {
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span>{{ audioProps.currentTime }}</span>
|
<span>{{ audioProps.currentTime }}</span>
|
||||||
<Slider v-model:value="audioProgress" color="#409eff" class="!w-40" />
|
<Slider
|
||||||
|
v-model:value="audioProgress"
|
||||||
|
:max="audioDuration"
|
||||||
|
color="#409eff"
|
||||||
|
class="!w-40"
|
||||||
|
@change="handleProgressChange"
|
||||||
|
/>
|
||||||
<span>{{ audioProps.duration }}</span>
|
<span>{{ audioProps.duration }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频 -->
|
<!-- 音频 -->
|
||||||
<audio
|
<audio
|
||||||
v-bind="audioProps"
|
:src="currentAudioUrl"
|
||||||
|
:autoplay="audioProps.autoplay"
|
||||||
|
:muted="audioProps.muted"
|
||||||
ref="audioRef"
|
ref="audioRef"
|
||||||
controls
|
controls
|
||||||
v-show="!audioProps"
|
v-show="!audioProps"
|
||||||
@timeupdate="audioTimeUpdate"
|
@timeupdate="audioTimeUpdate"
|
||||||
>
|
@loadedmetadata="audioLoadedMetadata"
|
||||||
<!-- <source :src="audioUrl" /> -->
|
></audio>
|
||||||
</audio>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
import type { MusicSong } from './types';
|
||||||
|
|
||||||
import { provide, ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
|
|
@ -8,14 +9,15 @@ import { Col, Empty, Row, TabPane, Tabs } from 'ant-design-vue';
|
||||||
import audioBar from './audioBar/index.vue';
|
import audioBar from './audioBar/index.vue';
|
||||||
import songCard from './songCard/index.vue';
|
import songCard from './songCard/index.vue';
|
||||||
import songInfo from './songInfo/index.vue';
|
import songInfo from './songInfo/index.vue';
|
||||||
|
import { currentSongKey } from './types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicListIndex' });
|
defineOptions({ name: 'AiMusicListIndex' });
|
||||||
|
|
||||||
const currentType = ref('mine');
|
const currentType = ref('mine');
|
||||||
const loading = ref(false); // loading 状态
|
const loading = ref(false); // loading 状态
|
||||||
const currentSong = ref({}); // 当前音乐
|
const currentSong = ref<MusicSong>({}); // 当前音乐
|
||||||
const mySongList = ref<Recordable<any>[]>([]);
|
const mySongList = ref<MusicSong[]>([]);
|
||||||
const squareSongList = ref<Recordable<any>[]>([]);
|
const squareSongList = ref<MusicSong[]>([]);
|
||||||
|
|
||||||
function generateMusic(_formData: Recordable<any>) {
|
function generateMusic(_formData: Recordable<any>) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -45,7 +47,7 @@ function generateMusic(_formData: Recordable<any>) {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentSong(music: Recordable<any>) {
|
function setCurrentSong(music: MusicSong) {
|
||||||
currentSong.value = music;
|
currentSong.value = music;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +55,7 @@ defineExpose({
|
||||||
generateMusic,
|
generateMusic,
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('currentSong', currentSong);
|
provide(currentSongKey, currentSong);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { Image } from 'ant-design-vue';
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongCardIndex' });
|
defineOptions({ name: 'AiMusicSongCardIndex' });
|
||||||
|
|
||||||
defineProps({
|
withDefaults(defineProps<{ songInfo?: MusicSong }>(), {
|
||||||
songInfo: {
|
songInfo: () => ({}),
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['play']);
|
const emits = defineEmits(['play']);
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
|
||||||
function playSong() {
|
function playSong() {
|
||||||
emits('play');
|
emits('play');
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { Button, Card, Image } from 'ant-design-vue';
|
import { Button, Card, Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, reactive, ref } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { computed, inject, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatPast } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Image, Slider } from 'antdv-next';
|
import { Image, Slider } from 'antdv-next';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
const currentAudioUrl = computed(() => currentSong.value.audioUrl || undefined);
|
||||||
|
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||||
const audioProgress = ref(0);
|
const audioProgress = ref(0);
|
||||||
|
const audioDuration = ref(0);
|
||||||
const audioProps = reactive<any>({
|
const audioProps = reactive<any>({
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
paused: false,
|
paused: false,
|
||||||
|
|
@ -21,6 +26,17 @@ const audioProps = reactive<any>({
|
||||||
volume: 50,
|
volume: 50,
|
||||||
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||||
|
|
||||||
|
function formatAudioTime(seconds: number) {
|
||||||
|
if (!Number.isFinite(seconds)) {
|
||||||
|
return '00:00';
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleStatus(type: string) {
|
function toggleStatus(type: string) {
|
||||||
audioProps[type] = !audioProps[type];
|
audioProps[type] = !audioProps[type];
|
||||||
if (type === 'paused' && audioRef.value) {
|
if (type === 'paused' && audioRef.value) {
|
||||||
|
|
@ -33,9 +49,40 @@ function toggleStatus(type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新播放位置 */
|
/** 更新播放位置 */
|
||||||
function audioTimeUpdate(args: any) {
|
function audioTimeUpdate() {
|
||||||
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss');
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioProgress.value = audioRef.value.currentTime;
|
||||||
|
audioProps.currentTime = formatAudioTime(audioRef.value.currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function audioLoadedMetadata() {
|
||||||
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioDuration.value = audioRef.value.duration;
|
||||||
|
audioProps.duration = formatAudioTime(audioRef.value.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProgressChange(value: number | [number, number]) {
|
||||||
|
if (!audioRef.value || Array.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioRef.value.currentTime = value;
|
||||||
|
audioProgress.value = value;
|
||||||
|
audioProps.currentTime = formatAudioTime(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentAudioUrl, () => {
|
||||||
|
audioProgress.value = 0;
|
||||||
|
audioDuration.value = 0;
|
||||||
|
audioProps.currentTime = '00:00';
|
||||||
|
audioProps.duration = '00:00';
|
||||||
|
nextTick(() => {
|
||||||
|
audioRef.value?.load();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -49,8 +96,10 @@ function audioTimeUpdate(args: any) {
|
||||||
:width="45"
|
:width="45"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div>{{ currentSong.name }}</div>
|
<div>{{ currentSong.title || '暂无音乐' }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ currentSong.singer }}</div>
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ currentSong.singer || currentSong.desc }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频controls -->
|
<!-- 音频controls -->
|
||||||
|
|
@ -74,19 +123,26 @@ function audioTimeUpdate(args: any) {
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span>{{ audioProps.currentTime }}</span>
|
<span>{{ audioProps.currentTime }}</span>
|
||||||
<Slider v-model:value="audioProgress" color="#409eff" class="!w-40" />
|
<Slider
|
||||||
|
v-model:value="audioProgress"
|
||||||
|
:max="audioDuration"
|
||||||
|
color="#409eff"
|
||||||
|
class="!w-40"
|
||||||
|
@change="handleProgressChange"
|
||||||
|
/>
|
||||||
<span>{{ audioProps.duration }}</span>
|
<span>{{ audioProps.duration }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频 -->
|
<!-- 音频 -->
|
||||||
<audio
|
<audio
|
||||||
v-bind="audioProps"
|
:src="currentAudioUrl"
|
||||||
|
:autoplay="audioProps.autoplay"
|
||||||
|
:muted="audioProps.muted"
|
||||||
ref="audioRef"
|
ref="audioRef"
|
||||||
controls
|
controls
|
||||||
v-show="!audioProps"
|
v-show="!audioProps"
|
||||||
@timeupdate="audioTimeUpdate"
|
@timeupdate="audioTimeUpdate"
|
||||||
>
|
@loadedmetadata="audioLoadedMetadata"
|
||||||
<!-- <source :src="audioUrl" /> -->
|
></audio>
|
||||||
</audio>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
import type { MusicSong } from './types';
|
||||||
|
|
||||||
import { provide, ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
|
|
@ -8,14 +9,15 @@ import { Col, Empty, Row, TabPane, Tabs } from 'antdv-next';
|
||||||
import audioBar from './audioBar/index.vue';
|
import audioBar from './audioBar/index.vue';
|
||||||
import songCard from './songCard/index.vue';
|
import songCard from './songCard/index.vue';
|
||||||
import songInfo from './songInfo/index.vue';
|
import songInfo from './songInfo/index.vue';
|
||||||
|
import { currentSongKey } from './types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicListIndex' });
|
defineOptions({ name: 'AiMusicListIndex' });
|
||||||
|
|
||||||
const currentType = ref('mine');
|
const currentType = ref('mine');
|
||||||
const loading = ref(false); // loading 状态
|
const loading = ref(false); // loading 状态
|
||||||
const currentSong = ref({}); // 当前音乐
|
const currentSong = ref<MusicSong>({}); // 当前音乐
|
||||||
const mySongList = ref<Recordable<any>[]>([]);
|
const mySongList = ref<MusicSong[]>([]);
|
||||||
const squareSongList = ref<Recordable<any>[]>([]);
|
const squareSongList = ref<MusicSong[]>([]);
|
||||||
|
|
||||||
function generateMusic(_formData: Recordable<any>) {
|
function generateMusic(_formData: Recordable<any>) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -45,7 +47,7 @@ function generateMusic(_formData: Recordable<any>) {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentSong(music: Recordable<any>) {
|
function setCurrentSong(music: MusicSong) {
|
||||||
currentSong.value = music;
|
currentSong.value = music;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +55,7 @@ defineExpose({
|
||||||
generateMusic,
|
generateMusic,
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('currentSong', currentSong);
|
provide(currentSongKey, currentSong);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { Image } from 'antdv-next';
|
import { Image } from 'antdv-next';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongCardIndex' });
|
defineOptions({ name: 'AiMusicSongCardIndex' });
|
||||||
|
|
||||||
defineProps({
|
withDefaults(defineProps<{ songInfo?: MusicSong }>(), {
|
||||||
songInfo: {
|
songInfo: () => ({}),
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['play']);
|
const emits = defineEmits(['play']);
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
|
||||||
function playSong() {
|
function playSong() {
|
||||||
emits('play');
|
emits('play');
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { Button, Card, Image } from 'antdv-next';
|
import { Button, Card, Image } from 'antdv-next';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, reactive, ref } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { computed, inject, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatPast } from '@vben/utils';
|
|
||||||
|
|
||||||
import { ElImage, ElSlider } from 'element-plus';
|
import { ElImage, ElSlider } from 'element-plus';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
defineOptions({ name: 'AiMusicAudioBarIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
const currentAudioUrl = computed(() => currentSong.value.audioUrl || undefined);
|
||||||
|
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||||
const audioProgress = ref(0);
|
const audioProgress = ref(0);
|
||||||
|
const audioDuration = ref(0);
|
||||||
const audioProps = reactive<any>({
|
const audioProps = reactive<any>({
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
paused: false,
|
paused: false,
|
||||||
|
|
@ -21,6 +26,17 @@ const audioProps = reactive<any>({
|
||||||
volume: 50,
|
volume: 50,
|
||||||
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||||
|
|
||||||
|
function formatAudioTime(seconds: number) {
|
||||||
|
if (!Number.isFinite(seconds)) {
|
||||||
|
return '00:00';
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleStatus(type: string) {
|
function toggleStatus(type: string) {
|
||||||
audioProps[type] = !audioProps[type];
|
audioProps[type] = !audioProps[type];
|
||||||
if (type === 'paused' && audioRef.value) {
|
if (type === 'paused' && audioRef.value) {
|
||||||
|
|
@ -33,9 +49,40 @@ function toggleStatus(type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新播放位置 */
|
/** 更新播放位置 */
|
||||||
function audioTimeUpdate(args: any) {
|
function audioTimeUpdate() {
|
||||||
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss');
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioProgress.value = audioRef.value.currentTime;
|
||||||
|
audioProps.currentTime = formatAudioTime(audioRef.value.currentTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function audioLoadedMetadata() {
|
||||||
|
if (!audioRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioDuration.value = audioRef.value.duration;
|
||||||
|
audioProps.duration = formatAudioTime(audioRef.value.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProgressChange(value: number | number[]) {
|
||||||
|
if (!audioRef.value || Array.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audioRef.value.currentTime = value;
|
||||||
|
audioProgress.value = value;
|
||||||
|
audioProps.currentTime = formatAudioTime(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentAudioUrl, () => {
|
||||||
|
audioProgress.value = 0;
|
||||||
|
audioDuration.value = 0;
|
||||||
|
audioProps.currentTime = '00:00';
|
||||||
|
audioProps.duration = '00:00';
|
||||||
|
nextTick(() => {
|
||||||
|
audioRef.value?.load();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -49,8 +96,10 @@ function audioTimeUpdate(args: any) {
|
||||||
class="!w-[45px]"
|
class="!w-[45px]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div>{{ currentSong.name }}</div>
|
<div>{{ currentSong.title || '暂无音乐' }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ currentSong.singer }}</div>
|
<div class="text-xs text-gray-400">
|
||||||
|
{{ currentSong.singer || currentSong.desc }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频controls -->
|
<!-- 音频controls -->
|
||||||
|
|
@ -74,19 +123,26 @@ function audioTimeUpdate(args: any) {
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span>{{ audioProps.currentTime }}</span>
|
<span>{{ audioProps.currentTime }}</span>
|
||||||
<ElSlider v-model="audioProgress" color="#409eff" class="!w-40" />
|
<ElSlider
|
||||||
|
v-model="audioProgress"
|
||||||
|
:max="audioDuration"
|
||||||
|
color="#409eff"
|
||||||
|
class="!w-40"
|
||||||
|
@change="handleProgressChange"
|
||||||
|
/>
|
||||||
<span>{{ audioProps.duration }}</span>
|
<span>{{ audioProps.duration }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 音频 -->
|
<!-- 音频 -->
|
||||||
<audio
|
<audio
|
||||||
v-bind="audioProps"
|
:src="currentAudioUrl"
|
||||||
|
:autoplay="audioProps.autoplay"
|
||||||
|
:muted="audioProps.muted"
|
||||||
ref="audioRef"
|
ref="audioRef"
|
||||||
controls
|
controls
|
||||||
v-show="!audioProps"
|
v-show="!audioProps"
|
||||||
@timeupdate="audioTimeUpdate"
|
@timeupdate="audioTimeUpdate"
|
||||||
>
|
@loadedmetadata="audioLoadedMetadata"
|
||||||
<!-- <source :src="audioUrl" /> -->
|
></audio>
|
||||||
</audio>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
import type { MusicSong } from './types';
|
||||||
|
|
||||||
import { provide, ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
|
|
||||||
|
|
@ -8,14 +9,15 @@ import { ElCol, ElEmpty, ElRow, ElTabPane, ElTabs } from 'element-plus';
|
||||||
import audioBar from './audioBar/index.vue';
|
import audioBar from './audioBar/index.vue';
|
||||||
import songCard from './songCard/index.vue';
|
import songCard from './songCard/index.vue';
|
||||||
import songInfo from './songInfo/index.vue';
|
import songInfo from './songInfo/index.vue';
|
||||||
|
import { currentSongKey } from './types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicListIndex' });
|
defineOptions({ name: 'AiMusicListIndex' });
|
||||||
|
|
||||||
const currentType = ref('mine');
|
const currentType = ref('mine');
|
||||||
const loading = ref(false); // loading 状态
|
const loading = ref(false); // loading 状态
|
||||||
const currentSong = ref({}); // 当前音乐
|
const currentSong = ref<MusicSong>({}); // 当前音乐
|
||||||
const mySongList = ref<Recordable<any>[]>([]);
|
const mySongList = ref<MusicSong[]>([]);
|
||||||
const squareSongList = ref<Recordable<any>[]>([]);
|
const squareSongList = ref<MusicSong[]>([]);
|
||||||
|
|
||||||
function generateMusic(_formData: Recordable<any>) {
|
function generateMusic(_formData: Recordable<any>) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -45,7 +47,7 @@ function generateMusic(_formData: Recordable<any>) {
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentSong(music: Recordable<any>) {
|
function setCurrentSong(music: MusicSong) {
|
||||||
currentSong.value = music;
|
currentSong.value = music;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +55,7 @@ defineExpose({
|
||||||
generateMusic,
|
generateMusic,
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('currentSong', currentSong);
|
provide(currentSongKey, currentSong);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { ElImage } from 'element-plus';
|
import { ElImage } from 'element-plus';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongCardIndex' });
|
defineOptions({ name: 'AiMusicSongCardIndex' });
|
||||||
|
|
||||||
defineProps({
|
withDefaults(defineProps<{ songInfo?: MusicSong }>(), {
|
||||||
songInfo: {
|
songInfo: () => ({}),
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['play']);
|
const emits = defineEmits(['play']);
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
|
|
||||||
function playSong() {
|
function playSong() {
|
||||||
emits('play');
|
emits('play');
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject } from 'vue';
|
import type { MusicSong } from '../types';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
import { ElButton, ElCard, ElImage } from 'element-plus';
|
import { ElButton, ElCard, ElImage } from 'element-plus';
|
||||||
|
|
||||||
|
import { currentSongKey } from '../types';
|
||||||
|
|
||||||
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
defineOptions({ name: 'AiMusicSongInfoIndex' });
|
||||||
|
|
||||||
const currentSong = inject<any>('currentSong', {});
|
const currentSong = inject(currentSongKey, ref<MusicSong>({}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue