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
74404e9d
Commit
74404e9d
authored
May 25, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
客户端页面优化figma
parent
dcfda87a
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
815 additions
and
269 deletions
+815
-269
Layout.tsx
doc/html/src/app/components/Layout.tsx
+54
-19
CharacterGeneration.tsx
doc/html/src/app/pages/CharacterGeneration.tsx
+109
-54
Dashboard.tsx
doc/html/src/app/pages/Dashboard.tsx
+38
-38
NewProject.tsx
doc/html/src/app/pages/NewProject.tsx
+40
-40
ProjectDetail.tsx
doc/html/src/app/pages/ProjectDetail.tsx
+185
-0
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+176
-94
theme.css
doc/html/src/styles/theme.css
+47
-24
2026-05-25-doc-html-figma-handoff.md
docs/superpowers/specs/2026-05-25-doc-html-figma-handoff.md
+166
-0
No files found.
doc/html/src/app/components/Layout.tsx
View file @
74404e9d
...
...
@@ -120,7 +120,7 @@ export function Layout() {
<
div
className=
"flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-[#0073ff] to-[#7b2cff]"
>
<
Film
className=
"h-4 w-4 text-white"
/>
</
div
>
<
span
className=
"text-base font-semibold tracking-normal text-[#
111827]"
>
Dramix
</
span
>
<
span
className=
"text-base font-semibold tracking-normal text-[#
7a5cf6]"
>
Dramix.ai
</
span
>
</
Link
>
</
div
>
...
...
@@ -309,31 +309,66 @@ export function Layout() {
<
section
className=
"relative flex min-w-0 flex-1 flex-col overflow-hidden"
>
{
isProjectRoute
&&
(
<
div
className=
"flex h-12 items-center gap-5 border-b border-[#dfe3ee] bg-white px-6"
>
<
div
className=
"flex min-w-0 items-center gap-2 border-r border-[#dfe3ee] pr-5"
>
<
Film
className=
"h-4 w-4 flex-shrink-0 text-[#1664ff]"
/>
<
span
className=
"max-w-[240px] truncate text-sm font-medium text-[#111827]"
>
{
currentProject
?.
name
??
projectId
}
</
span
>
<
div
className=
"flex h-16 items-center border-b border-[#e5e8f0] bg-[#fbfcff] px-5"
>
<
div
className=
"flex min-w-[310px] items-center gap-3"
>
<
button
type=
"button"
onClick=
{
()
=>
navigate
(
"/"
)
}
className=
"flex h-7 w-7 items-center justify-center rounded-md text-[#111827] hover:bg-[#edf1f8]"
aria
-
label=
"返回项目列表"
>
<
ChevronDown
className=
"h-4 w-4 rotate-90"
/>
</
button
>
<
div
className=
"min-w-0"
>
<
div
className=
"flex items-center gap-1.5"
>
<
span
className=
"max-w-[270px] truncate text-sm font-semibold text-[#111827]"
>
{
currentProject
?.
name
??
projectId
}
</
span
>
<
button
className=
"text-[#98a2b3] hover:text-[#6f55f2]"
aria
-
label=
"编辑项目名"
>
<
FileText
className=
"h-3.5 w-3.5"
/>
</
button
>
</
div
>
<
div
className=
"mt-1 flex items-center gap-3 text-[11px] text-[#667085]"
>
<
span
>
9:16
</
span
>
<
span
>
720
</
span
>
<
span
>
写实电影感
</
span
>
</
div
>
</
div
>
</
div
>
<
div
className=
"flex min-w-0 items-center gap-1.5 overflow-x-auto"
>
{
projectSections
.
map
((
section
)
=>
{
<
div
className=
"mx-auto hidden items-center gap-5 xl:flex"
>
{
projectSections
.
filter
((
section
)
=>
section
.
id
!==
"agent"
).
map
((
section
,
index
,
arr
)
=>
{
const
Icon
=
section
.
icon
;
const
active
=
getActiveSection
()
===
section
.
id
;
return
(
<
button
key=
{
section
.
id
}
onClick=
{
()
=>
navigate
(
section
.
path
)
}
className=
{
`flex h-8 flex-shrink-0 items-center gap-2 rounded-md px-3 text-sm transition ${
active ? "bg-[#eef3ff] text-[#1664ff]" : "text-[#475467] hover:bg-[#f2f4f8]"
}`
}
>
<
Icon
className=
"h-4 w-4"
/>
{
section
.
label
}
</
button
>
<
div
key=
{
section
.
id
}
className=
"flex items-center gap-5"
>
<
button
onClick=
{
()
=>
navigate
(
section
.
path
)
}
className=
{
`flex h-12 min-w-[96px] flex-col items-center justify-center gap-1 rounded-full border px-6 text-xs transition ${
active
? "border-[#7657ff] bg-white text-[#7657ff] shadow-[0_6px_18px_rgba(118,87,255,0.18)]"
: "border-[#e2e6ef] bg-white text-[#111827] hover:border-[#b8c1d4]"
}`
}
>
<
Icon
className=
"h-4 w-4"
/>
{
section
.
label
}
</
button
>
{
index
<
arr
.
length
-
1
&&
<
span
className=
"h-px w-8 bg-[#d9dee9]"
/>
}
</
div
>
);
})
}
</
div
>
<
div
className=
"ml-auto flex min-w-[260px] items-center justify-end gap-3"
>
<
button
className=
"text-xs text-[#667085] hover:text-[#6f55f2]"
>
优化建议
</
button
>
<
button
onClick=
{
()
=>
navigate
(
"/usage"
)
}
className=
"flex h-8 items-center gap-1.5 rounded-md border border-[#d9d2ff] bg-[#f6f2ff] px-3 text-xs font-semibold text-[#6f55f2]"
>
<
Sparkles
className=
"h-3.5 w-3.5"
/>
{
Number
(
balance
?.
balance
??
7492538
).
toLocaleString
()
}
</
button
>
</
div
>
</
div
>
)
}
...
...
doc/html/src/app/pages/CharacterGeneration.tsx
View file @
74404e9d
...
...
@@ -43,6 +43,14 @@ const ROLE_TYPES = [
{
value
:
"supporting"
,
label
:
"配角"
},
];
const
ROLE_TYPE_LABELS
:
Record
<
string
,
string
>
=
{
female_lead
:
"女主角"
,
male_lead
:
"男主角"
,
antagonist
:
"反派"
,
villain
:
"反派"
,
supporting
:
"配角"
,
};
const
CHARACTER_VIEWS
:
Array
<
{
type
:
CharacterViewType
;
label
:
string
;
shortLabel
:
string
}
>
=
[
{
type
:
"front"
,
label
:
"正面"
,
shortLabel
:
"正"
},
{
type
:
"side"
,
label
:
"侧面"
,
shortLabel
:
"侧"
},
...
...
@@ -57,6 +65,7 @@ const emptyForm = (): Partial<Character> => ({
personality
:
""
,
costume
:
""
,
visualHint
:
""
,
imagePrompt
:
""
,
});
const
emptyImageState
=
():
ImageState
=>
({
...
...
@@ -98,6 +107,35 @@ function hasCharacterImage(character: Partial<Character>) {
return
CHARACTER_VIEWS
.
every
((
view
)
=>
!!
getCharacterViewUrl
(
character
,
view
.
type
));
}
function
isMostlyEnglishPrompt
(
prompt
:
string
)
{
if
(
!
prompt
.
trim
())
return
false
;
const
latinChars
=
prompt
.
match
(
/
[
A-Za-z
]
/g
)?.
length
??
0
;
const
chineseChars
=
prompt
.
match
(
/
[\u
4e00-
\u
9fff
]
/g
)?.
length
??
0
;
return
latinChars
>
chineseChars
*
2
;
}
function
buildChineseCharacterPrompt
(
character
:
Partial
<
Character
>
)
{
const
name
=
character
.
name
?.
trim
()
||
"该角色"
;
const
role
=
character
.
roleType
?
ROLE_TYPE_LABELS
[
character
.
roleType
]
??
character
.
roleType
:
""
;
const
gender
=
character
.
gender
===
"male"
?
"男性"
:
character
.
gender
===
"female"
?
"女性"
:
character
.
gender
;
const
details
=
[
`角色:
${
name
}
`
,
role
?
`定位:
${
role
}
`
:
""
,
gender
?
`性别:
${
gender
}
`
:
""
,
character
.
age
?
`年龄:
${
character
.
age
}
`
:
""
,
character
.
personality
?
`性格气质:
${
character
.
personality
}
`
:
""
,
character
.
visualHint
?
`外貌特征:
${
character
.
visualHint
}
`
:
""
,
character
.
costume
?
`服装造型:
${
character
.
costume
}
`
:
""
,
].
filter
(
Boolean
);
return
[
"生成一张单张角色参考图,用于 AI 漫剧角色一致性。"
,
details
.
join
(
";"
),
"画面需要同时包含清晰大头照,以及正面、侧面、背面全身三视图;五官、发型、服装、体型在所有视角中保持一致。"
,
"全身视图自然站姿,双臂自然下垂,脚部完整入镜,背景干净,光线均匀,细节清晰。"
,
].
join
(
"
\
n"
);
}
export
function
CharacterGeneration
()
{
const
navigate
=
useNavigate
();
const
{
projectId
}
=
useParams
();
...
...
@@ -139,13 +177,7 @@ export function CharacterGeneration() {
{
id
:
"props"
as
SettingsTab
,
label
:
"道具"
,
icon
:
Box
,
path
:
`/project/
${
projectId
}
/props`
},
];
const
roleTypeLabel
:
Record
<
string
,
string
>
=
{
female_lead
:
"女主角"
,
male_lead
:
"男主角"
,
antagonist
:
"反派"
,
villain
:
"反派"
,
supporting
:
"配角"
,
};
const
roleTypeLabel
=
ROLE_TYPE_LABELS
;
const
generatedCount
=
characters
.
filter
((
character
)
=>
hasCharacterImage
(
character
)).
length
;
const
generatingCount
=
characters
.
filter
((
character
)
=>
character
.
status
===
"generating"
).
length
;
...
...
@@ -183,7 +215,13 @@ export function CharacterGeneration() {
};
const
openEdit
=
(
character
:
Character
)
=>
{
setEditingChar
({
...
character
});
const
prompt
=
character
.
imagePrompt
?.
trim
()
??
""
;
setEditingChar
({
...
character
,
imagePrompt
:
!
prompt
||
isMostlyEnglishPrompt
(
prompt
)
?
buildChineseCharacterPrompt
(
character
)
:
prompt
,
});
setImagePreviews
({
front
:
getCharacterViewUrl
(
character
,
"front"
),
side
:
getCharacterViewUrl
(
character
,
"side"
),
...
...
@@ -219,10 +257,9 @@ export function CharacterGeneration() {
setSaving
(
true
);
try
{
const
saved
=
await
saveCharacter
.
mutateAsync
({
...
editingChar
,
status
:
editingChar
.
status
??
"draft"
});
for
(
const
view
of
CHARACTER_VIEWS
)
{
const
pendingFile
=
pendingFiles
[
view
.
type
];
if
(
!
pendingFile
)
continue
;
await
uploadImage
.
mutateAsync
({
id
:
String
(
saved
.
id
),
file
:
pendingFile
,
viewType
:
view
.
type
});
const
pendingFile
=
pendingFiles
.
front
;
if
(
pendingFile
)
{
await
uploadImage
.
mutateAsync
({
id
:
String
(
saved
.
id
),
file
:
pendingFile
,
viewType
:
"front"
});
}
setShowModal
(
false
);
}
catch
(
error
)
{
...
...
@@ -555,11 +592,11 @@ export function CharacterGeneration() {
{
showModal
&&
(
<
div
className=
"fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-6"
>
<
div
className=
"bg-card rounded-2xl border border-border w-full max-w-
4
xl max-h-[90vh] flex flex-col"
>
<
div
className=
"bg-card rounded-2xl border border-border w-full max-w-
3
xl max-h-[90vh] flex flex-col"
>
<
div
className=
"flex items-center justify-between p-6 border-b border-border flex-shrink-0"
>
<
div
>
<
h2
className=
"text-lg font-semibold text-foreground"
>
{
editingChar
.
id
?
"编辑角色"
:
"手动添加角色"
}
</
h2
>
<
p
className=
"text-sm text-muted-foreground mt-1"
>
支持上传正面、侧面、背面三张标准视角图
。
</
p
>
<
p
className=
"text-sm text-muted-foreground mt-1"
>
角色图已合并为单张图片,只需要维护图片和生成提示词
。
</
p
>
</
div
>
<
button
onClick=
{
()
=>
setShowModal
(
false
)
}
className=
"p-2 hover:bg-muted rounded-lg transition-colors"
>
<
X
className=
"w-5 h-5 text-muted-foreground"
/>
...
...
@@ -567,48 +604,66 @@ export function CharacterGeneration() {
</
div
>
<
div
className=
"overflow-auto p-6 space-y-5 flex-1"
>
<
div
>
<
label
className=
"block text-sm font-medium text-foreground mb-3"
>
角色三视图上传(可选)
</
label
>
<
div
className=
"grid grid-cols-3 gap-4"
>
{
CHARACTER_VIEWS
.
map
((
view
)
=>
(
<
div
key=
{
view
.
type
}
className=
"rounded-xl border border-border bg-muted/20 p-3"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
span
className=
"text-sm font-medium text-foreground"
>
{
view
.
label
}
</
span
>
<
span
className=
"text-[11px] text-muted-foreground"
>
{
view
.
shortLabel
}
视图
</
span
>
</
div
>
<
div
className=
"aspect-[3/4] rounded-lg border-2 border-dashed border-border bg-background flex items-center justify-center cursor-pointer hover:border-primary transition-colors overflow-hidden"
onClick=
{
()
=>
fileInputRefs
.
current
[
view
.
type
]?.
click
()
}
>
{
imagePreviews
[
view
.
type
]
?
(
<
img
src=
{
imagePreviews
[
view
.
type
]
??
""
}
alt=
{
`${view.label}预览`
}
className=
"w-full h-full object-cover"
/>
)
:
(
<
div
className=
"text-center px-4"
>
<
Upload
className=
"w-5 h-5 text-muted-foreground mx-auto mb-2"
/>
<
span
className=
"text-xs text-muted-foreground"
>
点击上传
{
view
.
label
}
</
span
>
</
div
>
)
}
</
div
>
<
div
className=
"mt-2 flex items-center justify-between text-xs"
>
<
span
className=
"text-muted-foreground"
>
建议全身标准视角
</
span
>
{
imagePreviews
[
view
.
type
]
&&
(
<
button
onClick=
{
()
=>
clearImage
(
view
.
type
)
}
className=
"text-destructive hover:underline"
>
移除
</
button
>
)
}
</
div
>
<
input
ref=
{
(
node
)
=>
{
fileInputRefs
.
current
[
view
.
type
]
=
node
;
}
}
type=
"file"
accept=
"image/*"
className=
"hidden"
onChange=
{
(
e
)
=>
handleFileSelect
(
view
.
type
,
e
)
}
/>
<
div
className=
"rounded-xl border border-border bg-muted/20 p-4"
>
<
div
className=
"flex items-center justify-between mb-3"
>
<
div
>
<
label
className=
"block text-sm font-medium text-foreground"
>
角色图片(可选)
</
label
>
<
p
className=
"text-xs text-muted-foreground mt-1"
>
上传大头照与三视图合并后的单张角色参考图。
</
p
>
</
div
>
{
imagePreviews
.
front
&&
(
<
button
onClick=
{
()
=>
clearImage
(
"front"
)
}
className=
"text-xs text-destructive hover:underline"
>
移除
</
button
>
)
}
</
div
>
<
div
className=
"aspect-[16/9] rounded-lg border-2 border-dashed border-border bg-background flex items-center justify-center cursor-pointer hover:border-primary transition-colors overflow-hidden"
onClick=
{
()
=>
fileInputRefs
.
current
.
front
?.
click
()
}
>
{
imagePreviews
.
front
?
(
<
img
src=
{
imagePreviews
.
front
}
alt=
"角色图片预览"
className=
"w-full h-full object-contain"
/>
)
:
(
<
div
className=
"text-center px-4"
>
<
Upload
className=
"w-6 h-6 text-muted-foreground mx-auto mb-2"
/>
<
span
className=
"text-sm text-muted-foreground"
>
点击上传角色图片
</
span
>
</
div
>
))
}
)
}
</
div
>
<
input
ref=
{
(
node
)
=>
{
fileInputRefs
.
current
.
front
=
node
;
}
}
type=
"file"
accept=
"image/*"
className=
"hidden"
onChange=
{
(
e
)
=>
handleFileSelect
(
"front"
,
e
)
}
/>
</
div
>
<
div
>
<
div
className=
"mb-1 flex items-center justify-between gap-3"
>
<
label
className=
"block text-sm font-medium text-foreground"
>
角色图片提示词
</
label
>
<
button
type=
"button"
onClick=
{
()
=>
setEditingChar
({
...
editingChar
,
imagePrompt
:
buildChineseCharacterPrompt
(
editingChar
),
})
}
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"
>
<
Sparkles
className=
"h-3.5 w-3.5"
/>
生成中文提示词
</
button
>
</
div
>
<
textarea
value=
{
editingChar
.
imagePrompt
??
""
}
onChange=
{
(
e
)
=>
setEditingChar
({
...
editingChar
,
imagePrompt
:
e
.
target
.
value
})
}
placeholder=
"用于生成或重生成角色参考图的提示词,可描述外貌、服装、气质和画面要求。"
rows=
{
6
}
className=
"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary/20"
/>
</
div
>
<
div
>
...
...
doc/html/src/app/pages/Dashboard.tsx
View file @
74404e9d
...
...
@@ -20,7 +20,7 @@ function ProjectCover({ url, name, className = "" }: { url?: string; name: strin
return
<
img
src=
{
url
}
alt=
{
name
}
className=
{
`w-full h-full object-cover ${className}`
}
/>;
}
return
(
<
div
className=
{
`flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,#
e5fff2_0%,#b9d8ff_52%,#61d7ee
_100%)] ${className}`
}
>
<
div
className=
{
`flex h-full w-full items-center justify-center bg-[linear-gradient(135deg,#
f4f0ff_0%,#d9cdff_48%,#9f7bff
_100%)] ${className}`
}
>
<
div
className=
"relative text-white"
>
<
Film
className=
"h-8 w-8 stroke-[2.2]"
/>
<
Sparkles
className=
"absolute -right-2 bottom-0 h-4 w-4 fill-white stroke-white"
/>
...
...
@@ -299,17 +299,17 @@ export function Dashboard() {
},
[
page
,
totalPages
]);
return
(
<
div
className=
"h-full overflow-auto bg-[#f
6f7fb] p-5 text-[#0f172a
]"
>
<
div
className=
"h-full overflow-auto bg-[#f
8f9fa] p-6 text-[#15131f
]"
>
<
div
className=
"mx-auto max-w-[1440px]"
>
{
/* Header */
}
<
div
className=
"mb-5 flex items-center justify-between"
>
<
div
>
<
h1
className=
"mb-1 text-2xl font-semibold text-[#1
11827
]"
>
剧本生短剧
</
h1
>
<
h1
className=
"mb-1 text-2xl font-semibold text-[#1
5131f
]"
>
剧本生短剧
</
h1
>
</
div
>
<
Link
to=
"/new-project"
className=
"
flex h-9 items-center gap-2 rounded-md bg-[#1664ff] px-4 text-sm font-medium text-white transition hover:bg-[#0f56d9]
"
className=
"
dramix-primary flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium transition
"
>
<
Plus
className=
"w-4 h-4"
/>
新建项目
...
...
@@ -317,54 +317,54 @@ export function Dashboard() {
</
div
>
{
/* Search & Filters */
}
<
div
className=
"
mb-4 rounded-lg border border-[#e1e6f0] bg-white
p-4"
>
<
div
className=
"
dramix-panel mb-5 rounded-xl
p-4"
>
<
div
className=
"grid grid-cols-1 gap-3 lg:grid-cols-[minmax(320px,1fr)_minmax(260px,340px)_minmax(140px,160px)_auto]"
>
<
div
>
<
div
className=
"relative"
>
<
Search
className=
"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#8
a94a6
]"
/>
<
Search
className=
"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#8
d86a2
]"
/>
<
input
type=
"text"
value=
{
searchTerm
}
onChange=
{
(
e
)
=>
setSearchTerm
(
e
.
target
.
value
)
}
placeholder=
"搜索短剧项目名称"
className=
"h-9 w-full rounded-
md border border-[#d6dbe7] bg-white pl-9 pr-3 text-sm text-[#111827] outline-none transition placeholder:text-[#98a2b3] focus:border-[#1664ff] focus:ring-2 focus:ring-[#1664ff]/10
"
className=
"h-9 w-full rounded-
lg border border-[#d8dae9] bg-white pl-9 pr-3 text-sm text-[#15131f] outline-none transition placeholder:text-[#9c9c9c] focus:border-[#5337c7] focus:ring-2 focus:ring-[#f3eefe]
"
/>
</
div
>
</
div
>
<
div
className=
"flex h-9 items-center rounded-
md border border-[#d6dbe7
] bg-white px-3"
>
<
div
className=
"flex h-9 items-center rounded-
lg border border-[#d8dae9
] bg-white px-3"
>
<
div
className=
"relative min-w-0 flex-1"
>
<
input
type=
"date"
value=
{
dateRange
.
start
}
onChange=
{
(
e
)
=>
setDateRange
({
...
dateRange
,
start
:
e
.
target
.
value
})
}
className=
"h-7 w-full bg-transparent text-sm text-[#1
11827
] outline-none"
className=
"h-7 w-full bg-transparent text-sm text-[#1
5131f
] outline-none"
/>
</
div
>
<
span
className=
"px-2 text-sm text-[#9
8a2b3
]"
>
-
</
span
>
<
span
className=
"px-2 text-sm text-[#9
b94ad
]"
>
-
</
span
>
<
div
className=
"relative min-w-0 flex-1"
>
<
input
type=
"date"
value=
{
dateRange
.
end
}
onChange=
{
(
e
)
=>
setDateRange
({
...
dateRange
,
end
:
e
.
target
.
value
})
}
className=
"h-7 w-full bg-transparent text-sm text-[#1
11827
] outline-none"
className=
"h-7 w-full bg-transparent text-sm text-[#1
5131f
] outline-none"
/>
</
div
>
<
Calendar
className=
"ml-2 h-4 w-4 text-[#
667085
]"
/>
<
Calendar
className=
"ml-2 h-4 w-4 text-[#
7a748c
]"
/>
</
div
>
<
div
>
<
div
className=
"relative"
>
<
button
onClick=
{
()
=>
setShowMemberFilter
(
!
showMemberFilter
)
}
className=
"flex h-9 w-full items-center justify-between rounded-
md border border-[#d6dbe7] bg-white px-3 text-left text-sm text-[#111827] transition hover:border-[#b8c2d6
]"
className=
"flex h-9 w-full items-center justify-between rounded-
lg border border-[#d8dae9] bg-white px-3 text-left text-sm text-[#15131f] transition hover:border-[#cac3f3
]"
>
<
span
className=
"flex items-center gap-2"
>
<
Filter
className=
"h-4 w-4 text-[#8
a94a6
]"
/>
<
Filter
className=
"h-4 w-4 text-[#8
d86a2
]"
/>
{
selectedMembers
.
length
>
0
?
`${selectedMembers.length}个成员`
:
"选择成员"
}
</
span
>
<
span
className=
"text-xs text-[#8
a94a6
]"
>
{
showMemberFilter
?
"▲"
:
"▼"
}
</
span
>
<
span
className=
"text-xs text-[#8
d86a2
]"
>
{
showMemberFilter
?
"▲"
:
"▼"
}
</
span
>
</
button
>
{
showMemberFilter
&&
(
<
div
className=
"absolute left-0 right-0 top-full z-20 mt-2 rounded-
lg border border-[#dfe3ee
] bg-white p-3 shadow-lg"
>
<
div
className=
"absolute left-0 right-0 top-full z-20 mt-2 rounded-
xl border border-[#e6eaed
] bg-white p-3 shadow-lg"
>
<
div
className=
"flex flex-wrap gap-2"
>
{
allMembers
.
map
((
member
)
=>
(
<
button
...
...
@@ -373,7 +373,7 @@ export function Dashboard() {
prev
.
includes
(
member
)
?
prev
.
filter
((
m
)
=>
m
!==
member
)
:
[...
prev
,
member
]
)
}
className=
{
`rounded-md px-3 py-1.5 text-xs transition-colors ${
selectedMembers.includes(member) ? "bg-[#
1664ff] text-white" : "bg-[#f2f4f8] text-[#344054] hover:bg-[#e8edf6
]"
selectedMembers.includes(member) ? "bg-[#
5337c7] text-white" : "bg-[#f7f5fb] text-[#51476f] hover:bg-[#f3eefe
]"
}`
}
>
{
member
}
...
...
@@ -391,7 +391,7 @@ export function Dashboard() {
</
div
>
<
Link
to=
"/new-project"
className=
"
flex h-9 items-center justify-center gap-2 rounded-md bg-[#5d5bff] px-4 text-sm font-medium text-white transition hover:bg-[#4d4af0]
"
className=
"
dramix-primary flex h-9 items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium transition
"
>
<
Plus
className=
"h-4 w-4"
/>
创建项目
...
...
@@ -411,7 +411,7 @@ export function Dashboard() {
<
div
key=
{
project
.
id
}
className=
"group relative"
>
{
/* Cover */
}
<
Link
to=
{
`/project/${project.id}`
}
className=
"block aspect-[16/10] overflow-hidden rounded-
lg bg-[#eef1f7
]"
>
<
Link
to=
{
`/project/${project.id}`
}
className=
"block aspect-[16/10] overflow-hidden rounded-
xl bg-[#f0edf7] shadow-[0_12px_30px_rgba(37,31,63,0.08)
]"
>
<
ProjectCover
url=
{
project
.
coverUrl
}
name=
{
project
.
title
}
...
...
@@ -423,26 +423,26 @@ export function Dashboard() {
<
div
className=
"relative pt-3"
>
<
Link
to=
{
`/project/${project.id}`
}
className=
"block pr-9"
>
<
div
className=
"flex min-w-0 items-center gap-2"
>
<
h3
className=
"line-clamp-1 font-medium text-[#1
11827
]"
>
{
project
.
title
}
</
h3
>
<
span
className=
"shrink-0 rounded bg-[#
ecebff] px-1.5 py-0.5 text-xs text-[#5d5bff
]"
>
<
h3
className=
"line-clamp-1 font-medium text-[#1
5131f
]"
>
{
project
.
title
}
</
h3
>
<
span
className=
"shrink-0 rounded bg-[#
f3eefe] px-1.5 py-0.5 text-xs text-[#5337c7
]"
>
{
project
.
progress
===
100
?
"已完成"
:
"创作中"
}
</
span
>
</
div
>
<
div
className=
"mt-2 text-xs text-[#
a0a9bc
]"
>
<
div
className=
"mt-2 text-xs text-[#
8d86a2
]"
>
{
project
.
createdAt
?
`${project.createdAt.replace("T", " ").slice(0, 19)} 创建`
:
"—"
}
</
div
>
</
Link
>
<
div
className=
"absolute right-0 top-3 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"
>
<
button
onClick=
{
(
e
)
=>
{
e
.
preventDefault
();
setEditingProject
(
project
.
raw
);
}
}
className=
"flex h-7 w-7 items-center justify-center rounded text-[#
475467] hover:bg-[#eef1f7
]"
className=
"flex h-7 w-7 items-center justify-center rounded text-[#
5b546e] hover:bg-[#f3eefe
]"
title=
"编辑项目"
>
<
Pencil
className=
"h-3.5 w-3.5"
/>
</
button
>
<
button
onClick=
{
(
e
)
=>
{
e
.
preventDefault
();
setDeletingProject
(
project
.
raw
);
}
}
className=
"flex h-7 w-7 items-center justify-center rounded text-[#
475467
] hover:bg-[#fff1f0] hover:text-[#d92d20]"
className=
"flex h-7 w-7 items-center justify-center rounded text-[#
5b546e
] hover:bg-[#fff1f0] hover:text-[#d92d20]"
title=
"删除项目"
>
<
MoreHorizontal
className=
"h-4 w-4"
/>
...
...
@@ -452,29 +452,29 @@ export function Dashboard() {
</
div
>
))
}
</
div
>
<
div
className=
"mt-12 flex items-center justify-end gap-2 text-sm text-[#
344054
]"
>
<
div
className=
"mt-12 flex items-center justify-end gap-2 text-sm text-[#
51476f
]"
>
<
span
className=
"mr-1"
>
共
{
filtered
.
length
}
条
</
span
>
<
button
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
max
(
1
,
value
-
1
))
}
disabled=
{
safePage
===
1
}
className=
"flex h-8 w-8 items-center justify-center rounded-
md border border-[#d6dbe7
] bg-white disabled:cursor-not-allowed disabled:opacity-50"
className=
"flex h-8 w-8 items-center justify-center rounded-
lg border border-[#d8dae9
] bg-white disabled:cursor-not-allowed disabled:opacity-50"
>
‹
</
button
>
<
span
className=
"flex h-8 min-w-8 items-center justify-center rounded-
md border border-[#5d5bff] bg-white px-2 text-[#5d5bff
]"
>
<
span
className=
"flex h-8 min-w-8 items-center justify-center rounded-
lg border border-[#5337c7] bg-white px-2 text-[#5337c7
]"
>
{
safePage
}
</
span
>
<
button
onClick=
{
()
=>
setPage
((
value
)
=>
Math
.
min
(
totalPages
,
value
+
1
))
}
disabled=
{
safePage
===
totalPages
}
className=
"flex h-8 w-8 items-center justify-center rounded-
md border border-[#d6dbe7
] bg-white disabled:cursor-not-allowed disabled:opacity-50"
className=
"flex h-8 w-8 items-center justify-center rounded-
lg border border-[#d8dae9
] bg-white disabled:cursor-not-allowed disabled:opacity-50"
>
›
</
button
>
<
select
value=
{
pageSize
}
onChange=
{
(
e
)
=>
setPageSize
(
Number
(
e
.
target
.
value
))
}
className=
"ml-2 h-8 rounded-
md border border-[#d6dbe7
] bg-white px-3 text-sm outline-none"
className=
"ml-2 h-8 rounded-
lg border border-[#d8dae9
] bg-white px-3 text-sm outline-none"
>
{
[
10
,
20
,
50
].
map
((
size
)
=>
(
<
option
key=
{
size
}
value=
{
size
}
>
{
size
}
条/页
</
option
>
...
...
@@ -487,30 +487,30 @@ export function Dashboard() {
const
value
=
Number
(
e
.
target
.
value
);
if
(
Number
.
isFinite
(
value
))
setPage
(
Math
.
min
(
Math
.
max
(
1
,
value
),
totalPages
));
}
}
className=
"h-8 w-14 rounded-
md border border-[#d6dbe7
] bg-white px-2 text-center outline-none"
className=
"h-8 w-14 rounded-
lg border border-[#d8dae9
] bg-white px-2 text-center outline-none"
/>
<
span
>
页
</
span
>
</
div
>
</>
)
:
projects
.
length
===
0
?
(
<
div
className=
"rounded-
lg border border-dashed border-[#cfd6e4
] bg-white py-16 text-center"
>
<
Film
className=
"mx-auto mb-4 h-14 w-14 text-[#9
8a2b3
]"
/>
<
p
className=
"mb-4 text-[#
667085
]"
>
还没有项目,开始创建第一个吧
</
p
>
<
div
className=
"rounded-
xl border border-dashed border-[#d6cfea
] bg-white py-16 text-center"
>
<
Film
className=
"mx-auto mb-4 h-14 w-14 text-[#9
b94ad
]"
/>
<
p
className=
"mb-4 text-[#
7a748c
]"
>
还没有项目,开始创建第一个吧
</
p
>
<
Link
to=
"/new-project"
className=
"
inline-flex h-9 items-center gap-2 rounded-md bg-[#1664ff] px-4 text-sm font-medium text-white transition hover:bg-[#0f56d9]
"
className=
"
dramix-primary inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium transition
"
>
<
Plus
className=
"h-4 w-4"
/>
新建项目
</
Link
>
</
div
>
)
:
(
<
div
className=
"rounded-
lg border border-dashed border-[#cfd6e4
] bg-white py-16 text-center"
>
<
Film
className=
"mx-auto mb-4 h-14 w-14 text-[#9
8a2b3
]"
/>
<
p
className=
"text-[#
667085
]"
>
没有找到符合条件的项目
</
p
>
<
div
className=
"rounded-
xl border border-dashed border-[#d6cfea
] bg-white py-16 text-center"
>
<
Film
className=
"mx-auto mb-4 h-14 w-14 text-[#9
b94ad
]"
/>
<
p
className=
"text-[#
7a748c
]"
>
没有找到符合条件的项目
</
p
>
<
button
onClick=
{
()
=>
{
setSearchTerm
(
""
);
setDateRange
({
start
:
""
,
end
:
""
});
setSelectedMembers
([]);
}
}
className=
"mt-4 text-sm text-[#
1664ff
] hover:underline"
className=
"mt-4 text-sm text-[#
5337c7
] hover:underline"
>
清除所有筛选条件
</
button
>
...
...
doc/html/src/app/pages/NewProject.tsx
View file @
74404e9d
...
...
@@ -143,24 +143,24 @@ export function NewProject() {
const
activeSubmitMeta
=
submitStage
===
"idle"
?
null
:
submitStageMeta
[
submitStage
];
return
(
<
div
className=
"relative min-h-full overflow-auto bg-[#f
6f7fb] text-[#0f172a
]"
>
<
div
className=
"relative min-h-full overflow-auto bg-[#f
8f9fa] text-[#15131f
]"
>
{
activeSubmitMeta
&&
(
<
div
className=
"fixed inset-0 z-50 flex items-center justify-center bg-white/80 p-6 backdrop-blur-sm"
>
<
div
className=
"w-full max-w-md rounded-2xl border border-[#
dfe6f3
] bg-white p-6 shadow-2xl"
>
<
div
className=
"mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-
gradient-to-br from-[#1677ff] to-[#7c2dff
]"
>
<
div
className=
"w-full max-w-md rounded-2xl border border-[#
e6eaed
] bg-white p-6 shadow-2xl"
>
<
div
className=
"mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-
[#8064f1
]"
>
<
Loader2
className=
"h-7 w-7 animate-spin text-white"
/>
</
div
>
<
h2
className=
"mb-2 text-xl font-semibold text-[#
0f172a
]"
>
{
activeSubmitMeta
.
title
}
</
h2
>
<
p
className=
"mb-5 text-sm leading-relaxed text-[#
667085
]"
>
<
h2
className=
"mb-2 text-xl font-semibold text-[#
15131f
]"
>
{
activeSubmitMeta
.
title
}
</
h2
>
<
p
className=
"mb-5 text-sm leading-relaxed text-[#
7a748c
]"
>
{
activeSubmitMeta
.
description
}
</
p
>
<
Progress
value=
{
activeSubmitMeta
.
progress
}
className=
"mb-4 h-2.5"
/>
<
div
className=
"space-y-2 text-sm"
>
<
div
className=
"flex items-center justify-between text-[#2
02b3d
]"
>
<
div
className=
"flex items-center justify-between text-[#2
62238
]"
>
<
span
>
创建项目
</
span
>
<
span
>
{
submitStage
===
"creating"
?
"进行中"
:
"已完成"
}
</
span
>
</
div
>
<
div
className=
"flex items-center justify-between text-[#2
02b3d
]"
>
<
div
className=
"flex items-center justify-between text-[#2
62238
]"
>
<
span
>
上传剧本
</
span
>
<
span
>
{
!
hasUpload
...
...
@@ -172,28 +172,28 @@ export function NewProject() {
:
"等待中"
}
</
span
>
</
div
>
<
div
className=
"flex items-center justify-between text-[#2
02b3d
]"
>
<
div
className=
"flex items-center justify-between text-[#2
62238
]"
>
<
span
>
进入大纲页
</
span
>
<
span
>
{
submitStage
===
"redirecting"
?
"进行中"
:
"等待中"
}
</
span
>
</
div
>
</
div
>
<
p
className=
"mt-4 text-xs text-[#
8a94a6
]"
>
处理过程中按钮已锁定,避免重复创建项目。
</
p
>
<
p
className=
"mt-4 text-xs text-[#
7a748c
]"
>
处理过程中按钮已锁定,避免重复创建项目。
</
p
>
</
div
>
</
div
>
)
}
<
div
className=
"flex min-h-full flex-col"
>
<
div
className=
"flex h-12 items-center justify-center gap-2 border-b border-[#e
7ebf3] bg-[#f0f3ff] px-6 text-sm text-[#111827
]"
>
<
Megaphone
className=
"h-4 w-4 text-[#
1764ff
]"
/>
<
div
className=
"flex h-12 items-center justify-center gap-2 border-b border-[#e
6eaed] bg-[#f3eefe] px-6 text-sm text-[#262238
]"
>
<
Megaphone
className=
"h-4 w-4 text-[#
5337c7
]"
/>
<
span
>
公告:视频生成在使用视频素材作为输入时,积分计算逻辑已更新。针对不同的视频输出时长,现设有最低积分消耗限制,详情请参见
</
span
>
<
span
className=
"font-medium text-[#
1764ff
]"
>
视频生成计费说明。
</
span
>
<
span
className=
"font-medium text-[#
5337c7
]"
>
视频生成计费说明。
</
span
>
</
div
>
<
main
className=
"mx-auto flex w-full max-w-[1280px] flex-1 flex-col items-center px-8 pb-10 pt-14"
>
<
h1
className=
"text-center text-[40px] font-semibold leading-tight tracking-normal text-black"
>
翻开剧本,创作
<
span
className=
"text-[#5
b55ff
]"
>
精品短剧
</
span
>
翻开剧本,创作
<
span
className=
"text-[#5
337c7
]"
>
精品短剧
</
span
>
</
h1
>
{
submitError
&&
(
...
...
@@ -203,13 +203,13 @@ export function NewProject() {
</
div
>
)
}
<
section
className=
"
mt-12 w-full max-w-[1000px] rounded-[20px] border border-[#dde5f1] bg-white p-4 shadow-[0_22px_60px_rgba(38,51,77,0.08)]
"
>
<
section
className=
"
dramix-panel mt-12 w-full max-w-[1000px] rounded-2xl p-4
"
>
<
label
onDrop=
{
handleDrop
}
onDragOver=
{
(
e
)
=>
e
.
preventDefault
()
}
className=
{
`flex min-h-[228px] items-center justify-center rounded-
2xl border border-dashed border-[#d9e2f1] bg-[#f8
faff] transition-colors ${
className=
{
`flex min-h-[228px] items-center justify-center rounded-
xl border border-dashed border-[#d6cfea] bg-[#fb
faff] transition-colors ${
mode === "agent" && !isSubmitting
? "cursor-pointer hover:border-[#
7b8cff] hover:bg-[#f5f7
ff]"
? "cursor-pointer hover:border-[#
a58bff] hover:bg-[#f7f3
ff]"
: "cursor-default"
}`
}
>
...
...
@@ -226,11 +226,11 @@ export function NewProject() {
{
mode
===
"agent"
?
(
scriptFile
?
(
<
div
className=
"flex flex-col items-center text-center"
>
<
div
className=
"mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border border-[#
dfe6f3
] bg-white shadow-sm"
>
<
FileText
className=
"h-8 w-8 text-[#5
b55ff
]"
/>
<
div
className=
"mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border border-[#
e6eaed
] bg-white shadow-sm"
>
<
FileText
className=
"h-8 w-8 text-[#5
337c7
]"
/>
</
div
>
<
div
className=
"text-sm font-medium text-[#1
11827
]"
>
{
scriptFile
.
name
}
</
div
>
<
div
className=
"mt-1 text-xs text-[#
8a94a6
]"
>
{
formatFileSize
(
scriptFile
.
size
)
}
</
div
>
<
div
className=
"text-sm font-medium text-[#1
5131f
]"
>
{
scriptFile
.
name
}
</
div
>
<
div
className=
"mt-1 text-xs text-[#
7a748c
]"
>
{
formatFileSize
(
scriptFile
.
size
)
}
</
div
>
<
button
type=
"button"
onClick=
{
(
e
)
=>
{
...
...
@@ -238,7 +238,7 @@ export function NewProject() {
if
(
!
isSubmitting
)
setScriptFile
(
null
);
}
}
disabled=
{
isSubmitting
}
className=
"mt-4 rounded-lg border border-[#
dfe6f3] bg-white px-3 py-1.5 text-xs text-[#4f46e5] transition-colors hover:bg-[#f5f7
ff] disabled:cursor-not-allowed disabled:opacity-60"
className=
"mt-4 rounded-lg border border-[#
e6eaed] bg-white px-3 py-1.5 text-xs text-[#5337c7] transition-colors hover:bg-[#f7f3
ff] disabled:cursor-not-allowed disabled:opacity-60"
>
重新上传
</
button
>
...
...
@@ -246,34 +246,34 @@ export function NewProject() {
)
:
(
<
div
className=
"flex flex-col items-center text-center"
>
<
div
className=
"mb-6 flex items-center justify-center"
>
<
div
className=
"-mr-2 rotate-[-14deg] rounded-lg border border-[#
dfe6f3] bg-white p-2 text-[#a8b3c7
] shadow-sm"
>
<
div
className=
"-mr-2 rotate-[-14deg] rounded-lg border border-[#
e6eaed] bg-white p-2 text-[#a9a0bb
] shadow-sm"
>
<
FileText
className=
"h-5 w-5"
/>
</
div
>
<
div
className=
"z-10 rounded-xl border border-[#
dfe6f3] bg-white p-3 text-[#69758a
] shadow-md"
>
<
div
className=
"z-10 rounded-xl border border-[#
e6eaed] bg-white p-3 text-[#5b546e
] shadow-md"
>
<
BookOpen
className=
"h-6 w-6"
/>
</
div
>
<
div
className=
"-ml-2 rotate-[14deg] rounded-lg border border-[#
dfe6f3] bg-white p-2 text-[#a8b3c7
] shadow-sm"
>
<
div
className=
"-ml-2 rotate-[14deg] rounded-lg border border-[#
e6eaed] bg-white p-2 text-[#a9a0bb
] shadow-sm"
>
<
FileText
className=
"h-5 w-5"
/>
</
div
>
</
div
>
<
div
className=
"text-sm text-[#
8a94a6
]"
>
<
div
className=
"text-sm text-[#
7a748c
]"
>
点击或拖拽剧本至此,支持 doc、docx、txt、pdf 和 md 格式,文件大小不超过 20M
</
div
>
</
div
>
)
)
:
(
<
div
className=
"flex flex-col items-center text-center"
>
<
div
className=
"mb-5 flex h-16 w-16 items-center justify-center rounded-2xl border border-[#
dfe6f3
] bg-white shadow-sm"
>
<
Wand2
className=
"h-8 w-8 text-[#5
b55ff
]"
/>
<
div
className=
"mb-5 flex h-16 w-16 items-center justify-center rounded-2xl border border-[#
e6eaed
] bg-white shadow-sm"
>
<
Wand2
className=
"h-8 w-8 text-[#5
337c7
]"
/>
</
div
>
<
div
className=
"text-sm font-medium text-[#1
11827
]"
>
人工模式将在下一页手动输入剧本和大纲
</
div
>
<
div
className=
"mt-2 text-sm text-[#
8a94a6
]"
>
这里不需要填写项目名称,也不需要上传图片。
</
div
>
<
div
className=
"text-sm font-medium text-[#1
5131f
]"
>
人工模式将在下一页手动输入剧本和大纲
</
div
>
<
div
className=
"mt-2 text-sm text-[#
7a748c
]"
>
这里不需要填写项目名称,也不需要上传图片。
</
div
>
</
div
>
)
}
</
label
>
<
div
className=
"mt-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"
>
<
div
className=
"inline-flex w-fit rounded-lg border border-[#
dbe3f0
] bg-white p-0.5"
>
<
div
className=
"inline-flex w-fit rounded-lg border border-[#
e6eaed
] bg-white p-0.5"
>
<
button
type=
"button"
onClick=
{
()
=>
{
...
...
@@ -283,8 +283,8 @@ export function NewProject() {
disabled=
{
isSubmitting
}
className=
{
`h-9 rounded-md px-4 text-sm transition-colors ${
mode === "agent"
? "bg-[#
eef0ff] text-[#5b55ff
]"
: "text-[#1
11827] hover:bg-[#f6f7
fb]"
? "bg-[#
f3eefe] text-[#5337c7
]"
: "text-[#1
5131f] hover:bg-[#f7f5
fb]"
} disabled:cursor-not-allowed disabled:opacity-60`
}
>
Agent模式
...
...
@@ -298,8 +298,8 @@ export function NewProject() {
disabled=
{
isSubmitting
}
className=
{
`h-9 rounded-md px-4 text-sm transition-colors ${
mode === "manual"
? "bg-[#
eef0ff] text-[#5b55ff
]"
: "text-[#1
11827] hover:bg-[#f6f7
fb]"
? "bg-[#
f3eefe] text-[#5337c7
]"
: "text-[#1
5131f] hover:bg-[#f7f5
fb]"
} disabled:cursor-not-allowed disabled:opacity-60`
}
>
人工模式
...
...
@@ -308,12 +308,12 @@ export function NewProject() {
<
div
className=
"flex flex-wrap items-center gap-3"
>
<
label
className=
"relative"
>
<
Monitor
className=
"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#
445066
]"
/>
<
Monitor
className=
"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#
5b546e
]"
/>
<
select
value=
{
aspectRatio
}
onChange=
{
(
e
)
=>
setAspectRatio
(
e
.
target
.
value
)
}
disabled=
{
isSubmitting
}
className=
"h-10 min-w-[126px] appearance-none rounded-lg border border-[#
dbe3f0] bg-white py-0 pl-9 pr-8 text-sm text-[#111827] outline-none transition-colors focus:border-[#7b8cff
] disabled:cursor-not-allowed disabled:opacity-60"
className=
"h-10 min-w-[126px] appearance-none rounded-lg border border-[#
e6eaed] bg-white py-0 pl-9 pr-8 text-sm text-[#15131f] outline-none transition-colors focus:border-[#8064f1
] disabled:cursor-not-allowed disabled:opacity-60"
>
{
aspectRatioOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
...
...
@@ -327,7 +327,7 @@ export function NewProject() {
value=
{
resolution
}
onChange=
{
(
e
)
=>
setResolution
(
e
.
target
.
value
)
}
disabled=
{
isSubmitting
}
className=
"h-10 min-w-[112px] rounded-lg border border-[#
dbe3f0] bg-white px-3 text-sm text-[#111827] outline-none transition-colors focus:border-[#7b8cff
] disabled:cursor-not-allowed disabled:opacity-60"
className=
"h-10 min-w-[112px] rounded-lg border border-[#
e6eaed] bg-white px-3 text-sm text-[#15131f] outline-none transition-colors focus:border-[#8064f1
] disabled:cursor-not-allowed disabled:opacity-60"
>
{
resolutionOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
...
...
@@ -340,7 +340,7 @@ export function NewProject() {
value=
{
style
}
onChange=
{
(
e
)
=>
setStyle
(
e
.
target
.
value
)
}
disabled=
{
isSubmitting
}
className=
"h-10 min-w-[150px] rounded-lg border border-[#
dbe3f0] bg-white px-3 text-sm text-[#111827] outline-none transition-colors focus:border-[#7b8cff
] disabled:cursor-not-allowed disabled:opacity-60"
className=
"h-10 min-w-[150px] rounded-lg border border-[#
e6eaed] bg-white px-3 text-sm text-[#15131f] outline-none transition-colors focus:border-[#8064f1
] disabled:cursor-not-allowed disabled:opacity-60"
>
{
projectStyleOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
...
...
@@ -356,13 +356,13 @@ export function NewProject() {
type=
"button"
onClick=
{
handleSubmit
}
disabled=
{
isSubmitting
}
className=
"
mt-16 flex h-[52px] min-w-[250px] items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-[#1677ff] to-[#7c2dff] px-8 text-sm font-medium text-white shadow-[0_18px_38px_rgba(88,82,255,0.28)] transition-all hover:-translate-y-0.5 hover:shadow-[0_22px_46px_rgba(88,82,255,0.34)]
disabled:cursor-not-allowed disabled:translate-y-0 disabled:opacity-60"
className=
"
dramix-primary mt-16 flex h-[52px] min-w-[250px] items-center justify-center gap-2 rounded-xl px-8 text-sm font-medium transition-all hover:-translate-y-0.5
disabled:cursor-not-allowed disabled:translate-y-0 disabled:opacity-60"
>
{
isSubmitting
?
<
Loader2
className=
"h-5 w-5 animate-spin"
/>
:
<
Sparkles
className=
"h-5 w-5"
/>
}
<
span
>
{
isSubmitting
?
activeSubmitMeta
?.
title
??
"处理中..."
:
"创建短剧项目"
}
</
span
>
</
button
>
<
div
className=
"mt-8 flex items-center gap-2 text-sm text-[#
9aa4b8
]"
>
<
div
className=
"mt-8 flex items-center gap-2 text-sm text-[#
8d86a2
]"
>
<
CheckCircle2
className=
"h-4 w-4"
/>
<
span
>
平台内容均由人工智能模型生成,不代表平台立场
</
span
>
</
div
>
...
...
doc/html/src/app/pages/ProjectDetail.tsx
View file @
74404e9d
...
...
@@ -59,6 +59,17 @@ export function ProjectDetail() {
}
return
(
<
ProjectScriptView
project=
{
project
}
outline=
{
outline
}
episodes=
{
episodes
}
characters=
{
characters
}
projectId=
{
projectId
}
onNavigate=
{
(
path
)
=>
navigate
(
path
)
}
/>
);
return
(
<
div
className=
"h-full overflow-auto bg-background relative"
>
{
/* Header */
}
<
div
className=
"border-b border-border bg-card px-6 py-3"
>
...
...
@@ -169,6 +180,180 @@ export function ProjectDetail() {
);
}
function
ProjectScriptView
({
project
,
outline
,
episodes
,
characters
,
projectId
,
onNavigate
,
}:
{
project
:
import
(
"../../lib/api/projects"
).
ProjectDTO
|
undefined
;
outline
:
import
(
"../../lib/api/ai"
).
Outline
|
undefined
;
episodes
:
import
(
"../../lib/api/ai"
).
Episode
[];
characters
:
import
(
"../../lib/api/ai"
).
Character
[];
projectId
?:
string
;
onNavigate
:
(
path
:
string
)
=>
void
;
})
{
const
orderedEpisodes
=
[...
episodes
].
sort
(
(
a
,
b
)
=>
(
a
.
episodeNumber
||
0
)
-
(
b
.
episodeNumber
||
0
)
);
const
episodeTotal
=
Math
.
max
(
outline
?.
episodeCount
??
0
,
orderedEpisodes
.
length
,
orderedEpisodes
.
length
===
0
?
60
:
0
);
const
[
selectedEpisodeNo
,
setSelectedEpisodeNo
]
=
useState
(
1
);
const
selectedEpisode
=
orderedEpisodes
.
find
((
ep
)
=>
ep
.
episodeNumber
===
selectedEpisodeNo
)
??
null
;
const
selectedNo
=
selectedEpisodeNo
;
const
uniqueTags
=
Array
.
from
(
new
Set
(
[
outline
?.
genre
,
project
?.
style
,
"爽文"
,
"高燃"
,
"悬疑"
,
"玄幻"
].
filter
(
Boolean
)
as
string
[]
)
).
slice
(
0
,
6
);
const
defaultSynopsis
=
"一场意外让主角卷入仙门与妖族的暗潮之中。看似平凡的手艺成为破局关键,少年在一次次试炼里识破阴谋、守住同伴,也逐渐揭开自己身世与旧日神工之间的联系。"
;
const
fallbackCharacters
=
[
{
id
:
"fallback-1"
,
name
:
"夏母"
,
roleType
:
"关键人物"
,
gender
:
""
,
age
:
"中年"
,
personality
:
"温和坚韧,遇事有主见"
,
costume
:
"朴素家常服"
,
},
{
id
:
"fallback-2"
,
name
:
"萧医师"
,
roleType
:
"推动剧情"
,
gender
:
""
,
age
:
"青年"
,
personality
:
"理性克制,暗藏锋芒"
,
costume
:
"青色长衫"
,
},
];
const
displayCharacters
=
characters
.
length
>
0
?
characters
.
slice
(
0
,
4
)
:
fallbackCharacters
;
const
nextPath
=
episodes
.
length
===
0
?
`/project/
${
projectId
}
/outline`
:
`/project/
${
projectId
}
/characters`
;
return
(
<
div
className=
"relative h-full overflow-auto bg-[#f7f8fb] text-[#111827]"
>
<
main
className=
"mx-auto max-w-[860px] px-6 pb-32 pt-12"
>
<
section
className=
"flex flex-col gap-8 sm:flex-row sm:items-start"
>
<
div
className=
"h-[218px] w-[154px] flex-shrink-0 overflow-hidden rounded-[3px] bg-[#dfe3ea] shadow-[0_18px_48px_rgba(15,23,42,0.14)]"
>
{
project
?.
coverUrl
?
(
<
img
src=
{
project
.
coverUrl
}
alt=
"项目封面"
className=
"h-full w-full object-cover"
/>
)
:
(
<
div
className=
"flex h-full w-full flex-col items-center justify-center bg-[linear-gradient(145deg,#262b36,#10131a)] text-white"
>
<
Film
className=
"mb-3 h-9 w-9 opacity-70"
/>
<
span
className=
"px-5 text-center text-sm font-semibold leading-5"
>
{
project
?.
name
??
outline
?.
title
??
"剧本封面"
}
</
span
>
</
div
>
)
}
</
div
>
<
div
className=
"min-w-0 flex-1 pt-1"
>
<
h1
className=
"mb-4 text-[28px] font-semibold leading-[1.22] tracking-normal text-[#0b1220]"
>
{
outline
?.
title
??
project
?.
name
??
"未命名项目"
}
</
h1
>
<
div
className=
"mb-5 grid grid-cols-2 gap-x-8 gap-y-2 text-[13px] leading-6 text-[#6b7280] sm:grid-cols-4"
>
<
span
>
集数:
{
outline
?.
episodeCount
??
episodeTotal
}
集
</
span
>
<
span
>
比例:
{
project
?.
aspectRatio
??
"16:9"
}
</
span
>
<
span
>
清晰度:
{
project
?.
resolution
??
"720p"
}
</
span
>
<
span
>
风格:
{
project
?.
style
??
outline
?.
genre
??
"待生成"
}
</
span
>
</
div
>
<
div
className=
"flex flex-wrap gap-2"
>
{
uniqueTags
.
map
((
tag
)
=>
(
<
span
key=
{
tag
}
className=
"rounded-full border border-[#e4e7ef] bg-white px-3 py-1 text-xs font-medium text-[#536079]"
>
{
tag
}
</
span
>
))
}
</
div
>
</
div
>
</
section
>
<
div
className=
"my-10 h-px bg-[#e6e8ef]"
/>
<
section
className=
"mb-11"
>
<
h2
className=
"mb-4 text-[17px] font-semibold text-[#0b1220]"
>
大纲
</
h2
>
<
p
className=
"text-[14px] leading-8 text-[#384152]"
>
{
outline
?.
synopsis
??
project
?.
description
??
defaultSynopsis
}
</
p
>
</
section
>
<
section
className=
"mb-12"
>
<
h2
className=
"mb-5 text-[17px] font-semibold text-[#0b1220]"
>
人物小传
</
h2
>
<
div
className=
"grid gap-x-12 gap-y-6 md:grid-cols-2"
>
{
displayCharacters
.
map
((
char
)
=>
(
<
article
key=
{
char
.
id
}
className=
"text-[14px] leading-7 text-[#384152]"
>
<
h3
className=
"mb-1 font-semibold text-[#111827]"
>
{
char
.
name
}
</
h3
>
<
p
>
{
char
.
roleType
?
`${char.roleType},`
:
""
}
{
char
.
gender
?
`${char.gender},`
:
""
}
{
char
.
age
?
`${char.age}。`
:
""
}
{
char
.
personality
||
"性格鲜明,与主线冲突和情感推进紧密相关。"
}
{
char
.
costume
?
` 服饰:${char.costume}。`
:
""
}
</
p
>
</
article
>
))
}
</
div
>
</
section
>
<
section
>
<
h2
className=
"mb-5 text-[17px] font-semibold text-[#0b1220]"
>
分集剧情
</
h2
>
<
div
className=
"mb-7 grid grid-cols-8 gap-3 sm:grid-cols-10 md:grid-cols-12"
>
{
Array
.
from
({
length
:
episodeTotal
},
(
_
,
index
)
=>
index
+
1
).
map
((
num
)
=>
(
<
button
key=
{
num
}
type=
"button"
onClick=
{
()
=>
setSelectedEpisodeNo
(
num
)
}
className=
{
`h-9 rounded-full text-sm font-medium transition-all ${
num === selectedNo
? "bg-[#111827] text-white shadow-[0_8px_18px_rgba(17,24,39,0.2)]"
: "bg-white text-[#4b5563] ring-1 ring-[#e3e6ee] hover:bg-[#f1f3f7]"
}`
}
>
{
num
}
</
button
>
))
}
</
div
>
<
article
className=
"text-[14px] leading-8 text-[#384152]"
>
<
h3
className=
"mb-2 font-semibold text-[#111827]"
>
第
{
selectedNo
}
集
{
selectedEpisode
?.
title
?
`:${selectedEpisode.title}`
:
""
}
</
h3
>
<
p
className=
"mb-4"
>
{
selectedEpisode
?.
summary
??
"本集承接上一段危机,主角在新的线索中发现关键异常,并由此推动人物关系、外部冲突和最终选择进一步升级。"
}
</
p
>
<
p
className=
"whitespace-pre-wrap"
>
{
selectedEpisode
?.
script
??
"镜头从喧闹的街巷切入,主角察觉到细微异动。几句试探之后,暗处的矛盾浮出水面,众人的立场开始分化。主角在压力下做出决定,为下一集的分镜和视觉呈现留下清晰的动作线。"
}
</
p
>
</
article
>
</
section
>
</
main
>
<
div
className=
"pointer-events-none fixed bottom-8 left-0 right-0 z-20 flex justify-center px-6"
>
<
div
className=
"pointer-events-auto flex w-full max-w-[520px] items-center justify-center"
>
<
button
type=
"button"
onClick=
{
()
=>
onNavigate
(
nextPath
)
}
className=
"h-12 min-w-[260px] rounded-full bg-[#0b0f17] px-10 text-[15px] font-semibold text-white shadow-[0_18px_46px_rgba(17,24,39,0.3)] transition-transform hover:scale-[1.01] active:scale-[0.99]"
>
{
episodes
.
length
===
0
?
"生成分集"
:
"进入下一步"
}
</
button
>
</
div
>
</
div
>
</
div
>
);
}
// ---- Outline Tab ----
function
OutlineTab
({
outline
,
...
...
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
74404e9d
...
...
@@ -28,15 +28,15 @@ import {
import
type
{
Storyboard
,
Character
,
CharacterState
,
Scene
,
Episode
,
AiTask
}
from
"../../lib/api/ai"
;
import
{
useOperationCost
}
from
"../../hooks/useUsage"
;
const
DURATION_OPTIONS
=
[
5
,
10
,
15
,
20
,
30
];
const
DURATION_OPTIONS
=
[
5
,
10
,
1
1
,
1
5
,
20
,
30
];
const
RATIO_OPTIONS
=
[
{
value
:
"16:9"
,
label
:
"16:9 横屏"
},
{
value
:
"9:16"
,
label
:
"9:16 竖屏"
},
{
value
:
"1:1"
,
label
:
"1:1 方形"
},
];
const
VIDEO_MODEL_OPTIONS
=
[
{
value
:
"doubao-seedance-2-0-fast-260128"
,
label
:
"
doubao-seedance-2-0-fast
"
},
{
value
:
"doubao-seedance-2-0-260128"
,
label
:
"
doubao-seedance-2-0
"
},
{
value
:
"doubao-seedance-2-0-fast-260128"
,
label
:
"
Doubao-Seedance-2-0
"
},
{
value
:
"doubao-seedance-2-0-260128"
,
label
:
"
Doubao-Seedance-2-0 Pro
"
},
];
const
DEFAULT_VIDEO_MODEL
=
VIDEO_MODEL_OPTIONS
[
0
].
value
;
const
MAX_CHARACTERS
=
5
;
...
...
@@ -300,7 +300,7 @@ export function StoryboardWorkspace() {
const [styleKey] = useState<string>(""); // reserved; not exposed in this UI
const [shortDescription, setShortDescription] = useState<string>("");
const [freeformPrompt, setFreeformPrompt] = useState<string>("");
const [selectedDuration, setSelectedDuration] = useState(
5
);
const [selectedDuration, setSelectedDuration] = useState(
11
);
const [selectedRatio, setSelectedRatio] = useState<string>("16:9");
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL);
const [audioOn, setAudioOn] = useState(true);
...
...
@@ -372,11 +372,11 @@ export function StoryboardWorkspace() {
// Auto-expand first storyboard on load
useEffect(() => {
if (storyboards.length > 0 &&
!expandedId
) {
if (storyboards.length > 0 &&
(!expandedId || !storyboards.some((sb) => sb.id === expandedId))
) {
setExpandedId(storyboards[0].id);
setActiveVideoSbId(storyboards[0].id);
}
}, [storyboards
.length
]);
}, [storyboards
, expandedId
]);
useEffect(() => {
if (!expanded) return;
...
...
@@ -610,7 +610,8 @@ export function StoryboardWorkspace() {
() => storyboards.find((s) => s.id === activeVideoSbId) ?? expanded,
[storyboards, activeVideoSbId, expanded]
);
const previewTask = previewSb ? getTaskForSb(previewSb) : null;
const previewDisplaySb = previewSb ?? null;
const previewTask = previewDisplaySb ? getTaskForSb(previewDisplaySb) : null;
const promptPreviewText = freeformPrompt.trim();
const sceneRefDisplay = expanded
...
...
@@ -630,13 +631,20 @@ export function StoryboardWorkspace() {
{ label: "镜头运动", value: motionScriptDisplay },
{ label: "视频提示词", value: videoPromptDisplay, multiline: true },
] : [];
const currentEpisode = episodes.find((ep) => ep.id === activeEpisodeId) ?? episodes[0] ?? null;
const currentEpisodeTitle = currentEpisode
? currentEpisode.title || `
第
$
{
currentEpisode
.
episodeNumber
}
集
`
: "暂无分集";
const currentEpisodeSummary = currentEpisode?.summary
|| "当前项目还没有可用分集。生成分集后,可以在这里继续生成分镜。";
const selectedScene = sceneKey ? sceneByKey.get(sceneKey) ?? null : null;
return (
<div className="h-full flex overflow-hidden bg-[#f
4f7fb] text-foreground
">
<div className="h-full flex overflow-hidden bg-[#f
7f8fb] text-[#111827]
">
{/* ─── Far Left: Episode list strip ─── */}
<div className="flex-shrink-0 border-r border-
slate-200/80 bg-white/90 flex flex-col items-center overflow-hidden py-5 shadow-[1px_0_0_rgba(15,23,42,0.02)]" style={{ width: 80
}}>
<span className="
font-semibold mb-5" style={{ color: "#7c8598", fontSize: 16, letterSpacing: 1 }}
>
<div className="flex-shrink-0 border-r border-
[#e5e8f0] bg-[#fbfcff] flex flex-col items-center overflow-hidden py-4" style={{ width: 56
}}>
<span className="
mb-3 text-[13px] font-medium text-[#475467]"
>
集数
</span>
<div
...
...
@@ -648,8 +656,8 @@ export function StoryboardWorkspace() {
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
</div>
) : episodes.length === 0 ? (
<div className="px-
1 pt-4 text-center
">
<span className="text-[12px] text-muted-foreground leading-tight">暂无集数</span>
<div className="px-
2 pt-4 text-center text-[11px] leading-4 text-[#98a2b3]
">
暂无分集
</div>
) : (
episodes.map((ep) => {
...
...
@@ -663,14 +671,14 @@ export function StoryboardWorkspace() {
active
?
""
:
"hover:bg-[#f0f4fb] hover:text-[#475569] hover:shadow-sm"
}
`}
style={{
width:
4
4,
height:
50
,
borderRadius:
14
,
fontSize: 1
6
,
backgroundColor: active ? "#
1c64
ff" : "#ffffff",
width:
3
4,
height:
34
,
borderRadius:
6
,
fontSize: 1
4
,
backgroundColor: active ? "#
7657
ff" : "#ffffff",
color: active ? "#ffffff" : "#8b95a7",
boxShadow: active ? "0
12px 24px rgba(28, 100
, 255, 0.28)" : "0 1px 2px rgba(15, 23, 42, 0.05)",
border: active ? "1px solid rgba(
28, 100, 255, 0.35)" : "1px solid rgba(148, 163, 184, 0.22
)",
boxShadow: active ? "0
8px 18px rgba(118, 87
, 255, 0.28)" : "0 1px 2px rgba(15, 23, 42, 0.05)",
border: active ? "1px solid rgba(
118, 87, 255, 0.45)" : "1px solid rgba(148, 163, 184, 0.25
)",
}}
>
{ep.episodeNumber}
...
...
@@ -681,17 +689,63 @@ export function StoryboardWorkspace() {
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col overflow-hidden px-5 pb-5 pt-4">
<div className="mb-3 flex items-start gap-4">
<button className="h-8 rounded-md border border-[#dfe3ee] bg-white px-4 text-sm font-medium text-[#111827] shadow-sm">
分镜表
</button>
<div className="min-w-0 flex-1">
<h2 className="text-base font-semibold text-[#111827]">{currentEpisodeTitle}</h2>
<p className="mt-1 line-clamp-1 text-xs leading-5 text-[#475467]">{currentEpisodeSummary}</p>
</div>
<button
onClick={handleGenerateAll}
disabled={generateSbs.isPending || !eid}
className="flex h-9 items-center gap-2 rounded-md border border-[#dde3ee] bg-white px-4 text-sm font-medium text-[#111827] shadow-sm transition hover:border-[#b9c3d6] disabled:opacity-50"
>
{generateSbs.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Layers className="h-4 w-4" />}
批量生成视频
</button>
</div>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-5 text-sm">
<button className="flex h-8 items-center gap-2 rounded-md px-2 text-[#111827] hover:bg-white">
<span className="text-[#98a2b3]">⋮⋮</span>
{expanded ? `
分镜
$
{
expanded
.
sequenceNum
}
` : "暂无分镜"}
</button>
<span className="flex h-8 items-center gap-2 rounded-md border border-[#d7ceff] bg-[#f4f0ff] px-3 text-[#6147e8]">
<span className="h-2.5 w-2.5 rounded-full bg-[#7657ff]" />
全能参考生成视频
</span>
</div>
<div className="flex items-center gap-4 text-[#475467]">
<button onClick={handleCreate} disabled={createSb.isPending || !eid} className="hover:text-[#7657ff]" aria-label="新增分镜">
{createSb.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
</button>
<button className="hover:text-[#7657ff]" aria-label="复制分镜">
<Layers className="h-4 w-4" />
</button>
<button onClick={() => expanded && handleDelete(expanded)} disabled={!expanded} className="hover:text-red-500 disabled:opacity-40" aria-label="删除分镜">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 overflow-hidden rounded-lg border border-[#e4e8f0] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.04)]">
{/* ─── Column 2: Storyboard accordion list (top: prompt summary, then cards) ─── */}
<div className="flex flex-col flex-shrink-0
border-r border-slate-200/80 overflow-hidden bg-white/95" style={{ width: 380
}}>
<div className="flex flex-col flex-shrink-0
overflow-hidden rounded-lg border border-[#eef1f6] bg-[#fbfcff]" style={{ width: 394
}}>
{/* Top: 分镜表 / 备注描述 */}
<div className="px-4 py-3 border-b border-slate-200/80 bg-white/95 flex-shrink-0 relative shadow-[0_1px_0_rgba(15,23,42,0.03)]">
<span className="absolute left-0 top-3 bottom-3 w-[3px] bg-[#1c64ff]/55 rounded-r-full" />
<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-[11px] font-semibold tracking-[0.12em] uppercase text-muted-foreground">分镜备注</span>
<span className="text-sm font-semibold text-[#111827]">
{expanded ? `
分镜信息
$
{
String
(
expanded
.
sequenceNum
).
padStart
(
3
,
"0"
)}
` : "分镜信息"}
</span>
{(promptPreviewText || expanded) && (
<button
onClick={() => setShowPromptDetails(true)}
className="text-
[10px] text-[#1c64ff] hover:text-[#164fd0] flex items-center gap-0.5 font-semibold
transition-colors"
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" />
...
...
@@ -699,21 +753,18 @@ export function StoryboardWorkspace() {
</button>
)}
</div>
<p className={`
text
-
[
11
px
]
leading
-
relaxed
line
-
clamp
-
2
$
{
promptPreviewText
?
"text-foreground/85"
:
"text-muted-foreground italic"
}
`}>
{promptPreviewText
? (promptPreviewText.length > PROMPT_PREVIEW_LIMIT
? promptPreviewText.slice(0, PROMPT_PREVIEW_LIMIT) + "…"
: promptPreviewText)
: "选中分镜后,这里会显示已填写的视频提示词内容"}
<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
2 px-3 border-b border-slate-200/80 flex items-center gap-2 flex-shrink-0 bg-white/95
">
<div className="h-1
0 px-3 border-b border-[#edf0f5] flex items-center gap-2 flex-shrink-0 bg-white
">
<button
onClick={handleCreate}
disabled={createSb.isPending || !eid}
className="flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-
lg bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_6px_14px_rgba(28,100,255,0.22)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_8px_18px_rgba(28,100,255,0.28)] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none
"
className="flex-1 flex items-center justify-center gap-1.5 py-1.5 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" />}
新建分镜
...
...
@@ -722,25 +773,29 @@ export function StoryboardWorkspace() {
onClick={handleGenerateAll}
disabled={generateSbs.isPending || !eid}
title="AI批量生成分镜"
className={`
p
-
2
rounded
-
lg
text
-
muted
-
foreground
hover
:
bg
-
[
#
f6f9ff
]
hover
:
border
-
[
#
1
c64ff
]
/
30
hover
:
text
-
[
#
1
c64
ff
]
transition
-
colors
disabled
:
opacity
-
50
$
{
CONTROL_SURFACE
}
`}
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
}
`}
>
{generateSbs.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
</button>
</div>
{/* Cards (accordion) */}
<div className="flex-1 overflow-y-auto px-
2 py-2 bg-[#f7f9fd
]">
<div className="flex-1 overflow-y-auto px-
3 py-2 bg-[#fbfcff
]">
{!eid ? (
<div className="px-3 py-8 text-center">
<p className="text-xs text-muted-foreground">请从左侧选择集数</p>
<div className="flex h-full min-h-[360px] flex-col items-center justify-center rounded-lg border border-dashed border-[#d9dee8] bg-white px-6 text-center">
<Layers className="mb-3 h-8 w-8 text-[#98a2b3]" />
<p className="text-sm font-medium text-[#111827]">暂无分集数据</p>
<p className="mt-1 text-xs leading-5 text-[#98a2b3]">请先生成或选择分集,再继续创建分镜。</p>
</div>
) : isLoading ? (
<div className="flex items-center justify-center py-10">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
) : storyboards.length === 0 ? (
<div className="px-3 py-8 text-center">
<p className="text-xs text-muted-foreground">点击「AI生成」或「新建分镜」开始</p>
<div className="flex h-full min-h-[360px] flex-col items-center justify-center rounded-lg border border-dashed border-[#d9dee8] bg-white px-6 text-center">
<Layers className="mb-3 h-8 w-8 text-[#98a2b3]" />
<p className="text-sm font-medium text-[#111827]">暂无分镜数据</p>
<p className="mt-1 text-xs leading-5 text-[#98a2b3]">点击“新建分镜”手动添加,或使用 AI 批量生成。</p>
</div>
) : (
storyboards.map((sb) => (
...
...
@@ -776,9 +831,9 @@ export function StoryboardWorkspace() {
</div>
{/* ─── Column 3: Video generation panel (5-section prompt + duration + button) ─── */}
<div className="
flex flex-col flex-shrink-0 border-r border-slate-200/80 overflow-hidden bg-[#f7f9fd]" style={{ width: 340
}}>
<div className="h-1
4 px-4 border-b border-slate-200/80 flex items-center flex-shrink-0 bg-white/95 shadow-[0_1px_0_rgba(15,23,42,0.03)]
">
<span className="text-
[13px] font-semibold text-foreground
tracking-tight">分镜视频生成</span>
<div className="
ml-3 flex flex-col flex-shrink-0 overflow-hidden rounded-lg border border-[#eef1f6] bg-white" style={{ width: 407
}}>
<div className="h-1
2 px-4 flex items-center flex-shrink-0 bg-white
">
<span className="text-
sm font-semibold text-[#111827]
tracking-tight">分镜视频生成</span>
{expanded && (
<span className="ml-2 text-[11px] text-muted-foreground tabular-nums">
· 分镜 {String(expanded.sequenceNum).padStart(3, "0")}
...
...
@@ -788,12 +843,16 @@ export function StoryboardWorkspace() {
<div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3">
{!expanded ? (
<p className="text-xs text-muted-foreground italic mt-4">展开左侧分镜后填写提示词</p>
<div className="flex-1 min-h-[420px] rounded-md border border-dashed border-[#d9dee8] bg-white px-6 py-8 text-center flex flex-col items-center justify-center">
<Play className="mb-3 h-8 w-8 text-[#98a2b3]" />
<p className="text-sm font-medium text-[#111827]">暂无可生成的视频分镜</p>
<p className="mt-1 text-xs leading-5 text-[#98a2b3]">创建或选择分镜后,这里会显示视频生成提示词。</p>
</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)]">
<
Sparkles className="w-3.5 h-3.5 text-[#1c64ff
] mt-0.5 flex-shrink-0" />
<p className="text-[11px] text-[#
1c64ff
] leading-relaxed">
<
Plus 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 批量生成会自动预选可用角色/场景"}
...
...
@@ -803,13 +862,13 @@ export function StoryboardWorkspace() {
value={freeformPrompt}
onChange={(e) => setFreeformPrompt(e.target.value)}
placeholder={FREEFORM_PLACEHOLDER}
className={`
flex
-
1
min
-
h
-
[
280
px
]
px
-
3
py
-
2.5
rounded
-
xl
border
border
-
slate
-
200
/
80
bg
-
white
text
-
sm
text
-
foreground
leading
-
relaxed
resize
-
none
font
-
mono
shadow
-
inner
shadow
-
slate
-
100
/
80
$
{
FOCUS_RING
}
`}
className={`
flex
-
1
min
-
h
-
[
420
px
]
px
-
3
py
-
2.5
rounded
-
md
border
border
-
[
#
d9dee8
]
bg
-
white
text
-
sm
text
-
[
#
111827
]
leading
-
relaxed
resize
-
none
shadow
-
inner
shadow
-
slate
-
100
/
80
$
{
FOCUS_RING
}
`}
/>
</>
)}
</div>
<div className="px-3 py-3
border-t border-slate-200/80 flex-shrink-0 bg-white/95 space-y-2 shadow-[0_-1px_0_rgba(15,23,42,0.03)]
">
<div className="px-3 py-3
flex-shrink-0 bg-white space-y-2
">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<select
...
...
@@ -817,7 +876,7 @@ export function StoryboardWorkspace() {
onChange={(e) => setSelectedModel(e.target.value)}
disabled={!expanded}
title="视频生成模型"
className={`
w
-
full
appearance
-
none
pl
-
3
pr
-
7
py
-
2
rounded
-
lg
bg
-
[
#
f8fafc
]
text
-
xs
text
-
foreground
cursor
-
pointer
disabled
:
opacity
-
50
transition
-
colors
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
className={`
w
-
full
appearance
-
none
pl
-
3
pr
-
7
py
-
2
rounded
-
md
bg
-
white
text
-
xs
text
-
[
#
111827
]
cursor
-
pointer
disabled
:
opacity
-
50
transition
-
colors
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
>
{VIDEO_MODEL_OPTIONS.map((m) => (
<option key={m.value} value={m.value}>{m.label}</option>
...
...
@@ -830,7 +889,7 @@ export function StoryboardWorkspace() {
value={selectedRatio}
onChange={(e) => setSelectedRatio(e.target.value)}
disabled={!expanded}
className={`
w
-
full
appearance
-
none
pl
-
3
pr
-
7
py
-
2
rounded
-
lg
bg
-
[
#
f8fafc
]
text
-
xs
text
-
foreground
cursor
-
pointer
disabled
:
opacity
-
50
transition
-
colors
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
className={`
w
-
full
appearance
-
none
pl
-
3
pr
-
7
py
-
2
rounded
-
md
bg
-
white
text
-
xs
text
-
[
#
111827
]
cursor
-
pointer
disabled
:
opacity
-
50
transition
-
colors
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
>
{RATIO_OPTIONS.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option>
...
...
@@ -843,7 +902,7 @@ export function StoryboardWorkspace() {
value={selectedDuration}
onChange={(e) => setSelectedDuration(Number(e.target.value))}
disabled={!expanded}
className={`
appearance
-
none
pl
-
2
pr
-
6
py
-
2
rounded
-
lg
bg
-
[
#
f8fafc
]
text
-
xs
text
-
foreground
cursor
-
pointer
w
-
16
disabled
:
opacity
-
50
transition
-
colors
tabular
-
nums
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
className={`
appearance
-
none
pl
-
2
pr
-
6
py
-
2
rounded
-
md
bg
-
white
text
-
xs
text
-
[
#
111827
]
cursor
-
pointer
w
-
16
disabled
:
opacity
-
50
transition
-
colors
tabular
-
nums
$
{
CONTROL_SURFACE
}
$
{
FOCUS_RING
}
`}
>
{DURATION_OPTIONS.map((d) => (
<option key={d} value={d}>{d}</option>
...
...
@@ -869,7 +928,7 @@ export function StoryboardWorkspace() {
onClick={handleGenerate}
disabled={!canGenerate || generatingVideoId === expanded?.id}
title={!canGenerate ? "需要:至少 1 张参考图(角色/场景/道具任一)+ 提示词" : ""}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-
xl bg-[#1c64ff] text-white text-xs font-semibold tracking-wide shadow-[0_9px_20px_rgba(28,100,255,0.26)] transition-all duration-150 hover:bg-[#1857e8] hover:shadow-[0_12px_24px_rgba(28,100,255,0.32)
] hover:-translate-y-px disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none whitespace-nowrap"
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 ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
...
...
@@ -882,13 +941,13 @@ export function StoryboardWorkspace() {
</div>
{/* ─── Column 4: Big video preview + thumbnail timeline ─── */}
<div className="
flex-1 flex flex-col overflow-hidden min-w-0 bg-white/90
">
<div className="h-1
4 px-5 border-b border-slate-200/80 flex items-center justify-between flex-shrink-0 bg-white/95 shadow-[0_1px_0_rgba(15,23,42,0.03)]
">
<div className="
ml-3 flex-1 flex flex-col overflow-hidden min-w-0 rounded-lg bg-white
">
<div className="h-1
2 px-5 flex items-center justify-between flex-shrink-0 bg-white
">
<span className="text-[13px] font-semibold text-foreground tracking-tight">
视频预览
{previewSb && (
{preview
Display
Sb && (
<span className="ml-2 text-[11px] font-normal text-muted-foreground tabular-nums">
· 分镜
{String(preview
Sb.sequenceNum).padStart(3, "0")}
· 分镜
信息{String(previewDisplay
Sb.sequenceNum).padStart(3, "0")}
</span>
)}
</span>
...
...
@@ -931,20 +990,24 @@ export function StoryboardWorkspace() {
</
div
>
<
/
div
>
<
div
className=
"flex-1 overflow-y-auto p-5 min-h-0 bg-[#f4f7fb]"
>
{
!
previewSb
?
(
<
div
className=
"h-full flex items-center justify-center"
>
<
p
className=
"text-sm text-muted-foreground"
>
展开任意分镜后查看视频
</
p
>
<
div
className=
"flex-1 min-h-0 overflow-hidden bg-white px-5 pb-4 pt-2"
>
{
!
previewDisplaySb
?
(
<
div
className=
"h-full flex flex-col items-center justify-center gap-4"
>
<
div
className=
"flex w-full max-w-[920px] aspect-video flex-col items-center justify-center rounded-lg border border-dashed border-[#d9dee8] bg-[#fbfcff]"
>
<
ImageIcon
className=
"mb-3 h-9 w-9 text-[#98a2b3]"
/>
<
p
className=
"text-sm font-medium text-[#111827]"
>
暂无视频预览
</
p
>
<
p
className=
"mt-1 text-xs text-[#98a2b3]"
>
生成分镜视频后会在这里播放。
</
p
>
</
div
>
</
div
>
)
:
previewTask
?.
status
===
"succeeded"
&&
previewTask
.
resultVideoUrl
?
(
<
div
className=
"
space-y
-3"
>
<
div
className=
"
rounded-2xl overflow-hidden bg-black aspect-video shadow-[0_18px_45px_rgba(15,23,42,0.18
)] ring-1 ring-black/10"
>
<
div
className=
"
flex h-full flex-col justify-center gap
-3"
>
<
div
className=
"
overflow-hidden rounded-lg bg-black shadow-[0_18px_45px_rgba(15,23,42,0.16
)] ring-1 ring-black/10"
>
<
video
key=
{
previewTask
.
resultVideoUrl
}
src=
{
previewTask
.
resultVideoUrl
}
controls
autoPlay=
{
false
}
className=
"
w-full h
-full object-contain"
className=
"
aspect-video w
-full object-contain"
/>
</
div
>
<
a
...
...
@@ -976,10 +1039,27 @@ export function StoryboardWorkspace() {
</
div
>
)
:
(
<
div
className=
"h-full flex flex-col items-center justify-center gap-4"
>
<
div
className=
"w-full max-w-sm aspect-video rounded-2xl border-[1.5px] border-dashed border-slate-300/80 bg-white/90 shadow-[0_10px_30px_rgba(15,23,42,0.05)] flex flex-col items-center justify-center gap-3"
>
<
Play
className=
"w-12 h-12 text-muted-foreground/30"
/>
<
p
className=
"text-sm text-muted-foreground"
>
尚未生成视频
</
p
>
<
p
className=
"text-xs text-muted-foreground/70"
>
在左侧填好素材+提示词后生成
</
p
>
<
div
className=
"relative w-full max-w-[920px] aspect-video overflow-hidden rounded-lg bg-[#101319] shadow-[0_18px_45px_rgba(15,23,42,0.16)]"
>
{
selectedScene
?.
imageUrl
?
(
<
img
src=
{
selectedScene
.
imageUrl
}
alt=
{
selectedScene
.
name
}
className=
"h-full w-full object-cover"
/>
)
:
(
<
div
className=
"flex h-full w-full flex-col items-center justify-center bg-[#fbfcff]"
>
<
ImageIcon
className=
"mb-3 h-9 w-9 text-[#98a2b3]"
/>
<
p
className=
"text-sm font-medium text-[#111827]"
>
暂无生成视频
</
p
>
<
p
className=
"mt-1 text-xs text-[#98a2b3]"
>
点击生成视频后会显示结果。
</
p
>
</
div
>
)
}
{
selectedScene
?.
imageUrl
&&
(
<
div
className=
"absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 to-transparent px-4 pb-3 pt-14"
>
<
div
className=
"flex items-center justify-between text-white"
>
<
div
className=
"flex items-center gap-3 text-xs font-mono"
>
<
Play
className=
"h-4 w-4 fill-white"
/>
<
span
>
0:00 / 0:
{
String
(
selectedDuration
).
padStart
(
2
,
"0"
)
}
</
span
>
</
div
>
<
span
className=
"text-[11px] text-white/75"
>
等待生成
</
span
>
</
div
>
</
div
>
)
}
</
div
>
</
div
>
)
}
...
...
@@ -987,11 +1067,11 @@ export function StoryboardWorkspace() {
{
/* Thumbnail timeline */
}
{
storyboards
.
length
>
0
&&
(
<
div
className=
"b
order-t border-slate-200/80 bg-white/95 flex-shrink-0 shadow-[0_-1px_0_rgba(15,23,42,0.03)]
"
>
<
div
className=
"
px-3 py-3 flex items-center gap-2 overflow-x-auto
"
>
<
div
className=
"b
g-white flex-shrink-0
"
>
<
div
className=
"
flex items-center justify-start gap-3 overflow-x-auto px-3 py-3
"
>
{
storyboards
.
map
((
sb
)
=>
{
const
t
=
getTaskForSb
(
sb
);
const
isActive
=
activeVideoSbId
===
sb
.
id
;
const
isActive
=
activeVideoSbId
===
sb
.
id
||
(
!
activeVideoSbId
&&
previewDisplaySb
?.
id
===
sb
.
id
)
;
const
url
=
t
?.
resultVideoUrl
;
return
(
<
button
...
...
@@ -1001,10 +1081,10 @@ export function StoryboardWorkspace() {
setExpandedId
(
sb
.
id
);
}
}
title=
{
`分镜 ${String(sb.sequenceNum).padStart(3, "0")}`
}
className=
{
`relative flex-shrink-0 w-[
88px] h-[52px] rounded-lg
overflow-hidden border transition-all duration-150 ${
className=
{
`relative flex-shrink-0 w-[
70px] h-[40px] rounded-md
overflow-hidden border transition-all duration-150 ${
isActive
? "border-[#
1c64ff] ring-2 ring-[#1c64ff]/25 shadow-[0_8px_18px_rgba(28,100,255,0.22
)]"
: "border-slate-200 hover:border-[#
1c64
ff]/40 hover:-translate-y-px hover:shadow-sm"
? "border-[#
2f80ff] ring-2 ring-[#2f80ff]/25 shadow-[0_8px_18px_rgba(47,128,255,0.20
)]"
: "border-slate-200 hover:border-[#
2f80
ff]/40 hover:-translate-y-px hover:shadow-sm"
}`
}
>
{
url
?
(
...
...
@@ -1030,6 +1110,8 @@ export function StoryboardWorkspace() {
</
div
>
)
}
</
div
>
<
/
div
>
</
div
>
{
/* ─── Modals ─── */
}
{
showCharacterPicker
&&
expanded
&&
(
...
...
@@ -1117,23 +1199,23 @@ function StoryboardCard(props: StoryboardCardProps) {
const
[
descCollapsed
,
setDescCollapsed
]
=
useState
(
false
);
return
(
<
div
className=
{
`my-2 rounded-
2xl
border transition-all duration-200 ${
<
div
className=
{
`my-2 rounded-
lg
border transition-all duration-200 ${
isExpanded
? "border-[#
1c64ff]/35 bg-white shadow-[0_12px_30px_rgba(15,23,42,0.08)]
"
: "border-
slate-200/80 bg-white/80 hover:bg-white hover:border-[#1c64ff]/25 hover:shadow-[0_8px_18px_rgba(15,23,42,0.05)
]"
? "border-[#
e4e8f0] bg-white shadow-sm
"
: "border-
[#edf0f5] bg-white hover:bg-white hover:border-[#cfd6e4
]"
}`
}
>
<
button
onClick=
{
onToggle
}
className=
"w-full px-3
.5 pt-3 pb-2.5 flex items-center gap-2 text-left rounded-t-2xl
"
className=
"w-full px-3
pt-3 pb-2.5 flex items-center gap-2 text-left rounded-t-lg
"
>
<
span
className=
"text-sm font-semibold text-foreground flex-1 truncate"
>
分镜信息
{
String
(
sb
.
sequenceNum
).
padStart
(
3
,
"0"
)
}
</
span
>
<
span
className=
{
`w-2
.5 h-2.5
rounded-full flex-shrink-0 ring-4 ring-white ${dotClass}`
}
/>
<
span
className=
{
`w-2
h-2
rounded-full flex-shrink-0 ring-4 ring-white ${dotClass}`
}
/>
</
button
>
{
isExpanded
&&
(
<
div
className=
"px-3
.5 pb-3.5 space-y-3.5 border-t border-slate-100
"
>
<
div
className=
"px-3
pb-3 space-y-3 border-t border-[#f1f3f7]
"
>
{
/* 分镜描述 */
}
<
div
>
<
button
...
...
@@ -1151,7 +1233,7 @@ function StoryboardCard(props: StoryboardCardProps) {
value=
{
props
.
shortDescription
}
onChange=
{
(
e
)
=>
props
.
onShortDescriptionChange
(
e
.
target
.
value
)
}
placeholder=
"简要描述此分镜的剧情或画面"
className=
{
`w-full px-3 py-2 rounded-
xl border border-slate-200/80 bg-[#f8fafc] text-xs text-foreground
leading-relaxed resize-none shadow-inner shadow-slate-100/80 ${FOCUS_RING}`
}
className=
{
`w-full px-3 py-2 rounded-
md border border-[#d9dee8] bg-white text-xs text-[#111827]
leading-relaxed resize-none shadow-inner shadow-slate-100/80 ${FOCUS_RING}`
}
/>
)
}
</
div
>
...
...
@@ -1214,7 +1296,7 @@ function CharacterRowList({
characterKeys
,
keyToCharacterRef
,
characterRefOptions
,
onRemove
,
onSwap
,
onAdd
,
}
: CharacterRowListProps)
{
return
(
<
div
className=
"rounded-
xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/7
0"
>
<
div
className=
"rounded-
md bg-white p-
0"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
span
className=
"text-xs font-medium text-foreground"
>
出镜角色
</
span
>
<
button
...
...
@@ -1281,8 +1363,8 @@ function CharacterRow({
const
options
=
allRefOptions
.
filter
((
option
)
=>
option
.
key
===
tosKey
||
!
currentKeys
.
includes
(
option
.
key
));
return
(
<
div
className=
"flex items-center gap-1.5 rounded-
xl bg-white p-1.5 ring-1 ring-slate-200/70
"
>
<
div
className=
"w-
8 h-8 rounded-lg
overflow-hidden border border-slate-200 bg-slate-100 flex-shrink-0"
>
<
div
className=
"flex items-center gap-1.5 rounded-
md bg-white py-1
"
>
<
div
className=
"w-
10 h-10 rounded-md
overflow-hidden border border-slate-200 bg-slate-100 flex-shrink-0"
>
{
url
?
(
<
img
src=
{
url
}
alt=
{
refOption
?.
name
??
""
}
className=
"w-full h-full object-contain bg-white"
/>
)
:
(
...
...
@@ -1293,7 +1375,7 @@ function CharacterRow({
<
select
value=
{
tosKey
}
onChange=
{
(
e
)
=>
onSwap
(
e
.
target
.
value
)
}
className=
{
`w-full appearance-none pl-2 pr-6 py-1.5 rounded-
lg border border-slate-200/80 bg-white text-xs text-foreground
cursor-pointer truncate ${FOCUS_RING}`
}
className=
{
`w-full appearance-none pl-2 pr-6 py-1.5 rounded-
md border border-[#dfe3ee] bg-white text-xs text-[#111827]
cursor-pointer truncate ${FOCUS_RING}`
}
>
{
!
refOption
&&
<
option
value=
{
tosKey
}
>
(已失效)
</
option
>
}
{
options
.
map
((
option
)
=>
(
...
...
@@ -1307,7 +1389,7 @@ function CharacterRow({
<
div
className=
"relative flex-1 min-w-0"
>
<
select
disabled
className=
"w-full appearance-none pl-2 pr-6 py-1.5 rounded-
lg border border-slate-200/80 bg-slate-100/80 text-xs text-muted-foreground
cursor-not-allowed truncate"
className=
"w-full appearance-none pl-2 pr-6 py-1.5 rounded-
md border border-[#dfe3ee] bg-white text-xs text-[#111827]
cursor-not-allowed truncate"
>
<
option
>
未配置音色
</
option
>
</
select
>
...
...
@@ -1315,14 +1397,14 @@ function CharacterRow({
</
div
>
<
button
onClick=
{
onRemove
}
className=
"px-2 py-1.5 rounded-
lg border border-slate-200/80 text-[11px] text-muted-foreground
hover:bg-red-50 hover:text-red-600 transition-colors"
className=
"px-2 py-1.5 rounded-
md border border-[#dfe3ee] text-[11px] text-[#475467]
hover:bg-red-50 hover:text-red-600 transition-colors"
>
删除
</
button
>
<
button
onClick=
{
handleRegen
}
disabled=
{
!
refOption
||
isRegenerating
}
className=
"px-2 py-1.5 rounded-
lg border border-slate-200/80 text-[11px] text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#1c64
ff] disabled:opacity-40 flex items-center gap-1 transition-colors"
className=
"px-2 py-1.5 rounded-
md border border-[#dfe3ee] text-[11px] text-[#475467] hover:bg-[#f6f9ff] hover:text-[#7657
ff] disabled:opacity-40 flex items-center gap-1 transition-colors"
>
{
isRegenerating
&&
<
Loader2
className=
"w-3 h-3 animate-spin"
/>
}
重绘
...
...
@@ -1346,7 +1428,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
const
selected
=
usable
.
find
((
s
)
=>
s
.
imageTosKey
===
selectedKey
)
??
null
;
return
(
<
div
className=
"rounded-
xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/7
0"
>
<
div
className=
"rounded-
md bg-white p-
0"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
span
className=
"text-xs font-medium text-foreground"
>
分镜场景
</
span
>
{
usable
.
length
>
0
&&
(
...
...
@@ -1365,7 +1447,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
type=
"button"
onClick=
{
()
=>
usable
.
length
>
0
&&
setShowPicker
((
v
)
=>
!
v
)
}
disabled=
{
usable
.
length
===
0
}
className=
"w-full aspect-video rounded-
xl overflow-hidden border border-slate-200/80 bg-slate-100 mb-2 block hover:border-[#1c64
ff]/40 hover:shadow-sm transition disabled:cursor-not-allowed"
className=
"w-full aspect-video rounded-
md overflow-hidden border border-[#d9dee8] bg-slate-100 mb-2 block hover:border-[#7657
ff]/40 hover:shadow-sm transition disabled:cursor-not-allowed"
>
{
selected
?.
imageUrl
?
(
<
img
src=
{
selected
.
imageUrl
}
alt=
{
selected
.
name
}
className=
"w-full h-full object-cover"
/>
...
...
@@ -1381,17 +1463,17 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
{
/* Bottom row: scene name + role label + actions menu */
}
<
div
className=
"flex items-center gap-1.5"
>
<
div
className=
"flex-1 min-w-0 px-2.5 py-1.5 rounded-
lg border border-slate-200/80 bg-white text-xs text-foreground
truncate"
>
<
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-
lg border border-slate-200/80
bg-white/80 text-xs text-muted-foreground flex-shrink-0"
>
<
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
>
<
div
className=
"relative flex-shrink-0"
>
<
button
onClick=
{
()
=>
setMenuOpen
((
v
)
=>
!
v
)
}
disabled=
{
!
selected
}
className=
"p-1.5 rounded-
lg border border-slate-200/80 bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#1c64
ff] disabled:opacity-40 transition-colors"
className=
"p-1.5 rounded-
md border border-[#dfe3ee] bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#7657
ff] disabled:opacity-40 transition-colors"
>
<
MoreHorizontal
className=
"w-3.5 h-3.5"
/>
</
button
>
...
...
@@ -1483,7 +1565,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
const
canAddMore
=
selectedKeys
.
length
<
MAX_PROPS
;
return
(
<
div
className=
"rounded-
xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/7
0"
>
<
div
className=
"rounded-
md bg-white p-
0"
>
<
div
className=
"flex items-center justify-between mb-2"
>
<
span
className=
"text-xs font-medium text-foreground"
>
场景道具
</
span
>
<
button
...
...
@@ -1505,7 +1587,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
key=
{
s
.
id
}
onClick=
{
()
=>
onToggle
(
s
.
imageTosKey
!
)
}
title=
{
`移除 ${s.name}`
}
className=
"relative aspect-square rounded-xl overflow-hidden border border-slate-200/80
bg-slate-100 group hover:shadow-sm transition-shadow"
className=
"relative aspect-square rounded-md overflow-hidden border border-[#dfe3ee]
bg-slate-100 group hover:shadow-sm transition-shadow"
>
{
s
.
imageUrl
?
(
<
img
src=
{
s
.
imageUrl
}
alt=
{
s
.
name
}
className=
"w-full h-full object-cover"
/>
...
...
@@ -1524,7 +1606,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
)
}
{
showPicker
&&
(
<
div
className=
"rounded-
xl border border-slate-200/80
bg-white/80 p-2"
>
<
div
className=
"rounded-
md border border-[#dfe3ee]
bg-white/80 p-2"
>
{
available
.
length
===
0
?
(
<
p
className=
"text-[11px] text-muted-foreground italic text-center py-2"
>
{
allCandidates
.
length
===
0
?
"先到「场景」页创建场景"
:
"已添加全部可选道具"
}
...
...
@@ -1540,7 +1622,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
}
}
disabled=
{
!
canAddMore
}
title=
{
s
.
name
}
className=
"relative aspect-square rounded-
lg overflow-hidden border border-slate-200 bg-white hover:border-[#1c64
ff]/40 hover:shadow-sm disabled:opacity-40 transition"
className=
"relative aspect-square rounded-
md overflow-hidden border border-slate-200 bg-white hover:border-[#7657
ff]/40 hover:shadow-sm disabled:opacity-40 transition"
>
{
s
.
imageUrl
?
(
<
img
src=
{
s
.
imageUrl
}
alt=
{
s
.
name
}
className=
"w-full h-full object-cover"
/>
...
...
doc/html/src/styles/theme.css
View file @
74404e9d
...
...
@@ -3,42 +3,42 @@
:root
{
--font-size
:
14px
;
--background
:
#f8f9fa
;
--foreground
:
#1
a1a1a
;
--foreground
:
#1
5131f
;
--card
:
#ffffff
;
--card-foreground
:
#1
a1a1a
;
--card-foreground
:
#1
5131f
;
--popover
:
#ffffff
;
--popover-foreground
:
#1
a1a1a
;
--primary
:
#5
b5ff9
;
--popover-foreground
:
#1
5131f
;
--primary
:
#5
337c7
;
--primary-foreground
:
#ffffff
;
--secondary
:
#f
1f3f5
;
--secondary-foreground
:
#
1a1a1a
;
--secondary
:
#f
3eefe
;
--secondary-foreground
:
#
262238
;
--muted
:
#f8f9fa
;
--muted-foreground
:
#
6c757d
;
--accent
:
#
e7f5ff
;
--accent-foreground
:
#
1971c2
;
--muted-foreground
:
#
4b5864
;
--accent
:
#
f3eefe
;
--accent-foreground
:
#
5337c7
;
--destructive
:
#f03e3e
;
--destructive-foreground
:
#ffffff
;
--border
:
rgba
(
0
,
0
,
0
,
0.06
)
;
--input
:
rgba
(
0
,
0
,
0
,
0.06
)
;
--border
:
#e6eaed
;
--input
:
#d8dae9
;
--input-background
:
#ffffff
;
--switch-background
:
#dee2e6
;
--font-weight-medium
:
500
;
--font-weight-normal
:
400
;
--ring
:
#5
b5ff9
;
--chart-1
:
#5
b5ff9
;
--chart-2
:
#
22b8cf
;
--chart-3
:
#
51cf66
;
--chart-4
:
#f
f6b6b
;
--chart-5
:
#f
cc419
;
--radius
:
0.
7
5rem
;
--ring
:
#5
337c7
;
--chart-1
:
#5
337c7
;
--chart-2
:
#
36bffa
;
--chart-3
:
#
12b76a
;
--chart-4
:
#f
04438
;
--chart-5
:
#f
db022
;
--radius
:
0.
62
5rem
;
--sidebar
:
#ffffff
;
--sidebar-foreground
:
#1
a1a1a
;
--sidebar-primary
:
#5
b5ff9
;
--sidebar-foreground
:
#1
5131f
;
--sidebar-primary
:
#5
337c7
;
--sidebar-primary-foreground
:
#ffffff
;
--sidebar-accent
:
#f
8f9fa
;
--sidebar-accent-foreground
:
#
1a1a1a
;
--sidebar-border
:
rgba
(
0
,
0
,
0
,
0.06
)
;
--sidebar-ring
:
#5
b5ff9
;
--sidebar-accent
:
#f
3eefe
;
--sidebar-accent-foreground
:
#
5337c7
;
--sidebar-border
:
#e6eaed
;
--sidebar-ring
:
#5
337c7
;
}
.dark
{
...
...
@@ -188,3 +188,26 @@
line-height
:
1.5
;
}
}
@layer
components
{
.dramix-surface
{
background
:
#f8f9fa
;
}
.dramix-panel
{
border
:
1px
solid
#e6eaed
;
background
:
#ffffff
;
box-shadow
:
0
10px
30px
rgba
(
37
,
31
,
63
,
0.05
);
}
.dramix-primary
{
background
:
#8064f1
;
color
:
#ffffff
;
box-shadow
:
0
10px
22px
rgba
(
83
,
55
,
199
,
0.18
);
}
.dramix-primary
:hover
{
background
:
#7357e8
;
box-shadow
:
0
14px
28px
rgba
(
83
,
55
,
199
,
0.24
);
}
}
docs/superpowers/specs/2026-05-25-doc-html-figma-handoff.md
0 → 100644
View file @
74404e9d
# doc/html Figma 对齐交接记录
日期:2026-05-25
## 项目范围
-
前端项目:
`doc/html`
-
Figma 文件:
`https://www.figma.com/design/sRNoRpGZrFlmC2Du8SOIXw/AI%E6%BC%AB%E5%89%A7%E5%B9%B3%E5%8F%B0`
-
主要目标:把
`doc/html`
的 AI 漫剧工作流页面尽量对齐 Figma 设计稿。
-
本地后端接口可用时,前端
`.env.local`
指向
`http://localhost:8080`
。
## Figma 覆盖的主要页面
Figma MCP 已确认主工作流 section 内包含:
-
`首页`
-
`剧本1`
、
`剧本2`
、
`剧本3`
、
`剧本4`
-
`角色库`
-
`新增角色`
-
`场景库`
-
`场景库-空`
-
`道具库`
-
`分镜1`
、
`分镜2`
、
`分镜3`
、
`分镜4`
、
`分镜5`
-
`视频`
-
`余量统计`
本地导入参考图位于:
-
`doc/html/src/imports/1项目入口.png`
-
`doc/html/src/imports/2选择风格.png`
-
`doc/html/src/imports/3生成大纲.png`
-
`doc/html/src/imports/3生成大纲2.png`
-
`doc/html/src/imports/4生成分集.png`
-
`doc/html/src/imports/5生成角色.png`
-
`doc/html/src/imports/6生成场景.png`
-
`doc/html/src/imports/7生成道具.png`
-
`doc/html/src/imports/8生成每集分镜.png`
-
`doc/html/src/imports/10生成视频.png`
## 已完成或已接近 Figma 的页面
### Layout
文件:
`doc/html/src/app/components/Layout.tsx`
-
顶部品牌改为
`Dramix.ai`
。
-
项目页工作流导航已改成 Figma 风格:
`剧本 / 设定 / 分镜 / 视频`
。
-
右侧加入类似 Figma 的优化建议和余额区域。
### 项目入口 / 新建项目
文件:
`doc/html/src/app/pages/NewProject.tsx`
-
已接近 Figma 的项目入口方向。
-
上传剧本区域、创建中遮罩、模式切换已有较完整结构。
### 剧本展示页
文件:
`doc/html/src/app/pages/ProjectDetail.tsx`
-
`/project/:projectId`
已改成接近
`3生成大纲.png`
和
`4生成分集.png`
的阅读式页面。
-
结构包括:封面、标题信息、标签、大纲、人物小传、分集剧情按钮网格、底部黑色 CTA。
-
分集按钮可切换显示当前集内容。
### 分镜工作台
文件:
`doc/html/src/app/pages/StoryboardWorkspace.tsx`
-
已按 Figma
`分镜1-5`
做过主结构改造。
-
已修复视频预览区第一条分镜
`001`
横向滚动看不到的问题。
-
已取消无数据时的“垫图/假分镜”:
-
没有分集时显示“暂无分集”
-
没有分镜时显示“暂无分镜数据”
-
视频生成区没有真实分镜时显示空状态
-
视频预览没有真实视频时显示空状态
-
缩略图只在真实
`storyboards`
存在时显示
### 角色编辑弹窗
文件:
`doc/html/src/app/pages/CharacterGeneration.tsx`
-
角色图片已从正面/侧面/背面三个上传框改成单张角色图。
-
编辑弹窗新增
`角色图片提示词`
。
-
提示词支持中文显示和一键生成中文提示词。
-
保存仍写回后端
`imagePrompt`
字段。
-
上传单张图时仍走现有
`front`
兼容接口,后端会同步主图字段。
## 还没优化到位的页面
### 选择风格页
文件:
`doc/html/src/app/pages/StyleSelection.tsx`
-
对应
`2选择风格.png`
。
-
当前仍是旧的大卡片网格 + 紫色渐变按钮。
-
需要对齐 Figma 的工作流页头和轻量选择布局。
### 大纲生成过程页
文件:
`doc/html/src/app/pages/OutlineGeneration.tsx`
-
对应 Figma
`剧本1 / 剧本2`
的生成过程。
-
当前是功能型处理页,有大 loading 卡、进度卡、手动编辑区。
-
视觉还未完全贴 Figma。
### 分集独立页
文件:
`doc/html/src/app/pages/EpisodeGeneration.tsx`
-
对应
`剧本4 / 生成分集`
。
-
`/project/:projectId`
已经做了 Figma 风格分集展示,但这个独立路由仍是旧列表页。
### 角色库列表页
文件:
`doc/html/src/app/pages/CharacterGeneration.tsx`
-
只优化了编辑弹窗。
-
角色列表本体仍有旧统计条、旧卡片和旧 tab。
-
需要对齐 Figma
`角色库`
与
`新增角色`
。
### 场景库页
文件:
`doc/html/src/app/pages/SceneGeneration.tsx`
-
对应 Figma 多个
`场景库 / 场景库-空`
节点。
-
当前仍是旧的“项目设定 + 统计条 + 三列卡片”结构。
-
这是下一轮建议最高优先级页面。
### 道具库页
文件:
`doc/html/src/app/pages/PropsGeneration.tsx`
-
对应
`道具库`
。
-
当前仍是旧资产库风格,并且接的是 global assets。
-
需要按 Figma 道具库重新组织布局。
### 视频页
文件:
`doc/html/src/app/pages/VideoGeneration.tsx`
-
对应
`10生成视频.png`
/ Figma
`视频`
。
-
当前还是旧的左选集 + 播放器 + 分镜列表结构。
-
需要整体对齐 Figma 视频管理/预览页。
## 推荐下一步顺序
1.
优先优化
`SceneGeneration.tsx`
2.
优化
`CharacterGeneration.tsx`
的角色列表主体
3.
优化
`VideoGeneration.tsx`
4.
优化
`StyleSelection.tsx`
和
`OutlineGeneration.tsx`
5.
优化
`PropsGeneration.tsx`
6.
最后处理
`EpisodeGeneration.tsx`
,或考虑让它跳转/复用
`ProjectDetail.tsx`
的分集区域
## 注意事项
-
不要回滚这些已有改动:
-
`doc/html/src/app/components/Layout.tsx`
-
`doc/html/src/app/pages/ProjectDetail.tsx`
-
`doc/html/src/app/pages/StoryboardWorkspace.tsx`
-
`doc/html/src/app/pages/CharacterGeneration.tsx`
-
`doc/html/src/app/pages/NewProject.tsx`
-
`doc/html/src/app/pages/Dashboard.tsx`
-
`doc/html/src/styles/theme.css`
-
当前工作区里有用户已有修改,继续开发前应先看
`git status --short`
。
-
验证命令:在
`doc/html`
下执行
`npm run build`
。
-
不要把账号密码写进仓库文件。需要登录时由用户重新提供,或使用当前会话上下文。
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