MediaTemplate.vue 41.3 KB
Newer Older
LiangLiu's avatar
LiangLiu committed
1
<script setup>
LiangLiu's avatar
LiangLiu committed
2
import { ref, computed, watch } from 'vue'
LiangLiu's avatar
LiangLiu committed
3
4
5
6
import { useI18n } from 'vue-i18n'

const { t } = useI18n()

LiangLiu's avatar
LiangLiu committed
7
8
9
10
// 音频播放状态管理
const playingAudioId = ref(null)
const audioDurations = ref({})

LiangLiu's avatar
LiangLiu committed
11
12
13
14
15
16
17
18
19
20
21
22
import {
    getTemplateFileUrl,
    getHistoryImageUrl,
    goToTemplatePage,
    jumpToTemplatePage,
    getVisibleTemplatePages,
    selectImageHistory,
    selectImageTemplate,
    selectAudioHistory,
    selectAudioTemplate,
    previewAudioHistory,
    previewAudioTemplate,
LiangLiu's avatar
LiangLiu committed
23
24
    stopAudioPlayback,
    setAudioStopCallback,
LiangLiu's avatar
LiangLiu committed
25
26
27
28
29
30
31
32
33
34
35
    clearImageHistory,
    clearAudioHistory,
    templatePaginationInfo,
    templateCurrentPage,
    templatePageInput,
    showImageTemplates,
    showAudioTemplates,
    imageHistory,
    audioHistory,
    imageTemplates,
    audioTemplates,
36
    mergedTemplates,
LiangLiu's avatar
LiangLiu committed
37
38
39
    mediaModalTab,
    getImageHistory,
    getAudioHistory,
LiangLiu's avatar
LiangLiu committed
40
    isPageLoading
LiangLiu's avatar
LiangLiu committed
41
} from '../utils/other'
LiangLiu's avatar
LiangLiu committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142

// 格式化音频时长
const formatDuration = (seconds) => {
    if (!seconds || isNaN(seconds)) return '--:--'
    const mins = Math.floor(seconds / 60)
    const secs = Math.floor(seconds % 60)
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}

// 获取音频时长
const getAudioDuration = async (url, id) => {
    if (audioDurations.value[id]) return audioDurations.value[id]

    return new Promise((resolve) => {
        const audio = new Audio()
        audio.addEventListener('loadedmetadata', () => {
            audioDurations.value[id] = audio.duration
            resolve(audio.duration)
        })
        audio.addEventListener('error', () => {
            resolve(0)
        })
        audio.src = url
    })
}

// 处理音频预览播放/停止
const handleAudioPreview = async (item, isTemplate = false) => {
    const id = isTemplate ? `template_${item.filename}` : `history_${item.filename}`
    const url = isTemplate ? getTemplateFileUrl(item.filename, 'audios') : item.url

    // 如果当前正在播放这个音频,则停止
    if (playingAudioId.value === id) {
        playingAudioId.value = null
        stopAudioPlayback() // 调用停止音频播放函数
        return
    }

    // 停止其他正在播放的音频
    playingAudioId.value = null
    stopAudioPlayback() // 先停止当前播放的音频

    // 播放新音频
    try {
        // 设置停止回调,当音频停止时更新UI状态
        setAudioStopCallback(() => {
            playingAudioId.value = null
        })

        if (isTemplate) {
            previewAudioTemplate(item)
        } else {
            previewAudioHistory({ url })
        }
        playingAudioId.value = id

        // 获取音频时长
        await getAudioDuration(url, id)
    } catch (error) {
        console.error('音频播放失败:', error)
    }
}

// 检查是否正在播放
const isPlaying = (item, isTemplate = false) => {
    const id = isTemplate ? `template_${item.filename}` : `history_${item.filename}`
    return playingAudioId.value === id
}

// 获取音频时长显示
const getDurationDisplay = (item, isTemplate = false) => {
    const id = isTemplate ? `template_${item.filename}` : `history_${item.filename}`
    return formatDuration(audioDurations.value[id])
}

