2025-08-21 14:11:58 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="audio-player">
|
|
|
|
|
<div class="title">远程扬声器</div>
|
|
|
|
|
<div class="audio-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(audio, index) in audioList"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="audio-item"
|
|
|
|
|
:class="{ 'active': index === currentIndex }"
|
|
|
|
|
@click="playAudio(index)"
|
|
|
|
|
>
|
|
|
|
|
{{ audio.name }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="controls">
|
|
|
|
|
<el-button @click="playPrevious" :disabled="currentIndex === 0" circle size="small">
|
|
|
|
|
<svg :class="`icon ${currentIndex === 0 ? 'disabled-icon' : ''}`" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="20">
|
|
|
|
|
<path d="M362.3 512l445-332.3v664.5L362.3 512zM216.7 179.7h80v664.5h-80V179.7z" fill="" />
|
|
|
|
|
</svg>
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="togglePlay" circle>
|
|
|
|
|
<svg v-if="isPlaying" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
|
|
|
|
<path d="M512 1024C228.266667 1024 0 795.733333 0 512S228.266667 0 512 0s512 228.266667 512 512-228.266667 512-512 512z m0-42.666667c260.266667 0 469.333333-209.066667 469.333333-469.333333S772.266667 42.666667 512 42.666667 42.666667 251.733333 42.666667 512s209.066667 469.333333 469.333333 469.333333z m-106.666667-682.666666c12.8 0 21.333333 8.533333 21.333334 21.333333v384c0 12.8-8.533333 21.333333-21.333334 21.333333s-21.333333-8.533333-21.333333-21.333333V320c0-12.8 8.533333-21.333333 21.333333-21.333333z m213.333334 0c12.8 0 21.333333 8.533333 21.333333 21.333333v384c0 12.8-8.533333 21.333333-21.333333 21.333333s-21.333333-8.533333-21.333334-21.333333V320c0-12.8 8.533333-21.333333 21.333334-21.333333z" fill="#666666" fill-opacity=".9" />
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
|
|
|
|
<path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 981.333333C253.866667 981.333333 42.666667 770.133333 42.666667 512S253.866667 42.666667 512 42.666667s469.333333 211.2 469.333333 469.333333-211.2 469.333333-469.333333 469.333333z" fill="#666666" />
|
|
|
|
|
<path d="M672 441.6l-170.666667-113.066667c-57.6-38.4-106.666667-12.8-106.666666 57.6v256c0 70.4 46.933333 96 106.666666 57.6l170.666667-113.066666c57.6-42.666667 57.6-106.666667 0-145.066667z" fill="#666666" />
|
|
|
|
|
</svg>
|
|
|
|
|
</el-button>
|
|
|
|
|
<el-button @click="playNext" :disabled="currentIndex === audioList.length - 1" circle size="small">
|
|
|
|
|
<svg :class="`icon ${currentIndex === audioList.length - 1 ? 'disabled-icon' : ''}`" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="20">
|
|
|
|
|
<path d="M216.7 844.3V179.7l445 332.3-445 332.3z m590.6 0h-80V179.7h80v664.6z" fill="" />
|
|
|
|
|
</svg>
|
|
|
|
|
</el-button>
|
|
|
|
|
<div class="volume-control">
|
|
|
|
|
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
|
|
|
|
<path d="M448 938.666667a21.333333 21.333333 0 0 1-15.093333-6.246667L225.833333 725.333333H53.333333a53.393333 53.393333 0 0 1-53.333333-53.333333V352a53.393333 53.393333 0 0 1 53.333333-53.333333h172.5l207.08-207.086667A21.333333 21.333333 0 0 1 469.333333 106.666667v810.666666a21.333333 21.333333 0 0 1-21.333333 21.333334zM53.333333 341.333333a10.666667 10.666667 0 0 0-10.666666 10.666667v320a10.666667 10.666667 0 0 0 10.666666 10.666667h181.333334a21.333333 21.333333 0 0 1 15.086666 6.246666L426.666667 865.833333V158.166667L249.753333 335.086667A21.333333 21.333333 0 0 1 234.666667 341.333333z m750.48 586.553334a21.333333 21.333333 0 0 1-12.726666-38.466667 474.853333 474.853333 0 0 0 52.78-45.553333c182.993333-182.993333 182.993333-480.74 0-663.733334a474.246667 474.246667 0 0 0-52.78-45.553333 21.333333 21.333333 0 0 1 25.42-34.273333 518.346667 518.346667 0 0 1 57.533333 49.653333 511.606667 511.606667 0 0 1 0 724.08 519.026667 519.026667 0 0 1-57.54 49.653333 21.22 21.22 0 0 1-12.686667 4.193334z m-86.213333-149.333334a21.333333 21.333333 0 0 1-13.733333-37.666666c6.666667-5.586667 13.146667-11.553333 19.333333-17.726667C779.6 666.78 810.666667 591.78 810.666667 512s-31.066667-154.78-87.48-211.186667c-6.173333-6.173333-12.666667-12.14-19.333334-17.726666a21.333333 21.333333 0 1 1 27.446667-32.666667 346.585333 346.585333 0 0 1 22.046667 20.213333 341.066667 341.066667 0 0 1 0 482.72 346.585333 346.585333 0 0 1-22.046667 20.213334 21.24 21.24 0 0 1-13.7 5.013333zM629.333333 625.72a21.333333 21.333333 0 0 1-16.733333-34.546667 127.366667 127.366667 0 0 0 0-158.346666 21.333333 21.333333 0 0 1 33.486667-26.433334 170.733333 170.733333 0 0 1 0 211.213334A21.333333 21.333333 0 0 1 629.333333 625.72z" fill="#5C5C66" />
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
min="0"
|
|
|
|
|
max="100"
|
|
|
|
|
step="1"
|
|
|
|
|
v-model.number="volume"
|
|
|
|
|
@input="debouncedSetVolume"
|
|
|
|
|
/>
|
|
|
|
|
<span>{{ Math.round(volume) }}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
2026-08-05 09:27:55 +08:00
|
|
|
import { useRoute } from 'vue-router';
|
2025-08-21 14:11:58 +08:00
|
|
|
import {
|
|
|
|
|
pause,
|
|
|
|
|
play,
|
|
|
|
|
resume,
|
|
|
|
|
status,
|
|
|
|
|
stop,
|
|
|
|
|
getVolume,
|
|
|
|
|
setVolume as setVolumeApi,
|
|
|
|
|
getAll
|
|
|
|
|
} from '@/api/device/speaker';
|
|
|
|
|
|
|
|
|
|
const audioList = ref([
|
|
|
|
|
{ name: 'Track 1', path: '/audio/track1.mp3' },
|
|
|
|
|
{ name: 'Track 2', path: '/audio/track2.mp3' },
|
|
|
|
|
{ name: 'Track 3', path: '/audio/track3.mp3' },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const currentIndex = ref(0);
|
|
|
|
|
const isPlaying = ref(false);
|
|
|
|
|
const volume = ref(0);
|
|
|
|
|
|
2026-08-05 09:27:55 +08:00
|
|
|
const route = useRoute();
|
|
|
|
|
const robotId = ref(String(route.query.robotId || ''));
|
|
|
|
|
const deviceId = ref(String(route.query.deviceId || ''));
|
2025-08-21 14:11:58 +08:00
|
|
|
|
|
|
|
|
const getCommonParams = () => ({
|
2026-08-05 09:27:55 +08:00
|
|
|
robotId: robotId.value,
|
2025-08-21 14:11:58 +08:00
|
|
|
deviceId: deviceId.value
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const debounce = (func, delay) => {
|
|
|
|
|
let timeoutId;
|
|
|
|
|
return (...args) => {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
timeoutId = setTimeout(() => func.apply(null, args), delay);
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
let pollingTimeout;
|
|
|
|
|
|
|
|
|
|
const checkStatus = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await status(getCommonParams());
|
|
|
|
|
isPlaying.value = response.data.state.isRunning && !response.data.state.isPaused;
|
|
|
|
|
if (isPlaying.value && !response.data.state.isPaused) {
|
|
|
|
|
// 完成后再次调度
|
|
|
|
|
pollingTimeout = setTimeout(checkStatus, 1000); // 5秒间隔
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('状态检查失败:', error);
|
|
|
|
|
// 即使出错,也继续轮询
|
|
|
|
|
// pollingTimeout = setTimeout(checkStatus, 1000);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const allResponse = await getAll();
|
|
|
|
|
audioList.value = allResponse.data.map(item => ({
|
|
|
|
|
name: item.split('.')[0],
|
|
|
|
|
path: `/home/share/assets/upload/${item}`
|
|
|
|
|
}));
|
|
|
|
|
await initialize();
|
|
|
|
|
// 启动轮询
|
|
|
|
|
checkStatus();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await stop(getCommonParams());
|
|
|
|
|
if (pollingTimeout) {
|
|
|
|
|
clearTimeout(pollingTimeout); // 清除轮询
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('清理失败:', error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const initialize = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const volumeResponse = await getVolume({ ...getCommonParams() });
|
|
|
|
|
volume.value = volumeResponse.data || 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('初始化状态失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const playAudio = async (index) => {
|
|
|
|
|
currentIndex.value = index;
|
|
|
|
|
isPlaying.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await play({ ...getCommonParams(), audioPath: audioList.value[index].path });
|
|
|
|
|
checkStatus();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('播放失败:', error);
|
|
|
|
|
isPlaying.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const playPrevious = async () => {
|
|
|
|
|
if (currentIndex.value > 0) {
|
|
|
|
|
await playAudio(currentIndex.value - 1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const playNext = async () => {
|
|
|
|
|
if (currentIndex.value < audioList.value.length - 1) {
|
|
|
|
|
await playAudio(currentIndex.value + 1);
|
|
|
|
|
} else {
|
|
|
|
|
await playAudio(0);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const togglePlay = async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (isPlaying.value) {
|
|
|
|
|
await pause(getCommonParams());
|
|
|
|
|
} else {
|
|
|
|
|
await resume(getCommonParams());
|
|
|
|
|
}
|
|
|
|
|
isPlaying.value = !isPlaying.value;
|
|
|
|
|
checkStatus();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('操作失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setVolume = async () => {
|
|
|
|
|
try {
|
|
|
|
|
await setVolumeApi({ ...getCommonParams(), volume: volume.value });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('音量设置失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const debouncedSetVolume = debounce(setVolume, 500);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.audio-player {
|
2025-09-05 14:52:28 +08:00
|
|
|
display: flex;
|
|
|
|
|
flex: 1;
|
|
|
|
|
flex-direction: column;
|
2025-08-21 14:11:58 +08:00
|
|
|
max-width: 800px;
|
2025-09-05 14:52:28 +08:00
|
|
|
width: 100%;
|
2025-08-21 14:11:58 +08:00
|
|
|
height: calc(100vh - 125px);
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
margin: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-list {
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
max-height: calc(100% - 130px);
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
border-bottom: 1px solid #ddd;
|
|
|
|
|
border-top: 1px solid #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-item {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.audio-item:hover,
|
|
|
|
|
.audio-item.active {
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:disabled {
|
|
|
|
|
background-color: rgba(0, 0, 0, 0);
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
button:hover:not(:disabled) {
|
|
|
|
|
background-color: #eee;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.volume-control {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
input[type="range"] {
|
|
|
|
|
width: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.disabled-icon {
|
|
|
|
|
fill: #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 09:27:55 +08:00
|
|
|
</style>
|