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
0aa3bd46
Commit
0aa3bd46
authored
Jun 05, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
角色设定分镜ui调整
parent
ea37c382
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
275 additions
and
135 deletions
+275
-135
CharacterGeneration.tsx
doc/html/src/app/pages/CharacterGeneration.tsx
+67
-5
SceneGeneration.tsx
doc/html/src/app/pages/SceneGeneration.tsx
+54
-5
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+154
-125
No files found.
doc/html/src/app/pages/CharacterGeneration.tsx
View file @
0aa3bd46
...
...
@@ -303,6 +303,39 @@ export function CharacterGeneration() {
});
};
const
handleGenerateFromModal
=
async
()
=>
{
if
(
!
editingChar
.
name
?.
trim
())
return
;
const
draft
:
Partial
<
Character
>
=
{
...
editingChar
,
status
:
editingChar
.
status
??
"draft"
,
imagePrompt
:
editingChar
.
imagePrompt
?.
trim
()
||
buildChineseCharacterPrompt
(
editingChar
),
};
setSaving
(
true
);
setGeneratingImageId
(
editingChar
.
id
?
String
(
editingChar
.
id
)
:
"draft"
);
try
{
const
saved
=
await
saveCharacter
.
mutateAsync
(
draft
);
const
generated
=
await
generateImage
.
mutateAsync
(
String
(
saved
.
id
));
revokeImagePreviews
(
imagePreviews
);
setEditingChar
({
...
generated
,
imagePrompt
:
generated
.
imagePrompt
??
draft
.
imagePrompt
,
});
setImagePreviews
({
front
:
getCharacterViewUrl
(
generated
,
"front"
),
side
:
getCharacterViewUrl
(
generated
,
"side"
),
back
:
getCharacterViewUrl
(
generated
,
"back"
),
});
setPendingFiles
(
emptyPendingFiles
());
fileInputRefs
.
current
.
front
&&
(
fileInputRefs
.
current
.
front
.
value
=
""
);
}
catch
(
error
)
{
alert
(
`三视图生成失败:
${(
error
as
Error
).
message
}
`
);
}
finally
{
setSaving
(
false
);
setGeneratingImageId
(
null
);
}
};
const
handleSaveState
=
async
()
=>
{
if
(
!
editingCharacterId
||
!
stateDraft
.
name
.
trim
())
return
;
setSavingState
(
true
);
...
...
@@ -679,14 +712,43 @@ export function CharacterGeneration() {
</
div
>
)
}
</
div
>
{
imagePreviews
.
front
&&
(
{
imagePreviews
.
front
?
(
<
div
className=
"mt-2 flex items-center gap-2"
>
<
button
type=
"button"
onClick=
{
()
=>
fileInputRefs
.
current
.
front
?.
click
()
}
className=
"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-xs text-foreground hover:bg-muted"
>
<
Upload
className=
"h-3.5 w-3.5"
/>
更换图片
</
button
>
<
button
type=
"button"
onClick=
{
handleGenerateFromModal
}
disabled=
{
!
editingChar
.
name
?.
trim
()
||
saving
||
uploadImage
.
isPending
||
generateImage
.
isPending
}
className=
"inline-flex items-center gap-1.5 rounded-md border border-primary/30 bg-primary/5 px-2.5 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
>
{
generatingImageId
===
(
editingChar
.
id
?
String
(
editingChar
.
id
)
:
"draft"
)
?
(
<
Loader2
className=
"h-3.5 w-3.5 animate-spin"
/>
)
:
(
<
Sparkles
className=
"h-3.5 w-3.5"
/>
)
}
AI重新生成
</
button
>
</
div
>
)
:
(
<
button
type=
"button"
onClick=
{
()
=>
fileInputRefs
.
current
.
front
?.
click
()
}
className=
"mt-2 inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-xs text-foreground hover:bg-muted"
onClick=
{
handleGenerateFromModal
}
disabled=
{
!
editingChar
.
name
?.
trim
()
||
saving
||
uploadImage
.
isPending
||
generateImage
.
isPending
}
className=
"mt-2 inline-flex items-center gap-1.5 rounded-md border border-primary/30 bg-primary/5 px-2.5 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
>
<
Upload
className=
"h-3.5 w-3.5"
/>
更换图片
{
generatingImageId
===
(
editingChar
.
id
?
String
(
editingChar
.
id
)
:
"draft"
)
?
(
<
Loader2
className=
"h-3.5 w-3.5 animate-spin"
/>
)
:
(
<
Sparkles
className=
"h-3.5 w-3.5"
/>
)
}
{
editingChar
.
id
?
"AI生成三视图"
:
"保存并AI生成"
}
</
button
>
)
}
<
input
...
...
doc/html/src/app/pages/SceneGeneration.tsx
View file @
0aa3bd46
...
...
@@ -116,6 +116,26 @@ export function SceneGeneration() {
});
};
const
handleGenerateFromModal
=
async
()
=>
{
if
(
!
editingScene
.
name
?.
trim
())
return
;
setSaving
(
true
);
setGeneratingImageId
(
editingScene
.
id
?
String
(
editingScene
.
id
)
:
"draft"
);
try
{
const
saved
=
await
saveScene
.
mutateAsync
({
...
editingScene
,
status
:
editingScene
.
status
??
"draft"
});
const
generated
=
await
generateImage
.
mutateAsync
(
String
(
saved
.
id
));
revokeObjectPreview
(
imagePreview
);
setEditingScene
({
...
generated
});
setImagePreview
(
generated
.
imageUrl
??
null
);
setPendingFile
(
null
);
if
(
fileInputRef
.
current
)
fileInputRef
.
current
.
value
=
""
;
}
catch
(
e
)
{
alert
(
"图片生成失败: "
+
(
e
as
Error
).
message
);
}
finally
{
setSaving
(
false
);
setGeneratingImageId
(
null
);
}
};
const
handleDelete
=
(
scene
:
Scene
)
=>
{
if
(
!
confirm
(
`确定要删除场景「
${
scene
.
name
}
」吗?`
))
return
;
deleteScene
.
mutate
(
scene
.
id
,
{
onError
:
(
e
)
=>
alert
(
"删除失败: "
+
(
e
as
Error
).
message
)
});
...
...
@@ -404,14 +424,43 @@ export function SceneGeneration() {
</
div
>
)
}
</
div
>
{
imagePreview
&&
(
{
imagePreview
?
(
<
div
className=
"mt-2 flex items-center gap-2"
>
<
button
type=
"button"
onClick=
{
()
=>
fileInputRef
.
current
?.
click
()
}
className=
"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-xs text-foreground hover:bg-muted"
>
<
Upload
className=
"h-3.5 w-3.5"
/>
更换图片
</
button
>
<
button
type=
"button"
onClick=
{
handleGenerateFromModal
}
disabled=
{
!
editingScene
.
name
?.
trim
()
||
saving
||
uploadImage
.
isPending
||
generateImage
.
isPending
}
className=
"inline-flex items-center gap-1.5 rounded-md border border-primary/30 bg-primary/5 px-2.5 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
>
{
generatingImageId
===
(
editingScene
.
id
?
String
(
editingScene
.
id
)
:
"draft"
)
?
(
<
Loader2
className=
"h-3.5 w-3.5 animate-spin"
/>
)
:
(
<
Sparkles
className=
"h-3.5 w-3.5"
/>
)
}
AI重新生成
</
button
>
</
div
>
)
:
(
<
button
type=
"button"
onClick=
{
()
=>
fileInputRef
.
current
?.
click
()
}
className=
"mt-2 inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1 text-xs text-foreground hover:bg-muted"
onClick=
{
handleGenerateFromModal
}
disabled=
{
!
editingScene
.
name
?.
trim
()
||
saving
||
uploadImage
.
isPending
||
generateImage
.
isPending
}
className=
"mt-2 inline-flex items-center gap-1.5 rounded-md border border-primary/30 bg-primary/5 px-2.5 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
>
<
Upload
className=
"h-3.5 w-3.5"
/>
更换图片
{
generatingImageId
===
(
editingScene
.
id
?
String
(
editingScene
.
id
)
:
"draft"
)
?
(
<
Loader2
className=
"h-3.5 w-3.5 animate-spin"
/>
)
:
(
<
Sparkles
className=
"h-3.5 w-3.5"
/>
)
}
{
editingScene
.
id
?
"AI生成场景图"
:
"保存并AI生成"
}
</
button
>
)
}
<
input
ref=
{
fileInputRef
}
type=
"file"
accept=
"image/*"
className=
"hidden"
onChange=
{
handleFileSelect
}
/>
...
...
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
0aa3bd46
...
...
@@ -49,6 +49,7 @@ const DEFAULT_VIDEO_MODEL = VIDEO_MODEL_OPTIONS[0].value;
const
MAX_CHARACTERS
=
5
;
const
MAX_PROPS
=
5
;
const
PROMPT_PREVIEW_LIMIT
=
80
;
type
VideoGenerationMode
=
"reference"
|
"frames"
;
// 分镜编辑器草稿存到 localStorage,按 (project, episode, storyboard) 分 key。
// 跨页面切换/刷新都不会丢;展开分镜时优先用草稿,没草稿才回退到上一次任务。
...
...
@@ -58,6 +59,7 @@ const sbDraftKey = (pid: string, eid: string, sbId: string) =>
`
${
SB_DRAFT_KEY_PREFIX
}
:
${
pid
}
:
${
eid
}
:
${
sbId
}
`
;
interface
SbDraft
{
generationMode
?:
VideoGenerationMode
;
shortDescription
:
string
;
characterKeys
:
string
[];
sceneKey
:
string
;
...
...
@@ -321,6 +323,7 @@ export function StoryboardWorkspace() {
const [lastFrameImageKey, setLastFrameImageKey] = useState<string>("");
const [lastFrameImageUrl, setLastFrameImageUrl] = useState<string>("");
const [styleKey] = useState<string>(""); // reserved; not exposed in this UI
const [generationMode, setGenerationMode] = useState<VideoGenerationMode>("reference");
const [shortDescription, setShortDescription] = useState<string>("");
const [freeformPrompt, setFreeformPrompt] = useState<string>("");
const [selectedDuration, setSelectedDuration] = useState(DEFAULT_VIDEO_DURATION);
...
...
@@ -392,6 +395,7 @@ export function StoryboardWorkspace() {
setFirstFrameImageUrl("");
setLastFrameImageKey("");
setLastFrameImageUrl("");
setGenerationMode("reference");
setShortDescription("");
setActiveVideoSbId(null);
lastSyncedExpandedIdRef.current = null;
...
...
@@ -429,6 +433,7 @@ export function StoryboardWorkspace() {
setFirstFrameImageUrl(draft.firstFrameImageUrl || expanded.firstFrameImageUrl || "");
setLastFrameImageKey(draft.lastFrameImageKey || expanded.lastFrameImageKey || "");
setLastFrameImageUrl(draft.lastFrameImageUrl || expanded.lastFrameImageUrl || "");
setGenerationMode(draft.generationMode ?? "reference");
setFreeformPrompt(draft.freeformPrompt);
setSelectedDuration(normalizeVideoDuration(draft.selectedDuration));
setSelectedRatio(draft.selectedRatio);
...
...
@@ -465,6 +470,7 @@ export function StoryboardWorkspace() {
setFirstFrameImageUrl(expanded.firstFrameImageUrl ?? "");
setLastFrameImageKey(expanded.lastFrameImageKey ?? "");
setLastFrameImageUrl(expanded.lastFrameImageUrl ?? "");
setGenerationMode("reference");
setFreeformPrompt(expanded.videoPrompt?.trim() || (lastTask?.userPrompt?.freeformPrompt ?? ""));
if (typeof lastTask?.videoDuration === "number") {
setSelectedDuration(normalizeVideoDuration(lastTask.videoDuration));
...
...
@@ -485,6 +491,7 @@ export function StoryboardWorkspace() {
return;
}
saveSbDraft(sbDraftKey(pid, eid, expandedId), {
generationMode,
shortDescription,
characterKeys,
sceneKey,
...
...
@@ -501,7 +508,7 @@ export function StoryboardWorkspace() {
});
}, [
expandedId, pid, eid,
shortDescription, characterKeys, sceneKey, propKeys,
generationMode,
shortDescription, characterKeys, sceneKey, propKeys,
firstFrameImageKey, firstFrameImageUrl, lastFrameImageKey, lastFrameImageUrl,
freeformPrompt,
selectedDuration, selectedRatio, selectedModel, audioOn,
...
...
@@ -635,11 +642,12 @@ export function StoryboardWorkspace() {
const promptHasContent = freeformPrompt.trim().length > 0;
const frameReferenceCount = (firstFrameImageKey ? 1 : 0) + (lastFrameImageKey ? 1 : 0);
// 至少一张参考图(角色/场景/道具/首尾帧任一)+ 提示词非空
const hasAnyReferenceImage = characterKeys.length > 0 || !!sceneKey || propKeys.length > 0 || frameReferenceCount > 0;
const canGenerate = !!expanded && hasAnyReferenceImage
&& promptHasContent && !generateStructured.isPending;
const referenceCount = characterKeys.length + (sceneKey ? 1 : 0) + propKeys.length + frameReferenceCount;
const referenceImageCount = characterKeys.length + (sceneKey ? 1 : 0) + propKeys.length;
const hasRequiredImages = generationMode === "frames"
? !!firstFrameImageKey && !!lastFrameImageKey
: referenceImageCount > 0;
const canGenerate = !!expanded && hasRequiredImages && promptHasContent && !generateStructured.isPending;
const referenceCount = generationMode === "frames" ? frameReferenceCount : referenceImageCount;
const handleGenerate = async () => {
if (!expanded || !canGenerate) return;
...
...
@@ -663,11 +671,11 @@ export function StoryboardWorkspace() {
await generateStructured.mutateAsync({
episodeId: expanded.episodeId,
storyboardId: expanded.id,
characterImageKeys:
characterKeys
,
sceneImageKey:
sceneKey
,
propImageKeys: propKeys.length > 0 ? propKeys : null,
firstFrameImageKey:
firstFrameImageKey ||
null,
lastFrameImageKey:
lastFrameImageKey ||
null,
characterImageKeys:
generationMode === "reference" ? characterKeys : []
,
sceneImageKey:
generationMode === "reference" ? sceneKey : ""
,
propImageKeys:
generationMode === "reference" &&
propKeys.length > 0 ? propKeys : null,
firstFrameImageKey:
generationMode === "frames" ? firstFrameImageKey :
null,
lastFrameImageKey:
generationMode === "frames" ? lastFrameImageKey :
null,
styleImageKey: styleKey || null,
freeformPrompt: freeformPrompt.trim(),
duration: selectedDuration,
...
...
@@ -716,6 +724,10 @@ export function StoryboardWorkspace() {
const currentEpisodeSummary = currentEpisode?.summary
|| "当前项目还没有可用分集。生成分集后,可以在这里继续生成分镜。";
const selectedScene = sceneKey ? sceneByKey.get(sceneKey) ?? null : null;
const generationModeLabel = generationMode === "frames" ? "首尾帧生成" : "参考图生成";
const generateRequirementText = generationMode === "frames"
? "需要:上传首帧和尾帧,并填写提示词"
: "需要:至少 1 张参考图(角色/场景/道具)+ 提示词";
return (
<div className="h-full flex overflow-hidden bg-[#f7f8fb] text-[#111827]">
...
...
@@ -814,35 +826,12 @@ export function StoryboardWorkspace() {
{/* ─── Column 2: Storyboard accordion list (top: prompt summary, then cards) ─── */}
<div className="flex flex-col flex-shrink-0 overflow-hidden rounded-lg border border-[#eef1f6] bg-[#fbfcff]" style={{ width: 394 }}>
{/* Top: 分镜表 / 备注描述 */}
<div className="px-3 py-3 border-b border-[#edf0f5] bg-white flex-shrink-0 relative">
<div className="flex items-center justify-between mb-1.5">
<span className="text-sm font-semibold text-[#111827]">
{expanded ? `
分镜信息
$
{
String
(
expanded
.
sequenceNum
).
padStart
(
3
,
"0"
)}
` : "分镜信息"}
</span>
{(promptPreviewText || expanded) && (
<button
onClick={() => setShowPromptDetails(true)}
className="text-xs text-[#7657ff] hover:text-[#5e42df] flex items-center gap-0.5 font-medium transition-colors"
title="查看完整内容"
>
<Info className="w-3 h-3" />
详情
</button>
)}
</div>
<div className="mb-2 text-xs font-medium text-[#344054]">分镜描述</div>
<p className={`
min
-
h
-
[
96
px
]
rounded
-
md
border
border
-
[
#
d9dee8
]
bg
-
white
px
-
3
py
-
2
text
-
xs
leading
-
relaxed
$
{
shortDescription
?
"text-[#111827]"
:
"text-[#98a2b3]"
}
`}>
{shortDescription || "暂无分镜描述"}
</p>
</div>
{/* Action row: new + AI generate */}
<div className="h-1
0 px-3 border-b border-[#edf0f5] flex items-center gap-2
flex-shrink-0 bg-white">
<div className="h-1
1 px-3 border-b border-[#edf0f5] flex items-center gap-2.5
flex-shrink-0 bg-white">
<button
onClick={handleCreate}
disabled={createSb.isPending || !eid}
className="flex-1 flex items-center justify-center gap-1.5 p
y-1.5
rounded-md border border-[#dde3ee] bg-white text-xs font-medium text-[#111827] transition hover:border-[#b9c3d6] disabled:opacity-50"
className="flex-1 flex items-center justify-center gap-1.5 p
x-4 py-2
rounded-md border border-[#dde3ee] bg-white text-xs font-medium text-[#111827] transition hover:border-[#b9c3d6] disabled:opacity-50"
>
{createSb.isPending ? <Loader2 className="w-3 h-3 animate-spin" /> : <Plus className="w-3 h-3" />}
新建分镜
...
...
@@ -851,7 +840,7 @@ export function StoryboardWorkspace() {
onClick={handleGenerateAll}
disabled={generateSbs.isPending || !eid}
title="AI批量生成分镜"
className={`
p
-
2
rounded
-
md
text
-
[
#
475467
]
hover
:
bg
-
[
#
f6f9ff
]
hover
:
border
-
[
#
7657
ff
]
/
30
hover
:
text
-
[
#
7657
ff
]
transition
-
colors
disabled
:
opacity
-
50
$
{
CONTROL_SURFACE
}
`}
className={`
flex
h
-
8
w
-
11
items
-
center
justify
-
center
rounded
-
md
text
-
[
#
475467
]
hover
:
bg
-
[
#
f6f9ff
]
hover
:
border
-
[
#
7657
ff
]
/
30
hover
:
text
-
[
#
7657
ff
]
transition
-
colors
disabled
:
opacity
-
50
$
{
CONTROL_SURFACE
}
`}
>
{generateSbs.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
</button>
...
...
@@ -900,6 +889,8 @@ export function StoryboardWorkspace() {
onSelectScene={setSceneKey}
propKeys={expandedId === sb.id ? propKeys : []}
onTogglePropKey={togglePropKey}
generationMode={expandedId === sb.id ? generationMode : "reference"}
onGenerationModeChange={setGenerationMode}
firstFrameImageKey={expandedId === sb.id ? firstFrameImageKey : sb.firstFrameImageKey ?? ""}
firstFrameImageUrl={expandedId === sb.id ? firstFrameImageUrl : sb.firstFrameImageUrl ?? ""}
lastFrameImageKey={expandedId === sb.id ? lastFrameImageKey : sb.lastFrameImageKey ?? ""}
...
...
@@ -935,12 +926,14 @@ export function StoryboardWorkspace() {
</div>
) : (
<>
<div className="flex items-start gap-1.5
px-3 py-2.5 rounded-xl bg-white border border-[#1c64ff]/15 shadow-[0_4px_14px_rgba(28,100,255,0.06)]
">
<
Plus
className="w-3.5 h-3.5 text-[#98a2b3] mt-0.5 flex-shrink-0" />
<div className="flex items-start gap-1.5
rounded-md bg-[#f8fafc] px-2.5 py-1.5
">
<
Info
className="w-3.5 h-3.5 text-[#98a2b3] mt-0.5 flex-shrink-0" />
<p className="text-[11px] text-[#98a2b3] leading-relaxed">
{referenceCount > 0
? `
已预选
$
{
referenceCount
}
个参考素材,可继续手工调整
`
: "可手工选择参考素材;AI 批量生成会自动预选可用角色/场景"}
{generationMode === "frames"
? `
已上传
$
{
frameReferenceCount
}
/2 张关键帧,将按首帧、尾帧顺序参与生成
`
:
referenceCount
>
0
?
`已预选
${
referenceCount
}
个参考素材,可继续手工调整`
:
"可手工选择参考素材;AI 批量生成会自动预选可用角色/场景"
}
<
/
p
>
</
div
>
<
textarea
...
...
@@ -1012,7 +1005,7 @@ export function StoryboardWorkspace() {
<
button
onClick=
{
handleGenerate
}
disabled=
{
!
canGenerate
||
generatingVideoId
===
expanded
?.
id
}
title={!canGenerate ?
"需要:至少 1 张参考图(角色/场景/道具/首尾帧任一)+ 提示词"
: ""}
title=
{
!
canGenerate
?
generateRequirementText
:
""
}
className=
"w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-md bg-[#7657ff] text-white text-xs font-semibold tracking-wide shadow-[0_9px_20px_rgba(118,87,255,0.24)] transition-all duration-150 hover:bg-[#6747f4] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none whitespace-nowrap"
>
{
generatingVideoId
===
expanded
?.
id
?
(
...
...
@@ -1020,7 +1013,7 @@ export function StoryboardWorkspace() {
)
:
(
<
Play
className=
"w-3.5 h-3.5"
/>
)
}
生成
视频{videoCredits != null ? `
(
$
{
videoCredits
.
toLocaleString
()}
积分
)
` : ""}
{
generationModeLabel
}
视频
{
videoCredits
!=
null
?
` (${videoCredits.toLocaleString()}积分)`
:
""
}
</
button
>
</
div
>
</
div
>
...
...
@@ -1275,6 +1268,8 @@ interface StoryboardCardProps {
onSelectScene
:
(
key
:
string
)
=>
void
;
propKeys
:
string
[];
onTogglePropKey
:
(
key
:
string
)
=>
void
;
generationMode
:
VideoGenerationMode
;
onGenerationModeChange
:
(
mode
:
VideoGenerationMode
)
=>
void
;
firstFrameImageKey
:
string
;
firstFrameImageUrl
:
string
;
lastFrameImageKey
:
string
;
...
...
@@ -1289,6 +1284,10 @@ interface StoryboardCardProps {
function StoryboardCard(props: StoryboardCardProps)
{
const
{
sb
,
isExpanded
,
dotClass
,
onToggle
,
onDelete
}
=
props
;
const
[
descCollapsed
,
setDescCollapsed
]
=
useState
(
false
);
const
modeOptions
=
[
{
value
:
"reference"
,
label
:
"参考图生成"
,
icon
:
Sparkles
},
{
value
:
"frames"
,
label
:
"首尾帧生成"
,
icon
:
ImageIcon
},
]
as
const
;
return
(
<
div
className=
{
`my-2 rounded-lg border transition-all duration-200 ${
...
...
@@ -1308,6 +1307,30 @@ function StoryboardCard(props: StoryboardCardProps) {
{
isExpanded
&&
(
<
div
className=
"px-3 pb-3 space-y-3 border-t border-[#f1f3f7]"
>
<
div
className=
"pt-3"
>
<
div
className=
"grid grid-cols-2 gap-1 rounded-lg border border-[#dfe3ee] bg-[#f8fafc] p-1"
>
{
modeOptions
.
map
((
option
)
=>
{
const
Icon
=
option
.
icon
;
const
active
=
props
.
generationMode
===
option
.
value
;
return
(
<
button
key=
{
option
.
value
}
type=
"button"
onClick=
{
()
=>
props
.
onGenerationModeChange
(
option
.
value
)
}
className=
{
`flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-[11px] font-medium transition-all ${
active
? "bg-white text-[#7657ff] shadow-sm ring-1 ring-[#7657ff]/20"
: "text-muted-foreground hover:bg-white/70 hover:text-[#475467]"
}`
}
>
<
Icon
className=
"h-3.5 w-3.5"
/>
{
option
.
label
}
</
button
>
);
})
}
</
div
>
</
div
>
{
/* 分镜描述 */
}
<
div
>
<
button
...
...
@@ -1330,41 +1353,44 @@ function StoryboardCard(props: StoryboardCardProps) {
)
}
</
div
>
{
/* 出镜角色 */
}
<
CharacterRowList
characterKeys=
{
props
.
characterKeys
}
keyToCharacterRef=
{
props
.
keyToCharacterRef
}
characterRefOptions=
{
props
.
characterRefOptions
}
onRemove=
{
props
.
onRemoveCharacter
}
onSwap=
{
props
.
onSwapCharacter
}
onAdd=
{
props
.
onAddCharacter
}
/>
{
/* 分镜场景 */
}
<
SceneSection
scenes=
{
props
.
scenes
}
selectedKey=
{
props
.
sceneKey
}
onSelect=
{
props
.
onSelectScene
}
/>
{
/* 场景道具 */
}
<
PropGrid
scenes=
{
props
.
scenes
}
sceneKey=
{
props
.
sceneKey
}
selectedKeys=
{
props
.
propKeys
}
onToggle=
{
props
.
onTogglePropKey
}
/>
{
/* 首尾帧图 */
}
<
FrameReferenceSection
firstFrameImageKey=
{
props
.
firstFrameImageKey
}
firstFrameImageUrl=
{
props
.
firstFrameImageUrl
}
lastFrameImageKey=
{
props
.
lastFrameImageKey
}
lastFrameImageUrl=
{
props
.
lastFrameImageUrl
}
onUpload=
{
props
.
onUploadFrame
}
onClear=
{
props
.
onClearFrame
}
uploading=
{
props
.
frameUploading
}
/>
{
props
.
generationMode
===
"reference"
?
(
<>
{
/* 出镜角色 */
}
<
CharacterRowList
characterKeys=
{
props
.
characterKeys
}
keyToCharacterRef=
{
props
.
keyToCharacterRef
}
characterRefOptions=
{
props
.
characterRefOptions
}
onRemove=
{
props
.
onRemoveCharacter
}
onSwap=
{
props
.
onSwapCharacter
}
onAdd=
{
props
.
onAddCharacter
}
/>
{
/* 分镜场景 */
}
<
SceneSection
scenes=
{
props
.
scenes
}
selectedKey=
{
props
.
sceneKey
}
onSelect=
{
props
.
onSelectScene
}
/>
{
/* 场景道具 */
}
<
PropGrid
scenes=
{
props
.
scenes
}
sceneKey=
{
props
.
sceneKey
}
selectedKeys=
{
props
.
propKeys
}
onToggle=
{
props
.
onTogglePropKey
}
/>
</>
)
:
(
<
FrameReferenceSection
firstFrameImageKey=
{
props
.
firstFrameImageKey
}
firstFrameImageUrl=
{
props
.
firstFrameImageUrl
}
lastFrameImageKey=
{
props
.
lastFrameImageKey
}
lastFrameImageUrl=
{
props
.
lastFrameImageUrl
}
onUpload=
{
props
.
onUploadFrame
}
onClear=
{
props
.
onClearFrame
}
uploading=
{
props
.
frameUploading
}
/>
)
}
{
/* 删除按钮 */
}
<
div
className=
"pt-1 flex justify-end"
>
...
...
@@ -1564,23 +1590,20 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
)
}
</
button
>
{
/* Bottom row: s
cene name + role label
+ actions menu */
}
{
/* Bottom row: s
elected scene text
+ actions menu */
}
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex-1 min-w-0 px-2.5 py-1.5 rounded-md border border-[#dfe3ee] bg-white text-xs text-[#111827] truncate"
>
{
selected
?.
name
||
<
span
className=
"text-muted-foreground"
>
请选择场景
</
span
>
}
</
div
>
<
div
className=
"px-2.5 py-1.5 rounded-md border border-[#dfe3ee] bg-white/80 text-xs text-muted-foreground flex-shrink-0"
>
主场景图
<
div
className=
"flex-1 min-w-0 px-1 py-1.5 text-xs text-[#111827] truncate"
>
{
selected
?.
name
||
<
span
className=
"text-muted-foreground"
>
未选择场景
</
span
>
}
</
div
>
<
div
className=
"relative flex-shrink-0"
>
<
button
onClick=
{
()
=>
setMenuOpen
((
v
)
=>
!
v
)
}
disabled=
{
!
selected
}
disabled=
{
usable
.
length
===
0
}
className=
"p-1.5 rounded-md border border-[#dfe3ee] bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#7657ff] disabled:opacity-40 transition-colors"
>
<
MoreHorizontal
className=
"w-3.5 h-3.5"
/>
</
button
>
{
menuOpen
&&
selected
&&
(
{
menuOpen
&&
usable
.
length
>
0
&&
(
<>
<
button
className=
"fixed inset-0 z-10 cursor-default"
...
...
@@ -1591,14 +1614,16 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
onClick=
{
()
=>
{
setShowPicker
(
true
);
setMenuOpen
(
false
);
}
}
className=
"w-full px-3 py-1.5 text-left text-xs text-foreground hover:bg-[#f6f9ff]"
>
更换场景
</
button
>
<
button
onClick=
{
()
=>
{
onSelect
(
""
);
setMenuOpen
(
false
);
}
}
className=
"w-full px-3 py-1.5 text-left text-xs text-destructive hover:bg-red-50"
>
清除选择
场景图
</
button
>
{
selected
&&
(
<
button
onClick=
{
()
=>
{
onSelect
(
""
);
setMenuOpen
(
false
);
}
}
className=
"w-full px-3 py-1.5 text-left text-xs text-destructive hover:bg-red-50"
>
清除选择
</
button
>
)
}
</
div
>
</>
)
}
...
...
@@ -1610,38 +1635,42 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
{
usable
.
length
===
0
?
(
<
p
className=
"text-[11px] text-muted-foreground italic text-center py-2"
>
先到「场景」页创建场景
</
p
>
)
:
(
<
div
className=
"grid grid-cols-3 gap-2"
>
{
usable
.
map
((
s
)
=>
{
const
active
=
selectedKey
===
s
.
imageTosKey
;
return
(
<
button
key=
{
s
.
id
}
onClick=
{
()
=>
{
onSelect
(
active
?
""
:
s
.
imageTosKey
!
);
if
(
!
active
)
setShowPicker
(
false
);
}
}
title=
{
s
.
name
}
className=
{
`relative aspect-square rounded-md overflow-hidden border-2 ${
active ? "border-[#1c64ff] ring-2 ring-[#1c64ff]/25" : "border-slate-200 hover:border-[#1c64ff]/40"
}`
}
>
{
s
.
imageUrl
?
(
<
img
src=
{
s
.
imageUrl
}
alt=
{
s
.
name
}
className=
"w-full h-full object-cover"
/>
)
:
(
<
div
className=
"w-full h-full flex items-center justify-center text-[9px] text-muted-foreground"
>
无图
</
div
>
)
}
<
div
className=
"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent px-1 pt-3 pb-0.5"
>
<
span
className=
"text-[10px] text-white truncate block"
>
{
s
.
name
}
</
span
>
</
div
>
{
active
&&
(
<
div
className=
"absolute top-1 right-1 w-4 h-4 rounded-full bg-[#1c64ff] flex items-center justify-center shadow-sm"
>
<
Check
className=
"w-2.5 h-2.5 text-white"
/>
<>
<
div
className=
"mb-2 flex items-center justify-between"
>
<
span
className=
"text-[11px] font-medium text-[#344054]"
>
选择一个场景图
</
span
>
<
span
className=
"text-[10px] text-muted-foreground"
>
单选
</
span
>
</
div
>
<
div
className=
"grid grid-cols-3 gap-2"
>
{
usable
.
map
((
s
)
=>
{
const
active
=
selectedKey
===
s
.
imageTosKey
;
return
(
<
button
key=
{
s
.
id
}
onClick=
{
()
=>
{
onSelect
(
s
.
imageTosKey
!
);
setShowPicker
(
false
);
}
}
title=
{
s
.
name
}
className=
{
`relative aspect-square rounded-md overflow-hidden border-2 text-left transition ${
active ? "border-[#1c64ff] ring-2 ring-[#1c64ff]/25" : "border-slate-200 hover:border-[#1c64ff]/40"
}`
}
>
{
s
.
imageUrl
?
(
<
img
src=
{
s
.
imageUrl
}
alt=
{
s
.
name
}
className=
"w-full h-full object-cover"
/>
)
:
(
<
div
className=
"w-full h-full flex items-center justify-center text-[9px] text-muted-foreground"
>
无图
</
div
>
)
}
<
div
className=
"absolute left-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-white/90 shadow-sm ring-1 ring-black/10"
>
<
span
className=
{
`h-2.5 w-2.5 rounded-full ${active ? "bg-[#1c64ff]" : "bg-transparent ring-1 ring-slate-400"}`
}
/>
</
div
>
)
}
</
button
>
);
})
}
</
div
>
<
div
className=
"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent px-1 pt-3 pb-0.5"
>
<
span
className=
"text-[10px] text-white truncate block"
>
{
s
.
name
}
</
span
>
</
div
>
</
button
>
);
})
}
</
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