// 预加载音频时长
const preloadAudioDurations = (items, isTemplate = false) => {
    items.forEach(item => {
        const id = isTemplate ? `template_${item.filename}` : `history_${item.filename}`
        const url = isTemplate ? getTemplateFileUrl(item.filename, 'audios') : item.url

        // 如果已经有时长数据,跳过
        if (audioDurations.value[id] || !url) return

        // 异步加载时长
        getAudioDuration(url, id)
    })
}

// 监听音频历史和模板列表变化,预加载时长
watch(audioHistory, (newHistory) => {
    if (newHistory && newHistory.length > 0) {
        preloadAudioDurations(newHistory, false)
    }
}, { immediate: true, deep: true })

watch(audioTemplates, (newTemplates) => {
    if (newTemplates && newTemplates.length > 0) {
        preloadAudioDurations(newTemplates, true)
    }
}, { immediate: true, deep: true })
LiangLiu's avatar
LiangLiu committed
143
144
145
146
</script>

<template>

LiangLiu's avatar
LiangLiu committed
147
                        <!-- 模板选择浮窗 - Apple 极简风格 -->
LiangLiu's avatar
LiangLiu committed
148
149
                        <div v-cloak>
                            <div v-if="showImageTemplates || showAudioTemplates"
LiangLiu's avatar
LiangLiu committed
150
                                class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center"
LiangLiu's avatar
LiangLiu committed
151
                                @click="showImageTemplates = false; showAudioTemplates = false">
LiangLiu's avatar
LiangLiu committed
152
                                <div class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] backdrop-saturate-[180%] border border-black/8 dark:border-white/8 rounded-3xl px-8 py-8 max-w-4xl w-full mx-6 h-[90vh] overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.4)]"
LiangLiu's avatar
LiangLiu committed
153
                                    @click.stop>
LiangLiu's avatar
LiangLiu committed
154
155
156
                                    <!-- 浮窗头部 - Apple 风格 -->
                                    <div class="flex items-center justify-between mb-8">
                                        <h3 class="text-2xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight">
LiangLiu's avatar
LiangLiu committed
157
                                                <i v-if="showImageTemplates"
LiangLiu's avatar
LiangLiu committed
158
                                                    class="fas fa-image text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
LiangLiu's avatar
LiangLiu committed
159
                                                <i v-if="showAudioTemplates"
LiangLiu's avatar
LiangLiu committed
160
                                                    class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
LiangLiu's avatar
LiangLiu committed
161
162
163
                                                {{ showImageTemplates ? t('imageTemplates') : t('audioTemplates') }}
                                        </h3>
                                        <button @click="showImageTemplates = false; showAudioTemplates = false"
LiangLiu's avatar
LiangLiu committed
164
165
                                                class="w-9 h-9 flex items-center justify-center bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] hover:bg-white dark:hover:bg-[#3a3a3c] rounded-full transition-all duration-200 hover:scale-110 active:scale-100">
                                            <i class="fas fa-times text-base"></i>
LiangLiu's avatar
LiangLiu committed
166
167
168
                                        </button>
                                    </div>

LiangLiu's avatar
LiangLiu committed
169
170
                                    <!-- 标签页切换 - Apple 风格 -->
                                    <div class="flex gap-2 mb-8">
LiangLiu's avatar
LiangLiu committed
171
172
                                            <button
                                                @click="mediaModalTab = 'history'; showImageTemplates && getImageHistory(); showAudioTemplates && getAudioHistory()"
LiangLiu's avatar
LiangLiu committed
173
174
175
                                                class="px-5 py-2.5 text-sm font-medium rounded-full transition-all duration-200 tracking-tight" :class="mediaModalTab === 'history'
                                                    ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.25)] dark:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.3)]'
                                                    : 'bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7]'">
LiangLiu's avatar
LiangLiu committed
176
177
178
179
                                            <i class="fas fa-history mr-2"></i>
                                                {{ t('history') }}
                                        </button>
                                        <button @click="mediaModalTab = 'templates'"
