Unverified Commit 23aa1ef3 authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

Fix mobile frontend bug (#442)


Co-authored-by: default avatarqinxinyi <qxy118045534@163.com>
parent d914488a
...@@ -8,16 +8,36 @@ const { t, locale } = useI18n() ...@@ -8,16 +8,36 @@ const { t, locale } = useI18n()
// 处理操作按钮点击 // 处理操作按钮点击
const handleActionClick = () => { const handleActionClick = () => {
if (alert.value.action && alert.value.action.onClick) { if (alert.value.action && alert.value.action.onClick) {
// 先执行action的回调
alert.value.action.onClick() alert.value.action.onClick()
// 立即关闭alert
alert.value.show = false alert.value.show = false
} }
} }
// 处理transition离开完成后的回调
const handleAfterLeave = () => {
// 只有在alert确实关闭时才重置,避免覆盖正在显示的alert
if (alert.value && !alert.value.show) {
// 记录当前alert的时间戳,用于后续检查
const currentTimestamp = alert.value._timestamp
// 延迟一小段时间再重置,确保不会影响后续的alert显示
setTimeout(() => {
// 只有当alert仍然关闭,且时间戳没有变化(没有新alert创建)时才重置
if (alert.value && !alert.value.show && alert.value._timestamp === currentTimestamp) {
alert.value = { show: false, message: '', type: 'info', action: null }
}
}, 50)
}
}
// 响应式变量控制Alert位置 // 响应式变量控制Alert位置
const alertPosition = ref({ top: '1rem' }) const alertPosition = ref({ top: '1rem' })
// 防抖函数 // 防抖函数
let scrollTimeout = null let scrollTimeout = null
let scrollContainer = null
// 监听滚动事件,动态调整Alert位置 // 监听滚动事件,动态调整Alert位置
const handleScroll = () => { const handleScroll = () => {
...@@ -28,15 +48,21 @@ const handleScroll = () => { ...@@ -28,15 +48,21 @@ const handleScroll = () => {
// 设置新的定时器,防抖处理 // 设置新的定时器,防抖处理
scrollTimeout = setTimeout(() => { scrollTimeout = setTimeout(() => {
const scrollY = window.scrollY // 获取实际的滚动容器
const mainScrollable = scrollContainer || document.querySelector('.main-scrollbar')
if (!mainScrollable) {
alertPosition.value = { top: '1rem' }
return
}
const scrollY = mainScrollable.scrollTop
const viewportHeight = window.innerHeight const viewportHeight = window.innerHeight
// 如果用户滚动了超过50px,将Alert显示在视口内 // 如果用户滚动了超过50px,将Alert显示在视口内
if (scrollY > 50) { if (scrollY > 50) {
// 计算Alert应该显示的位置,确保在视口内可见 // 计算Alert应该显示的位置,确保在视口内可见
// 距离滚动位置20px,但不超过视口底部200px // 距离顶部80px(TopBar高度 + 一些间距)
const alertTop = Math.min(scrollY + 20, scrollY + viewportHeight - 200) alertPosition.value = { top: '80px' }
alertPosition.value = { top: `${alertTop}px` }
} else { } else {
// 在页面顶部时,显示在固定位置 // 在页面顶部时,显示在固定位置
alertPosition.value = { top: '1rem' } alertPosition.value = { top: '1rem' }
...@@ -45,12 +71,24 @@ const handleScroll = () => { ...@@ -45,12 +71,24 @@ const handleScroll = () => {
} }
onMounted(() => { onMounted(() => {
// 查找实际的滚动容器
scrollContainer = document.querySelector('.main-scrollbar')
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
}
// 也监听 window 的滚动(作为后备)
window.addEventListener('scroll', handleScroll, { passive: true }) window.addEventListener('scroll', handleScroll, { passive: true })
// 初始化时也调用一次,确保位置正确 // 初始化时也调用一次,确保位置正确
handleScroll() handleScroll()
}) })
onUnmounted(() => { onUnmounted(() => {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll)
}
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
if (scrollTimeout) { if (scrollTimeout) {
clearTimeout(scrollTimeout) clearTimeout(scrollTimeout)
...@@ -64,8 +102,10 @@ onUnmounted(() => { ...@@ -64,8 +102,10 @@ onUnmounted(() => {
enter-active-class="alert-enter-active" enter-active-class="alert-enter-active"
leave-active-class="alert-leave-active" leave-active-class="alert-leave-active"
enter-from-class="alert-enter-from" enter-from-class="alert-enter-from"
leave-to-class="alert-leave-to"> leave-to-class="alert-leave-to"
@after-leave="handleAfterLeave">
<div v-if="alert.show" <div v-if="alert.show"
:key="alert._timestamp || alert.message"
class="fixed left-1/2 transform -translate-x-1/2 z-[9999] w-auto min-w-[280px] sm:min-w-[320px] max-w-[calc(100vw-3rem)] sm:max-w-xl px-6 sm:px-6 transition-all duration-500 ease-out" class="fixed left-1/2 transform -translate-x-1/2 z-[9999] w-auto min-w-[280px] sm:min-w-[320px] max-w-[calc(100vw-3rem)] sm:max-w-xl px-6 sm:px-6 transition-all duration-500 ease-out"
:style="alertPosition"> :style="alertPosition">
<div class="alert-container"> <div class="alert-container">
...@@ -74,18 +114,21 @@ onUnmounted(() => { ...@@ -74,18 +114,21 @@ onUnmounted(() => {
<div class="alert-icon-wrapper"> <div class="alert-icon-wrapper">
<i :class="getAlertIcon(alert.type)" class="alert-icon"></i> <i :class="getAlertIcon(alert.type)" class="alert-icon"></i>
</div> </div>
<!-- 消息文本和操作按钮(一行显示) --> <!-- 消息文本 -->
<div class="alert-message"> <div class="alert-message">
<span>{{ alert.message }}</span> <span>{{ alert.message }}</span>
</div>
<!-- 操作按钮和关闭按钮(右侧,紧挨着) -->
<div class="alert-actions">
<!-- 操作链接 - Apple 风格 --> <!-- 操作链接 - Apple 风格 -->
<button v-if="alert.action" @click="handleActionClick" class="alert-action-link"> <button v-if="alert.action" @click="handleActionClick" class="alert-action-link">
{{ alert.action.label }} {{ alert.action.label }}
</button> </button>
<!-- 关闭按钮 -->
<button @click="alert.show = false" class="alert-close-btn" aria-label="Close">
<i class="fas fa-times"></i>
</button>
</div> </div>
<!-- 关闭按钮 -->
<button @click="alert.show = false" class="alert-close-btn" aria-label="Close">
<i class="fas fa-times"></i>
</button>
</div> </div>
</div> </div>
</div> </div>
...@@ -150,16 +193,21 @@ onUnmounted(() => { ...@@ -150,16 +193,21 @@ onUnmounted(() => {
line-height: 1.5; line-height: 1.5;
color: #1d1d1f; color: #1d1d1f;
letter-spacing: -0.01em; letter-spacing: -0.01em;
display: flex; min-width: 0; /* 允许文本收缩 */
flex-wrap: wrap;
align-items: center;
gap: 15px;
} }
:global(.dark) .alert-message { :global(.dark) .alert-message {
color: #f5f5f7; color: #f5f5f7;
} }
/* 操作按钮和关闭按钮容器 */
.alert-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* 关闭按钮 */ /* 关闭按钮 */
.alert-close-btn { .alert-close-btn {
display: flex; display: flex;
...@@ -197,12 +245,13 @@ onUnmounted(() => { ...@@ -197,12 +245,13 @@ onUnmounted(() => {
/* 操作链接 - Apple 风格下划线文本 */ /* 操作链接 - Apple 风格下划线文本 */
.alert-action-link { .alert-action-link {
display: inline; display: inline-flex;
align-items: center;
padding: 0; padding: 0;
border: none; border: none;
background: transparent; background: transparent;
color: var(--brand-primary); color: var(--brand-primary);
font-size: inherit; font-size: 14px;
font-weight: 600; font-weight: 600;
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
...@@ -210,6 +259,7 @@ onUnmounted(() => { ...@@ -210,6 +259,7 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap; white-space: nowrap;
height: 24px; /* 与关闭按钮高度一致 */
} }
.alert-action-link:hover { .alert-action-link:hover {
...@@ -266,6 +316,11 @@ onUnmounted(() => { ...@@ -266,6 +316,11 @@ onUnmounted(() => {
.alert-action-link { .alert-action-link {
font-size: 13px; font-size: 13px;
height: 22px; /* 移动端稍微小一点 */
}
.alert-actions {
gap: 6px; /* 移动端间距更小 */
} }
} }
</style> </style>
...@@ -7,7 +7,7 @@ const { t, locale } = useI18n() ...@@ -7,7 +7,7 @@ const { t, locale } = useI18n()
<template> <template>
<!-- 自定义确认对话框 - Apple 极简风格 --> <!-- 自定义确认对话框 - Apple 极简风格 -->
<div v-cloak> <div v-cloak>
<div v-if="confirmDialog.show" class="fixed inset-0 z-[70] flex items-center justify-center p-4"> <div v-if="confirmDialog.show" class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<!-- 背景遮罩 - Apple 风格 --> <!-- 背景遮罩 - Apple 风格 -->
<div class="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm" @click="confirmDialog.cancel()"> <div class="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm" @click="confirmDialog.cancel()">
</div> </div>
......
...@@ -278,15 +278,14 @@ import { ...@@ -278,15 +278,14 @@ import {
generateShareUrl, generateShareUrl,
copyShareLink, copyShareLink,
shareToSocial, shareToSocial,
openTaskFromRoute openTaskFromRoute,
showVoiceTTSModal
} from '../utils/other' } from '../utils/other'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { watch, onMounted, computed, ref, nextTick, onUnmounted } from 'vue' import { watch, onMounted, computed, ref, nextTick, onUnmounted } from 'vue'
import ModelDropdown from './ModelDropdown.vue' import ModelDropdown from './ModelDropdown.vue'
import MediaTemplate from './MediaTemplate.vue'
import Voice_tts from './Voice_tts.vue'
import TaskCarousel from './TaskCarousel.vue' import TaskCarousel from './TaskCarousel.vue'
// Props // Props
...@@ -310,8 +309,13 @@ const screenSize = ref('large') // 'small' 或 'large' ...@@ -310,8 +309,13 @@ const screenSize = ref('large') // 'small' 或 'large'
// 拖拽状态 // 拖拽状态
const isDragOver = ref(false) const isDragOver = ref(false)
// 语音合成模态框状态 // 音频预览播放器相关
const showVoiceTTSModal = ref(false) const audioPreviewElement = ref(null)
const audioPreviewIsPlaying = ref(false)
const audioPreviewDuration = ref(0)
const audioPreviewCurrentTime = ref(0)
const audioPreviewIsDragging = ref(false)
// 处理提交任务并滚动到任务区域 // 处理提交任务并滚动到任务区域
const handleSubmitTask = async () => { const handleSubmitTask = async () => {
...@@ -350,21 +354,11 @@ const scrollToTaskArea = () => { ...@@ -350,21 +354,11 @@ const scrollToTaskArea = () => {
if (taskArea) { if (taskArea) {
taskArea.scrollIntoView({ taskArea.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'center'
}) })
} }
} }
// 滚动到生成区域
const scrollToCreationArea = () => {
const creationArea = document.querySelector('#task-creator')
if (creationArea) {
creationArea.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}
// 包装 useTemplate 函数,在应用模板后滚动到生成区域 // 包装 useTemplate 函数,在应用模板后滚动到生成区域
const handleUseTemplate = async (item) => { const handleUseTemplate = async (item) => {
...@@ -373,35 +367,17 @@ const handleUseTemplate = async (item) => { ...@@ -373,35 +367,17 @@ const handleUseTemplate = async (item) => {
await nextTick() await nextTick()
// 延迟一下确保展开动画完成 // 延迟一下确保展开动画完成
setTimeout(() => { setTimeout(() => {
scrollToCreationArea() // 滚动到顶部
const mainScrollable = document.querySelector('.main-scrollbar');
if (mainScrollable) {
mainScrollable.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}, 100) }, 100)
} }
// 处理语音合成完成后的回调
const handleTTSComplete = (audioBlob) => {
// 创建File对象
const audioFile = new File([audioBlob], 'tts_audio.mp3', { type: 'audio/mpeg' })
// 模拟文件上传事件
const dataTransfer = new DataTransfer()
dataTransfer.items.add(audioFile)
const fileList = dataTransfer.files
const event = {
target: {
files: fileList
}
}
// 处理音频上传
handleAudioUpload(event)
// 关闭模态框
showVoiceTTSModal.value = false
// 显示成功提示
showAlert('语音合成完成,已自动添加到音频素材', 'success')
}
// 跳转到项目页面 // 跳转到项目页面
const goToProjects = () => { const goToProjects = () => {
...@@ -679,11 +655,97 @@ const handleAudioDrop = (e) => { ...@@ -679,11 +655,97 @@ const handleAudioDrop = (e) => {
} }
} }
// 格式化音频预览时间
const formatAudioPreviewTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// 切换音频预览播放/暂停
const toggleAudioPreviewPlayback = () => {
if (!audioPreviewElement.value) return
if (audioPreviewElement.value.paused) {
audioPreviewElement.value.play().catch(error => {
console.log('播放失败:', error)
})
} else {
audioPreviewElement.value.pause()
}
}
// 音频预览加载完成
const onAudioPreviewLoaded = () => {
if (audioPreviewElement.value) {
audioPreviewDuration.value = audioPreviewElement.value.duration || 0
}
}
// 音频预览时间更新
const onAudioPreviewTimeUpdate = () => {
if (audioPreviewElement.value && !audioPreviewIsDragging.value) {
audioPreviewCurrentTime.value = audioPreviewElement.value.currentTime || 0
}
}
// 音频预览进度条变化处理(点击或拖拽)
const onAudioPreviewProgressChange = (event) => {
if (audioPreviewDuration.value > 0 && audioPreviewElement.value && event.target) {
const newTime = parseFloat(event.target.value)
audioPreviewCurrentTime.value = newTime
// 立即更新音频位置
audioPreviewElement.value.currentTime = newTime
}
}
// 音频预览进度条拖拽结束处理
const onAudioPreviewProgressEnd = (event) => {
if (audioPreviewElement.value && audioPreviewDuration.value > 0 && event.target) {
const newTime = parseFloat(event.target.value)
audioPreviewElement.value.currentTime = newTime
audioPreviewCurrentTime.value = newTime
}
audioPreviewIsDragging.value = false
}
// 音频预览播放结束
const onAudioPreviewEnded = () => {
audioPreviewIsPlaying.value = false
audioPreviewCurrentTime.value = 0
}
// 监听音频预览变化,重置状态
watch(() => getCurrentAudioPreview(), (newPreview) => {
// 停止当前播放
if (audioPreviewElement.value) {
audioPreviewElement.value.pause()
}
audioPreviewIsPlaying.value = false
audioPreviewCurrentTime.value = 0
audioPreviewDuration.value = 0
if (newPreview) {
// 等待 DOM 更新后加载新音频
nextTick(() => {
if (audioPreviewElement.value) {
audioPreviewElement.value.load()
}
})
}
})
// 组件卸载时清理 // 组件卸载时清理
onUnmounted(() => { onUnmounted(() => {
if (resizeHandler) { if (resizeHandler) {
window.removeEventListener('resize', resizeHandler) window.removeEventListener('resize', resizeHandler)
} }
// 停止音频预览播放
if (audioPreviewElement.value) {
audioPreviewElement.value.pause()
audioPreviewElement.value = null
}
}) })
</script> </script>
...@@ -931,23 +993,73 @@ onUnmounted(() => { ...@@ -931,23 +993,73 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 音频预览 - Apple 风格 --> <!-- 音频预览 - Apple 风格(播放器卡片样式) -->
<div v-if="getCurrentAudioPreview()" class="relative w-full min-h-[220px] group flex items-center justify-center p-8"> <div v-if="getCurrentAudioPreview()" class="relative w-full min-h-[220px] flex items-center justify-center">
<audio controls class="w-full max-w-md" @error="handleAudioError" @loadstart="console.log('音频开始加载')" @canplay="console.log('音频可以播放')"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] w-full p-4">
<source :src="getCurrentAudioPreviewUrl()" :type="getAudioMimeType()" preload="metadata"> <div class="relative flex items-center mb-3">
</audio> <!-- 头像容器 -->
<div class="relative mr-3 flex-shrink-0">
<!-- 透明白色头像 -->
<div class="w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"></div>
<!-- 播放/暂停按钮 -->
<button
@click="toggleAudioPreviewPlayback"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i :class="audioPreviewIsPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs ml-0.5"></i>
</button>
</div>
<!-- 音频信息 -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate">
{{ t('audio') }}
</div>
</div>
<!-- 音频时长 -->
<div class="text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0 mr-3">
{{ formatAudioPreviewTime(audioPreviewCurrentTime) }} / {{ formatAudioPreviewTime(audioPreviewDuration) }}
</div>
<!-- 删除按钮 - Apple 风格 --> <!-- 删除按钮 -->
<div
class="absolute inset-x-0 bottom-4 flex items-center justify-center opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200">
<div class="flex gap-3">
<button @click.stop="removeAudio" <button @click.stop="removeAudio"
class="w-11 h-11 flex items-center justify-center bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] border border-black/8 dark:border-white/8 text-red-500 dark:text-red-400 rounded-full transition-all duration-200 hover:scale-110 hover:shadow-[0_4px_12px_rgba(239,68,68,0.2)] dark:hover:shadow-[0_4px_12px_rgba(248,113,113,0.3)] active:scale-100" 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-red-500 dark:text-red-400 rounded-full transition-all duration-200 hover:scale-110 hover:shadow-[0_4px_12px_rgba(239,68,68,0.2)] dark:hover:shadow-[0_4px_12px_rgba(248,113,113,0.3)] active:scale-100 flex-shrink-0"
:title="t('deleteAudio')"> :title="t('deleteAudio')">
<i class="fas fa-trash text-base"></i> <i class="fas fa-trash text-sm"></i>
</button> </button>
</div>
<!-- 进度条 -->
<div class="flex items-center gap-2" v-if="audioPreviewDuration > 0">
<input
type="range"
:min="0"
:max="audioPreviewDuration"
:value="audioPreviewCurrentTime"
@input="onAudioPreviewProgressChange"
@change="onAudioPreviewProgressChange"
@mousedown="audioPreviewIsDragging = true"
@mouseup="onAudioPreviewProgressEnd"
@touchstart="audioPreviewIsDragging = true"
@touchend="onAudioPreviewProgressEnd"
class="flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div> </div>
</div>
<!-- 隐藏的音频元素 -->
<audio
ref="audioPreviewElement"
:src="getCurrentAudioPreviewUrl()"
@loadedmetadata="onAudioPreviewLoaded"
@timeupdate="onAudioPreviewTimeUpdate"
@ended="onAudioPreviewEnded"
@play="audioPreviewIsPlaying = true"
@pause="audioPreviewIsPlaying = false"
@error="handleAudioError"
class="hidden"
></audio>
</div> </div>
<input type="file" ref="audioInput" @change="handleAudioUpload" accept="audio/*" <input type="file" ref="audioInput" @change="handleAudioUpload" accept="audio/*"
...@@ -1127,14 +1239,6 @@ onUnmounted(() => { ...@@ -1127,14 +1239,6 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<MediaTemplate />
<!-- 语音合成模态框 -->
<div v-if="showVoiceTTSModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 dark:bg-black/60 backdrop-blur-sm">
<div class="relative w-full h-full max-w-6xl max-h-[100vh] mx-4 my-8 bg-gray-900 rounded-xl shadow-2xl overflow-hidden">
<Voice_tts @tts-complete="handleTTSComplete" @close-modal="showVoiceTTSModal = false" />
</div>
</div>
<!-- GitHub 仓库链接 - Apple 极简风格 --> <!-- GitHub 仓库链接 - Apple 极简风格 -->
<div class="fixed bottom-6 right-6 z-50"> <div class="fixed bottom-6 right-6 z-50">
......
...@@ -4,17 +4,17 @@ import { useI18n } from 'vue-i18n' ...@@ -4,17 +4,17 @@ import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n() const { t, locale } = useI18n()
</script> </script>
<template> <template>
<!-- 左侧功能区 - Apple 极简风格 - 响应式布局 --> <!-- 左侧功能区 - 响应式悬浮按钮 -->
<div class="relative flex flex-col z-10 pl-0 sm:pl-5 w-full sm:w-24"> <div class="fixed top-20 sm:top-1/2 sm:-translate-y-1/2 right-3 sm:right-auto sm:left-5 z-[10] w-auto">
<!-- 功能导航 - Apple 风格统一容器 --> <!-- 功能导航-->
<div class="p-2 flex flex-col justify-center h-full mobile-nav-buttons sm:mt-[-10vh]"> <div class="p-2 flex flex-col justify-center">
<!-- 统一的圆角矩形容器 - Apple 风格 - 响应式方向 --> <!-- 统一的圆角矩形容器-->
<nav class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_4px_16px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] overflow-hidden flex flex-row sm:flex-col w-full sm:w-16"> <nav class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-full shadow-[0_4px_16px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_16px_rgba(0,0,0,0.4)] overflow-hidden flex flex-col w-12 sm:w-14">
<!-- 生成视频功能 --> <!-- 生成视频功能 -->
<div <div
@click="switchToCreateView" @click="switchToCreateView"
class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group" class="flex items-center justify-center h-16 cursor-pointer transition-all duration-200 ease-out group"
:class="$route.path === '/generate' :class="$route.path === '/generate'
? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
...@@ -22,13 +22,13 @@ const { t, locale } = useI18n() ...@@ -22,13 +22,13 @@ const { t, locale } = useI18n()
<i class="fas fa-plus text-xl transition-all duration-200 group-hover:scale-110"></i> <i class="fas fa-plus text-xl transition-all duration-200 group-hover:scale-110"></i>
</div> </div>
<!-- 分割线 - Apple 风格 - 响应式方向 --> <!-- 分割线 - Apple 风格 -->
<div class="w-px sm:w-auto sm:h-px bg-black/8 dark:bg-white/8 my-3 sm:my-0 sm:mx-3"></div> <div class="h-px bg-black/8 dark:bg-white/8 mx-3"></div>
<!-- 我的项目功能 --> <!-- 我的项目功能 -->
<div <div
@click="switchToProjectsView" @click="switchToProjectsView"
class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group" class="flex items-center justify-center h-16 cursor-pointer transition-all duration-200 ease-out group"
:class="$route.path === '/projects' :class="$route.path === '/projects'
? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
...@@ -36,13 +36,13 @@ const { t, locale } = useI18n() ...@@ -36,13 +36,13 @@ const { t, locale } = useI18n()
<i class="fas fa-folder-open text-lg transition-all duration-200 group-hover:scale-110"></i> <i class="fas fa-folder-open text-lg transition-all duration-200 group-hover:scale-110"></i>
</div> </div>
<!-- 分割线 - Apple 风格 - 响应式方向 --> <!-- 分割线 - Apple 风格 -->
<div class="w-px sm:w-auto sm:h-px bg-black/8 dark:bg-white/8 my-3 sm:my-0 sm:mx-3"></div> <div class="h-px bg-black/8 dark:bg-white/8 mx-3"></div>
<!-- 灵感广场功能 --> <!-- 灵感广场功能 -->
<div <div
@click="switchToInspirationView" @click="switchToInspirationView"
class="flex items-center justify-center flex-1 sm:flex-none h-14 sm:h-16 cursor-pointer transition-all duration-200 ease-out mobile-nav-btn group" class="flex items-center justify-center h-16 cursor-pointer transition-all duration-200 ease-out group"
:class="$route.path === '/inspirations' :class="$route.path === '/inspirations'
? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white' ? 'bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white'
: 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'" : 'text-[#86868b] dark:text-[#98989d] hover:bg-black/4 dark:hover:bg-white/6 hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)]'"
......
...@@ -7,6 +7,8 @@ const { t } = useI18n() ...@@ -7,6 +7,8 @@ const { t } = useI18n()
// 音频播放状态管理 // 音频播放状态管理
const playingAudioId = ref(null) const playingAudioId = ref(null)
const audioDurations = ref({}) const audioDurations = ref({})
// 图像加载失败状态
const imageLoadFailed = ref({})
import { import {
getTemplateFileUrl, getTemplateFileUrl,
...@@ -112,6 +114,30 @@ const getDurationDisplay = (item, isTemplate = false) => { ...@@ -112,6 +114,30 @@ const getDurationDisplay = (item, isTemplate = false) => {
return formatDuration(audioDurations.value[id]) return formatDuration(audioDurations.value[id])
} }
// 获取音频对应的图像URL
const getAudioImageUrl = (item, isTemplate = false) => {
if (isTemplate) {
// 对于模板,如果有 input_image 字段,获取对应的图像URL
if (item.inputs && item.inputs.input_image) {
return getTemplateFileUrl(item.inputs.input_image, 'images')
}
// 如果没有 input_image,尝试使用相同的 filename(可能在同一目录下)
// 这里假设音频文件名和图像文件名可能相同或有关联
return null
} else {
// 对于历史记录,可能没有对应的图像,返回 null
return null
}
}
// 检查是否有对应的图像
const hasAudioImage = (item, isTemplate = false) => {
if (isTemplate) {
return item.inputs && item.inputs.input_image
}
return false
}
// 预加载音频时长 // 预加载音频时长
const preloadAudioDurations = (items, isTemplate = false) => { const preloadAudioDurations = (items, isTemplate = false) => {
items.forEach(item => { items.forEach(item => {
...@@ -145,9 +171,9 @@ watch(audioTemplates, (newTemplates) => { ...@@ -145,9 +171,9 @@ watch(audioTemplates, (newTemplates) => {
<!-- 模板选择浮窗 - Apple 极简风格 --> <!-- 模板选择浮窗 - Apple 极简风格 -->
<div v-cloak> <div v-cloak>
<div v-if="showImageTemplates || showAudioTemplates" <div v-if="showImageTemplates || showAudioTemplates"
class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center" class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center p-2 sm:p-1"
@click="showImageTemplates = false; showAudioTemplates = false"> @click="showImageTemplates = false; showAudioTemplates = false">
<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-10 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)]" <div class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl px-6 sm:px-10 py-6 sm:py-8 max-w-4xl w-full h-[90vh] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] flex flex-col"
@click.stop> @click.stop>
<!-- 浮窗头部 - Apple 风格 --> <!-- 浮窗头部 - Apple 风格 -->
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
...@@ -322,10 +348,16 @@ watch(audioTemplates, (newTemplates) => { ...@@ -322,10 +348,16 @@ watch(audioTemplates, (newTemplates) => {
<div v-for="(history, index) in audioHistory" :key="index" <div v-for="(history, index) in audioHistory" :key="index"
@click="selectAudioHistory(history)" @click="selectAudioHistory(history)"
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"> 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 <div
class="w-12 h-12 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-xl flex items-center justify-center flex-shrink-0"> class="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 overflow-hidden bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15">
<i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i> <img v-if="getAudioImageUrl(history, false) && !imageLoadFailed[`history_${history.filename}`]"
</div> :src="getAudioImageUrl(history, false)"
:alt="history.filename"
class="w-full h-full object-cover"
@error="imageLoadFailed[`history_${history.filename}`] = true">
<i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
</div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div <div
class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight"> class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight">
...@@ -403,10 +435,16 @@ watch(audioTemplates, (newTemplates) => { ...@@ -403,10 +435,16 @@ watch(audioTemplates, (newTemplates) => {
<div v-for="template in audioTemplates" :key="template.filename" <div v-for="template in audioTemplates" :key="template.filename"
@click="selectAudioTemplate(template)" @click="selectAudioTemplate(template)"
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"> 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 <div
class="w-12 h-12 bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-xl flex items-center justify-center flex-shrink-0"> class="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 overflow-hidden bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15">
<i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i> <img v-if="hasAudioImage(template, true) && !imageLoadFailed[`template_${template.filename}`]"
</div> :src="getAudioImageUrl(template, true)"
:alt="template.filename"
class="w-full h-full object-cover"
@error="imageLoadFailed[`template_${template.filename}`] = true">
<i v-else class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] text-xl"></i>
</div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight">{{ template.filename }} <div class="text-[#1d1d1f] dark:text-[#f5f5f7] font-medium group-hover:text-[color:var(--brand-primary)] dark:group-hover:text-[color:var(--brand-primary-light)] transition-colors truncate tracking-tight">{{ template.filename }}
</div> </div>
......
...@@ -241,7 +241,7 @@ onUnmounted(() => { ...@@ -241,7 +241,7 @@ onUnmounted(() => {
<button <button
v-if="sortedTasks.length > 1" v-if="sortedTasks.length > 1"
@click="goToPreviousTask" @click="goToPreviousTask"
class="absolute top-1/2 -translate-y-1/2 left-[-60px] md:left-[-60px] sm:left-[-30px] w-[44px] h-[44px] rounded-full border-0 cursor-pointer flex items-center justify-center text-base transition-all duration-200 ease-out z-10 bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95 disabled:opacity-20 disabled:cursor-not-allowed" class="absolute top-1/2 -translate-y-1/2 left-[-10px] sm:left-[-20px] md:left-[-40px] lg:left-[-60px] w-[44px] h-[44px] rounded-full border-0 cursor-pointer flex items-center justify-center text-base transition-all duration-200 ease-out z-10 bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95 disabled:opacity-20 disabled:cursor-not-allowed"
:disabled="sortedTasks.length <= 1"> :disabled="sortedTasks.length <= 1">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</button> </button>
...@@ -250,13 +250,13 @@ onUnmounted(() => { ...@@ -250,13 +250,13 @@ onUnmounted(() => {
<button <button
v-if="sortedTasks.length > 1" v-if="sortedTasks.length > 1"
@click="goToNextTask" @click="goToNextTask"
class="absolute top-1/2 -translate-y-1/2 right-[-60px] md:right-[-60px] sm:right-[-30px] w-[44px] h-[44px] rounded-full border-0 cursor-pointer flex items-center justify-center text-base transition-all duration-200 ease-out z-10 bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95 disabled:opacity-20 disabled:cursor-not-allowed" class="absolute top-1/2 -translate-y-1/2 right-[-10px] sm:right-[-20px] md:right-[-40px] lg:right-[-60px] w-[44px] h-[44px] rounded-full border-0 cursor-pointer flex items-center justify-center text-base transition-all duration-200 ease-out z-10 bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95 disabled:opacity-20 disabled:cursor-not-allowed"
:disabled="sortedTasks.length <= 1"> :disabled="sortedTasks.length <= 1">
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</button> </button>
<!-- 视频容器 - Apple 圆角和阴影 --> <!-- 视频容器 - Apple 圆角和阴影 -->
<div class="w-full max-w-[500px] md:max-w-[400px] sm:max-w-[300px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-[16px] overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative cursor-pointer transition-all duration-200 hover:shadow-[0_12px_32px_rgba(0,0,0,0.2)] dark:hover:shadow-[0_12px_32px_rgba(0,0,0,0.6)]" <div class="w-full max-w-[280px] sm:max-w-[300px] md:max-w-[400px] lg:max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-[16px] overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative cursor-pointer transition-all duration-200 hover:shadow-[0_12px_32px_rgba(0,0,0,0.2)] dark:hover:shadow-[0_12px_32px_rgba(0,0,0,0.6)]"
@click="openTaskDetailModal(currentTask)" @click="openTaskDetailModal(currentTask)"
:title="t('viewTaskDetails')"> :title="t('viewTaskDetails')">
<!-- 已完成:显示视频播放器 --> <!-- 已完成:显示视频播放器 -->
...@@ -374,7 +374,7 @@ onUnmounted(() => { ...@@ -374,7 +374,7 @@ onUnmounted(() => {
<button <button
v-if="isCompleted && currentTask?.outputs?.output_video" v-if="isCompleted && currentTask?.outputs?.output_video"
@click="handleDownloadFile(currentTask.task_id, 'output_video', currentTask.outputs.output_video)" @click="handleDownloadFile(currentTask.task_id, 'output_video', currentTask.outputs.output_video)"
class="w-[44px] h-[44px] md:w-[44px] md:h-[44px] sm:w-[40px] sm:h-[40px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95" class="w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[#1d1d1f] dark:text-[#f5f5f7] hover:scale-105 active:scale-95"
:title="t('download')"> :title="t('download')">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
...@@ -383,7 +383,7 @@ onUnmounted(() => { ...@@ -383,7 +383,7 @@ onUnmounted(() => {
<button <button
v-if="isCompleted && currentTask?.outputs?.output_video" v-if="isCompleted && currentTask?.outputs?.output_video"
@click="handleShareTask" @click="handleShareTask"
class="w-[44px] h-[44px] md:w-[44px] md:h-[44px] sm:w-[40px] sm:h-[40px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:scale-105 active:scale-95" class="w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:scale-105 active:scale-95"
:title="t('share')"> :title="t('share')">
<i class="fas fa-share-alt"></i> <i class="fas fa-share-alt"></i>
</button> </button>
...@@ -392,7 +392,7 @@ onUnmounted(() => { ...@@ -392,7 +392,7 @@ onUnmounted(() => {
<button <button
v-if="isRunning" v-if="isRunning"
@click="handleCancel" @click="handleCancel"
class="w-[44px] h-[44px] md:w-[44px] md:h-[44px] sm:w-[40px] sm:h-[40px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-red-500 dark:text-red-400 hover:scale-105 active:scale-95" class="w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-red-500 dark:text-red-400 hover:scale-105 active:scale-95"
:title="t('cancel')"> :title="t('cancel')">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
...@@ -401,7 +401,7 @@ onUnmounted(() => { ...@@ -401,7 +401,7 @@ onUnmounted(() => {
<button <button
v-if="isFailed || isCancelled" v-if="isFailed || isCancelled"
@click="handleRetry" @click="handleRetry"
class="w-[44px] h-[44px] md:w-[44px] md:h-[44px] sm:w-[40px] sm:h-[40px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:scale-105 active:scale-95" class="w-[40px] h-[40px] sm:w-[44px] sm:h-[44px] rounded-full flex items-center justify-center text-base transition-all duration-200 ease-out border-0 cursor-pointer bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] shadow-[0_2px_8px_rgba(0,0,0,0.12)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.4)] text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] hover:scale-105 active:scale-95"
:title="t('retry')"> :title="t('retry')">
<i class="fas fa-redo"></i> <i class="fas fa-redo"></i>
</button> </button>
......
<script setup> <script setup>
import { ref, watch, onMounted, onUnmounted, computed } from 'vue' import { ref, watch, onMounted, onUnmounted, computed, nextTick } from 'vue'
import { showTaskDetailModal, import { showTaskDetailModal,
modalTask, modalTask,
closeTaskDetailModal, closeTaskDetailModal,
...@@ -41,6 +41,14 @@ const router = useRouter() ...@@ -41,6 +41,14 @@ const router = useRouter()
const showDetails = ref(false) const showDetails = ref(false)
const loadingTaskFiles = ref(false) const loadingTaskFiles = ref(false)
// 音频播放器相关
const audioElement = ref(null)
const isPlaying = ref(false)
const audioDuration = ref(0)
const currentTime = ref(0)
const isDragging = ref(false)
const currentAudioUrl = ref('')
// 获取图片素材 // 获取图片素材
const getImageMaterials = () => { const getImageMaterials = () => {
if (!modalTask.value?.inputs?.input_image) return [] if (!modalTask.value?.inputs?.input_image) return []
...@@ -144,7 +152,100 @@ onMounted(async () => { ...@@ -144,7 +152,100 @@ onMounted(async () => {
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown) document.removeEventListener('keydown', handleKeydown)
// 清理音频资源
const audio = getCurrentAudioElement()
if (audio) {
audio.pause()
}
}) })
// 格式化音频时间
const formatAudioTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// 获取当前音频元素(处理可能是数组的情况)
const getCurrentAudioElement = () => {
return Array.isArray(audioElement.value) ? audioElement.value[0] : audioElement.value
}
// 切换播放/暂停
const toggleAudioPlayback = () => {
const audio = getCurrentAudioElement()
if (!audio) return
if (audio.paused) {
audio.play().catch(error => {
console.log('播放失败:', error)
})
} else {
audio.pause()
}
}
// 音频加载完成
const onAudioLoaded = () => {
const audio = getCurrentAudioElement()
if (audio) {
audioDuration.value = audio.duration || 0
}
}
// 时间更新
const onTimeUpdate = () => {
const audio = getCurrentAudioElement()
if (audio && !isDragging.value) {
currentTime.value = audio.currentTime || 0
}
}
// 进度条变化处理
const onProgressChange = (event) => {
const audio = getCurrentAudioElement()
if (audioDuration.value > 0 && audio && event.target) {
const newTime = parseFloat(event.target.value)
currentTime.value = newTime
audio.currentTime = newTime
}
}
// 进度条拖拽结束处理
const onProgressEnd = (event) => {
const audio = getCurrentAudioElement()
if (audio && audioDuration.value > 0 && event.target) {
const newTime = parseFloat(event.target.value)
audio.currentTime = newTime
currentTime.value = newTime
}
isDragging.value = false
}
// 播放结束
const onAudioEnded = () => {
isPlaying.value = false
currentTime.value = 0
}
// 监听音频URL变化
watch(() => getAudioMaterials(), (newMaterials) => {
if (newMaterials && newMaterials.length > 0) {
currentAudioUrl.value = newMaterials[0][1]
nextTick(() => {
const audio = getCurrentAudioElement()
if (audio) {
audio.load()
}
})
} else {
currentAudioUrl.value = ''
isPlaying.value = false
currentTime.value = 0
audioDuration.value = 0
}
}, { immediate: true })
</script> </script>
<template> <template>
<!-- 任务详情弹窗 - Apple 极简风格 --> <!-- 任务详情弹窗 - Apple 极简风格 -->
...@@ -159,7 +260,7 @@ onUnmounted(() => { ...@@ -159,7 +260,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between px-8 py-5 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px]"> <div class="flex items-center justify-between px-8 py-5 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px]">
<h3 class="text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight"> <h3 class="text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight">
<i class="fas fa-check-circle text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-check-circle text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
{{ t('taskDetails') }} {{ t('taskDetail') }}
</h3> </h3>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button @click="closeWithRoute" <button @click="closeWithRoute"
...@@ -331,7 +432,64 @@ onUnmounted(() => { ...@@ -331,7 +432,64 @@ onUnmounted(() => {
<div class="p-6 min-h-[200px]"> <div class="p-6 min-h-[200px]">
<div v-if="getAudioMaterials().length > 0" class="space-y-4"> <div v-if="getAudioMaterials().length > 0" class="space-y-4">
<div v-for="[inputName, url] in getAudioMaterials()" :key="inputName"> <div v-for="[inputName, url] in getAudioMaterials()" :key="inputName">
<audio :src="url" controls class="w-full rounded-xl"></audio> <!-- 音频播放器卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] w-full p-4">
<div class="relative flex items-center mb-3">
<!-- 头像容器 -->
<div class="relative mr-3 flex-shrink-0">
<!-- 透明白色头像 -->
<div class="w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"></div>
<!-- 播放/暂停按钮 -->
<button
@click="toggleAudioPlayback"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs ml-0.5"></i>
</button>
</div>
<!-- 音频信息 -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate">
{{ t('audio') }}
</div>
</div>
<!-- 音频时长 -->
<div class="text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0">
{{ formatAudioTime(currentTime) }} / {{ formatAudioTime(audioDuration) }}
</div>
</div>
<!-- 进度条 -->
<div class="flex items-center gap-2" v-if="audioDuration > 0">
<input
type="range"
:min="0"
:max="audioDuration"
:value="currentTime"
@input="onProgressChange"
@change="onProgressChange"
@mousedown="isDragging = true"
@mouseup="onProgressEnd"
@touchstart="isDragging = true"
@touchend="onProgressEnd"
class="flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio
ref="audioElement"
:src="url"
@loadedmetadata="onAudioLoaded"
@timeupdate="onTimeUpdate"
@ended="onAudioEnded"
@play="isPlaying = true"
@pause="isPlaying = false"
class="hidden"
></audio>
</div> </div>
</div> </div>
<div v-else class="flex flex-col items-center justify-center h-[150px]"> <div v-else class="flex flex-col items-center justify-center h-[150px]">
...@@ -381,7 +539,7 @@ onUnmounted(() => { ...@@ -381,7 +539,7 @@ onUnmounted(() => {
<i v-if="modalTask?.status === 'FAILED'" class="fas fa-exclamation-triangle text-red-500 dark:text-red-400"></i> <i v-if="modalTask?.status === 'FAILED'" class="fas fa-exclamation-triangle text-red-500 dark:text-red-400"></i>
<i v-else-if="modalTask?.status === 'CANCEL'" class="fas fa-ban text-[#86868b] dark:text-[#98989d]"></i> <i v-else-if="modalTask?.status === 'CANCEL'" class="fas fa-ban text-[#86868b] dark:text-[#98989d]"></i>
<i v-else class="fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i v-else class="fas fa-spinner fa-spin text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<span>{{ t('taskDetails') }}</span> <span>{{ t('taskDetail') }}</span>
</h3> </h3>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button @click="closeWithRoute" <button @click="closeWithRoute"
...@@ -464,7 +622,7 @@ onUnmounted(() => { ...@@ -464,7 +622,7 @@ onUnmounted(() => {
<span v-else-if="modalTask?.status === 'CANCEL'">{{ t('taskCancelled') }}</span> <span v-else-if="modalTask?.status === 'CANCEL'">{{ t('taskCancelled') }}</span>
<span v-else-if="modalTask?.status === 'RUNNING'">{{ t('taskRunning') }}</span> <span v-else-if="modalTask?.status === 'RUNNING'">{{ t('taskRunning') }}</span>
<span v-else-if="modalTask?.status === 'PENDING'">{{ t('taskPending') }}</span> <span v-else-if="modalTask?.status === 'PENDING'">{{ t('taskPending') }}</span>
<span v-else>{{ t('taskDetails') }}</span> <span v-else>{{ t('taskDetail') }}</span>
</h1> </h1>
</div> </div>
...@@ -644,7 +802,64 @@ onUnmounted(() => { ...@@ -644,7 +802,64 @@ onUnmounted(() => {
<div class="p-6 min-h-[200px]"> <div class="p-6 min-h-[200px]">
<div v-if="getAudioMaterials().length > 0" class="space-y-4"> <div v-if="getAudioMaterials().length > 0" class="space-y-4">
<div v-for="[inputName, url] in getAudioMaterials()" :key="inputName"> <div v-for="[inputName, url] in getAudioMaterials()" :key="inputName">
<audio :src="url" controls class="w-full rounded-xl"></audio> <!-- 音频播放器卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] w-full p-4">
<div class="relative flex items-center mb-3">
<!-- 头像容器 -->
<div class="relative mr-3 flex-shrink-0">
<!-- 透明白色头像 -->
<div class="w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"></div>
<!-- 播放/暂停按钮 -->
<button
@click="toggleAudioPlayback"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs ml-0.5"></i>
</button>
</div>
<!-- 音频信息 -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate">
{{ t('audio') }}
</div>
</div>
<!-- 音频时长 -->
<div class="text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0">
{{ formatAudioTime(currentTime) }} / {{ formatAudioTime(audioDuration) }}
</div>
</div>
<!-- 进度条 -->
<div class="flex items-center gap-2" v-if="audioDuration > 0">
<input
type="range"
:min="0"
:max="audioDuration"
:value="currentTime"
@input="onProgressChange"
@change="onProgressChange"
@mousedown="isDragging = true"
@mouseup="onProgressEnd"
@touchstart="isDragging = true"
@touchend="onProgressEnd"
class="flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio
ref="audioElement"
:src="url"
@loadedmetadata="onAudioLoaded"
@timeupdate="onTimeUpdate"
@ended="onAudioEnded"
@play="isPlaying = true"
@pause="isPlaying = false"
class="hidden"
></audio>
</div> </div>
</div> </div>
<div v-else class="flex flex-col items-center justify-center h-[150px]"> <div v-else class="flex flex-col items-center justify-center h-[150px]">
......
...@@ -16,7 +16,7 @@ import { showTemplateDetailModal, ...@@ -16,7 +16,7 @@ import { showTemplateDetailModal,
} from '../utils/other' } from '../utils/other'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
...@@ -24,6 +24,14 @@ const router = useRouter() ...@@ -24,6 +24,14 @@ const router = useRouter()
// 添加响应式变量 // 添加响应式变量
const showDetails = ref(false) const showDetails = ref(false)
// 音频播放器相关
const audioElement = ref(null)
const isPlaying = ref(false)
const audioDuration = ref(0)
const currentTime = ref(0)
const isDragging = ref(false)
const currentAudioUrl = ref('')
// 获取图片素材 // 获取图片素材
const getImageMaterials = () => { const getImageMaterials = () => {
if (!selectedTemplate.value?.inputs?.input_image) return [] if (!selectedTemplate.value?.inputs?.input_image) return []
...@@ -54,13 +62,14 @@ const closeWithRoute = () => { ...@@ -54,13 +62,14 @@ const closeWithRoute = () => {
// 滚动到生成区域(仅在 generate 页面) // 滚动到生成区域(仅在 generate 页面)
const scrollToCreationArea = () => { const scrollToCreationArea = () => {
const creationArea = document.querySelector('#task-creator') const mainScrollable = document.querySelector('.main-scrollbar');
if (creationArea) { if (mainScrollable) {
creationArea.scrollIntoView({ mainScrollable.scrollTo({
behavior: 'smooth', top: 0,
block: 'start' behavior: 'smooth'
}) });
} }
} }
// 包装 useTemplate 函数,在 generate 页面时滚动到生成区域 // 包装 useTemplate 函数,在 generate 页面时滚动到生成区域
...@@ -89,7 +98,100 @@ onMounted(() => { ...@@ -89,7 +98,100 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown) document.removeEventListener('keydown', handleKeydown)
// 清理音频资源
const audio = getCurrentAudioElement()
if (audio) {
audio.pause()
}
}) })
// 格式化音频时间
const formatAudioTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// 获取当前音频元素(处理可能是数组的情况)
const getCurrentAudioElement = () => {
return Array.isArray(audioElement.value) ? audioElement.value[0] : audioElement.value
}
// 切换播放/暂停
const toggleAudioPlayback = () => {
const audio = getCurrentAudioElement()
if (!audio) return
if (audio.paused) {
audio.play().catch(error => {
console.log('播放失败:', error)
})
} else {
audio.pause()
}
}
// 音频加载完成
const onAudioLoaded = () => {
const audio = getCurrentAudioElement()
if (audio) {
audioDuration.value = audio.duration || 0
}
}
// 时间更新
const onTimeUpdate = () => {
const audio = getCurrentAudioElement()
if (audio && !isDragging.value) {
currentTime.value = audio.currentTime || 0
}
}
// 进度条变化处理
const onProgressChange = (event) => {
const audio = getCurrentAudioElement()
if (audioDuration.value > 0 && audio && event.target) {
const newTime = parseFloat(event.target.value)
currentTime.value = newTime
audio.currentTime = newTime
}
}
// 进度条拖拽结束处理
const onProgressEnd = (event) => {
const audio = getCurrentAudioElement()
if (audio && audioDuration.value > 0 && event.target) {
const newTime = parseFloat(event.target.value)
audio.currentTime = newTime
currentTime.value = newTime
}
isDragging.value = false
}
// 播放结束
const onAudioEnded = () => {
isPlaying.value = false
currentTime.value = 0
}
// 监听音频URL变化
watch(() => getAudioMaterials(), (newMaterials) => {
if (newMaterials && newMaterials.length > 0) {
currentAudioUrl.value = newMaterials[0][1]
nextTick(() => {
const audio = getCurrentAudioElement()
if (audio) {
audio.load()
}
})
} else {
currentAudioUrl.value = ''
isPlaying.value = false
currentTime.value = 0
audioDuration.value = 0
}
}, { immediate: true })
</script> </script>
<template> <template>
<!-- 模板详情弹窗 - Apple 极简风格 --> <!-- 模板详情弹窗 - Apple 极简风格 -->
...@@ -268,7 +370,64 @@ onUnmounted(() => { ...@@ -268,7 +370,64 @@ onUnmounted(() => {
<div class="p-6 min-h-[200px]"> <div class="p-6 min-h-[200px]">
<div v-if="getAudioMaterials().length > 0" class="space-y-4"> <div v-if="getAudioMaterials().length > 0" class="space-y-4">
<div v-for="[inputName, url] in getAudioMaterials()" :key="inputName"> <div v-for="[inputName, url] in getAudioMaterials()" :key="inputName">
<audio :src="url" controls class="w-full rounded-xl"></audio> <!-- 音频播放器卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] w-full p-4">
<div class="relative flex items-center mb-3">
<!-- 头像容器 -->
<div class="relative mr-3 flex-shrink-0">
<!-- 透明白色头像 -->
<div class="w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"></div>
<!-- 播放/暂停按钮 -->
<button
@click="toggleAudioPlayback"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs ml-0.5"></i>
</button>
</div>
<!-- 音频信息 -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate">
{{ t('audio') }}
</div>
</div>
<!-- 音频时长 -->
<div class="text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0">
{{ formatAudioTime(currentTime) }} / {{ formatAudioTime(audioDuration) }}
</div>
</div>
<!-- 进度条 -->
<div class="flex items-center gap-2" v-if="audioDuration > 0">
<input
type="range"
:min="0"
:max="audioDuration"
:value="currentTime"
@input="onProgressChange"
@change="onProgressChange"
@mousedown="isDragging = true"
@mouseup="onProgressEnd"
@touchstart="isDragging = true"
@touchend="onProgressEnd"
class="flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio
ref="audioElement"
:src="url"
@loadedmetadata="onAudioLoaded"
@timeupdate="onTimeUpdate"
@ended="onAudioEnded"
@play="isPlaying = true"
@pause="isPlaying = false"
class="hidden"
></audio>
</div> </div>
</div> </div>
<div v-else class="flex flex-col items-center justify-center h-[150px]"> <div v-else class="flex flex-col items-center justify-center h-[150px]">
......
...@@ -33,7 +33,7 @@ onMounted(() => { ...@@ -33,7 +33,7 @@ onMounted(() => {
<template> <template>
<!-- Apple 风格顶部栏 - Tailwind 深浅色 --> <!-- Apple 风格顶部栏 - Tailwind 深浅色 -->
<div class="sticky top-0 z-[100] bg-white/80 dark:bg-[#1e1e1e]/80 backdrop-blur-[20px] backdrop-saturate-[180%] border-b border-black/8 dark:border-white/8 shadow-[0_1px_3px_0_rgba(0,0,0,0.05)] dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.3)] transition-all duration-300"> <div class="sticky top-0 z-[100] bg-white/80 dark:bg-[#1e1e1e]/80 backdrop-blur-[20px] backdrop-saturate-[180%] border-b border-black/8 dark:border-white/8 shadow-[0_1px_3px_0_rgba(0,0,0,0.05)] dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.3)] transition-all duration-300 flex-shrink-0">
<div class="flex justify-between items-center max-w-full mx-auto px-6 py-3"> <div class="flex justify-between items-center max-w-full mx-auto px-6 py-3">
<!-- 左侧 Logo --> <!-- 左侧 Logo -->
<div class="flex items-center"> <div class="flex items-center">
......
<template> <template>
<!-- 模态框容器 - Apple 极简风格 --> <!-- 模态框遮罩和容器 - Apple 极简风格 -->
<div class="flex flex-col h-full bg-white dark:bg-[#1e1e1e] overflow-hidden"> <div class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center p-2 sm:p-1">
<div class="relative w-full h-full max-w-6xl max-h-[100vh] bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] overflow-hidden flex flex-col">
<!-- 模态框头部 - Apple 风格 --> <!-- 模态框头部 - Apple 风格 -->
<div class="flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px] flex-shrink-0"> <div class="flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px] flex-shrink-0">
<h3 class="text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight"> <h3 class="text-xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center gap-3 tracking-tight">
...@@ -24,17 +25,159 @@ ...@@ -24,17 +25,159 @@
</div> </div>
</div> </div>
<!-- 模态框内容 - Apple 风格 --> <!-- 固定区域:音频播放器和设置面板 - Apple 极简风格 -->
<div class="flex-1 overflow-y-auto p-6 main-scrollbar"> <div v-if="audioUrl || selectedVoice" class="flex-shrink-0 bg-[#f5f5f7]/30 dark:bg-[#1c1c1e]/30">
<div class="max-w-5xl mx-auto space-y-6"> <div class="max-w-5xl mx-auto px-6 py-5">
<!-- 音频播放器 - Apple 风格 --> <div class="flex flex-col lg:flex-row gap-6 lg:gap-8">
<div v-if="audioUrl" class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl p-5 transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <!-- 音频播放器卡片 - Apple 风格 -->
<div class="flex items-center gap-3 mb-3"> <div v-if="audioUrl || isGenerating" class="flex-1 lg:w-1/2">
<i class="fas fa-music text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] p-4">
<h6 class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('generatedAudio') }}</h6> <div class="relative flex items-center mb-3">
<!-- 头像容器 -->
<div class="relative mr-3 flex-shrink-0">
<!-- 透明白色头像 -->
<div class="w-12 h-12 rounded-full bg-white/40 dark:bg-white/20 border border-white/30 dark:border-white/20 transition-all duration-200"></div>
<!-- Loading 指示器 - Apple 风格 -->
<div v-if="isGenerating" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white z-20">
<i class="fas fa-spinner fa-spin text-xs"></i>
</div>
<!-- 播放/暂停按钮 -->
<button
v-else
@click="toggleAudioPlayback"
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]"
>
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-xs ml-0.5"></i>
</button>
</div>
<!-- 音频信息 -->
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate">
{{ t('synthesizedAudio') }}<span v-if="selectedVoiceData"> - {{ selectedVoiceData.name }}</span>
</div>
</div>
<!-- 音频时长 -->
<div class="text-xs font-medium text-[#86868b] dark:text-[#98989d] tracking-tight flex-shrink-0">
{{ formatAudioTime(currentTime) }} / {{ formatAudioTime(audioDuration) }}
</div>
</div>
<!-- 进度条 -->
<div class="flex items-center gap-2" v-if="audioDuration > 0">
<input
type="range"
:min="0"
:max="audioDuration"
:value="currentTime"
@input="onProgressChange"
@change="onProgressChange"
@mousedown="isDragging = true"
@mouseup="onProgressEnd"
@touchstart="isDragging = true"
@touchend="onProgressEnd"
class="flex-1 h-1 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio
v-if="audioUrl"
ref="audioElement"
:src="audioUrl"
@loadedmetadata="onAudioLoaded"
@timeupdate="onTimeUpdate"
@ended="onAudioEnded"
@play="isPlaying = true"
@pause="isPlaying = false"
class="hidden"
></audio>
</div>
<!-- 设置面板 - Apple 极简风格(无卡片,直接显示) -->
<div v-if="selectedVoice" class="flex-shrink-0 lg:w-1/2">
<div class="space-y-3">
<!-- 语速控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-14 tracking-tight">{{ t('speechRate') }}</label>
<input
type="range"
min="-50"
max="100"
v-model="speechRate"
class="flex-1 h-0.5 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getSpeechRateDisplayValue(speechRate) }}</span>
</div>
<!-- 音量控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-14 tracking-tight">{{ t('volume') }}</label>
<input
type="range"
min="-50"
max="100"
v-model="loudnessRate"
class="flex-1 h-0.5 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getLoudnessDisplayValue(loudnessRate) }}</span>
</div>
<!-- 音调控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-14 tracking-tight">{{ t('pitch') }}</label>
<input
type="range"
min="-12"
max="12"
v-model="pitch"
class="flex-1 h-0.5 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getPitchDisplayValue(pitch) }}</span>
</div>
<!-- 情感控制 - 仅当音色支持时显示 -->
<div v-if="selectedVoiceData && selectedVoiceData.emotions && selectedVoiceData.emotions.length > 0" class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-14 tracking-tight">{{ t('emotionIntensity') }}</label>
<input
type="range"
min="1"
max="5"
v-model="emotionScale"
class="flex-1 h-0.5 bg-black/6 dark:bg-white/15 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ emotionScale }}</span>
</div>
<div v-if="selectedVoiceData && selectedVoiceData.emotions && selectedVoiceData.emotions.length > 0" class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-14 tracking-tight">{{ t('emotionType') }}</label>
<div class="flex-1">
<DropdownMenu
:items="emotionItems"
:selected-value="selectedEmotion"
:placeholder="t('neutral')"
@select-item="handleEmotionSelect"
/>
</div>
</div>
</div>
</div> </div>
<audio :src="audioUrl" controls class="w-full rounded-xl"></audio>
</div> </div>
</div>
<!-- 装饰性分割线 - Apple 风格(带V形图标) -->
<div class="relative flex items-center justify-center py-3">
<!-- 左侧线条 -->
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-black/20 dark:via-white/20 to-black/20 dark:to-white/20"></div>
<!-- 中间V形图标 -->
<div class="mx-4 flex items-center justify-center w-6 h-6 rounded-full bg-white/60 dark:bg-[#2c2c2e]/60 border border-black/10 dark:border-white/10">
<i class="fas fa-chevron-down text-[8px] text-[#86868b] dark:text-[#98989d]"></i>
</div>
<!-- 右侧线条 -->
<div class="flex-1 h-px bg-gradient-to-l from-transparent via-black/20 dark:via-white/20 to-black/20 dark:to-white/20"></div>
</div>
</div>
<!-- 模态框内容 - Apple 风格(可滚动区域) -->
<div class="flex-1 overflow-y-auto p-6 main-scrollbar">
<div class="max-w-5xl mx-auto space-y-6">
<!-- 文本输入区域 - Apple 风格 --> <!-- 文本输入区域 - Apple 风格 -->
<div> <div>
<label class="block text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 tracking-tight"> <label class="block text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 tracking-tight">
...@@ -60,7 +203,7 @@ ...@@ -60,7 +203,7 @@
v-model="contextText" v-model="contextText"
:placeholder="t('voiceInstructionPlaceholder')" :placeholder="t('voiceInstructionPlaceholder')"
class="w-full bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl px-5 py-3 text-[15px] text-[#1d1d1f] dark:text-[#f5f5f7] placeholder-[#86868b] dark:placeholder-[#98989d] tracking-tight hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 focus:outline-none focus:border-[color:var(--brand-primary)]/50 dark:focus:border-[color:var(--brand-primary-light)]/60 focus:shadow-[0_4px_16px_rgba(var(--brand-primary-rgb),0.12)] dark:focus:shadow-[0_4px_16px_rgba(var(--brand-primary-light-rgb),0.2)] transition-all duration-200 resize-none" class="w-full bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl px-5 py-3 text-[15px] text-[#1d1d1f] dark:text-[#f5f5f7] placeholder-[#86868b] dark:placeholder-[#98989d] tracking-tight hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 focus:outline-none focus:border-[color:var(--brand-primary)]/50 dark:focus:border-[color:var(--brand-primary-light)]/60 focus:shadow-[0_4px_16px_rgba(var(--brand-primary-rgb),0.12)] dark:focus:shadow-[0_4px_16px_rgba(var(--brand-primary-light-rgb),0.2)] transition-all duration-200 resize-none"
rows="2" rows="3"
></textarea> ></textarea>
</div> </div>
...@@ -109,9 +252,13 @@ ...@@ -109,9 +252,13 @@
<div <div
class="relative flex items-center p-4 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]" class="relative flex items-center p-4 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"
:class="{ :class="{
'border-[color:var(--brand-primary)] dark:border-[color:var(--brand-primary-light)] bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 shadow-[0_4px_12px_rgba(var(--brand-primary-rgb),0.15)] dark:shadow-[0_4px_12px_rgba(var(--brand-primary-light-rgb),0.2)]': selectedVoice === voice.voice_type 'border-2 border-[color:var(--brand-primary)] dark:border-[color:var(--brand-primary-light)] bg-[color:var(--brand-primary)]/12 dark:bg-[color:var(--brand-primary-light)]/20 shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.25)] dark:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.35)] ring-2 ring-[color:var(--brand-primary)]/20 dark:ring-[color:var(--brand-primary-light)]/30': selectedVoice === voice.voice_type
}" }"
> >
<!-- 选中指示器 - Apple 风格 -->
<div v-if="selectedVoice === voice.voice_type" class="absolute top-2 left-2 w-5 h-5 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] rounded-full flex items-center justify-center z-10 shadow-[0_2px_8px_rgba(var(--brand-primary-rgb),0.3)] dark:shadow-[0_2px_8px_rgba(var(--brand-primary-light-rgb),0.4)]">
<i class="fas fa-check text-white text-[10px]"></i>
</div>
<!-- V2 标签 - Apple 风格 --> <!-- V2 标签 - Apple 风格 -->
<div v-if="voice.version === '2.0'" class="absolute top-2 right-2 px-2 py-1 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white text-[10px] font-semibold rounded-md z-10"> <div v-if="voice.version === '2.0'" class="absolute top-2 right-2 px-2 py-1 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white text-[10px] font-semibold rounded-md z-10">
v2.0 v2.0
...@@ -125,7 +272,6 @@ ...@@ -125,7 +272,6 @@
src="../../public/female.svg" src="../../public/female.svg"
alt="Female Avatar" alt="Female Avatar"
class="w-12 h-12 rounded-full object-cover bg-white transition-all duration-200" class="w-12 h-12 rounded-full object-cover bg-white transition-all duration-200"
:class="{ 'opacity-60': selectedVoice === voice.voice_type }"
/> />
<!-- Male Avatar --> <!-- Male Avatar -->
<img <img
...@@ -133,16 +279,11 @@ ...@@ -133,16 +279,11 @@
src="../../public/male.svg" src="../../public/male.svg"
alt="Male Avatar" alt="Male Avatar"
class="w-12 h-12 rounded-full object-cover bg-white transition-all duration-200" class="w-12 h-12 rounded-full object-cover bg-white transition-all duration-200"
:class="{ 'opacity-60': selectedVoice === voice.voice_type }"
/> />
<!-- Loading 指示器 - Apple 风格 --> <!-- Loading 指示器 - Apple 风格 -->
<div v-if="isGenerating && selectedVoice === voice.voice_type" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white z-20"> <div v-if="isGenerating && selectedVoice === voice.voice_type" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white z-20">
<i class="fas fa-spinner fa-spin text-xs"></i> <i class="fas fa-spinner fa-spin text-xs"></i>
</div> </div>
<!-- 设置按钮 - Apple 风格 -->
<div v-if="!isGenerating && selectedVoice === voice.voice_type" @click.stop="toggleControls" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-8 h-8 bg-[color:var(--brand-primary)]/90 dark:bg-[color:var(--brand-primary-light)]/90 rounded-full flex items-center justify-center text-white cursor-pointer hover:scale-110 transition-all duration-200 z-20">
<i class="fas fa-cog text-xs"></i>
</div>
</div> </div>
<!-- 音色信息 --> <!-- 音色信息 -->
...@@ -163,69 +304,6 @@ ...@@ -163,69 +304,6 @@
</span> </span>
</div> </div>
</div> </div>
<!-- TTS 控制面板 - Apple 风格 -->
<div v-if="selectedVoice === voice.voice_type && showControls" class="absolute top-full left-0 right-0 mt-2 bg-white/95 dark:bg-[#2c2c2e]/95 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl p-4 shadow-[0_8px_24px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.4)] z-50 space-y-3">
<!-- 语速控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-16 tracking-tight">{{ t('speechRate') }}:</label>
<input
type="range"
min="-50"
max="100"
v-model="speechRate"
class="flex-1 h-1 bg-black/8 dark:bg-white/8 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getSpeechRateDisplayValue(speechRate) }}</span>
</div>
<!-- 音量控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-16 tracking-tight">{{ t('volume') }}:</label>
<input
type="range"
min="-50"
max="100"
v-model="loudnessRate"
class="flex-1 h-1 bg-black/8 dark:bg-white/8 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getLoudnessDisplayValue(loudnessRate) }}</span>
</div>
<!-- 音调控制 -->
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-16 tracking-tight">{{ t('pitch') }}:</label>
<input
type="range"
min="-12"
max="12"
v-model="pitch"
class="flex-1 h-1 bg-black/8 dark:bg-white/8 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ getPitchDisplayValue(pitch) }}</span>
</div>
<!-- 情感控制 - 仅当音色支持时显示 -->
<div v-if="voice.emotions && voice.emotions.length > 0" class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-16 tracking-tight">{{ t('emotionIntensity') }}:</label>
<input
type="range"
min="1"
max="5"
v-model="emotionScale"
class="flex-1 h-1 bg-black/8 dark:bg-white/8 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-[color:var(--brand-primary)] dark:[&::-webkit-slider-thumb]:bg-[color:var(--brand-primary-light)] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:cursor-pointer"
/>
<span class="text-xs font-medium text-[#1d1d1f] dark:text-[#f5f5f7] w-12 text-right tracking-tight">{{ emotionScale }}</span>
</div>
<div v-if="voice.emotions && voice.emotions.length > 0" class="flex items-center gap-3">
<label class="text-xs font-medium text-[#86868b] dark:text-[#98989d] w-16 tracking-tight">{{ t('emotionType') }}:</label>
<div class="flex-1">
<DropdownMenu
:items="emotionItems"
:selected-value="selectedEmotion"
:placeholder="t('neutral')"
@select-item="handleEmotionSelect"
/>
</div>
</div>
</div>
</div> </div>
</label> </label>
</div> </div>
...@@ -234,10 +312,11 @@ ...@@ -234,10 +312,11 @@
</div> </div>
</div> </div>
</div>
</div> </div>
<!-- 筛选面板遮罩 - Apple 风格 --> <!-- 筛选面板遮罩 - Apple 风格 -->
<div v-if="showFilterPanel" class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[9999] flex items-center justify-center p-4" @click="closeFilterPanel"> <div v-if="showFilterPanel" class="fixed inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" @click="closeFilterPanel">
<div class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] flex flex-col" @click.stop> <div class="bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] backdrop-saturate-[180%] border border-black/10 dark:border-white/10 rounded-3xl w-full max-w-2xl max-h-[85vh] overflow-hidden shadow-[0_20px_60px_rgba(0,0,0,0.2)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.6)] flex flex-col" @click.stop>
<!-- 筛选面板头部 - Apple 风格 --> <!-- 筛选面板头部 - Apple 风格 -->
<div class="flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px]"> <div class="flex items-center justify-between px-6 py-4 border-b border-black/8 dark:border-white/8 bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[20px]">
...@@ -344,7 +423,7 @@ ...@@ -344,7 +423,7 @@
</template> </template>
<script> <script>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import DropdownMenu from './DropdownMenu.vue' import DropdownMenu from './DropdownMenu.vue'
...@@ -369,6 +448,12 @@ export default { ...@@ -369,6 +448,12 @@ export default {
const isGenerating = ref(false) const isGenerating = ref(false)
const audioUrl = ref('') const audioUrl = ref('')
const currentAudio = ref(null) // 当前播放的音频对象 const currentAudio = ref(null) // 当前播放的音频对象
const audioElement = ref(null) // 音频元素引用
const isPlaying = ref(false) // 播放状态
const audioDuration = ref(0) // 音频总时长
const currentTime = ref(0) // 当前播放时间
const shouldAutoPlay = ref(false) // 是否需要自动播放
const isDragging = ref(false) // 是否正在拖拽进度条
const voices = ref([]) const voices = ref([])
const emotions = ref([]) const emotions = ref([])
const voiceListContainer = ref(null) const voiceListContainer = ref(null)
...@@ -514,31 +599,6 @@ export default { ...@@ -514,31 +599,6 @@ export default {
}) })
// Filter voices based on search query, category, version, language, and gender // Filter voices based on search query, category, version, language, and gender
// Emotion items for dropdown
const emotionItems = computed(() => {
const items = []
const selectedVoiceData = voices.value.find(v => v.voice_type === selectedVoice.value)
if (selectedVoiceData && selectedVoiceData.emotions && emotions.value.length > 0) {
selectedVoiceData.emotions.forEach(emotionName => {
// Find the emotion data from emotions array
const emotionData = emotions.value.find(emotion => emotion.name === emotionName)
if (emotionData) {
items.push({ value: emotionName, label: emotionData.zh })
} else {
// Fallback if emotion not found in emotions data
items.push({ value: emotionName, label: emotionName })
}
})
}
// If no emotions found or no neutral emotion in the list, add neutral as default
if (items.length === 0 || !items.find(item => item.value === 'neutral')) {
items.unshift({ value: 'neutral', label: t('neutral') })
}
return items
})
const filteredVoices = computed(() => { const filteredVoices = computed(() => {
let filtered = [...voices.value] // 创建副本,避免修改原始数据 let filtered = [...voices.value] // 创建副本,避免修改原始数据
...@@ -619,10 +679,38 @@ export default { ...@@ -619,10 +679,38 @@ export default {
return name.toLowerCase().includes('female') return name.toLowerCase().includes('female')
} }
// Get selected voice data
const selectedVoiceData = computed(() => {
return voices.value.find(v => v.voice_type === selectedVoice.value)
})
// Emotion items for dropdown
const emotionItems = computed(() => {
const items = []
if (selectedVoiceData.value && selectedVoiceData.value.emotions && emotions.value.length > 0) {
selectedVoiceData.value.emotions.forEach(emotionName => {
// Find the emotion data from emotions array
const emotionData = emotions.value.find(emotion => emotion.name === emotionName)
if (emotionData) {
items.push({ value: emotionName, label: emotionData.zh })
} else {
// Fallback if emotion not found in emotions data
items.push({ value: emotionName, label: emotionName })
}
})
}
// If no emotions found or no neutral emotion in the list, add neutral as default
if (items.length === 0 || !items.find(item => item.value === 'neutral')) {
items.unshift({ value: 'neutral', label: t('neutral') })
}
return items
})
// Get available emotions for selected voice // Get available emotions for selected voice
const availableEmotions = computed(() => { const availableEmotions = computed(() => {
const selectedVoiceData = voices.value.find(v => v.voice_type === selectedVoice.value) return selectedVoiceData.value?.emotions || []
return selectedVoiceData?.emotions || []
}) })
// Reset scroll position // Reset scroll position
...@@ -647,9 +735,6 @@ export default { ...@@ -647,9 +735,6 @@ export default {
selectedEmotion.value = '' selectedEmotion.value = ''
} }
// Reset scroll position when voice is selected
resetScrollPosition()
// Auto-generate TTS when voice is selected and text is available // Auto-generate TTS when voice is selected and text is available
await generateTTS() await generateTTS()
} }
...@@ -663,6 +748,10 @@ export default { ...@@ -663,6 +748,10 @@ export default {
if (!selectedVoice.value) return if (!selectedVoice.value) return
// 停止当前播放的音频 // 停止当前播放的音频
if (audioElement.value) {
audioElement.value.pause()
audioElement.value.currentTime = 0
}
if (currentAudio.value) { if (currentAudio.value) {
currentAudio.value.pause() currentAudio.value.pause()
currentAudio.value.currentTime = 0 currentAudio.value.currentTime = 0
...@@ -693,14 +782,8 @@ export default { ...@@ -693,14 +782,8 @@ export default {
if (response.ok) { if (response.ok) {
const blob = await response.blob() const blob = await response.blob()
audioUrl.value = URL.createObjectURL(blob) audioUrl.value = URL.createObjectURL(blob)
// 标记需要自动播放
// 自动播放生成的音频 shouldAutoPlay.value = true
const audio = new Audio(audioUrl.value)
currentAudio.value = audio // 保存当前播放的音频对象
audio.play().catch(error => {
console.log('自动播放被阻止:', error)
// 如果自动播放失败,用户仍可以手动播放
})
} else { } else {
throw new Error('TTS generation failed') throw new Error('TTS generation failed')
} }
...@@ -712,6 +795,98 @@ export default { ...@@ -712,6 +795,98 @@ export default {
} }
} }
// 格式化音频时间
const formatAudioTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// 切换播放/暂停
const toggleAudioPlayback = () => {
if (!audioElement.value) return
if (audioElement.value.paused) {
audioElement.value.play().catch(error => {
console.log('播放失败:', error)
})
} else {
audioElement.value.pause()
}
}
// 音频加载完成
const onAudioLoaded = () => {
if (audioElement.value) {
audioDuration.value = audioElement.value.duration || 0
// 如果需要自动播放,则播放
if (shouldAutoPlay.value) {
setTimeout(() => {
if (audioElement.value && !audioElement.value.paused) {
return // 如果已经在播放,不重复播放
}
audioElement.value.play().catch(error => {
console.log('自动播放被阻止:', error)
})
shouldAutoPlay.value = false // 重置自动播放标志
}, 100)
}
}
}
// 时间更新
const onTimeUpdate = () => {
if (audioElement.value && !isDragging.value) {
currentTime.value = audioElement.value.currentTime || 0
}
}
// 进度条变化处理(点击或拖拽)
const onProgressChange = (event) => {
if (audioDuration.value > 0 && audioElement.value && event.target) {
const newTime = parseFloat(event.target.value)
currentTime.value = newTime
// 立即更新音频位置
audioElement.value.currentTime = newTime
}
}
// 进度条拖拽结束处理
const onProgressEnd = (event) => {
if (audioElement.value && audioDuration.value > 0 && event.target) {
const newTime = parseFloat(event.target.value)
audioElement.value.currentTime = newTime
currentTime.value = newTime
}
isDragging.value = false
}
// 播放结束
const onAudioEnded = () => {
isPlaying.value = false
currentTime.value = 0
}
// 监听音频 URL 变化,重置状态
watch(audioUrl, (newUrl) => {
if (newUrl) {
isPlaying.value = false
currentTime.value = 0
audioDuration.value = 0
// 等待 DOM 更新后加载音频
nextTick(() => {
if (audioElement.value) {
audioElement.value.load()
}
})
} else {
// URL 清空时重置自动播放标志
shouldAutoPlay.value = false
}
})
// Apply selected voice (emit the generated audio) // Apply selected voice (emit the generated audio)
const applySelectedVoice = () => { const applySelectedVoice = () => {
if (audioUrl.value) { if (audioUrl.value) {
...@@ -824,12 +999,25 @@ export default { ...@@ -824,12 +999,25 @@ export default {
selectedEmotion, selectedEmotion,
isGenerating, isGenerating,
audioUrl, audioUrl,
audioElement,
isPlaying,
audioDuration,
currentTime,
isDragging,
onProgressChange,
onProgressEnd,
voices, voices,
voiceListContainer, voiceListContainer,
showControls, showControls,
showFilterPanel, showFilterPanel,
filteredVoices, filteredVoices,
isFemaleVoice, isFemaleVoice,
selectedVoiceData,
formatAudioTime,
toggleAudioPlayback,
onAudioLoaded,
onTimeUpdate,
onAudioEnded,
availableEmotions, availableEmotions,
onVoiceSelect, onVoiceSelect,
generateTTS, generateTTS,
...@@ -884,4 +1072,13 @@ export default { ...@@ -884,4 +1072,13 @@ export default {
white-space: nowrap; white-space: nowrap;
border-width: 0; border-width: 0;
} }
/* 深色模式下增强滑动条可见性 */
.dark input[type="range"]::-webkit-slider-thumb {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}
.dark input[type="range"]::-moz-range-thumb {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}
</style> </style>
...@@ -199,7 +199,7 @@ ...@@ -199,7 +199,7 @@
"supportedAudioFormatsShort": "Supported mp3, m4a, wav formats", "supportedAudioFormatsShort": "Supported mp3, m4a, wav formats",
"clearCharacterImageTip": "Upload a clear character image", "clearCharacterImageTip": "Upload a clear character image",
"maxFileSize": "Max file size", "maxFileSize": "Max file size",
"taskDetails": "Task Details", "taskDetail": "Task Details",
"taskId": "Task ID", "taskId": "Task ID",
"taskType": "Task Type", "taskType": "Task Type",
"taskStatus": "Task Status", "taskStatus": "Task Status",
...@@ -212,6 +212,8 @@ ...@@ -212,6 +212,8 @@
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"close": "Close", "close": "Close",
"copyLink": "Copy Link",
"pleaseCopyManually": "Please manually select and copy the text below",
"back": "Back", "back": "Back",
"next": "Next", "next": "Next",
"previous": "Previous", "previous": "Previous",
...@@ -323,6 +325,7 @@ ...@@ -323,6 +325,7 @@
"voiceSynthesis": "Voice Synthesis", "voiceSynthesis": "Voice Synthesis",
"applySelectedVoice": "Apply selected voice", "applySelectedVoice": "Apply selected voice",
"generatedAudio": "Generated Audio", "generatedAudio": "Generated Audio",
"synthesizedAudio": "Synthesized Audio",
"enterTextToConvert": "Enter text to convert", "enterTextToConvert": "Enter text to convert",
"ttsPlaceholder": "Hello, how can I help you?", "ttsPlaceholder": "Hello, how can I help you?",
"voiceInstruction": "Voice Instruction", "voiceInstruction": "Voice Instruction",
...@@ -332,6 +335,7 @@ ...@@ -332,6 +335,7 @@
"searchVoice": "Search Voice", "searchVoice": "Search Voice",
"filter": "Filter", "filter": "Filter",
"filterVoices": "Filter Voices", "filterVoices": "Filter Voices",
"voiceSettings": "Voice Settings",
"speechRate": "Speech Rate", "speechRate": "Speech Rate",
"volume": "Volume", "volume": "Volume",
"pitch": "Pitch", "pitch": "Pitch",
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
"templateDetail": "模板详情", "templateDetail": "模板详情",
"viewTemplateDetail": "查看模板详情", "viewTemplateDetail": "查看模板详情",
"viewTaskDetails": "查看任务详情", "viewTaskDetails": "查看任务详情",
"taskDetail": "任务详情",
"templateInfo": "模板信息", "templateInfo": "模板信息",
"useTemplate": "使用模板", "useTemplate": "使用模板",
"model": "模型", "model": "模型",
...@@ -203,6 +204,8 @@ ...@@ -203,6 +204,8 @@
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
"close": "关闭", "close": "关闭",
"copyLink": "复制链接",
"pleaseCopyManually": "请手动选择并复制下面的文本",
"back": "返回", "back": "返回",
"next": "下一步", "next": "下一步",
"previous": "上一步", "previous": "上一步",
...@@ -325,7 +328,7 @@ ...@@ -325,7 +328,7 @@
"userGeneratedVideo": "生成的视频", "userGeneratedVideo": "生成的视频",
"noImage": "暂无图片", "noImage": "暂无图片",
"noAudio": "暂无音频", "noAudio": "暂无音频",
"taskCompletedSuccessfully": "LightX2V 已成功为您生成视频", "taskCompletedSuccessfully": "视频生成完成!",
"onlyUseImage": "仅使用图片", "onlyUseImage": "仅使用图片",
"onlyUseAudio": "仅使用音频", "onlyUseAudio": "仅使用音频",
"reUseImage": "复用图片", "reUseImage": "复用图片",
...@@ -336,6 +339,7 @@ ...@@ -336,6 +339,7 @@
"voiceSynthesis": "语音合成", "voiceSynthesis": "语音合成",
"applySelectedVoice": "应用当前选择的声音", "applySelectedVoice": "应用当前选择的声音",
"generatedAudio": "生成的音频", "generatedAudio": "生成的音频",
"synthesizedAudio": "合成音频",
"enterTextToConvert": "输入要转换的文本", "enterTextToConvert": "输入要转换的文本",
"ttsPlaceholder": "你好,请问我有什么可以帮您?", "ttsPlaceholder": "你好,请问我有什么可以帮您?",
"voiceInstruction": "语音指令", "voiceInstruction": "语音指令",
...@@ -345,6 +349,7 @@ ...@@ -345,6 +349,7 @@
"searchVoice": "搜索音色", "searchVoice": "搜索音色",
"filter": "筛选", "filter": "筛选",
"filterVoices": "筛选音色", "filterVoices": "筛选音色",
"voiceSettings": "语音设置",
"speechRate": "语速", "speechRate": "语速",
"volume": "音量", "volume": "音量",
"pitch": "音调", "pitch": "音调",
......
...@@ -81,7 +81,7 @@ const router = createRouter({ ...@@ -81,7 +81,7 @@ const router = createRouter({
// 路由守卫 - 整合和优化后的逻辑 // 路由守卫 - 整合和优化后的逻辑
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const token = localStorage.getItem('accessToken') const token = localStorage.getItem('accessToken')
console.log('token', token)
// 检查 URL 中是否有 code 参数(OAuth 回调) // 检查 URL 中是否有 code 参数(OAuth 回调)
// 可以从路由查询参数或实际 URL 中获取 // 可以从路由查询参数或实际 URL 中获取
const hasOAuthCode = to.query?.code !== undefined || const hasOAuthCode = to.query?.code !== undefined ||
......
...@@ -630,7 +630,7 @@ body { ...@@ -630,7 +630,7 @@ body {
/* 任务创建面板全屏 */ /* 任务创建面板全屏 */
#task-creator { #task-creator {
max-width: none; max-width: none;
width: 90%; width: 80%;
} }
#inspiration-gallery { #inspiration-gallery {
...@@ -642,8 +642,7 @@ body { ...@@ -642,8 +642,7 @@ body {
/* 移动端全屏显示 */ /* 移动端全屏显示 */
@media (max-width: 768px) { @media (max-width: 768px) {
#task-creator { #task-creator {
width: 100%; width: 95%;
padding: 0 0.5rem;
} }
#inspiration-gallery { #inspiration-gallery {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,28 +6,56 @@ import Confirm from '../components/Confirm.vue' ...@@ -6,28 +6,56 @@ import Confirm from '../components/Confirm.vue'
import TaskDetails from '../components/TaskDetails.vue' import TaskDetails from '../components/TaskDetails.vue'
import TemplateDetails from '../components/TemplateDetails.vue' import TemplateDetails from '../components/TemplateDetails.vue'
import PromptTemplate from '../components/PromptTemplate.vue' import PromptTemplate from '../components/PromptTemplate.vue'
import Voice_tts from '../components/Voice_tts.vue'
import MediaTemplate from '../components/MediaTemplate.vue'
import Loading from '../components/Loading.vue' import Loading from '../components/Loading.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isLoading } from '../utils/other' import { isLoading, showVoiceTTSModal, handleAudioUpload, showAlert } from '../utils/other'
const { t } = useI18n() const { t } = useI18n()
// 处理 TTS 完成回调
const handleTTSComplete = (audioBlob) => {
// 创建File对象
const audioFile = new File([audioBlob], 'tts_audio.mp3', { type: 'audio/mpeg' })
// 模拟文件上传事件
const dataTransfer = new DataTransfer()
dataTransfer.items.add(audioFile)
const fileList = dataTransfer.files
const event = {
target: {
files: fileList
}
}
// 处理音频上传
handleAudioUpload(event)
// 关闭模态框
showVoiceTTSModal.value = false
// 显示成功提示
showAlert('语音合成完成,已自动添加到音频素材', 'success')
}
</script> </script>
<template> <template>
<!-- 主容器 - Apple 极简风格 - 配合80%缩放铺满屏幕 --> <!-- 主容器 - Apple 极简风格 - 配合80%缩放铺满屏幕 -->
<div class="bg-[#f5f5f7] dark:bg-[#000000] transition-colors duration-300 w-full h-full"> <div class="bg-[#f5f5f7] dark:bg-[#000000] transition-colors duration-300 w-full h-full overflow-y-auto main-scrollbar">
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="flex flex-col w-full h-full"> <div class="flex flex-col w-full min-h-full">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<TopBar /> <TopBar />
<!-- 内容区域 - 响应式布局 --> <!-- 内容区域 - 响应式布局 -->
<div class="flex flex-col sm:flex-row flex-1 h-full"> <div class="flex flex-col sm:flex-row flex-1">
<!-- 左侧/底部导航栏 - 响应式 --> <!-- 左侧/底部导航栏 - 响应式 -->
<LeftBar /> <LeftBar />
<!-- 路由视图内容 --> <!-- 路由视图内容 -->
<div class="flex-1 overflow-y-auto main-scrollbar"> <div class="flex-1 pb-16 sm:pb-20">
<router-view></router-view> <router-view></router-view>
</div> </div>
</div> </div>
...@@ -39,6 +67,8 @@ const { t } = useI18n() ...@@ -39,6 +67,8 @@ const { t } = useI18n()
<TaskDetails /> <TaskDetails />
<TemplateDetails /> <TemplateDetails />
<PromptTemplate /> <PromptTemplate />
<Voice_tts v-if="showVoiceTTSModal" @tts-complete="handleTTSComplete" @close-modal="showVoiceTTSModal = false" />
<MediaTemplate />
<!-- 全局加载覆盖层 - Apple 风格 --> <!-- 全局加载覆盖层 - Apple 风格 -->
<div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999] transition-opacity duration-300"> <div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999] transition-opacity duration-300">
......
...@@ -276,33 +276,35 @@ onMounted(async () => { ...@@ -276,33 +276,35 @@ onMounted(async () => {
<template> <template>
<!-- Apple 极简风格分享页面 --> <!-- Apple 极简风格分享页面 -->
<div class="min-h-screen w-full bg-[#f5f5f7] dark:bg-[#000000]"> <div class="bg-[#f5f5f7] dark:bg-[#000000] transition-colors duration-300 w-full h-full">
<!-- TopBar --> <!-- 主内容区域 -->
<topMenu /> <div class="flex flex-col w-full h-full">
<!-- TopBar -->
<!-- 主要内容区域 --> <topMenu />
<div class="w-full min-h-[calc(100vh-80px)] overflow-y-auto main-scrollbar">
<!-- 错误状态 - Apple 风格 --> <!-- 滚动内容区域 - 带滚动条 -->
<div v-if="error" class="flex items-center justify-center min-h-[60vh] px-6"> <div class="flex-1 overflow-y-auto main-scrollbar">
<!-- 错误状态 - Apple 风格 - 响应式 -->
<div v-if="error" class="flex items-center justify-center min-h-[60vh] px-4 sm:px-6">
<div class="text-center max-w-md"> <div class="text-center max-w-md">
<div class="inline-flex items-center justify-center w-20 h-20 bg-red-500/10 dark:bg-red-400/10 rounded-3xl mb-6"> <div class="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 bg-red-500/10 dark:bg-red-400/10 rounded-2xl sm:rounded-3xl mb-4 sm:mb-6">
<i class="fas fa-exclamation-triangle text-3xl text-red-500 dark:text-red-400"></i> <i class="fas fa-exclamation-triangle text-2xl sm:text-3xl text-red-500 dark:text-red-400"></i>
</div> </div>
<h2 class="text-2xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-4 tracking-tight">{{ t('shareNotFound') }}</h2> <h2 class="text-xl sm:text-2xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 sm:mb-4 tracking-tight">{{ t('shareNotFound') }}</h2>
<p class="text-base text-[#86868b] dark:text-[#98989d] mb-8 tracking-tight">{{ error }}</p> <p class="text-sm sm:text-base text-[#86868b] dark:text-[#98989d] mb-6 sm:mb-8 tracking-tight">{{ error }}</p>
<button @click="router.push('/')" <button @click="router.push('/')"
class="inline-flex items-center justify-center gap-2 px-8 py-3 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white rounded-full text-[15px] font-semibold tracking-tight transition-all duration-200 hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100"> class="inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-2.5 sm:py-3 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white rounded-full text-sm sm:text-[15px] font-semibold tracking-tight transition-all duration-200 hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100">
<i class="fas fa-home text-sm"></i> <i class="fas fa-home text-xs sm:text-sm"></i>
<span>{{ t('backToHome') }}</span> <span>{{ t('backToHome') }}</span>
</button> </button>
</div> </div>
</div> </div>
<!-- 分享内容 - Apple 风格 --> <!-- 分享内容 - Apple 风格 - 响应式布局 -->
<div v-else-if="shareData" class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 w-full max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 py-12 lg:py-16 items-center"> <div v-else-if="shareData" class="flex flex-col lg:grid lg:grid-cols-2 gap-8 lg:gap-16 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-12 py-8 sm:py-12 lg:py-16 items-center">
<!-- 左侧视频区域 --> <!-- 左侧视频区域 - 响应式尺寸 -->
<div class="flex justify-center items-center"> <div class="flex justify-center items-center w-full order-1">
<div class="w-full max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-2xl overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative"> <div class="w-full max-w-[300px] sm:max-w-[350px] lg:max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-2xl overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative">
<!-- 视频加载占位符 - Apple 风格 --> <!-- 视频加载占位符 - Apple 风格 -->
<div v-if="!videoUrl" class="w-full h-full flex flex-col items-center justify-center bg-[#f5f5f7] dark:bg-[#1c1c1e]"> <div v-if="!videoUrl" class="w-full h-full flex flex-col items-center justify-center bg-[#f5f5f7] dark:bg-[#1c1c1e]">
<div class="relative w-12 h-12 mb-6"> <div class="relative w-12 h-12 mb-6">
...@@ -337,94 +339,94 @@ onMounted(async () => { ...@@ -337,94 +339,94 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 右侧信息区域 - Apple 风格 --> <!-- 右侧信息区域 - Apple 风格 - 响应式 -->
<div class="flex items-center justify-center"> <div class="flex items-center justify-center w-full order-2">
<div class="w-full max-w-[500px]"> <div class="w-full max-w-[300px] sm:max-w-[500px]">
<!-- 标题 - Apple 风格 --> <!-- 标题 - Apple 风格 - 响应式字体 -->
<h1 class="text-4xl sm:text-5xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-4 tracking-tight leading-tight"> <h1 class="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 sm:mb-4 tracking-tight leading-tight text-center lg:text-left">
{{ getShareTitle() }} {{ getShareTitle() }}
</h1> </h1>
<!-- 描述 - Apple 风格 --> <!-- 描述 - Apple 风格 - 响应式字体 -->
<p class="text-lg text-[#86868b] dark:text-[#98989d] mb-8 leading-relaxed tracking-tight"> <p class="text-base sm:text-lg text-[#86868b] dark:text-[#98989d] mb-6 sm:mb-8 leading-relaxed tracking-tight text-center lg:text-left">
{{ getShareDescription() }} {{ getShareDescription() }}
</p> </p>
<!-- 特性列表 - Apple 风格 --> <!-- 特性列表 - Apple 风格 - 响应式 -->
<div class="grid grid-cols-1 gap-3 mb-8"> <div class="grid grid-cols-1 gap-2 sm:gap-3 mb-6 sm:mb-8">
<div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-rocket text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-rocket text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('latestAIModel') }}</span> <span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('latestAIModel') }}</span>
</div> </div>
<div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-bolt text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-bolt text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('oneClickReplication') }}</span> <span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('oneClickReplication') }}</span>
</div> </div>
<div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-user-cog text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-user-cog text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('customizableCharacter') }}</span> <span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('customizableCharacter') }}</span>
</div> </div>
</div> </div>
<!-- 操作按钮 - Apple 风格 --> <!-- 操作按钮 - Apple 风格 - 响应式 -->
<div class="space-y-3 mb-8"> <div class="space-y-2.5 sm:space-y-3 mb-6 sm:mb-8">
<button @click="createSimilar" <button @click="createSimilar"
class="w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-8 py-3.5 text-[15px] font-semibold text-white hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100 transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2"> class="w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-6 sm:px-8 py-3 sm:py-3.5 text-sm sm:text-[15px] font-semibold text-white hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100 transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2">
<i class="fas fa-magic text-sm"></i> <i class="fas fa-magic text-sm"></i>
<span>{{ getShareButtonText() }}</span> <span>{{ getShareButtonText() }}</span>
</button> </button>
<!-- 详细信息按钮 --> <!-- 详细信息按钮 -->
<button @click="showDetails = !showDetails" <button @click="showDetails = !showDetails"
class="w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-8 py-3 text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2"> class="w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-6 sm:px-8 py-2.5 sm:py-3 text-sm sm:text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2">
<i :class="showDetails ? 'fas fa-chevron-up' : 'fas fa-info-circle'" class="text-sm"></i> <i :class="showDetails ? 'fas fa-chevron-up' : 'fas fa-info-circle'" class="text-sm"></i>
<span>{{ showDetails ? t('hideDetails') : t('showDetails') }}</span> <span>{{ showDetails ? t('hideDetails') : t('showDetails') }}</span>
</button> </button>
</div> </div>
<!-- 技术信息 - Apple 风格 --> <!-- 技术信息 - Apple 风格 - 响应式 -->
<div class="text-center pt-6 border-t border-black/8 dark:border-white/8"> <div class="text-center lg:text-left pt-4 sm:pt-6 border-t border-black/8 dark:border-white/8">
<a href="https://github.com/ModelTC/LightX2V" <a href="https://github.com/ModelTC/LightX2V"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-sm text-[#86868b] dark:text-[#98989d] hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)] transition-colors tracking-tight"> class="inline-flex items-center gap-2 text-xs sm:text-sm text-[#86868b] dark:text-[#98989d] hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)] transition-colors tracking-tight">
<i class="fab fa-github text-base"></i> <i class="fab fa-github text-sm sm:text-base"></i>
<span>{{ t('poweredByLightX2V') }}</span> <span>{{ t('poweredByLightX2V') }}</span>
<i class="fas fa-external-link-alt text-xs"></i> <i class="fas fa-external-link-alt text-[10px] sm:text-xs"></i>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 详细信息面板 - Apple 风格 --> <!-- 详细信息面板 - Apple 风格 - 响应式 -->
<div v-if="showDetails && shareData" class="w-full bg-white dark:bg-[#1c1c1e] border-t border-black/8 dark:border-white/8 py-16"> <div v-if="showDetails && shareData" class="w-full bg-white dark:bg-[#1c1c1e] border-t border-black/8 dark:border-white/8 py-8 sm:py-12 lg:py-16">
<div class="max-w-6xl mx-auto px-6 sm:px-8 lg:px-12"> <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-12">
<!-- 输入素材标题 - Apple 风格 --> <!-- 输入素材标题 - Apple 风格 - 响应式 -->
<h2 class="text-2xl sm:text-3xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center justify-center gap-3 mb-10 tracking-tight"> <h2 class="text-xl sm:text-2xl lg:text-3xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3 mb-6 sm:mb-8 lg:mb-10 tracking-tight">
<i class="fas fa-upload text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-upload text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<span>{{ t('inputMaterials') }}</span> <span>{{ t('inputMaterials') }}</span>
</h2> </h2>
<!-- 三个并列的分块卡片 - Apple 风格 --> <!-- 三个卡片 - Apple 风格 - 响应式竖向排列 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="flex flex-col md:grid md:grid-cols-3 gap-4 sm:gap-6">
<!-- 图片卡片 - Apple 风格 --> <!-- 图片卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 --> <!-- 卡片头部 - 响应式 -->
<div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2 sm:gap-3">
<i class="fas fa-image text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-image text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('image') }}</h3> <h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('image') }}</h3>
</div> </div>
</div> </div>
<!-- 卡片内容 --> <!-- 卡片内容 - 响应式 - 带滚动条 -->
<div class="p-6 min-h-[200px]"> <div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px] max-h-[300px] overflow-y-auto main-scrollbar">
<div v-if="getImageMaterials().length > 0"> <div v-if="getImageMaterials().length > 0">
<div v-for="[inputName, url] in getImageMaterials()" :key="inputName" class="rounded-xl overflow-hidden border border-black/8 dark:border-white/8"> <div v-for="[inputName, url] in getImageMaterials()" :key="inputName" class="rounded-xl overflow-hidden border border-black/8 dark:border-white/8">
<img :src="url" :alt="inputName" <img :src="url" :alt="inputName"
...@@ -440,17 +442,17 @@ onMounted(async () => { ...@@ -440,17 +442,17 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 音频卡片 - Apple 风格 --> <!-- 音频卡片 - Apple 风格 - 响应式 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 --> <!-- 卡片头部 - 响应式 -->
<div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2 sm:gap-3">
<i class="fas fa-music text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-music text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('audio') }}</h3> <h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('audio') }}</h3>
</div> </div>
</div> </div>
<!-- 卡片内容 --> <!-- 卡片内容 - 响应式 - 带滚动条 -->
<div class="p-6 min-h-[200px]"> <div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px] max-h-[300px] overflow-y-auto main-scrollbar">
<div v-if="getAudioMaterials().length > 0" class="space-y-4"> <div v-if="getAudioMaterials().length > 0" class="space-y-4">
<div v-for="[inputName, url] in getAudioMaterials()" :key="inputName"> <div v-for="[inputName, url] in getAudioMaterials()" :key="inputName">
<audio :src="url" controls class="w-full rounded-xl"></audio> <audio :src="url" controls class="w-full rounded-xl"></audio>
...@@ -463,23 +465,23 @@ onMounted(async () => { ...@@ -463,23 +465,23 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 提示词卡片 - Apple 风格 --> <!-- 提示词卡片 - Apple 风格 - 响应式 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 --> <!-- 卡片头部 - 响应式 -->
<div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2 sm:gap-3">
<i class="fas fa-file-alt text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-file-alt text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('prompt') }}</h3> <h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('prompt') }}</h3>
</div> </div>
<button v-if="shareData.prompt" <button v-if="shareData.prompt"
@click="copyPrompt(shareData.prompt)" @click="copyPrompt(shareData.prompt)"
class="w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100" class="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:title="t('copy')"> :title="t('copy')">
<i class="fas fa-copy text-xs"></i> <i class="fas fa-copy text-[10px] sm:text-xs"></i>
</button> </button>
</div> </div>
<!-- 卡片内容 --> <!-- 卡片内容 - 响应式 -->
<div class="p-6 min-h-[200px]"> <div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px]">
<div v-if="shareData.prompt" class="bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[10px] border border-black/6 dark:border-white/6 rounded-xl p-4"> <div v-if="shareData.prompt" class="bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[10px] border border-black/6 dark:border-white/6 rounded-xl p-4">
<p class="text-sm text-[#1d1d1f] dark:text-[#f5f5f7] leading-relaxed tracking-tight break-words">{{ shareData.prompt }}</p> <p class="text-sm text-[#1d1d1f] dark:text-[#f5f5f7] leading-relaxed tracking-tight break-words">{{ shareData.prompt }}</p>
</div> </div>
...@@ -492,12 +494,13 @@ onMounted(async () => { ...@@ -492,12 +494,13 @@ onMounted(async () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div>
<!-- 全局路由跳转Loading覆盖层 - Apple 风格 --> <!-- 全局路由跳转Loading覆盖层 - Apple 风格 -->
<div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999]"> <div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999]">
<Loading /> <Loading />
</div>
</div> </div>
</template> </template>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment