Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
183c750e
authored
Feb 22, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add audio preview functionality
parent
c1b05d3b
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
248 additions
and
2 deletions
+248
-2
web/src/components/table/task-logs/TaskLogsColumnDefs.jsx
+21
-0
web/src/components/table/task-logs/TaskLogsTable.jsx
+3
-1
web/src/components/table/task-logs/index.jsx
+6
-0
web/src/components/table/task-logs/modals/AudioPreviewModal.jsx
+181
-0
web/src/hooks/task-logs/useTaskLogsData.js
+16
-1
web/src/i18n/locales/en.json
+3
-0
web/src/i18n/locales/fr.json
+3
-0
web/src/i18n/locales/ja.json
+3
-0
web/src/i18n/locales/ru.json
+3
-0
web/src/i18n/locales/vi.json
+3
-0
web/src/i18n/locales/zh-CN.json
+3
-0
web/src/i18n/locales/zh-TW.json
+3
-0
No files found.
web/src/components/table/task-logs/TaskLogsColumnDefs.jsx
View file @
183c750e
...
...
@@ -240,6 +240,7 @@ export const getTaskLogsColumns = ({
openContentModal
,
isAdminUser
,
openVideoModal
,
openAudioModal
,
})
=>
{
return
[
{
...
...
@@ -386,6 +387,26 @@ export const getTaskLogsColumns = ({
dataIndex
:
'fail_reason'
,
fixed
:
'right'
,
render
:
(
text
,
record
,
index
)
=>
{
// Suno audio preview
const
isSunoSuccess
=
record
.
platform
===
'suno'
&&
record
.
status
===
'SUCCESS'
&&
Array
.
isArray
(
record
.
data
)
&&
record
.
data
.
some
((
c
)
=>
c
.
audio_url
);
if
(
isSunoSuccess
)
{
return
(
<
a
href=
'#'
onClick=
{
(
e
)
=>
{
e
.
preventDefault
();
openAudioModal
(
record
.
data
);
}
}
>
{
t
(
'点击预览音乐'
)
}
</
a
>
);
}
// 视频预览:优先使用 result_url,兼容旧数据 fail_reason 中的 URL
const
isVideoTask
=
record
.
action
===
TASK_ACTION_GENERATE
||
...
...
web/src/components/table/task-logs/TaskLogsTable.jsx
View file @
183c750e
...
...
@@ -40,6 +40,7 @@ const TaskLogsTable = (taskLogsData) => {
copyText
,
openContentModal
,
openVideoModal
,
openAudioModal
,
showUserInfoFunc
,
isAdminUser
,
t
,
...
...
@@ -54,10 +55,11 @@ const TaskLogsTable = (taskLogsData) => {
copyText
,
openContentModal
,
openVideoModal
,
openAudioModal
,
showUserInfoFunc
,
isAdminUser
,
});
},
[
t
,
COLUMN_KEYS
,
copyText
,
openContentModal
,
openVideoModal
,
showUserInfoFunc
,
isAdminUser
]);
},
[
t
,
COLUMN_KEYS
,
copyText
,
openContentModal
,
openVideoModal
,
openAudioModal
,
showUserInfoFunc
,
isAdminUser
]);
// Filter columns based on visibility settings
const
getVisibleColumns
=
()
=>
{
...
...
web/src/components/table/task-logs/index.jsx
View file @
183c750e
...
...
@@ -25,6 +25,7 @@ import TaskLogsActions from './TaskLogsActions';
import
TaskLogsFilters
from
'./TaskLogsFilters'
;
import
ColumnSelectorModal
from
'./modals/ColumnSelectorModal'
;
import
ContentModal
from
'./modals/ContentModal'
;
import
AudioPreviewModal
from
'./modals/AudioPreviewModal'
;
import
{
useTaskLogsData
}
from
'../../../hooks/task-logs/useTaskLogsData'
;
import
{
useIsMobile
}
from
'../../../hooks/common/useIsMobile'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
...
...
@@ -45,6 +46,11 @@ const TaskLogsPage = () => {
modalContent=
{
taskLogsData
.
videoUrl
}
isVideo=
{
true
}
/>
<
AudioPreviewModal
isModalOpen=
{
taskLogsData
.
isAudioModalOpen
}
setIsModalOpen=
{
taskLogsData
.
setIsAudioModalOpen
}
audioClips=
{
taskLogsData
.
audioClips
}
/>
<
Layout
>
<
CardPro
...
...
web/src/components/table/task-logs/modals/AudioPreviewModal.jsx
0 → 100644
View file @
183c750e
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
,
{
useState
,
useRef
,
useEffect
}
from
'react'
;
import
{
Modal
,
Typography
,
Tag
,
Button
}
from
'@douyinfe/semi-ui'
;
import
{
IconExternalOpen
,
IconCopy
}
from
'@douyinfe/semi-icons'
;
import
{
useTranslation
}
from
'react-i18next'
;
const
{
Text
,
Title
}
=
Typography
;
const
formatDuration
=
(
seconds
)
=>
{
if
(
!
seconds
||
seconds
<=
0
)
return
'--:--'
;
const
m
=
Math
.
floor
(
seconds
/
60
);
const
s
=
Math
.
floor
(
seconds
%
60
);
return
`
${
m
}
:
${
s
.
toString
().
padStart
(
2
,
'0'
)}
`
;
};
const
AudioClipCard
=
({
clip
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
hasError
,
setHasError
]
=
useState
(
false
);
const
audioRef
=
useRef
(
null
);
useEffect
(()
=>
{
setHasError
(
false
);
},
[
clip
.
audio_url
]);
const
title
=
clip
.
title
||
t
(
'未命名'
);
const
tags
=
clip
.
tags
||
clip
.
metadata
?.
tags
||
''
;
const
duration
=
clip
.
duration
||
clip
.
metadata
?.
duration
;
const
imageUrl
=
clip
.
image_url
||
clip
.
image_large_url
;
const
audioUrl
=
clip
.
audio_url
;
return
(
<
div
style=
{
{
display
:
'flex'
,
gap
:
'16px'
,
padding
:
'16px'
,
borderRadius
:
'8px'
,
border
:
'1px solid var(--semi-color-border)'
,
background
:
'var(--semi-color-bg-1)'
,
}
}
>
{
imageUrl
&&
(
<
img
src=
{
imageUrl
}
alt=
{
title
}
style=
{
{
width
:
80
,
height
:
80
,
borderRadius
:
'8px'
,
objectFit
:
'cover'
,
flexShrink
:
0
,
}
}
onError=
{
(
e
)
=>
{
e
.
target
.
style
.
display
=
'none'
;
}
}
/>
)
}
<
div
style=
{
{
flex
:
1
,
minWidth
:
0
}
}
>
<
div
style=
{
{
display
:
'flex'
,
alignItems
:
'center'
,
gap
:
'8px'
,
marginBottom
:
'4px'
,
}
}
>
<
Text
strong
ellipsis=
{
{
showTooltip
:
true
}
}
style=
{
{
fontSize
:
15
}
}
>
{
title
}
</
Text
>
{
duration
>
0
&&
(
<
Tag
size=
'small'
color=
'grey'
shape=
'circle'
>
{
formatDuration
(
duration
)
}
</
Tag
>
)
}
</
div
>
{
tags
&&
(
<
div
style=
{
{
marginBottom
:
'8px'
}
}
>
<
Text
type=
'tertiary'
size=
'small'
ellipsis=
{
{
showTooltip
:
true
,
rows
:
1
}
}
>
{
tags
}
</
Text
>
</
div
>
)
}
{
hasError
?
(
<
div
style=
{
{
display
:
'flex'
,
alignItems
:
'center'
,
gap
:
'8px'
,
flexWrap
:
'wrap'
,
}
}
>
<
Text
type=
'warning'
size=
'small'
>
{
t
(
'音频无法播放'
)
}
</
Text
>
<
Button
size=
'small'
icon=
{
<
IconExternalOpen
/>
}
onClick=
{
()
=>
window
.
open
(
audioUrl
,
'_blank'
)
}
>
{
t
(
'在新标签页中打开'
)
}
</
Button
>
<
Button
size=
'small'
icon=
{
<
IconCopy
/>
}
onClick=
{
()
=>
navigator
.
clipboard
.
writeText
(
audioUrl
)
}
>
{
t
(
'复制链接'
)
}
</
Button
>
</
div
>
)
:
(
<
audio
ref=
{
audioRef
}
src=
{
audioUrl
}
controls
preload=
'none'
onError=
{
()
=>
setHasError
(
true
)
}
style=
{
{
width
:
'100%'
,
height
:
36
}
}
/>
)
}
</
div
>
</
div
>
);
};
const
AudioPreviewModal
=
({
isModalOpen
,
setIsModalOpen
,
audioClips
})
=>
{
const
{
t
}
=
useTranslation
();
const
clips
=
Array
.
isArray
(
audioClips
)
?
audioClips
:
[];
return
(
<
Modal
title=
{
t
(
'音乐预览'
)
}
visible=
{
isModalOpen
}
onOk=
{
()
=>
setIsModalOpen
(
false
)
}
onCancel=
{
()
=>
setIsModalOpen
(
false
)
}
closable=
{
null
}
footer=
{
null
}
bodyStyle=
{
{
maxHeight
:
'70vh'
,
overflow
:
'auto'
,
padding
:
'16px'
,
}
}
width=
{
560
}
>
{
clips
.
length
===
0
?
(
<
Text
type=
'tertiary'
>
{
t
(
'无'
)
}
</
Text
>
)
:
(
<
div
style=
{
{
display
:
'flex'
,
flexDirection
:
'column'
,
gap
:
'12px'
}
}
>
{
clips
.
map
((
clip
,
idx
)
=>
(
<
AudioClipCard
key=
{
clip
.
clip_id
||
clip
.
id
||
idx
}
clip=
{
clip
}
/>
))
}
</
div
>
)
}
</
Modal
>
);
};
export
default
AudioPreviewModal
;
web/src/hooks/task-logs/useTaskLogsData.js
View file @
183c750e
...
...
@@ -72,6 +72,10 @@ export const useTaskLogsData = () => {
const
[
isVideoModalOpen
,
setIsVideoModalOpen
]
=
useState
(
false
);
const
[
videoUrl
,
setVideoUrl
]
=
useState
(
''
);
// Audio preview modal state
const
[
isAudioModalOpen
,
setIsAudioModalOpen
]
=
useState
(
false
);
const
[
audioClips
,
setAudioClips
]
=
useState
([]);
// User info modal state
const
[
showUserInfo
,
setShowUserInfoModal
]
=
useState
(
false
);
const
[
userInfoData
,
setUserInfoData
]
=
useState
(
null
);
...
...
@@ -277,6 +281,11 @@ export const useTaskLogsData = () => {
setIsVideoModalOpen
(
true
);
};
const
openAudioModal
=
(
clips
)
=>
{
setAudioClips
(
clips
);
setIsAudioModalOpen
(
true
);
};
// User info function
const
showUserInfoFunc
=
async
(
userId
)
=>
{
if
(
!
isAdminUser
)
{
...
...
@@ -319,6 +328,11 @@ export const useTaskLogsData = () => {
setIsVideoModalOpen
,
videoUrl
,
// Audio preview modal
isAudioModalOpen
,
setIsAudioModalOpen
,
audioClips
,
// Form state
formApi
,
setFormApi
,
...
...
@@ -351,7 +365,8 @@ export const useTaskLogsData = () => {
refresh
,
copyText
,
openContentModal
,
openVideoModal
,
// 新增
openVideoModal
,
openAudioModal
,
enrichLogs
,
syncPageData
,
...
...
web/src/i18n/locales/en.json
View file @
183c750e
...
...
@@ -1634,6 +1634,9 @@
"点击查看差异"
:
"Click to view differences"
,
"点击此处"
:
"click here"
,
"点击预览视频"
:
"Click to preview video"
,
"点击预览音乐"
:
"Click to preview music"
,
"音乐预览"
:
"Music Preview"
,
"音频无法播放"
:
"Audio cannot be played"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"Click the verification button and use your biometrics or security key"
,
"版权所有"
:
"All rights reserved"
,
"状态"
:
"Status"
,
...
...
web/src/i18n/locales/fr.json
View file @
183c750e
...
...
@@ -1646,6 +1646,9 @@
"点击查看差异"
:
"Cliquez pour voir les différences"
,
"点击此处"
:
"cliquez ici"
,
"点击预览视频"
:
"Cliquez pour prévisualiser la vidéo"
,
"点击预览音乐"
:
"Cliquez pour écouter la musique"
,
"音乐预览"
:
"Aperçu musical"
,
"音频无法播放"
:
"Impossible de lire l'audio"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"Cliquez sur le bouton de vérification pour utiliser vos caractéristiques biométriques ou votre clé de sécurité"
,
"版权所有"
:
"Tous droits réservés"
,
"状态"
:
"Statut"
,
...
...
web/src/i18n/locales/ja.json
View file @
183c750e
...
...
@@ -1631,6 +1631,9 @@
"点击查看差异"
:
"差分を表示"
,
"点击此处"
:
"こちらをクリック"
,
"点击预览视频"
:
"動画をプレビュー"
,
"点击预览音乐"
:
"音楽をプレビュー"
,
"音乐预览"
:
"音楽プレビュー"
,
"音频无法播放"
:
"音声を再生できません"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"認証ボタンをクリックし、生体情報またはセキュリティキーを使用してください"
,
"版权所有"
:
"All rights reserved"
,
"状态"
:
"ステータス"
,
...
...
web/src/i18n/locales/ru.json
View file @
183c750e
...
...
@@ -1657,6 +1657,9 @@
"点击查看差异"
:
"Нажмите для просмотра различий"
,
"点击此处"
:
"Нажмите здесь"
,
"点击预览视频"
:
"Нажмите для предварительного просмотра видео"
,
"点击预览音乐"
:
"Нажмите для прослушивания музыки"
,
"音乐预览"
:
"Предварительное прослушивание"
,
"音频无法播放"
:
"Не удалось воспроизвести аудио"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"Нажмите кнопку проверки, используйте ваши биометрические данные или ключ безопасности"
,
"版权所有"
:
"Все права защищены"
,
"状态"
:
"Статус"
,
...
...
web/src/i18n/locales/vi.json
View file @
183c750e
...
...
@@ -1773,6 +1773,9 @@
"点击链接重置密码"
:
"Nhấp vào liên kết để đặt lại mật khẩu"
,
"点击阅读"
:
"Nhấp để đọc"
,
"点击预览视频"
:
"Nhấp để xem trước video"
,
"点击预览音乐"
:
"Nhấp để nghe nhạc"
,
"音乐预览"
:
"Xem trước nhạc"
,
"音频无法播放"
:
"Không thể phát âm thanh"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"Nhấp vào nút xác minh và sử dụng sinh trắc học hoặc khóa bảo mật của bạn"
,
"版"
:
"Phiên bản"
,
"版本"
:
"Phiên bản"
,
...
...
web/src/i18n/locales/zh-CN.json
View file @
183c750e
...
...
@@ -1624,6 +1624,9 @@
"点击查看差异"
:
"点击查看差异"
,
"点击此处"
:
"点击此处"
,
"点击预览视频"
:
"点击预览视频"
,
"点击预览音乐"
:
"点击预览音乐"
,
"音乐预览"
:
"音乐预览"
,
"音频无法播放"
:
"音频无法播放"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"点击验证按钮,使用您的生物特征或安全密钥"
,
"版权所有"
:
"版权所有"
,
"状态"
:
"状态"
,
...
...
web/src/i18n/locales/zh-TW.json
View file @
183c750e
...
...
@@ -1628,6 +1628,9 @@
"点击查看差异"
:
"點擊查看差異"
,
"点击此处"
:
"點擊此處"
,
"点击预览视频"
:
"點擊預覽影片"
,
"点击预览音乐"
:
"點擊預覽音樂"
,
"音乐预览"
:
"音樂預覽"
,
"音频无法播放"
:
"音訊無法播放"
,
"点击验证按钮,使用您的生物特征或安全密钥"
:
"點擊驗證按鈕,使用您的生物特徵或安全密鑰"
,
"版权所有"
:
"版權所有"
,
"状态"
:
"狀態"
,
...
...
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