LiangLiu's avatar
LiangLiu committed
180
181
182
                                                class="px-5 py-2.5 text-sm font-medium rounded-full transition-all duration-200 tracking-tight" :class="mediaModalTab === 'templates'
                                                    ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.25)] dark:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.3)]'
                                                    : 'bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7]'">
LiangLiu's avatar
LiangLiu committed
183
184
185
186
187
                                            <i class="fas fa-layer-group mr-2"></i>
                                                {{ t('templates') }}
                                        </button>
                                    </div>

LiangLiu's avatar
LiangLiu committed
188
                                    <!-- 图片历史记录 - Apple 风格 -->
LiangLiu's avatar
LiangLiu committed
189
                                          <div v-if="showImageTemplates && mediaModalTab === 'history'"
LiangLiu's avatar
LiangLiu committed
190
                                             class="overflow-y-auto flex-1 max-h-[60vh] main-scrollbar pr-6 pl-1">
LiangLiu's avatar
LiangLiu committed
191
192
193
                                            <div v-if="imageHistory.length === 0"
                                                class="flex flex-col items-center justify-center py-12 text-center">
                                                <div
LiangLiu's avatar
LiangLiu committed
194
195
                                                    class="w-16 h-16 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-4">
                                                <i class="fas fa-history text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-2xl"></i>
LiangLiu's avatar
LiangLiu committed
196
                                            </div>
LiangLiu's avatar
LiangLiu committed
197
198
                                                <p class="text-[#1d1d1f] dark:text-[#f5f5f7] text-lg font-medium mb-2 tracking-tight">{{ t('noHistoryRecords') }}</p>
                                                <p class="text-[#86868b] dark:text-[#98989d] text-sm tracking-tight">{{ t('imageHistoryAutoSave') }}</p>
LiangLiu's avatar
LiangLiu committed
199
                                        </div>
LiangLiu's avatar
LiangLiu committed
200
201
202
                                        <div v-else class="space-y-4 pt-2">
                                            <div class="flex items-center justify-between mb-6 px-1">
                                                    <span class="text-sm text-[#86868b] dark:text-[#98989d] tracking-tight">{{ t('total') }} {{ imageHistory.length }}
LiangLiu's avatar
LiangLiu committed
203
204
                                                        {{ t('records') }}</span>
                                                <button @click="clearImageHistory"
LiangLiu's avatar
LiangLiu committed
205
                                                        class="text-xs text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 transition-colors flex items-center gap-1.5 tracking-tight"
LiangLiu's avatar
LiangLiu committed
206
207
208
209
210
                                                        :title="t('clearHistory')">
                                                    <i class="fas fa-trash"></i>
                                                        {{ t('clear') }}
                                                </button>
                                            </div>
LiangLiu's avatar
LiangLiu committed
211
                                            <div class="columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 px-1">
LiangLiu's avatar
LiangLiu committed
212
213
                                                <div v-for="(history, index) in imageHistory" :key="index"
                                                    @click="selectImageHistory(history)"
LiangLiu's avatar
LiangLiu committed
214
                                                    class="break-inside-avoid mb-4 relative group cursor-pointer rounded-2xl overflow-hidden border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)]">
LiangLiu's avatar
LiangLiu committed
215
216
217
                                                        <img :src="getHistoryImageUrl(history)" :alt="history.filename"
                                                            class="w-full h-auto object-contain">
                                                        <div
LiangLiu's avatar
LiangLiu committed
218
                                                            class="absolute inset-0 bg-black/50 dark:bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
LiangLiu's avatar
LiangLiu committed
219
220
221
222
223
224
225
                                                        <i class="fas fa-check text-white text-xl"></i>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>

LiangLiu's avatar
LiangLiu committed
226
227
                                     <!-- 图片模板网格 - Apple 风格 -->
                                         <div v-if="showImageTemplates && mediaModalTab === 'templates'" class="pr-6 pl-1">
LiangLiu's avatar
LiangLiu committed
228

LiangLiu's avatar
LiangLiu committed
229
230
231
232
                                            <!-- 图片模板分页组件 - Apple 风格 -->
                                            <div v-if="templatePaginationInfo" class="mt-6">
                                                <div class="flex items-center justify-between text-xs mb-4">
                                                    <div class="flex items-center space-x-1 text-[#86868b] dark:text-[#98989d] tracking-tight">
LiangLiu's avatar
LiangLiu committed
233
234
235
236
                                                        <span>{{ templatePaginationInfo.total }} {{ t('records') }}</span>
                                                    </div>
                                                </div>
                                                <div v-if="templatePaginationInfo.total_pages > 1" class="flex justify-center">
LiangLiu's avatar
LiangLiu committed
237
                                                    <nav class="isolate inline-flex gap-1" aria-label="Pagination">
LiangLiu's avatar
LiangLiu committed
238
239
240
                                                        <!-- 上一页按钮 -->
                                                        <button @click="goToTemplatePage(templateCurrentPage - 1)"
                                                            :disabled="templateCurrentPage <= 1"
LiangLiu's avatar
LiangLiu committed
241
                                                            class="relative inline-flex items-center w-9 h-9 rounded-lg bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] transition-all duration-200"
LiangLiu's avatar
LiangLiu committed
242
243
244
                                                            :class="{ 'opacity-50 cursor-not-allowed': templateCurrentPage <= 1 }"
                                                            :title="t('previousPage')">
                                                            <span class="sr-only">{{ t('previousPage') }}</span>
LiangLiu's avatar
LiangLiu committed
245
                                                            <i class="fas fa-chevron-left text-xs mx-auto" aria-hidden="true"></i>
LiangLiu's avatar
LiangLiu committed
246
247
248
249
250
251
                                                        </button>

                                                        <!-- 页码按钮 -->
                                                        <template v-for="page in getVisibleTemplatePages()" :key="page">
                                                            <button v-if="page !== '...'" @click="goToTemplatePage(page)"
                                                                :class="[
LiangLiu's avatar
LiangLiu committed
252
                                                                    'relative inline-flex items-center justify-center min-w-[36px] h-9 px-3 text-sm font-medium rounded-lg transition-all duration-200',
LiangLiu's avatar
LiangLiu committed
253
                                                                    page === templateCurrentPage
LiangLiu's avatar
LiangLiu committed
254
255
                                                                        ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.25)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.3)]'
                                                                        : 'bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7]'
LiangLiu's avatar
LiangLiu committed
256
257
258
259
                                                                ]"
                                                                :aria-current="page === templateCurrentPage ? 'page' : undefined">
                                                                {{ page }}
                                                            </button>
LiangLiu's avatar
LiangLiu committed
260
                                                            <span v-else class="relative inline-flex items-center px-2 text-sm font-semibold text-[#86868b] dark:text-[#98989d]">...</span>
LiangLiu's avatar
LiangLiu committed
261
262
263
264
265
                                                        </template>

                                                        <!-- 下一页按钮 -->
                                                        <button @click="goToTemplatePage(templateCurrentPage + 1)"
                                                            :disabled="templateCurrentPage >= templatePaginationInfo.total_pages"
LiangLiu's avatar
LiangLiu committed
266
                                                            class="relative inline-flex items-center w-9 h-9 rounded-lg bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] transition-all duration-200"
LiangLiu's avatar
LiangLiu committed
267
268
269
                                                            :class="{ 'opacity-50 cursor-not-allowed': templateCurrentPage >= templatePaginationInfo.total_pages }"
                                                            :title="t('nextPage')">
                                                            <span class="sr-only">{{ t('nextPage') }}</span>
LiangLiu's avatar
LiangLiu committed
270
                                                            <i class="fas fa-chevron-right text-xs mx-auto" aria-hidden="true"></i>
LiangLiu's avatar
LiangLiu committed
271
272
273
274
                                                        </button>
                                                    </nav>
                                                </div>
                                            </div>
LiangLiu's avatar
LiangLiu committed
275
                                         <div class="overflow-y-auto flex-1 max-h-[60vh] main-scrollbar pr-2 pt-2">
