Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
xuwx1
LightX2V
Commits
23aa1ef3
Unverified
Commit
23aa1ef3
authored
Nov 05, 2025
by
LiangLiu
Committed by
GitHub
Nov 05, 2025
Browse files
Fix mobile frontend bug (#442)
Co-authored-by:
qinxinyi
<
qxy118045534@163.com
>
parent
d914488a
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
6979 additions
and
5571 deletions
+6979
-5571
lightx2v/deploy/server/frontend/src/components/Alert.vue
lightx2v/deploy/server/frontend/src/components/Alert.vue
+71
-16
lightx2v/deploy/server/frontend/src/components/Confirm.vue
lightx2v/deploy/server/frontend/src/components/Confirm.vue
+1
-1
lightx2v/deploy/server/frontend/src/components/Generate.vue
lightx2v/deploy/server/frontend/src/components/Generate.vue
+167
-63
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
+13
-13
lightx2v/deploy/server/frontend/src/components/MediaTemplate.vue
...v/deploy/server/frontend/src/components/MediaTemplate.vue
+46
-8
lightx2v/deploy/server/frontend/src/components/TaskCarousel.vue
...2v/deploy/server/frontend/src/components/TaskCarousel.vue
+7
-7
lightx2v/deploy/server/frontend/src/components/TaskDetails.vue
...x2v/deploy/server/frontend/src/components/TaskDetails.vue
+221
-6
lightx2v/deploy/server/frontend/src/components/TemplateDetails.vue
...deploy/server/frontend/src/components/TemplateDetails.vue
+167
-8
lightx2v/deploy/server/frontend/src/components/TopBar.vue
lightx2v/deploy/server/frontend/src/components/TopBar.vue
+1
-1
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
+319
-122
lightx2v/deploy/server/frontend/src/locales/en.json
lightx2v/deploy/server/frontend/src/locales/en.json
+5
-1
lightx2v/deploy/server/frontend/src/locales/zh.json
lightx2v/deploy/server/frontend/src/locales/zh.json
+6
-1
lightx2v/deploy/server/frontend/src/router/index.js
lightx2v/deploy/server/frontend/src/router/index.js
+1
-1
lightx2v/deploy/server/frontend/src/style.css
lightx2v/deploy/server/frontend/src/style.css
+2
-3
lightx2v/deploy/server/frontend/src/utils/other.js
lightx2v/deploy/server/frontend/src/utils/other.js
+5829
-5230
lightx2v/deploy/server/frontend/src/views/Layout.vue
lightx2v/deploy/server/frontend/src/views/Layout.vue
+35
-5
lightx2v/deploy/server/frontend/src/views/Share.vue
lightx2v/deploy/server/frontend/src/views/Share.vue
+88
-85
No files found.
lightx2v/deploy/server/frontend/src/components/Alert.vue
View file @
23aa1ef3
...
@@ -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,14 +114,16 @@ onUnmounted(() => {
...
@@ -74,14 +114,16 @@ 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>
</div>
<!-- 关闭按钮 -->
<!-- 关闭按钮 -->
<button
@
click=
"alert.show = false"
class=
"alert-close-btn"
aria-label=
"Close"
>
<button
@
click=
"alert.show = false"
class=
"alert-close-btn"
aria-label=
"Close"
>
<i
class=
"fas fa-times"
></i>
<i
class=
"fas fa-times"
></i>
...
@@ -89,6 +131,7 @@ onUnmounted(() => {
...
@@ -89,6 +131,7 @@ onUnmounted(() => {
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</transition>
</div>
</div>
</
template
>
</
template
>
...
@@ -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
>
lightx2v/deploy/server/frontend/src/components/Confirm.vue
View file @
23aa1ef3
...
@@ -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>
...
...
lightx2v/deploy/server/frontend/src/components/Generate.vue
View file @
23aa1ef3
...
@@ -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,25 +993,75 @@ onUnmounted(() => {
...
@@ -931,25 +993,75 @@ 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>
<!-- 删除按钮 - Apple 风格 -->
<!-- 音频信息 -->
<div
<div
class=
"flex-1 min-w-0"
>
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=
"text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight truncate"
>
<div
class=
"flex gap-3"
>
{{
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>
<!-- 删除按钮 -->
<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>
<!-- 进度条 -->
<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>
<input
type=
"file"
ref=
"audioInput"
@
change=
"handleAudioUpload"
accept=
"audio/*"
<input
type=
"file"
ref=
"audioInput"
@
change=
"handleAudioUpload"
accept=
"audio/*"
style=
"display: none;"
>
style=
"display: none;"
>
</div>
</div>
...
@@ -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"
>
...
...
lightx2v/deploy/server/frontend/src/components/LeftBar.vue
View file @
23aa1ef3
...
@@ -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-
3x
l 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-1
6
"
>
<nav
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[40px] border border-black/10 dark:border-white/10 rounded-
ful
l 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-1
4
"
>
<!-- 生成视频功能 -->
<!-- 生成视频功能 -->
<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)]'"
...
...
lightx2v/deploy/server/frontend/src/components/MediaTemplate.vue
View file @
23aa1ef3
...
@@ -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-[
2
0px] 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_
8
px_
32
px_rgba(0,0,0,0.
1
2)] dark:shadow-[0_
8
px_
32
px_rgba(0,0,0,0.
4
)]"
<div
class=
"bg-white/95 dark:bg-[#1e1e1e]/95 backdrop-blur-[
4
0px] 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_
20
px_
60
px_rgba(0,0,0,0.2)] dark:shadow-[0_
20
px_
60
px_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,9 +348,15 @@ watch(audioTemplates, (newTemplates) => {
...
@@ -322,9 +348,15 @@ 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}`]"
: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>
<div
class=
"flex-1 min-w-0"
>
<div
class=
"flex-1 min-w-0"
>
<div
<div
...
@@ -403,9 +435,15 @@ watch(audioTemplates, (newTemplates) => {
...
@@ -403,9 +435,15 @@ 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}`]"
: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>
<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 }}
...
...
lightx2v/deploy/server/frontend/src/components/TaskCarousel.vue
View file @
23aa1ef3
...
@@ -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-[-
6
0px] m
d
:left-[-
6
0px]
s
m:left-[-
3
0px] 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-[-
1
0px]
s
m:left-[-
2
0px] m
d
:left-[-
40px] lg:left-[-6
0px] 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-[-
6
0px] md:right-[-
6
0px]
sm
:right-[-
3
0px] 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-[-2
0px] md:right-[-
4
0px]
lg
:right-[-
6
0px] 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-[
5
00px] md:max-w-[400px]
sm
:max-w-[
3
00px] 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-[3
00px] md:max-w-[400px]
lg
:max-w-[
4
00px] 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-[4
4px] h-[44px] md:w-[44
px]
md:
h-[4
4
px] sm:w-[4
0
px] sm:h-[4
0
px] 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-[4
0
px] h-[4
0
px] sm:w-[4
4
px] sm:h-[4
4
px] 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-[4
4px] h-[44px] md:w-[44
px]
md:
h-[4
4
px] sm:w-[4
0
px] sm:h-[4
0
px] 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-[4
0
px] h-[4
0
px] sm:w-[4
4
px] sm:h-[4
4
px] 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-[4
4px] h-[44px] md:w-[44
px]
md:
h-[4
4
px] sm:w-[4
0
px] sm:h-[4
0
px] 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-[4
0
px] h-[4
0
px] sm:w-[4
4
px] sm:h-[4
4
px] 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-[4
4px] h-[44px] md:w-[44
px]
md:
h-[4
4
px] sm:w-[4
0
px] sm:h-[4
0
px] 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-[4
0
px] h-[4
0
px] sm:w-[4
4
px] sm:h-[4
4
px] 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>
...
...
lightx2v/deploy/server/frontend/src/components/TaskDetails.vue
View file @
23aa1ef3
<
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
(
'
taskDetail
s
'
)
}}
{{
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
(
'
taskDetail
s
'
)
}}
</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
(
'
taskDetail
s
'
)
}}
</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]"
>
...
...
lightx2v/deploy/server/frontend/src/components/TemplateDetails.vue
View file @
23aa1ef3
...
@@ -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-creato
r
'
)
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollba
r
'
)
;
if
(
creationArea
)
{
if
(
mainScrollable
)
{
creationArea
.
scrollIntoView
({
mainScrollable
.
scrollTo
({
behavior
:
'
smooth
'
,
top
:
0
,
b
lock
:
'
start
'
b
ehavior
:
'
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]"
>
...
...
lightx2v/deploy/server/frontend/src/components/TopBar.vue
View file @
23aa1ef3
...
@@ -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"
>
...
...
lightx2v/deploy/server/frontend/src/components/Voice_tts.vue
View file @
23aa1ef3
<
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>
</div>
<audio
:src=
"audioUrl"
controls
class=
"w-full rounded-xl"
></audio>
<!-- 播放/暂停按钮 -->
<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>
<!-- 音频信息 -->
<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>
</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)]/
1
0 shadow-[0_
4
px_
1
2px_rgba(var(--brand-primary-rgb),0.
1
5)] dark:shadow-[0_
4
px_
1
2px_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)]/
2
0 shadow-[0_
8
px_2
4
px_rgba(var(--brand-primary-rgb),0.
2
5)] dark:shadow-[0_
8
px_2
4
px_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>
...
@@ -235,9 +313,10 @@
...
@@ -235,9 +313,10 @@
</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
>
lightx2v/deploy/server/frontend/src/locales/en.json
View file @
23aa1ef3
...
@@ -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"
,
"taskDetail
s
"
:
"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"
,
...
...
lightx2v/deploy/server/frontend/src/locales/zh.json
View file @
23aa1ef3
...
@@ -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"
:
"音调"
,
...
...
lightx2v/deploy/server/frontend/src/router/index.js
View file @
23aa1ef3
...
@@ -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
||
...
...
lightx2v/deploy/server/frontend/src/style.css
View file @
23aa1ef3
...
@@ -630,7 +630,7 @@ body {
...
@@ -630,7 +630,7 @@ body {
/* 任务创建面板全屏 */
/* 任务创建面板全屏 */
#task-creator
{
#task-creator
{
max-width
:
none
;
max-width
:
none
;
width
:
9
0%
;
width
:
8
0%
;
}
}
#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
{
...
...
lightx2v/deploy/server/frontend/src/utils/other.js
View file @
23aa1ef3
import
{
ref
,
computed
,
watch
,
nextTick
}
from
'
vue
'
;
import
{
ref
,
computed
,
watch
,
nextTick
}
from
'
vue
'
;
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
;
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
;
import
i18n
from
'
./i18n
'
import
i18n
from
'
./i18n
'
import
router
from
'
../router
'
import
router
from
'
../router
'
export
const
t
=
i18n
.
global
.
t
export
const
t
=
i18n
.
global
.
t
export
const
locale
=
i18n
.
global
.
locale
export
const
locale
=
i18n
.
global
.
locale
// 响应式数据
// 响应式数据
const
loading
=
ref
(
false
);
const
loading
=
ref
(
false
);
...
@@ -58,6 +58,9 @@
...
@@ -58,6 +58,9 @@
const
showTaskDetailModal
=
ref
(
false
);
const
showTaskDetailModal
=
ref
(
false
);
const
modalTask
=
ref
(
null
);
const
modalTask
=
ref
(
null
);
// TTS 模态框状态
const
showVoiceTTSModal
=
ref
(
false
);
// TaskCarousel当前任务状态
// TaskCarousel当前任务状态
const
currentTask
=
ref
(
null
);
const
currentTask
=
ref
(
null
);
...
@@ -527,11 +530,65 @@
...
@@ -527,11 +530,65 @@
return
getCachedUrl
(
`current_audio_preview`
,
()
=>
preview
);
return
getCachedUrl
(
`current_audio_preview`
,
()
=>
preview
);
};
};
// Alert定时器,用于清除之前的定时器
let
alertTimeout
=
null
;
// 方法
// 方法
const
showAlert
=
(
message
,
type
=
'
info
'
,
action
=
null
)
=>
{
const
showAlert
=
(
message
,
type
=
'
info
'
,
action
=
null
)
=>
{
alert
.
value
=
{
show
:
true
,
message
,
type
,
action
};
// 清除之前的定时器
if
(
alertTimeout
)
{
clearTimeout
(
alertTimeout
);
alertTimeout
=
null
;
}
// 如果当前有alert正在显示,先关闭它
if
(
alert
.
value
&&
alert
.
value
.
show
)
{
alert
.
value
.
show
=
false
;
// 等待transition完成(约400ms)后再显示新的alert
setTimeout
(()
=>
{
setTimeout
(()
=>
{
createNewAlert
(
message
,
type
,
action
);
},
450
);
}
else
{
// 如果没有alert在显示,立即创建新的
// 如果alert存在但已关闭,先重置它以确保状态干净
if
(
alert
.
value
&&
!
alert
.
value
.
show
)
{
alert
.
value
=
{
show
:
false
,
message
:
''
,
type
:
'
info
'
,
action
:
null
};
}
// 立即创建新alert,不需要等待nextTick
createNewAlert
(
message
,
type
,
action
);
}
};
// 创建新alert的辅助函数
const
createNewAlert
=
(
message
,
type
,
action
)
=>
{
// 再次清除定时器,防止重复设置
if
(
alertTimeout
)
{
clearTimeout
(
alertTimeout
);
alertTimeout
=
null
;
}
// 创建全新的对象,使用时间戳确保每次都是新对象
const
newAlert
=
{
show
:
true
,
message
:
String
(
message
),
type
:
String
(
type
),
action
:
action
?
{
label
:
String
(
action
.
label
),
onClick
:
action
.
onClick
}
:
null
,
// 添加一个时间戳确保每次都是新对象,用于key
_timestamp
:
Date
.
now
()
};
// 直接赋值新对象
alert
.
value
=
newAlert
;
// 设置自动关闭定时器
alertTimeout
=
setTimeout
(()
=>
{
if
(
alert
.
value
&&
alert
.
value
.
show
&&
alert
.
value
.
_timestamp
===
newAlert
.
_timestamp
)
{
alert
.
value
.
show
=
false
;
alert
.
value
.
show
=
false
;
}
alertTimeout
=
null
;
},
5000
);
},
5000
);
};
};
...
@@ -2780,11 +2837,22 @@
...
@@ -2780,11 +2837,22 @@
}
}
// 等待 DOM 更新后滚动到生成区域
// 等待 DOM 更新后滚动到生成区域
await
nextTick
();
await
nextTick
();
const
creationArea
=
document
.
querySelector
(
'
#task-creator
'
);
// 如果之前有展开过创作区域,保持展开状态
const
creationArea
=
document
.
querySelector
(
'
.creation-area
'
);
if
(
isCreationAreaExpanded
.
value
)
{
// 延迟一点时间确保DOM更新完成
setTimeout
(()
=>
{
if
(
creationArea
)
{
if
(
creationArea
)
{
creationArea
.
scrollIntoView
({
creationArea
.
classList
.
add
(
'
show
'
);
behavior
:
'
smooth
'
,
}
block
:
'
start
'
},
50
);
}
// 滚动到顶部
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
);
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
});
});
}
}
}
else
{
}
else
{
...
@@ -3433,21 +3501,21 @@
...
@@ -3433,21 +3501,21 @@
// 显示任务完成提示
// 显示任务完成提示
if
(
updatedTask
.
status
===
'
SUCCEED
'
)
{
if
(
updatedTask
.
status
===
'
SUCCEED
'
)
{
showAlert
(
'
视频生成完成!
'
,
'
success
'
,
{
showAlert
(
t
(
'
taskCompletedSuccessfully
'
)
,
'
success
'
,
{
label
:
t
(
'
view
'
),
label
:
t
(
'
view
'
),
onClick
:
()
=>
{
onClick
:
()
=>
{
openTaskDetailModal
(
updatedTask
);
openTaskDetailModal
(
updatedTask
);
}
}
});
});
}
else
if
(
updatedTask
.
status
===
'
FAILED
'
)
{
}
else
if
(
updatedTask
.
status
===
'
FAILED
'
)
{
showAlert
(
'
视频生成失败,请查看详情
'
,
'
danger
'
,
{
showAlert
(
t
(
'
videoGeneratingFailed
'
)
,
'
danger
'
,
{
label
:
t
(
'
view
'
),
label
:
t
(
'
view
'
),
onClick
:
()
=>
{
onClick
:
()
=>
{
openTaskDetailModal
(
updatedTask
);
openTaskDetailModal
(
updatedTask
);
}
}
});
});
}
else
if
(
updatedTask
.
status
===
'
CANCEL
'
)
{
}
else
if
(
updatedTask
.
status
===
'
CANCEL
'
)
{
showAlert
(
'
任务已取消
'
,
'
warning
'
);
showAlert
(
t
(
'
taskCancelled
'
)
,
'
warning
'
);
}
}
}
}
}
}
...
@@ -3701,7 +3769,7 @@
...
@@ -3701,7 +3769,7 @@
return
t
(
'
completed
'
);
return
t
(
'
completed
'
);
};
};
});
});
const
formatDuration
=
(
seconds
)
=>
{
const
formatDuration
=
(
seconds
)
=>
{
if
(
seconds
<
60
)
{
if
(
seconds
<
60
)
{
...
@@ -4828,7 +4896,17 @@
...
@@ -4828,7 +4896,17 @@
// 移动端视频播放切换
// 移动端视频播放切换
const
toggleVideoPlay
=
(
event
)
=>
{
const
toggleVideoPlay
=
(
event
)
=>
{
const
button
=
event
.
target
.
closest
(
'
button
'
);
const
button
=
event
.
target
.
closest
(
'
button
'
);
if
(
!
button
)
{
console
.
error
(
'
toggleVideoPlay: 未找到按钮元素
'
);
return
;
}
const
video
=
button
.
parentElement
.
querySelector
(
'
video
'
);
const
video
=
button
.
parentElement
.
querySelector
(
'
video
'
);
if
(
!
video
)
{
console
.
error
(
'
toggleVideoPlay: 未找到视频元素
'
);
return
;
}
const
icon
=
button
.
querySelector
(
'
i
'
);
const
icon
=
button
.
querySelector
(
'
i
'
);
if
(
video
.
paused
)
{
if
(
video
.
paused
)
{
...
@@ -4862,12 +4940,27 @@
...
@@ -4862,12 +4940,27 @@
});
});
}
else
{
}
else
{
// 视频未加载完成,显示loading并等待
// 视频未加载完成,显示loading并等待
console
.
log
(
'
视频还没加载完成,等待加载(移动端)
'
);
console
.
log
(
'
视频还没加载完成,等待加载(移动端)
, readyState:
'
,
video
.
readyState
);
icon
.
className
=
'
fas fa-spinner fa-spin text-sm
'
;
icon
.
className
=
'
fas fa-spinner fa-spin text-sm
'
;
currentLoadingVideo
=
video
;
currentLoadingVideo
=
video
;
// 等待视频加载完成
// 主动触发视频加载
video
.
addEventListener
(
'
loadeddata
'
,
()
=>
{
video
.
load
();
// 设置超时保护(10秒后如果还未加载完成,重置状态)
const
loadingTimeout
=
setTimeout
(()
=>
{
if
(
currentLoadingVideo
===
video
)
{
console
.
warn
(
'
视频加载超时(移动端)
'
);
icon
.
className
=
'
fas fa-play text-sm
'
;
currentLoadingVideo
=
null
;
showAlert
(
'
视频加载超时,请重试
'
,
'
warning
'
);
}
},
10000
);
// 等待视频可以播放
const
playHandler
=
()
=>
{
clearTimeout
(
loadingTimeout
);
// 检查这个视频是否仍然是当前等待加载的视频
// 检查这个视频是否仍然是当前等待加载的视频
if
(
currentLoadingVideo
===
video
)
{
if
(
currentLoadingVideo
===
video
)
{
currentLoadingVideo
=
null
;
currentLoadingVideo
=
null
;
...
@@ -4886,8 +4979,27 @@
...
@@ -4886,8 +4979,27 @@
icon
.
className
=
'
fas fa-play text-sm
'
;
icon
.
className
=
'
fas fa-play text-sm
'
;
console
.
log
(
'
视频加载完成但等待已被取消(移动端)
'
);
console
.
log
(
'
视频加载完成但等待已被取消(移动端)
'
);
}
}
},
{
once
:
true
});
// 移除事件监听器
video
.
removeEventListener
(
'
canplay
'
,
playHandler
);
video
.
removeEventListener
(
'
error
'
,
errorHandler
);
};
};
const
errorHandler
=
()
=>
{
clearTimeout
(
loadingTimeout
);
console
.
error
(
'
视频加载失败(移动端)
'
);
icon
.
className
=
'
fas fa-play text-sm
'
;
currentLoadingVideo
=
null
;
// 移除事件监听器
video
.
removeEventListener
(
'
canplay
'
,
playHandler
);
video
.
removeEventListener
(
'
error
'
,
errorHandler
);
};
// 使用 canplay 事件,比 loadeddata 更适合移动端
video
.
addEventListener
(
'
canplay
'
,
playHandler
,
{
once
:
true
});
video
.
addEventListener
(
'
error
'
,
errorHandler
,
{
once
:
true
});
}
}
else
{
}
else
{
video
.
pause
();
video
.
pause
();
video
.
currentTime
=
0
;
video
.
currentTime
=
0
;
...
@@ -4987,52 +5099,319 @@
...
@@ -4987,52 +5099,319 @@
zoomedImageUrl
.
value
=
''
;
zoomedImageUrl
.
value
=
''
;
};
};
// 通过后端API代理获取文件(避免CORS问题)
const
fetchFileThroughProxy
=
async
(
fileKey
,
fileType
)
=>
{
try
{
// 尝试通过后端API代理获取文件
const
proxyUrl
=
`/api/v1/template/asset/
${
fileType
}
/
${
fileKey
}
`
;
const
response
=
await
apiRequest
(
proxyUrl
);
if
(
response
&&
response
.
ok
)
{
return
await
response
.
blob
();
}
// 如果代理API不存在,尝试直接获取URL然后fetch
const
fileUrl
=
await
getTemplateFileUrlAsync
(
fileKey
,
fileType
);
if
(
!
fileUrl
)
{
return
null
;
}
// 检查是否是同源URL
const
urlObj
=
new
URL
(
fileUrl
,
window
.
location
.
origin
);
const
isSameOrigin
=
urlObj
.
origin
===
window
.
location
.
origin
;
if
(
isSameOrigin
)
{
// 同源,直接fetch
const
directResponse
=
await
fetch
(
fileUrl
);
if
(
directResponse
.
ok
)
{
return
await
directResponse
.
blob
();
}
}
else
{
// 跨域,尝试使用no-cors模式(但这样无法读取响应)
// 或者使用img/audio元素加载(不适用于需要File对象的情况)
// 这里我们尝试直接fetch,如果失败会抛出错误
try
{
const
directResponse
=
await
fetch
(
fileUrl
,
{
mode
:
'
cors
'
});
if
(
directResponse
.
ok
)
{
return
await
directResponse
.
blob
();
}
}
catch
(
corsError
)
{
console
.
warn
(
'
CORS错误,尝试使用代理:
'
,
corsError
);
// 如果后端有代理API,应该使用上面的代理方式
// 如果没有,这里会返回null,然后调用方会显示错误
}
}
return
null
;
}
catch
(
error
)
{
console
.
error
(
'
获取文件失败:
'
,
error
);
return
null
;
}
};
// 应用模板图片
// 应用模板图片
const
applyTemplateImage
=
(
template
)
=>
{
const
applyTemplateImage
=
async
(
template
)
=>
{
if
(
template
?.
inputs
?.
input_image
)
{
if
(
!
template
?.
inputs
?.
input_image
)
{
const
imageUrl
=
getTemplateFileUrl
(
template
.
inputs
.
input_image
,
'
images
'
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
// 这里需要根据当前任务类型设置图片
return
;
if
(
selectedTaskId
.
value
===
'
i2v
'
||
selectedTaskId
.
value
===
'
s2v
'
)
{
// 模拟文件上传,将图片URL转换为File对象
fetch
(
imageUrl
)
.
then
(
response
=>
response
.
blob
())
.
then
(
blob
=>
{
const
file
=
new
File
([
blob
],
'
template_image.jpg
'
,
{
type
:
blob
.
type
});
if
(
selectedTaskId
.
value
===
'
i2v
'
)
{
i2vForm
.
value
.
imageFile
=
file
;
}
else
if
(
selectedTaskId
.
value
===
'
s2v
'
)
{
s2vForm
.
value
.
imageFile
=
file
;
}
}
try
{
// 先设置任务类型(如果模板有任务类型)
if
(
template
.
task_type
&&
(
template
.
task_type
===
'
i2v
'
||
template
.
task_type
===
'
s2v
'
))
{
selectedTaskId
.
value
=
template
.
task_type
;
}
// 检查当前任务类型是否支持图片
if
(
selectedTaskId
.
value
!==
'
i2v
'
&&
selectedTaskId
.
value
!==
'
s2v
'
)
{
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
return
;
}
// 获取图片URL(用于预览)
const
imageUrl
=
await
getTemplateFileUrlAsync
(
template
.
inputs
.
input_image
,
'
images
'
);
if
(
!
imageUrl
)
{
console
.
error
(
'
无法获取模板图片URL:
'
,
template
.
inputs
.
input_image
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
return
;
}
// 根据任务类型设置图片
const
currentForm
=
getCurrentForm
();
if
(
currentForm
)
{
currentForm
.
imageUrl
=
imageUrl
;
}
// 设置预览
setCurrentImagePreview
(
imageUrl
);
setCurrentImagePreview
(
imageUrl
);
updateUploadedContentStatus
();
showAlert
(
t
(
'
imageApplied
'
),
'
success
'
);
// 加载图片文件(与useTemplate相同的逻辑)
})
try
{
.
catch
(
error
=>
{
// 直接使用获取到的URL fetch(与useTemplate相同)
console
.
error
(
'
应用图片失败:
'
,
error
);
const
imageResponse
=
await
fetch
(
imageUrl
);
if
(
imageResponse
.
ok
)
{
const
blob
=
await
imageResponse
.
blob
();
// 验证返回的是图片而不是HTML
if
(
blob
.
type
&&
blob
.
type
.
startsWith
(
'
text/html
'
))
{
console
.
error
(
'
返回的是HTML而不是图片:
'
,
blob
.
type
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
return
;
}
const
filename
=
template
.
inputs
.
input_image
||
'
template_image.jpg
'
;
const
file
=
new
File
([
blob
],
filename
,
{
type
:
blob
.
type
||
'
image/jpeg
'
});
if
(
currentForm
)
{
currentForm
.
imageFile
=
file
;
}
console
.
log
(
'
模板图片文件已加载
'
);
}
else
{
console
.
warn
(
'
Failed to fetch image from URL:
'
,
imageUrl
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
return
;
}
}
catch
(
error
)
{
console
.
error
(
'
Failed to load template image file:
'
,
error
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
return
;
}
updateUploadedContentStatus
();
// 关闭所有弹窗的辅助函数
const
closeAllModals
=
()
=>
{
closeTaskDetailModal
();
// 使用函数确保状态完全重置
showVoiceTTSModal
.
value
=
false
;
closeTemplateDetailModal
();
// 使用函数确保状态完全重置
showImageTemplates
.
value
=
false
;
showAudioTemplates
.
value
=
false
;
showPromptModal
.
value
=
false
;
closeImageZoomModal
();
// 使用函数确保状态完全重置
};
// 跳转到创作区域的函数
const
scrollToCreationArea
=
()
=>
{
// 先关闭所有弹窗
closeAllModals
();
// 如果不在生成页面,先切换视图
if
(
router
.
currentRoute
.
value
.
path
!==
'
/generate
'
)
{
switchToCreateView
();
// 等待路由切换完成后再展开和滚动
setTimeout
(()
=>
{
expandCreationArea
();
setTimeout
(()
=>
{
// 滚动到顶部(TopBar 之后的位置,约60px)
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
);
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
});
});
}
}
},
100
);
},
100
);
}
else
{
// 已经在生成页面,直接展开和滚动
expandCreationArea
();
setTimeout
(()
=>
{
// 滚动到顶部(TopBar 之后的位置,约60px)
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
);
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
});
}
},
100
);
}
};
showAlert
(
t
(
'
imageApplied
'
),
'
success
'
,
{
label
:
t
(
'
view
'
),
onClick
:
scrollToCreationArea
});
}
catch
(
error
)
{
console
.
error
(
'
应用图片失败:
'
,
error
);
showAlert
(
t
(
'
applyImageFailed
'
),
'
danger
'
);
}
}
};
};
// 应用模板音频
// 应用模板音频
const
applyTemplateAudio
=
(
template
)
=>
{
const
applyTemplateAudio
=
async
(
template
)
=>
{
if
(
template
?.
inputs
?.
input_audio
&&
selectedTaskId
.
value
===
'
s2v
'
)
{
if
(
!
template
?.
inputs
?.
input_audio
)
{
const
audioUrl
=
getTemplateFileUrl
(
template
.
inputs
.
input_audio
,
'
audios
'
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
// 模拟文件上传,将音频URL转换为File对象
return
;
fetch
(
audioUrl
)
}
.
then
(
response
=>
response
.
blob
())
.
then
(
blob
=>
{
try
{
const
file
=
new
File
([
blob
],
'
template_audio.mp3
'
,
{
type
:
blob
.
type
});
// 先设置任务类型(如果模板有任务类型)
s2vForm
.
value
.
audioFile
=
file
;
if
(
template
.
task_type
&&
template
.
task_type
===
'
s2v
'
)
{
selectedTaskId
.
value
=
template
.
task_type
;
}
// 检查当前任务类型是否支持音频
if
(
selectedTaskId
.
value
!==
'
s2v
'
)
{
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
return
;
}
// 获取音频URL(用于预览)
const
audioUrl
=
await
getTemplateFileUrlAsync
(
template
.
inputs
.
input_audio
,
'
audios
'
);
if
(
!
audioUrl
)
{
console
.
error
(
'
无法获取模板音频URL:
'
,
template
.
inputs
.
input_audio
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
return
;
}
// 设置音频文件
const
currentForm
=
getCurrentForm
();
if
(
currentForm
)
{
currentForm
.
audioUrl
=
audioUrl
;
}
// 设置预览
setCurrentAudioPreview
(
audioUrl
);
setCurrentAudioPreview
(
audioUrl
);
// 加载音频文件(与useTemplate相同的逻辑)
try
{
// 直接使用获取到的URL fetch(与useTemplate相同)
const
audioResponse
=
await
fetch
(
audioUrl
);
if
(
audioResponse
.
ok
)
{
const
blob
=
await
audioResponse
.
blob
();
// 验证返回的是音频而不是HTML
if
(
blob
.
type
&&
blob
.
type
.
startsWith
(
'
text/html
'
))
{
console
.
error
(
'
返回的是HTML而不是音频:
'
,
blob
.
type
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
return
;
}
const
filename
=
template
.
inputs
.
input_audio
||
'
template_audio.mp3
'
;
// 根据文件扩展名确定正确的MIME类型
let
mimeType
=
blob
.
type
;
if
(
!
mimeType
||
mimeType
===
'
application/octet-stream
'
)
{
const
ext
=
filename
.
toLowerCase
().
split
(
'
.
'
).
pop
();
const
mimeTypes
=
{
'
mp3
'
:
'
audio/mpeg
'
,
'
wav
'
:
'
audio/wav
'
,
'
mp4
'
:
'
audio/mp4
'
,
'
aac
'
:
'
audio/aac
'
,
'
ogg
'
:
'
audio/ogg
'
,
'
m4a
'
:
'
audio/mp4
'
};
mimeType
=
mimeTypes
[
ext
]
||
'
audio/mpeg
'
;
}
const
file
=
new
File
([
blob
],
filename
,
{
type
:
mimeType
});
if
(
currentForm
)
{
currentForm
.
audioFile
=
file
;
}
console
.
log
(
'
模板音频文件已加载
'
);
}
else
{
console
.
warn
(
'
Failed to fetch audio from URL:
'
,
audioUrl
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
return
;
}
}
catch
(
error
)
{
console
.
error
(
'
Failed to load template audio file:
'
,
error
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
return
;
}
updateUploadedContentStatus
();
updateUploadedContentStatus
();
showAlert
(
t
(
'
audioApplied
'
),
'
success
'
);
})
// 关闭所有弹窗的辅助函数
.
catch
(
error
=>
{
const
closeAllModals
=
()
=>
{
closeTaskDetailModal
();
// 使用函数确保状态完全重置
showVoiceTTSModal
.
value
=
false
;
closeTemplateDetailModal
();
// 使用函数确保状态完全重置
showImageTemplates
.
value
=
false
;
showAudioTemplates
.
value
=
false
;
showPromptModal
.
value
=
false
;
closeImageZoomModal
();
// 使用函数确保状态完全重置
};
// 跳转到创作区域的函数
const
scrollToCreationArea
=
()
=>
{
// 先关闭所有弹窗
closeAllModals
();
// 如果不在生成页面,先切换视图
if
(
router
.
currentRoute
.
value
.
path
!==
'
/generate
'
)
{
switchToCreateView
();
// 等待路由切换完成后再展开和滚动
setTimeout
(()
=>
{
expandCreationArea
();
setTimeout
(()
=>
{
// 滚动到顶部(TopBar 之后的位置,约60px)
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
);
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
});
}
},
100
);
},
100
);
}
else
{
// 已经在生成页面,直接展开和滚动
expandCreationArea
();
setTimeout
(()
=>
{
// 滚动到顶部(TopBar 之后的位置,约60px)
const
mainScrollable
=
document
.
querySelector
(
'
.main-scrollbar
'
);
if
(
mainScrollable
)
{
mainScrollable
.
scrollTo
({
top
:
0
,
behavior
:
'
smooth
'
});
}
},
100
);
}
};
showAlert
(
t
(
'
audioApplied
'
),
'
success
'
,
{
label
:
t
(
'
view
'
),
onClick
:
scrollToCreationArea
});
}
catch
(
error
)
{
console
.
error
(
'
应用音频失败:
'
,
error
);
console
.
error
(
'
应用音频失败:
'
,
error
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
showAlert
(
t
(
'
applyAudioFailed
'
),
'
danger
'
);
});
}
}
};
};
...
@@ -5048,23 +5427,231 @@
...
@@ -5048,23 +5427,231 @@
}
}
};
};
// 复制
Prompt到剪贴板
// 复制
文本到剪贴板的辅助函数(支持移动端降级)
const
copy
Prompt
=
async
(
promptT
ext
)
=>
{
const
copy
ToClipboard
=
async
(
t
ext
)
=>
{
if
(
!
promptText
)
return
;
// 检查是否支持现代 Clipboard API
if
(
navigator
.
clipboard
&&
navigator
.
clipboard
.
writeText
)
{
try
{
try
{
await
navigator
.
clipboard
.
writeText
(
promptT
ext
);
await
navigator
.
clipboard
.
writeText
(
t
ext
);
showAlert
(
t
(
'
promptCopied
'
),
'
success
'
)
;
return
true
;
}
catch
(
error
)
{
}
catch
(
error
)
{
// 降级方案:使用传统方法
console
.
warn
(
'
Clipboard API 失败,尝试降级方案:
'
,
error
);
// 降级到传统方法
}
}
// 降级方案:使用传统方法(适用于移动端和不支持Clipboard API的浏览器)
try
{
const
textArea
=
document
.
createElement
(
'
textarea
'
);
const
textArea
=
document
.
createElement
(
'
textarea
'
);
textArea
.
value
=
promptText
;
textArea
.
value
=
text
;
// 移动端需要元素可见且可聚焦,所以先设置可见样式
textArea
.
style
.
position
=
'
fixed
'
;
textArea
.
style
.
left
=
'
0
'
;
textArea
.
style
.
top
=
'
0
'
;
textArea
.
style
.
width
=
'
2em
'
;
textArea
.
style
.
height
=
'
2em
'
;
textArea
.
style
.
padding
=
'
0
'
;
textArea
.
style
.
border
=
'
none
'
;
textArea
.
style
.
outline
=
'
none
'
;
textArea
.
style
.
boxShadow
=
'
none
'
;
textArea
.
style
.
background
=
'
transparent
'
;
textArea
.
style
.
opacity
=
'
0
'
;
textArea
.
style
.
zIndex
=
'
-1
'
;
textArea
.
setAttribute
(
'
readonly
'
,
''
);
textArea
.
setAttribute
(
'
aria-hidden
'
,
'
true
'
);
textArea
.
setAttribute
(
'
tabindex
'
,
'
-1
'
);
document
.
body
.
appendChild
(
textArea
);
document
.
body
.
appendChild
(
textArea
);
// 聚焦元素(移动端需要)
textArea
.
focus
();
textArea
.
select
();
textArea
.
select
();
document
.
execCommand
(
'
copy
'
);
// 移动端需要 setSelectionRange
if
(
textArea
.
setSelectionRange
)
{
textArea
.
setSelectionRange
(
0
,
text
.
length
);
}
// 尝试复制
let
successful
=
false
;
try
{
successful
=
document
.
execCommand
(
'
copy
'
);
}
catch
(
e
)
{
console
.
warn
(
'
execCommand 执行失败:
'
,
e
);
}
// 立即移除元素
document
.
body
.
removeChild
(
textArea
);
document
.
body
.
removeChild
(
textArea
);
if
(
successful
)
{
return
true
;
}
else
{
// 如果仍然失败,尝试另一种方法:在视口中心创建可见的输入框
return
await
fallbackCopyToClipboard
(
text
);
}
}
catch
(
error
)
{
console
.
error
(
'
复制失败,尝试备用方案:
'
,
error
);
// 尝试备用方案
return
await
fallbackCopyToClipboard
(
text
);
}
};
// 备用复制方案:显示一个可选择的文本区域(Apple风格)
const
fallbackCopyToClipboard
=
async
(
text
)
=>
{
return
new
Promise
((
resolve
)
=>
{
// 创建遮罩层
const
overlay
=
document
.
createElement
(
'
div
'
);
overlay
.
style
.
cssText
=
`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
`
;
// 创建弹窗容器(Apple风格)
const
container
=
document
.
createElement
(
'
div
'
);
container
.
style
.
cssText
=
`
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-radius: 20px;
padding: 24px;
max-width: 90%;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
`
;
// 深色模式支持
if
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
{
container
.
style
.
background
=
'
rgba(30, 30, 30, 0.95)
'
;
}
const
title
=
document
.
createElement
(
'
div
'
);
title
.
textContent
=
t
(
'
copyLink
'
)
||
'
复制链接
'
;
title
.
style
.
cssText
=
`
font-size: 18px;
font-weight: 600;
color: #1d1d1f;
margin-bottom: 12px;
text-align: center;
`
;
if
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
{
title
.
style
.
color
=
'
#f5f5f7
'
;
}
const
message
=
document
.
createElement
(
'
div
'
);
message
.
textContent
=
t
(
'
pleaseCopyManually
'
)
||
'
请手动选择并复制下面的文本
'
;
message
.
style
.
cssText
=
`
color: #86868b;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
`
;
if
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
{
message
.
style
.
color
=
'
#98989d
'
;
}
const
input
=
document
.
createElement
(
'
input
'
);
input
.
type
=
'
text
'
;
input
.
value
=
text
;
input
.
readOnly
=
true
;
input
.
style
.
cssText
=
`
width: 100%;
padding: 12px 16px;
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
color: #1d1d1f;
margin-bottom: 16px;
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
`
;
if
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
{
input
.
style
.
border
=
'
1px solid rgba(255, 255, 255, 0.1)
'
;
input
.
style
.
background
=
'
rgba(44, 44, 46, 0.8)
'
;
input
.
style
.
color
=
'
#f5f5f7
'
;
}
const
button
=
document
.
createElement
(
'
button
'
);
button
.
textContent
=
t
(
'
close
'
)
||
'
关闭
'
;
button
.
style
.
cssText
=
`
width: 100%;
padding: 12px 24px;
background: var(--brand-primary, #007AFF);
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.2s;
`
;
button
.
onmouseover
=
()
=>
{
button
.
style
.
opacity
=
'
0.9
'
;
button
.
style
.
transform
=
'
scale(1.02)
'
;
};
button
.
onmouseout
=
()
=>
{
button
.
style
.
opacity
=
'
1
'
;
button
.
style
.
transform
=
'
scale(1)
'
;
};
container
.
appendChild
(
title
);
container
.
appendChild
(
message
);
container
.
appendChild
(
input
);
container
.
appendChild
(
button
);
overlay
.
appendChild
(
container
);
const
close
=
()
=>
{
document
.
body
.
removeChild
(
overlay
);
resolve
(
false
);
// 返回false表示需要用户手动复制
};
button
.
onclick
=
close
;
overlay
.
onclick
=
(
e
)
=>
{
if
(
e
.
target
===
overlay
)
close
();
};
document
.
body
.
appendChild
(
overlay
);
// 选中文本(延迟以确保DOM已渲染)
setTimeout
(()
=>
{
input
.
focus
();
input
.
select
();
if
(
input
.
setSelectionRange
)
{
input
.
setSelectionRange
(
0
,
text
.
length
);
}
},
150
);
});
};
// 复制Prompt到剪贴板
const
copyPrompt
=
async
(
promptText
)
=>
{
if
(
!
promptText
)
return
;
try
{
// 使用辅助函数复制,支持移动端
const
success
=
await
copyToClipboard
(
promptText
);
if
(
success
)
{
showAlert
(
t
(
'
promptCopied
'
),
'
success
'
);
showAlert
(
t
(
'
promptCopied
'
),
'
success
'
);
}
}
// 如果返回false,说明已经显示了手动复制的弹窗,不需要额外提示
}
catch
(
error
)
{
console
.
error
(
'
复制Prompt失败:
'
,
error
);
showAlert
(
t
(
'
copyFailed
'
),
'
error
'
);
}
};
};
// 使用模板
// 使用模板
...
@@ -5279,8 +5866,11 @@
...
@@ -5279,8 +5866,11 @@
const
data
=
await
response
.
json
();
const
data
=
await
response
.
json
();
const
shareUrl
=
`
${
window
.
location
.
origin
}${
data
.
share_url
}
`
;
const
shareUrl
=
`
${
window
.
location
.
origin
}${
data
.
share_url
}
`
;
await
navigator
.
clipboard
.
writeText
(
shareUrl
);
// 使用辅助函数复制,支持移动端
const
success
=
await
copyToClipboard
(
shareUrl
);
// 如果成功复制,显示成功提示
if
(
success
)
{
// 显示带操作按钮的alert
// 显示带操作按钮的alert
showAlert
(
t
(
'
shareLinkCopied
'
),
'
success
'
,
{
showAlert
(
t
(
'
shareLinkCopied
'
),
'
success
'
,
{
label
:
t
(
'
view
'
),
label
:
t
(
'
view
'
),
...
@@ -5288,6 +5878,8 @@
...
@@ -5288,6 +5878,8 @@
window
.
open
(
shareUrl
,
'
_blank
'
);
window
.
open
(
shareUrl
,
'
_blank
'
);
}
}
});
});
}
// 如果返回false,说明已经显示了手动复制的弹窗,不需要额外提示
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
error
(
'
复制失败:
'
,
err
);
console
.
error
(
'
复制失败:
'
,
err
);
showAlert
(
t
(
'
copyFailed
'
),
'
error
'
);
showAlert
(
t
(
'
copyFailed
'
),
'
error
'
);
...
@@ -5367,13 +5959,19 @@
...
@@ -5367,13 +5959,19 @@
const
copyTemplateShareLink
=
async
(
templateId
)
=>
{
const
copyTemplateShareLink
=
async
(
templateId
)
=>
{
try
{
try
{
const
shareUrl
=
generateTemplateShareUrl
(
templateId
);
const
shareUrl
=
generateTemplateShareUrl
(
templateId
);
await
navigator
.
clipboard
.
writeText
(
shareUrl
);
// 使用辅助函数复制,支持移动端
const
success
=
await
copyToClipboard
(
shareUrl
);
// 如果成功复制,显示成功提示
if
(
success
)
{
showAlert
(
t
(
'
templateShareLinkCopied
'
),
'
success
'
,
{
showAlert
(
t
(
'
templateShareLinkCopied
'
),
'
success
'
,
{
label
:
t
(
'
view
'
),
label
:
t
(
'
view
'
),
onClick
:
()
=>
{
onClick
:
()
=>
{
window
.
open
(
shareUrl
,
'
_blank
'
);
window
.
open
(
shareUrl
,
'
_blank
'
);
}
}
});
});
}
// 如果返回false,说明已经显示了手动复制的弹窗,不需要额外提示
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
error
(
'
复制模板分享链接失败:
'
,
err
);
console
.
error
(
'
复制模板分享链接失败:
'
,
err
);
showAlert
(
t
(
'
copyFailed
'
),
'
error
'
);
showAlert
(
t
(
'
copyFailed
'
),
'
error
'
);
...
@@ -5615,7 +6213,7 @@
...
@@ -5615,7 +6213,7 @@
}
}
};
};
export
{
export
{
// 任务类型下拉菜单
// 任务类型下拉菜单
showTaskTypeMenu
,
showTaskTypeMenu
,
showModelMenu
,
showModelMenu
,
...
@@ -5658,6 +6256,7 @@
...
@@ -5658,6 +6256,7 @@
showConfirmDialog
,
showConfirmDialog
,
showTaskDetailModal
,
showTaskDetailModal
,
modalTask
,
modalTask
,
showVoiceTTSModal
,
currentTask
,
currentTask
,
t2vForm
,
t2vForm
,
i2vForm
,
i2vForm
,
...
...
lightx2v/deploy/server/frontend/src/views/Layout.vue
View file @
23aa1ef3
...
@@ -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"
>
...
...
lightx2v/deploy/server/frontend/src/views/Share.vue
View file @
23aa1ef3
...
@@ -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"
>
<!-- 主内容区域 -->
<div
class=
"flex flex-col w-full h-full"
>
<!-- TopBar -->
<!-- TopBar -->
<topMenu
/>
<topMenu
/>
<!--
主要
内容区域 -->
<!--
滚动
内容区域
- 带滚动条
-->
<div
class=
"
w-full min-h-[calc(100vh-80px)]
overflow-y-auto main-scrollbar"
>
<div
class=
"
flex-1
overflow-y-auto main-scrollbar"
>
<!-- 错误状态 - Apple 风格 -->
<!-- 错误状态 - Apple 风格
- 响应式
-->
<div
v-if=
"error"
class=
"flex items-center justify-center min-h-[60vh] px-6"
>
<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-
4
xl sm:text-5xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-4 tracking-tight leading-tight"
>
<h1
class=
"text-
2
xl 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-
2
xl 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>
...
@@ -499,6 +501,7 @@ onMounted(async () => {
...
@@ -499,6 +501,7 @@ onMounted(async () => {
<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
>
<
style
scoped
>
<
style
scoped
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment