| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- <template>
- <div class="audio-selector">
- <!-- 未选择状态 -->
- <div
- v-if="!selectedValue"
- class="audio-empty-state"
- @click="showAudioModal = true"
- >
- <div class="empty-icon">
- <SoundOutlined />
- </div>
- <div class="empty-text empty-action">{{ placeholder || "点击选择音频" }}</div>
- <!-- <div class="empty-action">点击选择音频</div> -->
- </div>
- <!-- 已选择状态 -->
- <div
- v-else
- class="audio-selected-state"
- @click="showAudioModal = true"
- >
- <div class="selected-info">
- <div class="selected-name"><span v-if="buttonLabel">{{buttonLabel}}:</span> {{ selectedValue }}</div>
- <div class="selected-action">点击更换音频</div>
- </div>
- <div class="selected-controls">
- <PauseCircleOutlined
- v-if="currentPlayingAudio === selectedValue && isPlaying"
- class="control-btn pause-btn"
- @click.stop="pauseCurrentAudio"
- />
- <PlayCircleOutlined
- v-else
- class="control-btn play-btn"
- @click.stop="playSelectedAudio"
- />
- </div>
- </div>
- <!-- 音频选择弹窗 -->
- <a-modal
- v-model:open="showAudioModal"
- :title="`选择${buttonLabel}音频`"
- width="600px"
- :footer="null"
- @cancel="handleModalCancel"
- >
- <div class="audio-modal-content">
- <!-- 搜索框 -->
- <div class="search-section">
- <a-input
- v-model:value="searchKey"
- placeholder="搜索音频名称"
- @input="handleSearch"
- allowClear
- >
- <template #prefix>
- <SearchOutlined />
- </template>
- </a-input>
- </div>
- <!-- 音频列表 -->
- <div class="audio-list">
- <div
- v-for="audio in audioList"
- :key="audio.title"
- class="audio-item"
- :class="{ 'selected': audio.title === selectedValue }"
- @click="selectAudio(audio)"
- >
- <div class="audio-info">
- <div class="audio-name">{{ audio.title }}</div>
- <div class="audio-status">
- <!-- <span v-if="audioManager.isAudioCached(audio.url)" class="cached-tag">已缓存</span>
- <span v-else class="loading-tag">需加载</span> -->
- </div>
- </div>
- <div class="audio-controls">
- <PauseCircleOutlined
- v-if="audio.state === 'playing'"
- class="control-btn pause-btn"
- @click.stop="pauseAudio($event, audio)"
- />
- <PlayCircleOutlined
- v-else
- class="control-btn play-btn"
- @click.stop="playAudio($event, audio)"
- />
- </div>
- </div>
- <!-- 空状态 -->
- <div v-if="audioList.length === 0" class="empty-list">
- <div class="empty-icon">
- <SoundOutlined />
- </div>
- <div class="empty-text">暂无音频数据</div>
- </div>
- </div>
- </div>
- </a-modal>
- </div>
- </template>
- <script lang='ts' setup>
- import { ref, watch, onMounted, computed, nextTick } from 'vue'
- import {
- PlayCircleOutlined,
- PauseCircleOutlined,
- SoundOutlined,
- SearchOutlined
- } from '@ant-design/icons-vue'
- import { MediaController } from '@/controller'
- import { audioManager } from '@/utils/AudioManaer'
- import AudioDataManager from '@/utils/AudioDataManager'
- import { message } from 'ant-design-vue'
- interface Props {
- modelValue?: string
- placeholder?: string
- buttonLabel?: string
- }
- const props = withDefaults(defineProps<Props>(), {
- modelValue: '',
- placeholder: '请选择音频',
- buttonLabel: ''
- })
- const emits = defineEmits<{
- 'update:modelValue': [value: string]
- 'search': [text: string]
- 'change': [value: string]
- 'blur': []
- }>()
- const selectedValue = computed({
- get: () => props.modelValue,
- set: (value) => emits('update:modelValue', value)
- })
- // 弹窗状态
- const showAudioModal = ref(false)
- const searchKey = ref('')
- const audioList = ref<API.Audio[]>([])
- // 播放状态
- const currentPlayingAudio = ref('')
- const isPlaying = ref(false)
- // 使用音频数据管理器
- const audioDataManager = AudioDataManager.getInstance()
- // 获取音频列表
- const getAudioList = async () => {
- // try {
- if (!searchKey.value) {
- const cachedData = await audioDataManager.getAudioList({ page: 1, key: '' })
- audioList.value = cachedData as API.Audio[]
- } else {
- const { data } = await MediaController.getAudioList({ page: 1, key: searchKey.value })
- audioList.value = data as API.Audio[]
- }
- // if (audioList.value.length > 0) {
- // const audioUrls = audioList.value.map(audio => audio.url).filter(url => url)
- // audioManager.preloadAudioList(audioUrls).catch(error => {
- // console.error('预加载音频失败:', error)
- // })
- // }
- // } catch (error) {
- // console.error('获取音频列表失败:', error)
- // }
- }
- // 设置所有音频状态
- const setAudioListState = (state: 'playing' | 'paused' | 'stopped') => {
- audioList.value.forEach(audio => {
- audio.state = state
- })
- }
- // 选择音频
- const selectAudio = (audio: API.Audio) => {
- selectedValue.value = audio.title
- showAudioModal.value = false
- emits('change', audio.title)
- }
- // 播放选中的音频
- const playSelectedAudio = async () => {
- if (!selectedValue.value) return
- const { data } = await MediaController.getAudioUrlByName(selectedValue.value)
- if (!data) {
- message.error('音频不存在')
- return
- }
- await playAudio(new Event('click'), {
- title: selectedValue.value,
- url: data,
- state: 'stopped'
- })
- currentPlayingAudio.value = selectedValue.value
- isPlaying.value = true
- }
- // 暂停当前音频
- const pauseCurrentAudio = () => {
- try {
- audioManager.stop()
- setAudioListState('stopped')
- currentPlayingAudio.value = ''
- isPlaying.value = false
- } catch (error) {
- console.error('暂停音频失败:', error)
- }
- }
- // 播放音频
- const playAudio = async (e: Event, audio: API.Audio) => {
- e.stopPropagation()
- e.preventDefault()
- audioManager.stop()
- try {
- // 先设置所有音频为停止状态
- setAudioListState('stopped')
- currentPlayingAudio.value = ''
- isPlaying.value = false
- // 设置当前音频为播放状态
- audio.state = 'playing'
- // 强制DOM更新,解决鼠标悬停时图标不切换的问题
- await nextTick()
- // 检查音频是否已缓存
- const isCached = audioManager.isAudioCached(audio.url)
- console.log(`播放音频: ${audio.title}, 缓存状态: ${isCached ? '已缓存' : '需要加载'}`)
- // 播放音频,传入播放结束回调
- await audioManager.playFromUrl(audio.url, () => {
- // 播放结束时将当前音频状态设置为停止
- audio.state = 'stopped'
- currentPlayingAudio.value = ''
- isPlaying.value = false
- })
- // 更新播放状态
- currentPlayingAudio.value = audio.title
- isPlaying.value = true
- } catch (error) {
- console.error('播放音频失败:', error)
- audio.state = 'stopped'
- currentPlayingAudio.value = ''
- isPlaying.value = false
- }
- }
- // 暂停音频
- const pauseAudio = (e: Event, audio: API.Audio) => {
- e.stopPropagation()
- e.preventDefault()
- try {
- audioManager.stop()
- // 根据audioManager状态同步
- setAudioListState('stopped')
- currentPlayingAudio.value = ''
- isPlaying.value = false
- } catch (error) {
- console.error('暂停音频失败:', error)
- }
- }
- // 处理搜索
- let debounceTimer: ReturnType<typeof setTimeout> | null = null
- const handleSearch = () => {
- if (debounceTimer) clearTimeout(debounceTimer)
- debounceTimer = setTimeout(() => {
- getAudioList()
- emits('search', searchKey.value)
- }, 300)
- }
- // 处理弹窗取消
- const handleModalCancel = () => {
- audioManager.stop()
- searchKey.value = ''
- getAudioList()
- emits('blur')
- }
- // 初始化
- onMounted(() => {
- getAudioList()
- })
- // 监听搜索关键字变化
- watch(searchKey, () => {
- if (!searchKey.value) {
- getAudioList()
- }
- })
- </script>
- <style lang='less' scoped>
- .audio-selector {
- width: 100%;
- .audio-empty-state {
- height: 60px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 12px;
- border: 2px dashed #d9d9d9;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s;
- background: #fafafa;
- &:hover {
- border-color: #1890ff;
- background: #f0f8ff;
- }
- .empty-icon {
- font-size: 24px;
- color: #bfbfbf;
- margin-bottom: 4px;
- }
- .empty-text {
- font-size: 13px;
- color: #666;
- margin-bottom: 2px;
- }
- .empty-action {
- font-size: 12px;
- color: #1890ff;
- }
- }
- .audio-selected-state {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- border: 1px solid #d9d9d9;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s;
- background: #fff;
- &:hover {
- border-color: #1890ff;
- box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
- }
- .selected-info {
- flex: 1;
- .selected-name {
- font-size: 14px;
- color: #262626;
- font-weight: 500;
- margin-bottom: 2px;
- }
- .selected-action {
- font-size: 12px;
- color: #8c8c8c;
- }
- }
- .selected-controls {
- .control-btn {
- font-size: 20px;
- cursor: pointer;
- transition: all 0.3s;
- &.play-btn {
- color: #1890ff;
- &:hover {
- color: #40a9ff;
- }
- }
- &.pause-btn {
- color: #ff4d4f;
- &:hover {
- color: #ff7875;
- }
- }
- }
- }
- }
- }
- .audio-modal-content {
- .search-section {
- margin-bottom: 16px;
- }
- .audio-list {
- max-height: 400px;
- overflow-y: auto;
- .audio-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px;
- border: 1px solid #f0f0f0;
- border-radius: 6px;
- margin-bottom: 8px;
- cursor: pointer;
- transition: all 0.3s;
- &:hover {
- background: #f5f5f5;
- border-color: #d9d9d9;
- }
- &.selected {
- background: #e6f7ff;
- border-color: #1890ff;
- }
- .audio-info {
- flex: 1;
- .audio-name {
- font-size: 14px;
- color: #262626;
- margin-bottom: 4px;
- }
- .audio-status {
- .cached-tag {
- font-size: 12px;
- color: #52c41a;
- background: #f6ffed;
- padding: 2px 6px;
- border-radius: 4px;
- border: 1px solid #b7eb8f;
- }
- .loading-tag {
- font-size: 12px;
- color: #faad14;
- background: #fffbe6;
- padding: 2px 6px;
- border-radius: 4px;
- border: 1px solid #ffe58f;
- }
- }
- }
- .audio-controls {
- .control-btn {
- font-size: 18px;
- cursor: pointer;
- transition: all 0.3s;
- &.play-btn {
- color: #1890ff;
- &:hover {
- color: #40a9ff;
- }
- }
- &.pause-btn {
- color: #ff4d4f;
- &:hover {
- color: #ff7875;
- }
- }
- }
- }
- }
- .empty-list {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px;
- color: #bfbfbf;
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- }
- .empty-text {
- font-size: 14px;
- }
- }
- }
- }
- </style>
|