Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yaoai-video
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姚珂
yaoai-video
Commits
ea37c382
Commit
ea37c382
authored
Jun 03, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
角色和场景图片预览功能
parent
2d68dceb
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
183 additions
and
16 deletions
+183
-16
ImagePreviewModal.tsx
doc/html/src/app/components/ImagePreviewModal.tsx
+57
-0
CharacterGeneration.tsx
doc/html/src/app/pages/CharacterGeneration.tsx
+43
-11
PropsGeneration.tsx
doc/html/src/app/pages/PropsGeneration.tsx
+44
-3
SceneGeneration.tsx
doc/html/src/app/pages/SceneGeneration.tsx
+39
-2
No files found.
doc/html/src/app/components/ImagePreviewModal.tsx
0 → 100644
View file @
ea37c382
import
{
useEffect
}
from
"react"
;
import
{
X
}
from
"lucide-react"
;
interface
ImagePreviewModalProps
{
src
:
string
|
null
;
alt
?:
string
;
title
?:
string
;
onClose
:
()
=>
void
;
}
export
function
ImagePreviewModal
({
src
,
alt
,
title
,
onClose
}:
ImagePreviewModalProps
)
{
useEffect
(()
=>
{
if
(
!
src
)
return
;
const
handleKeyDown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
"Escape"
)
{
onClose
();
}
};
window
.
addEventListener
(
"keydown"
,
handleKeyDown
);
return
()
=>
window
.
removeEventListener
(
"keydown"
,
handleKeyDown
);
},
[
onClose
,
src
]);
if
(
!
src
)
return
null
;
return
(
<
div
className=
"fixed inset-0 z-[80] flex items-center justify-center bg-black/75 p-5 backdrop-blur-sm"
role=
"dialog"
aria
-
modal=
"true"
aria
-
label=
{
title
??
alt
??
"Image preview"
}
onClick=
{
onClose
}
>
<
div
className=
"relative flex max-h-[92vh] max-w-[94vw] flex-col items-center"
onClick=
{
(
event
)
=>
event
.
stopPropagation
()
}
>
<
button
type=
"button"
onClick=
{
onClose
}
className=
"absolute -right-3 -top-3 z-10 rounded-full bg-white p-2 text-slate-900 shadow-lg transition-colors hover:bg-slate-100"
aria
-
label=
"Close preview"
>
<
X
className=
"h-5 w-5"
/>
</
button
>
{
title
&&
(
<
div
className=
"mb-3 max-w-[84vw] rounded-full bg-black/45 px-4 py-1.5 text-sm font-medium text-white shadow-sm"
>
{
title
}
</
div
>
)
}
<
img
src=
{
src
}
alt=
{
alt
??
title
??
"Image preview"
}
className=
"max-h-[84vh] max-w-[94vw] rounded-xl object-contain shadow-2xl"
/>
</
div
>
</
div
>
);
}
doc/html/src/app/pages/CharacterGeneration.tsx
View file @
ea37c382
...
@@ -29,6 +29,7 @@ import {
...
@@ -29,6 +29,7 @@ import {
useUploadCharacterImage
,
useUploadCharacterImage
,
}
from
"../../hooks/useAi"
;
}
from
"../../hooks/useAi"
;
import
type
{
Character
,
CharacterState
}
from
"../../lib/api/ai"
;
import
type
{
Character
,
CharacterState
}
from
"../../lib/api/ai"
;
import
{
ImagePreviewModal
}
from
"../components/ImagePreviewModal"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
type
CharacterViewType
=
"front"
|
"side"
|
"back"
;
type
CharacterViewType
=
"front"
|
"side"
|
"back"
;
...
@@ -171,6 +172,7 @@ export function CharacterGeneration() {
...
@@ -171,6 +172,7 @@ export function CharacterGeneration() {
const
[
stateDraft
,
setStateDraft
]
=
useState
<
CharacterStateDraft
>
(
emptyStateDraft
());
const
[
stateDraft
,
setStateDraft
]
=
useState
<
CharacterStateDraft
>
(
emptyStateDraft
());
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
savingState
,
setSavingState
]
=
useState
(
false
);
const
[
savingState
,
setSavingState
]
=
useState
(
false
);
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
src
:
string
;
title
:
string
}
|
null
>
(
null
);
const
editingCharacterId
=
editingChar
.
id
?
String
(
editingChar
.
id
)
:
""
;
const
editingCharacterId
=
editingChar
.
id
?
String
(
editingChar
.
id
)
:
""
;
const
{
data
:
characterStates
=
[]
}
=
useCharacterStates
(
pid
,
editingCharacterId
);
const
{
data
:
characterStates
=
[]
}
=
useCharacterStates
(
pid
,
editingCharacterId
);
...
@@ -477,7 +479,13 @@ export function CharacterGeneration() {
...
@@ -477,7 +479,13 @@ export function CharacterGeneration() {
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"w-14 h-14 rounded-full overflow-hidden bg-muted flex items-center justify-center"
>
<
div
className=
"w-14 h-14 rounded-full overflow-hidden bg-muted flex items-center justify-center"
>
{
mainImage
?
(
{
mainImage
?
(
<
button
type=
"button"
onClick=
{
()
=>
setPreviewImage
({
src
:
mainImage
,
title
:
character
.
name
})
}
className=
"block h-full w-full cursor-zoom-in"
>
<
img
src=
{
mainImage
}
alt=
{
character
.
name
}
className=
"w-full h-full object-cover"
/>
<
img
src=
{
mainImage
}
alt=
{
character
.
name
}
className=
"w-full h-full object-cover"
/>
</
button
>
)
:
(
)
:
(
<
ImageIcon
className=
"w-6 h-6 text-muted-foreground"
/>
<
ImageIcon
className=
"w-6 h-6 text-muted-foreground"
/>
)
}
)
}
...
@@ -552,25 +560,26 @@ export function CharacterGeneration() {
...
@@ -552,25 +560,26 @@ export function CharacterGeneration() {
if
(
sheetUrl
)
{
if
(
sheetUrl
)
{
return
(
return
(
<
div
className=
"rounded-lg overflow-hidden bg-white border border-border"
>
<
div
className=
"rounded-lg overflow-hidden bg-white border border-border"
>
<
button
type=
"button"
onClick=
{
()
=>
setPreviewImage
({
src
:
sheetUrl
,
title
:
`${character.name} - 角色三视图`
})
}
className=
"block w-full cursor-zoom-in"
>
<
img
<
img
src=
{
sheetUrl
}
src=
{
sheetUrl
}
alt=
{
`${character.name}-角色三视图`
}
alt=
{
`${character.name}-角色三视图`
}
className=
"w-full h-auto object-contain"
className=
"w-full h-auto object-contain"
/>
/>
</
button
>
</
div
>
</
div
>
);
);
}
}
const
viewImages
=
CHARACTER_VIEWS
.
map
((
view
)
=>
({
...
view
,
imageUrl
:
getCharacterViewUrl
(
character
,
view
.
type
)
}))
.
filter
((
view
)
=>
view
.
imageUrl
);
if
(
viewImages
.
length
===
0
)
{
return
(
return
(
<
div
className=
"grid grid-cols-3 gap-3"
>
<
div
className=
"aspect-[16/9] rounded-lg overflow-hidden bg-white border border-border"
>
{
CHARACTER_VIEWS
.
map
((
view
)
=>
{
const
imageUrl
=
getCharacterViewUrl
(
character
,
view
.
type
);
return
(
<
div
key=
{
view
.
type
}
>
<
div
className=
"text-[11px] text-muted-foreground mb-1 text-center"
>
{
view
.
label
}
</
div
>
<
div
className=
"aspect-[3/4] rounded-lg overflow-hidden bg-white border border-border"
>
{
imageUrl
?
(
<
img
src=
{
imageUrl
}
alt=
{
`${character.name}-${view.label}`
}
className=
"w-full h-full object-cover"
/>
)
:
(
<
div
className=
"w-full h-full flex flex-col items-center justify-center gap-2"
>
<
div
className=
"w-full h-full flex flex-col items-center justify-center gap-2"
>
{
isGenerating
?
(
{
isGenerating
?
(
<>
<>
...
@@ -584,7 +593,24 @@ export function CharacterGeneration() {
...
@@ -584,7 +593,24 @@ export function CharacterGeneration() {
</>
</>
)
}
)
}
</
div
>
</
div
>
)
}
</
div
>
);
}
return
(
<
div
className=
{
`grid gap-3 ${viewImages.length === 1 ? "grid-cols-1" : viewImages.length === 2 ? "grid-cols-2" : "grid-cols-3"}`
}
>
{
viewImages
.
map
((
view
)
=>
{
const
imageUrl
=
view
.
imageUrl
!
;
return
(
<
div
key=
{
view
.
type
}
>
<
div
className=
"text-[11px] text-muted-foreground mb-1 text-center"
>
{
view
.
label
}
</
div
>
<
div
className=
"aspect-[3/4] rounded-lg overflow-hidden bg-white border border-border"
>
<
button
type=
"button"
onClick=
{
()
=>
setPreviewImage
({
src
:
imageUrl
,
title
:
`${character.name} - ${view.label}`
})
}
className=
"block h-full w-full cursor-zoom-in"
>
<
img
src=
{
imageUrl
}
alt=
{
`${character.name}-${view.label}`
}
className=
"w-full h-full object-cover"
/>
</
button
>
</
div
>
</
div
>
</
div
>
</
div
>
);
);
...
@@ -927,6 +953,12 @@ export function CharacterGeneration() {
...
@@ -927,6 +953,12 @@ export function CharacterGeneration() {
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
}
<
ImagePreviewModal
src=
{
previewImage
?.
src
??
null
}
title=
{
previewImage
?.
title
}
alt=
{
previewImage
?.
title
}
onClose=
{
()
=>
setPreviewImage
(
null
)
}
/>
</
div
>
</
div
>
);
);
}
}
doc/html/src/app/pages/PropsGeneration.tsx
View file @
ea37c382
...
@@ -13,6 +13,7 @@ import {
...
@@ -13,6 +13,7 @@ import {
X
,
X
,
Check
,
Check
,
RefreshCw
,
RefreshCw
,
ImageIcon
,
Loader2
,
Loader2
,
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
{
import
{
...
@@ -24,6 +25,7 @@ import {
...
@@ -24,6 +25,7 @@ import {
useDeleteAsset
,
useDeleteAsset
,
}
from
"../../hooks/useAssets"
;
}
from
"../../hooks/useAssets"
;
import
type
{
GlobalAsset
}
from
"../../lib/api/assets"
;
import
type
{
GlobalAsset
}
from
"../../lib/api/assets"
;
import
{
ImagePreviewModal
}
from
"../components/ImagePreviewModal"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
...
@@ -69,6 +71,7 @@ export function PropsGeneration() {
...
@@ -69,6 +71,7 @@ export function PropsGeneration() {
const
[
pendingFile
,
setPendingFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
pendingFile
,
setPendingFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
generatingImageId
,
setGeneratingImageId
]
=
useState
<
string
|
null
>
(
null
);
const
[
generatingImageId
,
setGeneratingImageId
]
=
useState
<
string
|
null
>
(
null
);
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
src
:
string
;
title
:
string
}
|
null
>
(
null
);
const
fileInputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
fileInputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
settingsTabs
=
[
const
settingsTabs
=
[
...
@@ -295,15 +298,44 @@ export function PropsGeneration() {
...
@@ -295,15 +298,44 @@ export function PropsGeneration() {
<
div
key=
{
prop
.
id
}
className=
"rounded-xl border border-border bg-card overflow-hidden group"
>
<
div
key=
{
prop
.
id
}
className=
"rounded-xl border border-border bg-card overflow-hidden group"
>
<
div
className=
"aspect-square relative bg-muted"
>
<
div
className=
"aspect-square relative bg-muted"
>
{
prop
.
imageUrl
?
(
{
prop
.
imageUrl
?
(
<
button
type=
"button"
onClick=
{
()
=>
setPreviewImage
({
src
:
prop
.
imageUrl
!
,
title
:
prop
.
name
})
}
className=
"block h-full w-full cursor-zoom-in"
>
<
img
src=
{
prop
.
imageUrl
}
alt=
{
prop
.
name
}
className=
"w-full h-full object-cover"
/>
<
img
src=
{
prop
.
imageUrl
}
alt=
{
prop
.
name
}
className=
"w-full h-full object-cover"
/>
</
button
>
)
:
(
)
:
(
<
div
className=
"w-full h-full flex items-center justify-center"
>
<
div
className=
"w-full h-full flex items-center justify-center"
>
<
Box
className=
"w-10 h-10 text-muted-foreground/30"
/>
<
Box
className=
"w-10 h-10 text-muted-foreground/30"
/>
</
div
>
</
div
>
)
}
)
}
<
div
className=
"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1"
>
<
div
className=
{
`pointer-events-none absolute inset-0 bg-black/40 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1 ${
prop.imageUrl ? "cursor-zoom-in" : ""
}`
}
onClick=
{
()
=>
{
if
(
prop
.
imageUrl
)
setPreviewImage
({
src
:
prop
.
imageUrl
,
title
:
prop
.
name
});
}
}
>
{
prop
.
imageUrl
&&
(
<
button
type=
"button"
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
();
setPreviewImage
({
src
:
prop
.
imageUrl
!
,
title
:
prop
.
name
});
}
}
className=
"p-1.5 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors"
title=
"查看大图"
>
<
ImageIcon
className=
"w-4 h-4 text-white"
/>
</
button
>
)
}
<
button
<
button
onClick=
{
()
=>
handleGenerateImage
(
prop
)
}
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
();
handleGenerateImage
(
prop
);
}
}
disabled=
{
generatingImageId
===
prop
.
id
||
generateImage
.
isPending
}
disabled=
{
generatingImageId
===
prop
.
id
||
generateImage
.
isPending
}
className=
"p-1.5 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors disabled:opacity-50"
className=
"p-1.5 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors disabled:opacity-50"
title=
{
prop
.
imageUrl
?
"AI重新生成道具图"
:
"AI生成道具图"
}
title=
{
prop
.
imageUrl
?
"AI重新生成道具图"
:
"AI生成道具图"
}
...
@@ -317,7 +349,10 @@ export function PropsGeneration() {
...
@@ -317,7 +349,10 @@ export function PropsGeneration() {
)
}
)
}
</
button
>
</
button
>
<
button
<
button
onClick=
{
()
=>
openEdit
(
prop
)
}
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
();
openEdit
(
prop
);
}
}
className=
"p-1.5 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors"
className=
"p-1.5 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors"
>
>
<
Edit2
className=
"w-4 h-4 text-white"
/>
<
Edit2
className=
"w-4 h-4 text-white"
/>
...
@@ -501,6 +536,12 @@ export function PropsGeneration() {
...
@@ -501,6 +536,12 @@ export function PropsGeneration() {
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
}
<
ImagePreviewModal
src=
{
previewImage
?.
src
??
null
}
title=
{
previewImage
?.
title
}
alt=
{
previewImage
?.
title
}
onClose=
{
()
=>
setPreviewImage
(
null
)
}
/>
</
div
>
</
div
>
);
);
}
}
doc/html/src/app/pages/SceneGeneration.tsx
View file @
ea37c382
...
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router";
...
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router";
import
{
ArrowRight
,
Sparkles
,
Plus
,
Edit2
,
Users
,
MapPin
,
Box
,
Trash2
,
X
,
Check
,
RefreshCw
,
ImageIcon
,
Loader2
,
Upload
}
from
"lucide-react"
;
import
{
ArrowRight
,
Sparkles
,
Plus
,
Edit2
,
Users
,
MapPin
,
Box
,
Trash2
,
X
,
Check
,
RefreshCw
,
ImageIcon
,
Loader2
,
Upload
}
from
"lucide-react"
;
import
{
useScenes
,
useExtractScenes
,
useGenerateSceneImage
,
useDeleteScene
,
useSaveScene
,
useUploadSceneImage
}
from
"../../hooks/useAi"
;
import
{
useScenes
,
useExtractScenes
,
useGenerateSceneImage
,
useDeleteScene
,
useSaveScene
,
useUploadSceneImage
}
from
"../../hooks/useAi"
;
import
type
{
Scene
}
from
"../../lib/api/ai"
;
import
type
{
Scene
}
from
"../../lib/api/ai"
;
import
{
ImagePreviewModal
}
from
"../components/ImagePreviewModal"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
type
SettingsTab
=
"characters"
|
"scenes"
|
"props"
;
...
@@ -37,6 +38,7 @@ export function SceneGeneration() {
...
@@ -37,6 +38,7 @@ export function SceneGeneration() {
const
[
imagePreview
,
setImagePreview
]
=
useState
<
string
|
null
>
(
null
);
const
[
imagePreview
,
setImagePreview
]
=
useState
<
string
|
null
>
(
null
);
const
[
pendingFile
,
setPendingFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
pendingFile
,
setPendingFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
previewImage
,
setPreviewImage
]
=
useState
<
{
src
:
string
;
title
:
string
}
|
null
>
(
null
);
const
fileInputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
fileInputRef
=
useRef
<
HTMLInputElement
>
(
null
);
const
settingsTabs
=
[
const
settingsTabs
=
[
...
@@ -249,7 +251,13 @@ export function SceneGeneration() {
...
@@ -249,7 +251,13 @@ export function SceneGeneration() {
{
/* Image */
}
{
/* Image */
}
<
div
className=
"aspect-video relative overflow-hidden bg-muted"
>
<
div
className=
"aspect-video relative overflow-hidden bg-muted"
>
{
scene
.
imageUrl
?
(
{
scene
.
imageUrl
?
(
<
button
type=
"button"
onClick=
{
()
=>
setPreviewImage
({
src
:
scene
.
imageUrl
!
,
title
:
scene
.
name
})
}
className=
"block h-full w-full cursor-zoom-in"
>
<
img
src=
{
scene
.
imageUrl
}
alt=
{
scene
.
name
}
className=
"w-full h-full object-cover"
/>
<
img
src=
{
scene
.
imageUrl
}
alt=
{
scene
.
name
}
className=
"w-full h-full object-cover"
/>
</
button
>
)
:
(
)
:
(
<
div
className=
"w-full h-full flex flex-col items-center justify-center gap-2"
>
<
div
className=
"w-full h-full flex flex-col items-center justify-center gap-2"
>
{
scene
.
status
===
"generating"
?
(
{
scene
.
status
===
"generating"
?
(
...
@@ -259,9 +267,32 @@ export function SceneGeneration() {
...
@@ -259,9 +267,32 @@ export function SceneGeneration() {
)
}
)
}
</
div
>
</
div
>
)
}
)
}
<
div
className=
"absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2"
>
<
div
className=
{
`pointer-events-none absolute inset-0 bg-black/40 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2 ${
scene.imageUrl ? "cursor-zoom-in" : ""
}`
}
onClick=
{
()
=>
{
if
(
scene
.
imageUrl
)
setPreviewImage
({
src
:
scene
.
imageUrl
,
title
:
scene
.
name
});
}
}
>
{
scene
.
imageUrl
&&
(
<
button
type=
"button"
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
();
setPreviewImage
({
src
:
scene
.
imageUrl
!
,
title
:
scene
.
name
});
}
}
className=
"p-2 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors"
title=
"查看大图"
>
<
ImageIcon
className=
"w-5 h-5 text-white"
/>
</
button
>
)
}
<
button
<
button
onClick=
{
()
=>
handleGenerateImage
(
scene
)
}
onClick=
{
(
event
)
=>
{
event
.
stopPropagation
();
handleGenerateImage
(
scene
);
}
}
disabled=
{
generatingImageId
===
scene
.
id
||
scene
.
status
===
"generating"
}
disabled=
{
generatingImageId
===
scene
.
id
||
scene
.
status
===
"generating"
}
className=
"p-2 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors disabled:opacity-50"
className=
"p-2 rounded-lg bg-white/20 backdrop-blur-sm hover:bg-white/30 transition-colors disabled:opacity-50"
title=
"AI重新生成"
title=
"AI重新生成"
...
@@ -458,6 +489,12 @@ export function SceneGeneration() {
...
@@ -458,6 +489,12 @@ export function SceneGeneration() {
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
}
<
ImagePreviewModal
src=
{
previewImage
?.
src
??
null
}
title=
{
previewImage
?.
title
}
alt=
{
previewImage
?.
title
}
onClose=
{
()
=>
setPreviewImage
(
null
)
}
/>
</
div
>
</
div
>
);
);
}
}
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