LiangLiu's avatar
LiangLiu committed
276
277
278
279
280
281
282
                                            <div class="space-y-4">
                                                <div v-if="isPageLoading" class="flex items-center justify-center">
                                                    <div class="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-white/90 dark:bg-[#2c2c2e]/90 border border-black/8 dark:border-white/8 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] shadow-[0_4px_16px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.35)]">
                                                        <i class="fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
                                                        <span>{{ t('loading') }}</span>
                                                    </div>
                                                </div>
283
284
285
                                                <div v-if="mergedTemplates.filter(t => t.image).length > 0" class="columns-2 sm:columns-2 md:columns-3 lg:columns-4 xl:columns-5 gap-4 px-1">
                                                    <div v-for="template in mergedTemplates.filter(t => t.image)" :key="template.id"
                                                        @click="selectImageTemplate(template.image)"
LiangLiu's avatar
LiangLiu committed
286
                                                        class="break-inside-avoid mb-4 relative group cursor-pointer rounded-2xl overflow-hidden border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)]">
287
                                                            <img :src="template.image.url" :alt="template.image.filename"
LiangLiu's avatar
LiangLiu committed
288
289
290
291
292
293
294
295
296
                                                            class="w-full h-auto object-contain" preload="metadata">
                                                            <div
                                                                class="absolute inset-0 bg-black/50 dark:bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
                                                            <i class="fas fa-check text-white text-2xl"></i>
                                                        </div>
                                                    </div>
                                                </div>
                                                <div v-else
                                                    class="flex flex-col items-center justify-center py-12 text-center">
LiangLiu's avatar
LiangLiu committed
297
                                                    <div
LiangLiu's avatar
LiangLiu committed
298
299
300
301
                                                        class="w-16 h-16 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-4">
                                                    <i class="fas fa-image text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-2xl"></i>
                                                </div>
                                                    <p class="text-[#1d1d1f] dark:text-[#f5f5f7] text-lg font-medium tracking-tight">{{ t('noImageTemplates') }}</p>
LiangLiu's avatar
LiangLiu committed
302
303
                                                </div>
                                            </div>
LiangLiu's avatar
LiangLiu committed
304
                                         </div>
LiangLiu's avatar
LiangLiu committed
305
306
307

                                    </div>

LiangLiu's avatar
LiangLiu committed
308
                                    <!-- 音频历史记录 - Apple 风格 -->
LiangLiu's avatar
LiangLiu committed
309
                                          <div v-if="showAudioTemplates && mediaModalTab === 'history'"
LiangLiu's avatar
LiangLiu committed
310
                                             class="overflow-y-auto flex-1 max-h-[60vh] main-scrollbar pr-6 pl-1">
LiangLiu's avatar
LiangLiu committed
311
312
313
                                            <div v-if="audioHistory.length === 0"
                                                class="flex flex-col items-center justify-center py-12 text-center">
                                                <div
LiangLiu's avatar
LiangLiu committed
314
315
                                                    class="w-16 h-16 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-4">
                                                <i class="fas fa-history text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-2xl"></i>
LiangLiu's avatar
LiangLiu committed
316
                                            </div>
LiangLiu's avatar
LiangLiu committed
317
318
                                                <p class="text-[#1d1d1f] dark:text-[#f5f5f7] text-lg font-medium mb-2 tracking-tight">{{ t('noHistoryRecords') }}</p>
                                                <p class="text-[#86868b] dark:text-[#98989d] text-sm tracking-tight">{{ t('audioHistoryAutoSave') }}</p>
LiangLiu's avatar
LiangLiu committed
319
                                        </div>
LiangLiu's avatar
LiangLiu committed
320
321
322
                                        <div v-else class="space-y-3 pt-2">
                                            <div class="flex items-center justify-between mb-6 px-1">
                                                    <span class="text-sm text-[#86868b] dark:text-[#98989d] tracking-tight">{{ t('total') }} {{ audioHistory.length }}
LiangLiu's avatar
LiangLiu committed
323
324
                                                        {{ t('records') }}</span>
                                                <button @click="clearAudioHistory"
LiangLiu's avatar
LiangLiu committed
325
                                                        class="text-xs text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 transition-colors flex items-center gap-1.5 tracking-tight"
