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
175f406c
Commit
175f406c
authored
Apr 21, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
项目编辑风格和三视图
parent
1e9c434e
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
328 additions
and
99 deletions
+328
-99
CharacterGeneration.tsx
doc/html/src/app/pages/CharacterGeneration.tsx
+0
-0
Dashboard.tsx
doc/html/src/app/pages/Dashboard.tsx
+73
-7
NewProject.tsx
doc/html/src/app/pages/NewProject.tsx
+0
-0
OutlineGeneration.tsx
doc/html/src/app/pages/OutlineGeneration.tsx
+0
-0
ProjectDetail.tsx
doc/html/src/app/pages/ProjectDetail.tsx
+5
-2
ProjectSettings.tsx
doc/html/src/app/pages/ProjectSettings.tsx
+22
-2
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+79
-20
StyleSelection.tsx
doc/html/src/app/pages/StyleSelection.tsx
+24
-63
useAi.ts
doc/html/src/hooks/useAi.ts
+5
-1
ai.ts
doc/html/src/lib/api/ai.ts
+27
-4
projects.ts
doc/html/src/lib/api/projects.ts
+7
-0
projectStyles.ts
doc/html/src/lib/projectStyles.ts
+86
-0
No files found.
doc/html/src/app/pages/CharacterGeneration.tsx
View file @
175f406c
This diff is collapsed.
Click to expand it.
doc/html/src/app/pages/Dashboard.tsx
View file @
175f406c
...
@@ -9,6 +9,14 @@ import { useTeamMembers } from "../../hooks/useTeam";
...
@@ -9,6 +9,14 @@ import { useTeamMembers } from "../../hooks/useTeam";
import
{
projectsApi
}
from
"../../lib/api/projects"
;
import
{
projectsApi
}
from
"../../lib/api/projects"
;
import
{
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
useQueryClient
}
from
"@tanstack/react-query"
;
import
type
{
ProjectDTO
}
from
"../../lib/api/projects"
;
import
type
{
ProjectDTO
}
from
"../../lib/api/projects"
;
import
{
aspectRatioOptions
,
getAspectRatioLabel
,
getProjectStyleLabel
,
getResolutionLabel
,
projectStyleOptions
,
resolutionOptions
,
}
from
"../../lib/projectStyles"
;
// ── Default cover generator ─────────────────────────────────────────────────
// ── Default cover generator ─────────────────────────────────────────────────
function
getProjectGradient
(
name
:
string
):
string
{
function
getProjectGradient
(
name
:
string
):
string
{
...
@@ -48,6 +56,9 @@ function EditModal({
...
@@ -48,6 +56,9 @@ function EditModal({
const
qc
=
useQueryClient
();
const
qc
=
useQueryClient
();
const
updateProject
=
useUpdateProject
(
project
.
id
);
const
updateProject
=
useUpdateProject
(
project
.
id
);
const
[
name
,
setName
]
=
useState
(
project
.
name
);
const
[
name
,
setName
]
=
useState
(
project
.
name
);
const
[
style
,
setStyle
]
=
useState
(
project
.
style
??
""
);
const
[
aspectRatio
,
setAspectRatio
]
=
useState
(
project
.
aspectRatio
??
""
);
const
[
resolution
,
setResolution
]
=
useState
(
project
.
resolution
??
""
);
const
[
coverFile
,
setCoverFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
coverFile
,
setCoverFile
]
=
useState
<
File
|
null
>
(
null
);
const
[
coverPreview
,
setCoverPreview
]
=
useState
<
string
>
(
project
.
coverUrl
??
""
);
const
[
coverPreview
,
setCoverPreview
]
=
useState
<
string
>
(
project
.
coverUrl
??
""
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
const
[
saving
,
setSaving
]
=
useState
(
false
);
...
@@ -64,11 +75,12 @@ function EditModal({
...
@@ -64,11 +75,12 @@ function EditModal({
if
(
!
name
.
trim
())
return
;
if
(
!
name
.
trim
())
return
;
setSaving
(
true
);
setSaving
(
true
);
try
{
try
{
// Update name if changed
await
updateProject
.
mutateAsync
({
if
(
name
.
trim
()
!==
project
.
name
)
{
name
:
name
.
trim
(),
await
updateProject
.
mutateAsync
({
name
:
name
.
trim
()
});
style
:
style
||
undefined
,
}
aspectRatio
:
aspectRatio
||
undefined
,
// Upload new cover if selected
resolution
:
resolution
||
undefined
,
});
if
(
coverFile
)
{
if
(
coverFile
)
{
await
projectsApi
.
uploadAsset
(
project
.
id
,
coverFile
,
"cover"
);
await
projectsApi
.
uploadAsset
(
project
.
id
,
coverFile
,
"cover"
);
qc
.
invalidateQueries
({
queryKey
:
[
"projects"
]
});
qc
.
invalidateQueries
({
queryKey
:
[
"projects"
]
});
...
@@ -137,6 +149,54 @@ function EditModal({
...
@@ -137,6 +149,54 @@ function EditModal({
maxLength=
{
200
}
maxLength=
{
200
}
/>
/>
</
div
>
</
div
>
<
div
className=
"grid grid-cols-3 gap-3"
>
<
div
>
<
label
className=
"block text-xs text-muted-foreground mb-2"
>
视觉风格
</
label
>
<
select
value=
{
style
}
onChange=
{
(
e
)
=>
setStyle
(
e
.
target
.
value
)
}
className=
"w-full px-3 py-2.5 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
>
<
option
value=
""
>
未设置
</
option
>
{
projectStyleOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
option
.
label
}
</
option
>
))
}
</
select
>
</
div
>
<
div
>
<
label
className=
"block text-xs text-muted-foreground mb-2"
>
画幅比例
</
label
>
<
select
value=
{
aspectRatio
}
onChange=
{
(
e
)
=>
setAspectRatio
(
e
.
target
.
value
)
}
className=
"w-full px-3 py-2.5 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
>
<
option
value=
""
>
未设置
</
option
>
{
aspectRatioOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
option
.
label
}
{
option
.
helper
??
""
}
</
option
>
))
}
</
select
>
</
div
>
<
div
>
<
label
className=
"block text-xs text-muted-foreground mb-2"
>
清晰度
</
label
>
<
select
value=
{
resolution
}
onChange=
{
(
e
)
=>
setResolution
(
e
.
target
.
value
)
}
className=
"w-full px-3 py-2.5 rounded-lg border border-border bg-background text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
>
<
option
value=
""
>
未设置
</
option
>
{
resolutionOptions
.
map
((
option
)
=>
(
<
option
key=
{
option
.
value
}
value=
{
option
.
value
}
>
{
option
.
label
}
{
option
.
helper
??
""
}
</
option
>
))
}
</
select
>
</
div
>
</
div
>
</
div
>
</
div
>
{
/* Footer */
}
{
/* Footer */
}
...
@@ -249,7 +309,11 @@ export function Dashboard() {
...
@@ -249,7 +309,11 @@ export function Dashboard() {
updatedAt
:
p
.
updatedAt
?.
slice
(
0
,
10
)
??
""
,
updatedAt
:
p
.
updatedAt
?.
slice
(
0
,
10
)
??
""
,
createdAt
:
p
.
createdAt
?.
slice
(
0
,
10
)
??
""
,
createdAt
:
p
.
createdAt
?.
slice
(
0
,
10
)
??
""
,
coverUrl
:
p
.
coverUrl
,
coverUrl
:
p
.
coverUrl
,
tags
:
p
.
style
?
[
p
.
style
]
:
[],
tags
:
[
{
key
:
"style"
,
label
:
getProjectStyleLabel
(
p
.
style
)
},
{
key
:
"aspectRatio"
,
label
:
getAspectRatioLabel
(
p
.
aspectRatio
)
},
{
key
:
"resolution"
,
label
:
getResolutionLabel
(
p
.
resolution
)
},
].
filter
((
tag
)
=>
tag
.
label
!==
"未设置"
),
}));
}));
const
filtered
=
projects
.
filter
((
p
)
=>
{
const
filtered
=
projects
.
filter
((
p
)
=>
{
...
@@ -435,7 +499,9 @@ export function Dashboard() {
...
@@ -435,7 +499,9 @@ export function Dashboard() {
{
project
.
tags
.
length
>
0
&&
(
{
project
.
tags
.
length
>
0
&&
(
<
div
className=
"flex flex-wrap gap-1.5 mb-3"
>
<
div
className=
"flex flex-wrap gap-1.5 mb-3"
>
{
project
.
tags
.
slice
(
0
,
3
).
map
((
tag
)
=>
(
{
project
.
tags
.
slice
(
0
,
3
).
map
((
tag
)
=>
(
<
span
key=
{
tag
}
className=
"px-2 py-0.5 rounded bg-muted text-xs text-foreground"
>
{
tag
}
</
span
>
<
span
key=
{
tag
.
key
}
className=
"px-2 py-0.5 rounded bg-muted text-xs text-foreground"
>
{
tag
.
label
}
</
span
>
))
}
))
}
</
div
>
</
div
>
)
}
)
}
...
...
doc/html/src/app/pages/NewProject.tsx
View file @
175f406c
This diff is collapsed.
Click to expand it.
doc/html/src/app/pages/OutlineGeneration.tsx
View file @
175f406c
This diff is collapsed.
Click to expand it.
doc/html/src/app/pages/ProjectDetail.tsx
View file @
175f406c
...
@@ -271,6 +271,9 @@ function CharactersTab({
...
@@ -271,6 +271,9 @@ function CharactersTab({
onNavigateChars
:
()
=>
void
;
onNavigateChars
:
()
=>
void
;
onNavigateScenes
:
()
=>
void
;
onNavigateScenes
:
()
=>
void
;
})
{
})
{
const
getMainCharacterImage
=
(
char
:
import
(
"../../lib/api/ai"
).
Character
)
=>
char
.
frontImageUrl
??
char
.
imageUrl
??
char
.
sideImageUrl
??
char
.
backImageUrl
??
null
;
return
(
return
(
<
div
className=
"space-y-8"
>
<
div
className=
"space-y-8"
>
{
/* Characters */
}
{
/* Characters */
}
...
@@ -306,8 +309,8 @@ function CharactersTab({
...
@@ -306,8 +309,8 @@ function CharactersTab({
className=
"rounded-xl border border-border bg-card p-4 text-center cursor-pointer hover:border-primary transition-colors"
className=
"rounded-xl border border-border bg-card p-4 text-center cursor-pointer hover:border-primary transition-colors"
>
>
<
div
className=
"w-20 h-20 rounded-full overflow-hidden mx-auto mb-3 bg-muted flex items-center justify-center"
>
<
div
className=
"w-20 h-20 rounded-full overflow-hidden mx-auto mb-3 bg-muted flex items-center justify-center"
>
{
char
.
imageUrl
?
(
{
getMainCharacterImage
(
char
)
?
(
<
img
src=
{
char
.
imageUrl
}
alt=
{
char
.
name
}
className=
"w-full h-full object-cover"
/>
<
img
src=
{
getMainCharacterImage
(
char
)
??
""
}
alt=
{
char
.
name
}
className=
"w-full h-full object-cover"
/>
)
:
(
)
:
(
<
ImageIcon
className=
"w-8 h-8 text-muted-foreground"
/>
<
ImageIcon
className=
"w-8 h-8 text-muted-foreground"
/>
)
}
)
}
...
...
doc/html/src/app/pages/ProjectSettings.tsx
View file @
175f406c
...
@@ -5,6 +5,14 @@ import {
...
@@ -5,6 +5,14 @@ import {
}
from
"lucide-react"
;
}
from
"lucide-react"
;
import
{
useTeamMembers
,
useUpdateMember
,
useRemoveMember
,
useAddMember
}
from
"../../hooks/useTeam"
;
import
{
useTeamMembers
,
useUpdateMember
,
useRemoveMember
,
useAddMember
}
from
"../../hooks/useTeam"
;
import
{
useProject
,
useUpdateProject
}
from
"../../hooks/useProjects"
;
import
{
useProject
,
useUpdateProject
}
from
"../../hooks/useProjects"
;
import
{
aspectRatioOptions
,
getAspectRatioLabel
,
getProjectStyleLabel
,
getResolutionLabel
,
projectStyleOptions
,
resolutionOptions
,
}
from
"../../lib/projectStyles"
;
const
ROLES
=
[
"owner"
,
"admin"
,
"member"
]
as
const
;
const
ROLES
=
[
"owner"
,
"admin"
,
"member"
]
as
const
;
type
Role
=
typeof
ROLES
[
number
];
type
Role
=
typeof
ROLES
[
number
];
...
@@ -37,10 +45,22 @@ export function ProjectSettings() {
...
@@ -37,10 +45,22 @@ export function ProjectSettings() {
const
[
inviteError
,
setInviteError
]
=
useState
(
""
);
const
[
inviteError
,
setInviteError
]
=
useState
(
""
);
const
[
editingProject
,
setEditingProject
]
=
useState
(
false
);
const
[
editingProject
,
setEditingProject
]
=
useState
(
false
);
const
[
projectForm
,
setProjectForm
]
=
useState
({
name
:
""
,
description
:
""
});
const
[
projectForm
,
setProjectForm
]
=
useState
({
name
:
""
,
description
:
""
,
style
:
""
,
aspectRatio
:
""
,
resolution
:
""
,
});
const
openEditProject
=
()
=>
{
const
openEditProject
=
()
=>
{
setProjectForm
({
name
:
project
?.
name
??
""
,
description
:
project
?.
description
??
""
});
setProjectForm
({
name
:
project
?.
name
??
""
,
description
:
project
?.
description
??
""
,
style
:
project
?.
style
??
""
,
aspectRatio
:
project
?.
aspectRatio
??
""
,
resolution
:
project
?.
resolution
??
""
,
});
setEditingProject
(
true
);
setEditingProject
(
true
);
};
};
...
...
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
175f406c
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useState
,
useEffect
,
useMemo
}
from
"react"
;
import
{
useParams
,
useNavigate
}
from
"react-router"
;
import
{
useParams
,
useNavigate
}
from
"react-router"
;
import
{
import
{
Plus
,
Wand2
,
Play
,
Trash2
,
Copy
,
Plus
,
Wand2
,
Play
,
Trash2
,
Copy
,
...
@@ -69,6 +69,9 @@ export function StoryboardWorkspace() {
...
@@ -69,6 +69,9 @@ export function StoryboardWorkspace() {
const
[
generatingVideoId
,
setGeneratingVideoId
]
=
useState
<
string
|
null
>
(
null
);
const
[
generatingVideoId
,
setGeneratingVideoId
]
=
useState
<
string
|
null
>
(
null
);
const
[
batchGenerating
,
setBatchGenerating
]
=
useState
(
false
);
const
[
batchGenerating
,
setBatchGenerating
]
=
useState
(
false
);
const
getCharacterPrimaryImage
=
(
character
:
Character
)
=>
character
.
frontImageUrl
??
character
.
imageUrl
??
character
.
sideImageUrl
??
character
.
backImageUrl
??
null
;
// Reset selection when episode changes
// Reset selection when episode changes
useEffect
(()
=>
{
useEffect
(()
=>
{
setSelected
(
null
);
setSelected
(
null
);
...
@@ -171,11 +174,58 @@ export function StoryboardWorkspace() {
...
@@ -171,11 +174,58 @@ export function StoryboardWorkspace() {
setPromptChanged
(
true
);
setPromptChanged
(
true
);
};
};
// 所有有 imageTosKey 的角色/场景,按顺序编号为 图1, 图2...
// 所有可引用的角色/场景参考图,角色支持正面/侧面/背面三视图。
const
refImages
:
Array
<
{
label
:
string
;
name
:
string
;
imageUrl
:
string
|
null
;
imageTosKey
:
string
}
>
=
[
const
refImages
=
useMemo
<
Array
<
{
...
characters
.
filter
((
c
)
=>
c
.
imageTosKey
).
map
((
c
)
=>
({
label
:
c
.
name
,
name
:
c
.
name
,
imageUrl
:
c
.
imageUrl
,
imageTosKey
:
c
.
imageTosKey
!
})),
label
:
string
;
...
scenes
.
filter
((
s
)
=>
s
.
imageTosKey
).
map
((
s
)
=>
({
label
:
s
.
name
,
name
:
s
.
name
,
imageUrl
:
s
.
imageUrl
,
imageTosKey
:
s
.
imageTosKey
!
})),
name
:
string
;
];
imageUrl
:
string
|
null
;
imageTosKey
:
string
;
kind
:
"character"
|
"scene"
;
viewLabel
?:
string
;
}
>>
(()
=>
{
const
characterRefImages
=
characters
.
flatMap
((
character
)
=>
{
const
views
=
[
{
viewLabel
:
"正面"
,
imageUrl
:
character
.
frontImageUrl
??
character
.
imageUrl
??
null
,
imageTosKey
:
character
.
frontImageTosKey
??
character
.
imageTosKey
??
null
,
},
{
viewLabel
:
"侧面"
,
imageUrl
:
character
.
sideImageUrl
??
null
,
imageTosKey
:
character
.
sideImageTosKey
??
null
,
},
{
viewLabel
:
"背面"
,
imageUrl
:
character
.
backImageUrl
??
null
,
imageTosKey
:
character
.
backImageTosKey
??
null
,
},
];
return
views
.
filter
((
view
)
=>
!!
view
.
imageTosKey
)
.
map
((
view
)
=>
({
label
:
`
${
character
.
name
}
·
${
view
.
viewLabel
}
`
,
name
:
character
.
name
,
imageUrl
:
view
.
imageUrl
,
imageTosKey
:
view
.
imageTosKey
!
,
kind
:
"character"
as
const
,
viewLabel
:
view
.
viewLabel
,
}));
});
const
sceneRefImages
=
scenes
.
filter
((
scene
)
=>
!!
scene
.
imageTosKey
)
.
map
((
scene
)
=>
({
label
:
scene
.
name
,
name
:
scene
.
name
,
imageUrl
:
scene
.
imageUrl
,
imageTosKey
:
scene
.
imageTosKey
!
,
kind
:
"scene"
as
const
,
}));
return
[...
characterRefImages
,
...
sceneRefImages
];
},
[
characters
,
scenes
]);
const
insertMention
=
(
name
:
string
)
=>
{
const
insertMention
=
(
name
:
string
)
=>
{
const
mention
=
`@
${
name
}
`
;
const
mention
=
`@
${
name
}
`
;
...
@@ -459,7 +509,7 @@ export function StoryboardWorkspace() {
...
@@ -459,7 +509,7 @@ export function StoryboardWorkspace() {
onClick=
{
()
=>
insertMention
(
c
.
name
)
}
onClick=
{
()
=>
insertMention
(
c
.
name
)
}
className=
"flex items-center gap-1 px-2 py-0.5 rounded-full bg-violet-50 dark:bg-violet-900/20 border border-violet-200 dark:border-violet-700 text-[11px] text-violet-700 dark:text-violet-300 hover:bg-violet-100 transition"
className=
"flex items-center gap-1 px-2 py-0.5 rounded-full bg-violet-50 dark:bg-violet-900/20 border border-violet-200 dark:border-violet-700 text-[11px] text-violet-700 dark:text-violet-300 hover:bg-violet-100 transition"
>
>
{
c
.
imageUrl
&&
<
img
src=
{
c
.
imageUrl
}
className=
"w-3.5 h-3.5 rounded-full object-cover"
/>
}
{
getCharacterPrimaryImage
(
c
)
&&
<
img
src=
{
getCharacterPrimaryImage
(
c
)
??
""
}
className=
"w-3.5 h-3.5 rounded-full object-cover"
/>
}
@
{
c
.
name
}
@
{
c
.
name
}
</
button
>
</
button
>
))
}
))
}
...
@@ -480,7 +530,9 @@ export function StoryboardWorkspace() {
...
@@ -480,7 +530,9 @@ export function StoryboardWorkspace() {
{
/* @图N:参考图面板 */
}
{
/* @图N:参考图面板 */
}
{
refImages
.
length
>
0
&&
(
{
refImages
.
length
>
0
&&
(
<
div
className=
"mt-3"
>
<
div
className=
"mt-3"
>
<
div
className=
"text-[11px] text-muted-foreground mb-1.5"
>
@图N 引用参考图(生成视频时跳过文生图,直接用此图)
</
div
>
<
div
className=
"text-[11px] text-muted-foreground mb-1.5"
>
@图N 引用参考图。角色支持正面、侧面、背面三视图,生成视频时会直接使用所选参考图。
</
div
>
<
div
className=
"flex gap-2 flex-wrap"
>
<
div
className=
"flex gap-2 flex-wrap"
>
{
refImages
.
map
((
img
,
idx
)
=>
(
{
refImages
.
map
((
img
,
idx
)
=>
(
<
button
<
button
...
@@ -488,18 +540,25 @@ export function StoryboardWorkspace() {
...
@@ -488,18 +540,25 @@ export function StoryboardWorkspace() {
onClick=
{
()
=>
insertRefImage
(
idx
)
}
onClick=
{
()
=>
insertRefImage
(
idx
)
}
className=
"flex flex-col items-center gap-0.5 group"
className=
"flex flex-col items-center gap-0.5 group"
title=
{
`点击插入 @图${idx + 1}(${img.label})`
}
title=
{
`点击插入 @图${idx + 1}(${img.label})`
}
>
>
<
div
className=
"w-12 h-12 rounded-lg overflow-hidden border border-border group-hover:border-primary transition bg-muted"
>
<
div
className=
"w-12 h-12 rounded-lg overflow-hidden border border-border group-hover:border-primary transition bg-muted"
>
{
img
.
imageUrl
?
(
{
img
.
imageUrl
?
(
<
img
src=
{
img
.
imageUrl
}
alt=
{
img
.
label
}
className=
"w-full h-full object-cover"
/>
<
img
src=
{
img
.
imageUrl
}
alt=
{
img
.
label
}
className=
"w-full h-full object-cover"
/>
)
:
(
)
:
(
<
div
className=
"w-full h-full flex items-center justify-center text-muted-foreground text-[10px]"
>
无图
</
div
>
<
div
className=
"w-full h-full flex items-center justify-center text-muted-foreground text-[10px]"
>
无图
</
div
>
)
}
)
}
</
div
>
</
div
>
<
span
className=
"text-[10px] text-primary font-medium"
>
@图
{
idx
+
1
}
</
span
>
<
span
className=
"text-[10px] text-primary font-medium"
>
@图
{
idx
+
1
}
</
span
>
<
span
className=
"text-[9px] text-muted-foreground leading-none max-w-[48px] truncate"
>
{
img
.
label
}
</
span
>
<
span
className=
"text-[9px] text-muted-foreground leading-none max-w-[56px] truncate"
>
{
img
.
label
}
</
span
>
</
button
>
<
span
))
}
className=
{
`text-[9px] leading-none ${
img.kind === "character" ? "text-violet-600" : "text-sky-600"
}`
}
>
{
img
.
kind
===
"character"
?
img
.
viewLabel
:
"场景"
}
</
span
>
</
button
>
))
}
</
div
>
</
div
>
</
div
>
</
div
>
)
}
)
}
...
...
doc/html/src/app/pages/StyleSelection.tsx
View file @
175f406c
import
{
useState
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
{
useNavigate
,
useParams
}
from
"react-router"
;
import
{
useNavigate
,
useParams
}
from
"react-router"
;
import
{
Check
,
ArrowRight
,
Loader2
}
from
"lucide-react"
;
import
{
ArrowRight
,
Check
,
Loader2
}
from
"lucide-react"
;
import
{
useUpdateProject
}
from
"../../hooks/useProjects"
;
import
{
useUpdateProject
}
from
"../../hooks/useProjects"
;
import
{
projectStyleOptions
}
from
"../../lib/projectStyles"
;
const
styles
=
[
{
id
:
"urban"
,
name
:
"都市情感"
,
description
:
"现代都市背景,情感纠葛,职场爱情"
,
image
:
"https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=400&h=300&fit=crop"
,
},
{
id
:
"fantasy"
,
name
:
"玄幻修仙"
,
description
:
"仙侠世界观,修炼升级,恢弘场景"
,
image
:
"https://images.unsplash.com/photo-1518709268805-4e9042af9f23?w=400&h=300&fit=crop"
,
},
{
id
:
"romance"
,
name
:
"甜宠恋爱"
,
description
:
"校园青春,浪漫爱情,轻松甜蜜"
,
image
:
"https://images.unsplash.com/photo-1522529599102-193c0d76b5b6?w=400&h=300&fit=crop"
,
},
{
id
:
"historical"
,
name
:
"古装宫廷"
,
description
:
"古代背景,宫廷权谋,历史传奇"
,
image
:
"https://images.unsplash.com/photo-1548198264-8fc644a0c8f1?w=400&h=300&fit=crop"
,
},
{
id
:
"suspense"
,
name
:
"悬疑推理"
,
description
:
"烧脑剧情,逻辑推理,紧张刺激"
,
image
:
"https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=400&h=300&fit=crop"
,
},
{
id
:
"comedy"
,
name
:
"轻喜剧"
,
description
:
"幽默搞笑,轻松愉快,生活趣事"
,
image
:
"https://images.unsplash.com/photo-1511632765486-a01980e01a18?w=400&h=300&fit=crop"
,
},
];
export
function
StyleSelection
()
{
export
function
StyleSelection
()
{
const
navigate
=
useNavigate
();
const
navigate
=
useNavigate
();
...
@@ -56,58 +18,57 @@ export function StyleSelection() {
...
@@ -56,58 +18,57 @@ export function StyleSelection() {
return
(
return
(
<
div
className=
"h-full overflow-auto bg-background p-6"
>
<
div
className=
"h-full overflow-auto bg-background p-6"
>
<
div
className=
"max-w-6xl mx-auto"
>
<
div
className=
"mx-auto max-w-6xl"
>
{
/* Header */
}
<
div
className=
"mb-8 text-center"
>
<
div
className=
"mb-8 text-center"
>
<
h1
className=
"text-3xl font-semibold text-foreground mb-2"
>
选择剧集风格
</
h1
>
<
h1
className=
"mb-2 text-3xl font-semibold text-foreground"
>
选择项目视觉风格
</
h1
>
<
p
className=
"text-sm text-muted-foreground"
>
选择一个风格,AI将根据风格生成相应的大纲和场景
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
这里选择的是视觉呈现方式,不是题材类型。AI 会根据所选风格影响人物、场景与后续图像生成。
</
p
>
</
div
>
</
div
>
{
/* Style Grid */
}
<
div
className=
"mb-8 grid grid-cols-3 gap-4"
>
<
div
className=
"grid grid-cols-3 gap-4 mb-8"
>
{
projectStyleOptions
.
map
((
style
)
=>
(
{
styles
.
map
((
style
)
=>
(
<
button
<
button
key=
{
style
.
id
}
key=
{
style
.
value
}
onClick=
{
()
=>
setSelectedStyle
(
style
.
id
)
}
onClick=
{
()
=>
setSelectedStyle
(
style
.
value
)
}
className=
{
`group relative
rounded-xl overflow-hidden transition-all border
${
className=
{
`group relative
overflow-hidden rounded-xl border transition-all
${
selectedStyle === style.
id
selectedStyle === style.
value
? "border-primary shadow-md"
? "border-primary shadow-md"
: "border-border hover:border-primary/50"
: "border-border hover:border-primary/50"
}`
}
}`
}
>
>
<
div
className=
"
aspect-[4/3] relative
overflow-hidden"
>
<
div
className=
"
relative aspect-[4/3]
overflow-hidden"
>
<
img
<
img
src=
{
style
.
image
}
src=
{
style
.
image
}
alt=
{
style
.
name
}
alt=
{
style
.
label
}
className=
"
w-full h-full object-cover group-hover:scale-105 transition-transform
"
className=
"
h-full w-full object-cover transition-transform group-hover:scale-105
"
/>
/>
{
selectedStyle
===
style
.
id
&&
(
{
selectedStyle
===
style
.
value
&&
(
<
div
className=
"absolute
top-3 right-3 w-6 h-6 rounded-full bg-primary flex items-center justify-center
"
>
<
div
className=
"absolute
right-3 top-3 flex h-6 w-6 items-center justify-center rounded-full bg-primary
"
>
<
Check
className=
"
w-4 h
-4 text-white"
/>
<
Check
className=
"
h-4 w
-4 text-white"
/>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
<
div
className=
"
p-4 bg-card
"
>
<
div
className=
"
bg-card p-4 text-left
"
>
<
h3
className=
"
font-medium text-foreground mb-1"
>
{
style
.
name
}
</
h3
>
<
h3
className=
"
mb-1 font-medium text-foreground"
>
{
style
.
label
}
</
h3
>
<
p
className=
"text-sm text-muted-foreground"
>
{
style
.
description
}
</
p
>
<
p
className=
"text-sm text-muted-foreground"
>
{
style
.
description
}
</
p
>
</
div
>
</
div
>
</
button
>
</
button
>
))
}
))
}
</
div
>
</
div
>
{
/* Continue Button */
}
<
button
<
button
onClick=
{
handleContinue
}
onClick=
{
handleContinue
}
disabled=
{
!
selectedStyle
||
updateProject
.
isPending
}
disabled=
{
!
selectedStyle
||
updateProject
.
isPending
}
className=
"
w-full py-3 rounded-xl bg-gradient-to-r from-[#5b5ff9] to-[#8b5ff9] text-white flex items-center justify-center gap-2 hover:shadow-md transition-all disabled:opacity-50 disabled:cursor-not-allowed
disabled:hover:shadow-none"
className=
"
flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-[#5b5ff9] to-[#8b5ff9] py-3 text-white transition-all hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50
disabled:hover:shadow-none"
>
>
{
updateProject
.
isPending
?
(
{
updateProject
.
isPending
?
(
<
Loader2
className=
"
w-5 h
-5 animate-spin"
/>
<
Loader2
className=
"
h-5 w
-5 animate-spin"
/>
)
:
(
)
:
(
<>
<>
<
span
>
继续
</
span
>
<
span
>
继续
</
span
>
<
ArrowRight
className=
"
w-5 h
-5"
/>
<
ArrowRight
className=
"
h-5 w
-5"
/>
</>
</>
)
}
)
}
</
button
>
</
button
>
...
...
doc/html/src/hooks/useAi.ts
View file @
175f406c
...
@@ -22,7 +22,11 @@ export function useGenerateCharacterImage(projectId: string) {
...
@@ -22,7 +22,11 @@ export function useGenerateCharacterImage(projectId: string) {
}
}
export
function
useUploadCharacterImage
(
projectId
:
string
)
{
export
function
useUploadCharacterImage
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
({
id
,
file
}:
{
id
:
string
;
file
:
File
})
=>
aiApi
.
uploadCharacterImage
(
projectId
,
id
,
file
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
charactersKey
(
projectId
)
})
});
return
useMutation
({
mutationFn
:
({
id
,
file
,
viewType
}:
{
id
:
string
;
file
:
File
;
viewType
:
"front"
|
"side"
|
"back"
})
=>
aiApi
.
uploadCharacterImage
(
projectId
,
id
,
file
,
viewType
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
charactersKey
(
projectId
)
}),
});
}
}
export
function
useDeleteCharacter
(
projectId
:
string
)
{
export
function
useDeleteCharacter
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
const
qc
=
useQueryClient
();
...
...
doc/html/src/lib/api/ai.ts
View file @
175f406c
...
@@ -56,6 +56,12 @@ export interface Character {
...
@@ -56,6 +56,12 @@ export interface Character {
imagePrompt
:
string
;
imagePrompt
:
string
;
imageUrl
:
string
|
null
;
imageUrl
:
string
|
null
;
imageTosKey
:
string
|
null
;
imageTosKey
:
string
|
null
;
frontImageUrl
?:
string
|
null
;
frontImageTosKey
?:
string
|
null
;
sideImageUrl
?:
string
|
null
;
sideImageTosKey
?:
string
|
null
;
backImageUrl
?:
string
|
null
;
backImageTosKey
?:
string
|
null
;
status
:
string
;
status
:
string
;
createdAt
:
string
;
createdAt
:
string
;
}
}
...
@@ -160,13 +166,26 @@ export const aiApi = {
...
@@ -160,13 +166,26 @@ export const aiApi = {
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
generateCharacterImage
:
async
(
projectId
:
string
,
characterId
:
string
):
Promise
<
Character
>
=>
{
generateCharacterImage
:
async
(
projectId
:
string
,
characterId
:
string
):
Promise
<
Character
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/characters/
${
characterId
}
/generate-image`
);
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/characters/
${
characterId
}
/generate-image`
,
undefined
,
{
timeout
:
180
_000
}
);
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
uploadCharacterImage
:
async
(
projectId
:
string
,
characterId
:
string
,
file
:
File
):
Promise
<
Character
>
=>
{
uploadCharacterImage
:
async
(
projectId
:
string
,
characterId
:
string
,
file
:
File
,
viewType
:
"front"
|
"side"
|
"back"
=
"front"
):
Promise
<
Character
>
=>
{
const
form
=
new
FormData
();
const
form
=
new
FormData
();
form
.
append
(
"file"
,
file
);
form
.
append
(
"file"
,
file
);
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/characters/
${
characterId
}
/upload-image`
,
form
,
{
headers
:
{
"Content-Type"
:
undefined
}
});
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/characters/
${
characterId
}
/upload-image?viewType=
${
viewType
}
`
,
form
,
{
headers
:
{
"Content-Type"
:
undefined
}
}
);
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
deleteCharacter
:
async
(
projectId
:
string
,
characterId
:
string
):
Promise
<
void
>
=>
{
deleteCharacter
:
async
(
projectId
:
string
,
characterId
:
string
):
Promise
<
void
>
=>
{
...
@@ -187,7 +206,11 @@ export const aiApi = {
...
@@ -187,7 +206,11 @@ export const aiApi = {
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
generateSceneImage
:
async
(
projectId
:
string
,
sceneId
:
string
):
Promise
<
Scene
>
=>
{
generateSceneImage
:
async
(
projectId
:
string
,
sceneId
:
string
):
Promise
<
Scene
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/scenes/
${
sceneId
}
/generate-image`
);
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/scenes/
${
sceneId
}
/generate-image`
,
undefined
,
{
timeout
:
120
_000
}
);
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
uploadSceneImage
:
async
(
projectId
:
string
,
sceneId
:
string
,
file
:
File
):
Promise
<
Scene
>
=>
{
uploadSceneImage
:
async
(
projectId
:
string
,
sceneId
:
string
,
file
:
File
):
Promise
<
Scene
>
=>
{
...
...
doc/html/src/lib/api/projects.ts
View file @
175f406c
...
@@ -3,12 +3,17 @@ import { apiClient } from "./client";
...
@@ -3,12 +3,17 @@ import { apiClient } from "./client";
export
interface
ProjectCreatePayload
{
export
interface
ProjectCreatePayload
{
name
:
string
;
name
:
string
;
description
?:
string
;
description
?:
string
;
style
?:
string
;
aspectRatio
?:
string
;
resolution
?:
string
;
}
}
export
interface
ProjectUpdatePayload
{
export
interface
ProjectUpdatePayload
{
name
?:
string
;
name
?:
string
;
description
?:
string
;
description
?:
string
;
style
?:
string
;
style
?:
string
;
aspectRatio
?:
string
;
resolution
?:
string
;
}
}
export
interface
ProjectDTO
{
export
interface
ProjectDTO
{
...
@@ -17,6 +22,8 @@ export interface ProjectDTO {
...
@@ -17,6 +22,8 @@ export interface ProjectDTO {
description
?:
string
;
description
?:
string
;
status
:
"draft"
|
"processing"
|
"completed"
|
"failed"
;
status
:
"draft"
|
"processing"
|
"completed"
|
"failed"
;
style
?:
string
;
style
?:
string
;
aspectRatio
?:
string
;
resolution
?:
string
;
coverUrl
?:
string
;
coverUrl
?:
string
;
assetCount
:
number
;
assetCount
:
number
;
createdAt
:
string
;
createdAt
:
string
;
...
...
doc/html/src/lib/projectStyles.ts
0 → 100644
View file @
175f406c
export
interface
SelectOption
{
value
:
string
;
label
:
string
;
helper
?:
string
;
}
export
interface
ProjectStyleOption
{
value
:
string
;
label
:
string
;
description
:
string
;
image
:
string
;
}
export
const
aspectRatioOptions
:
SelectOption
[]
=
[
{
value
:
"16:9"
,
label
:
"16:9"
,
helper
:
"横屏"
},
{
value
:
"9:16"
,
label
:
"9:16"
,
helper
:
"竖屏"
},
{
value
:
"1:1"
,
label
:
"1:1"
,
helper
:
"方屏"
},
];
export
const
resolutionOptions
:
SelectOption
[]
=
[
{
value
:
"720p"
,
label
:
"720p"
,
helper
:
"标清"
},
{
value
:
"1080p"
,
label
:
"1080p"
,
helper
:
"高清"
},
{
value
:
"4K"
,
label
:
"4K"
,
helper
:
"超清"
},
];
export
const
projectStyleOptions
:
ProjectStyleOption
[]
=
[
{
value
:
"live_action_drama"
,
label
:
"真人短剧视觉"
,
description
:
"真人演员质感,贴近短剧成片观感,服装与人物比例更写实。"
,
image
:
"https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=400&h=300&fit=crop"
,
},
{
value
:
"japanese_anime"
,
label
:
"2D日漫"
,
description
:
"日系动画角色设计,线条干净,赛璐璐上色,角色辨识度高。"
,
image
:
"https://images.unsplash.com/photo-1519608487953-e999c86e7455?w=400&h=300&fit=crop"
,
},
{
value
:
"korean_webtoon"
,
label
:
"2D韩漫都市"
,
description
:
"韩漫都市感,时装感更强,光影柔和,适合情感与都市题材。"
,
image
:
"https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?w=400&h=300&fit=crop"
,
},
{
value
:
"chinese_anime"
,
label
:
"2D国漫"
,
description
:
"国漫人物设定,造型更利落,兼顾东方气质与动画表现力。"
,
image
:
"https://images.unsplash.com/photo-1514539079130-25950c84af65?w=400&h=300&fit=crop"
,
},
{
value
:
"chinese_fantasy_3d"
,
label
:
"3D国风仙侠"
,
description
:
"三维国风与仙侠质感,服饰层次更丰富,适合奇幻与古风项目。"
,
image
:
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=300&fit=crop"
,
},
{
value
:
"cg_cinematic"
,
label
:
"CG电影感"
,
description
:
"高质感 CG 角色设定,偏电影海报与游戏宣传级别的视觉效果。"
,
image
:
"https://images.unsplash.com/photo-1516035069371-29a1b244cc32?w=400&h=300&fit=crop"
,
},
];
const
projectStyleLabelMap
=
new
Map
(
projectStyleOptions
.
map
((
option
)
=>
[
option
.
value
,
option
.
label
]));
const
aspectRatioLabelMap
=
new
Map
(
aspectRatioOptions
.
map
((
option
)
=>
[
option
.
value
,
option
.
helper
?
`
${
option
.
label
}
${
option
.
helper
}
`
:
option
.
label
])
);
const
resolutionLabelMap
=
new
Map
(
resolutionOptions
.
map
((
option
)
=>
[
option
.
value
,
option
.
helper
?
`
${
option
.
label
}
${
option
.
helper
}
`
:
option
.
label
])
);
export
function
getProjectStyleLabel
(
style
?:
string
|
null
):
string
{
if
(
!
style
)
return
"未设置"
;
return
projectStyleLabelMap
.
get
(
style
)
??
style
;
}
export
function
getAspectRatioLabel
(
aspectRatio
?:
string
|
null
):
string
{
if
(
!
aspectRatio
)
return
"未设置"
;
return
aspectRatioLabelMap
.
get
(
aspectRatio
)
??
aspectRatio
;
}
export
function
getResolutionLabel
(
resolution
?:
string
|
null
):
string
{
if
(
!
resolution
)
return
"未设置"
;
return
resolutionLabelMap
.
get
(
resolution
)
??
resolution
;
}
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