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">
......
...@@ -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 {
......
...@@ -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">
......
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