LiangLiu's avatar
LiangLiu committed
326
327
328
329
330
                                                        :title="t('clearHistory')">
                                                    <i class="fas fa-trash"></i>
                                                        {{ t('clear') }}
                                                </button>
                                            </div>
LiangLiu's avatar
LiangLiu committed
331
                                            <div class="space-y-3 px-1">
LiangLiu's avatar
LiangLiu committed
332
333
                                                <div v-for="(history, index) in audioHistory" :key="index"
                                                    @click="selectAudioHistory(history)"
LiangLiu's avatar
LiangLiu committed
334
                                                    class="flex items-center gap-4 p-4 rounded-2xl border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all cursor-pointer bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)] group">
335
336
337
                                                    <div class="w-12 h-12 rounded-xl overflow-hidden flex-shrink-0 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 flex items-center justify-center">
                                                        <img v-if="history.imageUrl" :src="history.imageUrl" :alt="history.filename" class="w-full h-full object-cover" @error="history.imageUrl = null" />
                                                        <i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
LiangLiu's avatar
LiangLiu committed
338
                                                    </div>
LiangLiu's avatar
LiangLiu committed
339
                                                    <div class="flex-1 min-w-0">
340
341
342
343
344
                                                        <div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
                                                            <span>{{ t('historyAudio') }}</span>
                                                            <span class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
                                                            <span>{{ getDurationDisplay(history, false) }}</span>
                                                        </div>
LiangLiu's avatar
LiangLiu committed
345
                                                    </div>
LiangLiu's avatar
LiangLiu committed
346
347
348
349
350
                                                    <button @click.stop="handleAudioPreview(history, false)"
                                                            class="px-4 py-2 rounded-lg transition-all cursor-pointer relative z-10 flex items-center gap-2 flex-shrink-0 tracking-tight"
                                                            :class="isPlaying(history, false)
                                                                ? 'text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300'
                                                                : 'text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:text-[color:var(--brand-primary)]/80 dark:hover:text-[color:var(--brand-primary-light)]/80'"
LiangLiu's avatar
LiangLiu committed
351
                                                            style="pointer-events: auto;">
LiangLiu's avatar
LiangLiu committed
352
353
                                                        <i :class="isPlaying(history, false) ? 'fas fa-stop' : 'fas fa-play'"></i>
                                                        <span class="text-sm font-medium">{{ isPlaying(history, false) ? t('stop') : t('preview') }}</span>
LiangLiu's avatar
LiangLiu committed
354
355
356
357
358
359
                                                    </button>
                                                </div>
                                            </div>
                                        </div>
                                    </div>

LiangLiu's avatar
LiangLiu committed
360
361
362
363
364
365
                                    <!-- 音频模板列表 - Apple 风格 -->
                                          <div v-if="showAudioTemplates && mediaModalTab === 'templates'" class="pr-6 pl-1">
                                                                                    <!-- 音频模板分页组件 - Apple 风格 -->
                                                                                    <div v-if="templatePaginationInfo" class="mt-6">
                                                                                        <div class="flex items-center justify-between text-xs mb-4">
                                                                                            <div class="flex items-center space-x-1 text-[#86868b] dark:text-[#98989d] tracking-tight">
LiangLiu's avatar
LiangLiu committed
366
367
368
369
                                                                                                <span>{{ templatePaginationInfo.total }} {{ t('records') }}</span>
                                                                                            </div>
                                                                                        </div>
                                                                                        <div v-if="templatePaginationInfo.total_pages > 1" class="flex justify-center">
LiangLiu's avatar
LiangLiu committed
370
                                                                                            <nav class="isolate inline-flex gap-1" aria-label="Pagination">
LiangLiu's avatar
LiangLiu committed
371
372
373
                                                                                                <!-- 上一页按钮 -->
                                                                                                <button @click="goToTemplatePage(templateCurrentPage - 1)"
                                                                                                    :disabled="templateCurrentPage <= 1"
LiangLiu's avatar
LiangLiu committed
374
                                                                                                    class="relative inline-flex items-center w-9 h-9 rounded-lg bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] transition-all duration-200"
LiangLiu's avatar
LiangLiu committed
375
376
377
                                                                                                    :class="{ 'opacity-50 cursor-not-allowed': templateCurrentPage <= 1 }"
                                                                                                    :title="t('previousPage')">
                                                                                                    <span class="sr-only">{{ t('previousPage') }}</span>
LiangLiu's avatar
LiangLiu committed
378
                                                                                                    <i class="fas fa-chevron-left text-xs mx-auto" aria-hidden="true"></i>
LiangLiu's avatar
LiangLiu committed
379
380
381
382
383
384
                                                                                                </button>

                                                                                                <!-- 页码按钮 -->
                                                                                                <template v-for="page in getVisibleTemplatePages()" :key="page">
                                                                                                    <button v-if="page !== '...'" @click="goToTemplatePage(page)"
                                                                                                        :class="[
LiangLiu's avatar
LiangLiu committed
385
                                                                                                            'relative inline-flex items-center justify-center min-w-[36px] h-9 px-3 text-sm font-medium rounded-lg transition-all duration-200',
LiangLiu's avatar
LiangLiu committed
386
                                                                                                            page === templateCurrentPage
LiangLiu's avatar
LiangLiu committed
387
388
                                                                                                                ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.25)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.3)]'
                                                                                                                : 'bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7]'
LiangLiu's avatar
LiangLiu committed
389
390
391
392
                                                                                                        ]"
                                                                                                        :aria-current="page === templateCurrentPage ? 'page' : undefined">
                                                                                                        {{ page }}
                                                                                                    </button>
LiangLiu's avatar
LiangLiu committed
393
                                                                                                    <span v-else class="relative inline-flex items-center px-2 text-sm font-semibold text-[#86868b] dark:text-[#98989d]">...</span>
LiangLiu's avatar
LiangLiu committed
394
395
396
397
398
                                                                                                </template>

                                                                                                <!-- 下一页按钮 -->
                                                                                                <button @click="goToTemplatePage(templateCurrentPage + 1)"
                                                                                                    :disabled="templateCurrentPage >= templatePaginationInfo.total_pages"
LiangLiu's avatar
LiangLiu committed
399
                                                                                                    class="relative inline-flex items-center w-9 h-9 rounded-lg bg-white/80 dark:bg-[#2c2c2e]/80 border border-black/8 dark:border-white/8 text-[#86868b] dark:text-[#98989d] hover:bg-white dark:hover:bg-[#3a3a3c] hover:text-[#1d1d1f] dark:hover:text-[#f5f5f7] transition-all duration-200"
LiangLiu's avatar
LiangLiu committed
400
401
402
                                                                                                    :class="{ 'opacity-50 cursor-not-allowed': templateCurrentPage >= templatePaginationInfo.total_pages }"
                                                                                                    :title="t('nextPage')">
                                                                                                    <span class="sr-only">{{ t('nextPage') }}</span>
LiangLiu's avatar
LiangLiu committed
403
                                                                                                    <i class="fas fa-chevron-right text-xs mx-auto" aria-hidden="true"></i>
LiangLiu's avatar
LiangLiu committed
404
405
406
407
                                                                                                </button>
                                                                                            </nav>
                                                                                        </div>
                                                                                    </div>
LiangLiu's avatar
LiangLiu committed
408
                                        <div class="overflow-y-auto flex-1 max-h-[60vh] main-scrollbar pr-2 pt-2">
LiangLiu's avatar
LiangLiu committed
409
410
411
412
413
                                            <div class="space-y-4">
                                                <div v-if="isPageLoading" class="flex items-center justify-center">
                                                    <div class="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-white/90 dark:bg-[#2c2c2e]/90 border border-black/8 dark:border-white/8 text-sm text-[#1d1d1f] dark:text-[#f5f5f7] shadow-[0_4px_16px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.35)]">
                                                        <i class="fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
                                                        <span>{{ t('loading') }}</span>
LiangLiu's avatar
LiangLiu committed
414
                                                    </div>
LiangLiu's avatar
LiangLiu committed
415
                                                </div>
416
417
418
                                                <div v-if="mergedTemplates.length > 0" class="space-y-3 px-1">
                                                    <div v-for="template in mergedTemplates" :key="template.id"
                                                    @click="selectAudioTemplate(template.audio)"
LiangLiu's avatar
LiangLiu committed
419
420
                                                        class="flex items-center gap-4 p-4 rounded-2xl border border-black/8 dark:border-white/8 hover:border-[color:var(--brand-primary)]/50 dark:hover:border-[color:var(--brand-primary-light)]/50 transition-all cursor-pointer bg-white/80 dark:bg-[#2c2c2e]/80 hover:bg-white dark:hover:bg-[#3a3a3c] hover:shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:hover:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)] group">
                                                            <div
421
                                                        class="w-12 h-12 rounded-xl overflow-hidden flex-shrink-0 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 flex items-center justify-center">
422
                                                        <img v-if="template.image?.url" :src="template.image.url" :alt="t('audioTemplates')" class="w-full h-full object-cover" @error="template.image.url = null" />
423
                                                        <i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
LiangLiu's avatar
LiangLiu committed
424
                                                        </div>
LiangLiu's avatar
LiangLiu committed
425
                                                        <div class="flex-1 min-w-0">
426
427
                                                        <div class="text-[#86868b] dark:text-[#98989d] text-sm flex items-center gap-2 tracking-tight">
                                                            <span>{{ t('audioTemplates') }}</span>
428
429
                                                            <span v-if="template.audio" class="text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></span>
                                                            <span v-if="template.audio">{{ getDurationDisplay(template.audio, true) }}</span>
430
                                                        </div>
LiangLiu's avatar
LiangLiu committed
431
                                                        </div>
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
                                                        <div class="flex items-center gap-2 flex-shrink-0">
                                                            <button v-if="template.image" @click.stop="selectImageTemplate(template.image)"
                                                                    class="px-4 py-2 rounded-lg transition-all cursor-pointer relative z-10 flex items-center gap-2 tracking-tight text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:text-[color:var(--brand-primary)]/80 dark:hover:text-[color:var(--brand-primary-light)]/80"
                                                                    style="pointer-events: auto;">
                                                                <i class="fas fa-image"></i>
                                                                <span class="text-sm font-medium">{{ t('useImage') }}</span>
                                                            </button>
                                                            <button v-if="template.audio" @click.stop="handleAudioPreview(template.audio, true)"
                                                                    class="px-4 py-2 rounded-lg transition-all cursor-pointer relative z-10 flex items-center gap-2 tracking-tight"
                                                                    :class="isPlaying(template.audio, true)
                                                                        ? 'text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300'
                                                                        : 'text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:text-[color:var(--brand-primary)]/80 dark:hover:text-[color:var(--brand-primary-light)]/80'"
                                                                    style="pointer-events: auto;">
                                                                <i :class="isPlaying(template.audio, true) ? 'fas fa-stop' : 'fas fa-play'"></i>
                                                                <span class="text-sm font-medium">{{ isPlaying(template.audio, true) ? t('stop')  : t('preview') }}</span>
                                                            </button>
                                                        </div>
LiangLiu's avatar
LiangLiu committed
449
                                                    </div>
LiangLiu's avatar
LiangLiu committed
450
                                                </div>
LiangLiu's avatar
LiangLiu committed
451
452
453
454
455
456
457
                                                <div v-else
                                                    class="flex flex-col items-center justify-center py-12 text-center">
                                                    <div
                                                        class="w-16 h-16 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-full flex items-center justify-center mb-4">
                                                    <i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-2xl"></i>
                                                </div>
                                                <p class="text-[#1d1d1f] dark:text-[#f5f5f7] text-lg font-medium tracking-tight">目前暂无音频模板</p>
LiangLiu's avatar
LiangLiu committed
458
459
460
461
462
463
464
465
466
                                            </div>
                                            </div>
                                        </div>

                                        </div>
                                </div>
                        </div>
                    </div>
</template>