Commit fbe3f0e7 authored by 黄同智's avatar 黄同智
parents 06978b88 c2bcc050
...@@ -120,7 +120,7 @@ export function Layout() { ...@@ -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]"> <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" /> <Film className="h-4 w-4 text-white" />
</div> </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> </Link>
</div> </div>
...@@ -309,31 +309,66 @@ export function Layout() { ...@@ -309,31 +309,66 @@ export function Layout() {
<section className="relative flex min-w-0 flex-1 flex-col overflow-hidden"> <section className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
{isProjectRoute && ( {isProjectRoute && (
<div className="flex h-12 items-center gap-5 border-b border-[#dfe3ee] bg-white px-6"> <div className="flex h-16 items-center border-b border-[#e5e8f0] bg-[#fbfcff] px-5">
<div className="flex min-w-0 items-center gap-2 border-r border-[#dfe3ee] pr-5"> <div className="flex min-w-[310px] items-center gap-3">
<Film className="h-4 w-4 flex-shrink-0 text-[#1664ff]" /> <button
<span className="max-w-[240px] truncate text-sm font-medium text-[#111827]"> type="button"
{currentProject?.name ?? projectId} onClick={() => navigate("/")}
</span> 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>
<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 Icon = section.icon;
const active = getActiveSection() === section.id; const active = getActiveSection() === section.id;
return ( return (
<button <div key={section.id} className="flex items-center gap-5">
key={section.id} <button
onClick={() => navigate(section.path)} onClick={() => navigate(section.path)}
className={`flex h-8 flex-shrink-0 items-center gap-2 rounded-md px-3 text-sm transition ${ className={`flex h-12 min-w-[96px] flex-col items-center justify-center gap-1 rounded-full border px-6 text-xs transition ${
active ? "bg-[#eef3ff] text-[#1664ff]" : "text-[#475467] hover:bg-[#f2f4f8]" 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> <Icon className="h-4 w-4" />
{section.label}
</button>
{index < arr.length - 1 && <span className="h-px w-8 bg-[#d9dee9]" />}
</div>
); );
})} })}
</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> </div>
)} )}
......
...@@ -43,6 +43,14 @@ const ROLE_TYPES = [ ...@@ -43,6 +43,14 @@ const ROLE_TYPES = [
{ value: "supporting", label: "配角" }, { 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 }> = [ const CHARACTER_VIEWS: Array<{ type: CharacterViewType; label: string; shortLabel: string }> = [
{ type: "front", label: "正面", shortLabel: "正" }, { type: "front", label: "正面", shortLabel: "正" },
{ type: "side", label: "侧面", shortLabel: "侧" }, { type: "side", label: "侧面", shortLabel: "侧" },
...@@ -57,6 +65,7 @@ const emptyForm = (): Partial<Character> => ({ ...@@ -57,6 +65,7 @@ const emptyForm = (): Partial<Character> => ({
personality: "", personality: "",
costume: "", costume: "",
visualHint: "", visualHint: "",
imagePrompt: "",
}); });
const emptyImageState = (): ImageState => ({ const emptyImageState = (): ImageState => ({
...@@ -98,6 +107,35 @@ function hasCharacterImage(character: Partial<Character>) { ...@@ -98,6 +107,35 @@ function hasCharacterImage(character: Partial<Character>) {
return CHARACTER_VIEWS.every((view) => !!getCharacterViewUrl(character, view.type)); 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(/[\u4e00-\u9fff]/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() { export function CharacterGeneration() {
const navigate = useNavigate(); const navigate = useNavigate();
const { projectId } = useParams(); const { projectId } = useParams();
...@@ -139,13 +177,7 @@ export function CharacterGeneration() { ...@@ -139,13 +177,7 @@ export function CharacterGeneration() {
{ id: "props" as SettingsTab, label: "道具", icon: Box, path: `/project/${projectId}/props` }, { id: "props" as SettingsTab, label: "道具", icon: Box, path: `/project/${projectId}/props` },
]; ];
const roleTypeLabel: Record<string, string> = { const roleTypeLabel = ROLE_TYPE_LABELS;
female_lead: "女主角",
male_lead: "男主角",
antagonist: "反派",
villain: "反派",
supporting: "配角",
};
const generatedCount = characters.filter((character) => hasCharacterImage(character)).length; const generatedCount = characters.filter((character) => hasCharacterImage(character)).length;
const generatingCount = characters.filter((character) => character.status === "generating").length; const generatingCount = characters.filter((character) => character.status === "generating").length;
...@@ -183,7 +215,13 @@ export function CharacterGeneration() { ...@@ -183,7 +215,13 @@ export function CharacterGeneration() {
}; };
const openEdit = (character: Character) => { const openEdit = (character: Character) => {
setEditingChar({ ...character }); const prompt = character.imagePrompt?.trim() ?? "";
setEditingChar({
...character,
imagePrompt: !prompt || isMostlyEnglishPrompt(prompt)
? buildChineseCharacterPrompt(character)
: prompt,
});
setImagePreviews({ setImagePreviews({
front: getCharacterViewUrl(character, "front"), front: getCharacterViewUrl(character, "front"),
side: getCharacterViewUrl(character, "side"), side: getCharacterViewUrl(character, "side"),
...@@ -219,10 +257,9 @@ export function CharacterGeneration() { ...@@ -219,10 +257,9 @@ export function CharacterGeneration() {
setSaving(true); setSaving(true);
try { try {
const saved = await saveCharacter.mutateAsync({ ...editingChar, status: editingChar.status ?? "draft" }); const saved = await saveCharacter.mutateAsync({ ...editingChar, status: editingChar.status ?? "draft" });
for (const view of CHARACTER_VIEWS) { const pendingFile = pendingFiles.front;
const pendingFile = pendingFiles[view.type]; if (pendingFile) {
if (!pendingFile) continue; await uploadImage.mutateAsync({ id: String(saved.id), file: pendingFile, viewType: "front" });
await uploadImage.mutateAsync({ id: String(saved.id), file: pendingFile, viewType: view.type });
} }
setShowModal(false); setShowModal(false);
} catch (error) { } catch (error) {
...@@ -555,11 +592,11 @@ export function CharacterGeneration() { ...@@ -555,11 +592,11 @@ export function CharacterGeneration() {
{showModal && ( {showModal && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50 p-6"> <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-4xl max-h-[90vh] flex flex-col"> <div className="bg-card rounded-2xl border border-border w-full max-w-3xl max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-border flex-shrink-0"> <div className="flex items-center justify-between p-6 border-b border-border flex-shrink-0">
<div> <div>
<h2 className="text-lg font-semibold text-foreground">{editingChar.id ? "编辑角色" : "手动添加角色"}</h2> <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> </div>
<button onClick={() => setShowModal(false)} className="p-2 hover:bg-muted rounded-lg transition-colors"> <button onClick={() => setShowModal(false)} className="p-2 hover:bg-muted rounded-lg transition-colors">
<X className="w-5 h-5 text-muted-foreground" /> <X className="w-5 h-5 text-muted-foreground" />
...@@ -567,48 +604,66 @@ export function CharacterGeneration() { ...@@ -567,48 +604,66 @@ export function CharacterGeneration() {
</div> </div>
<div className="overflow-auto p-6 space-y-5 flex-1"> <div className="overflow-auto p-6 space-y-5 flex-1">
<div> <div className="rounded-xl border border-border bg-muted/20 p-4">
<label className="block text-sm font-medium text-foreground mb-3">角色三视图上传(可选)</label> <div className="flex items-center justify-between mb-3">
<div className="grid grid-cols-3 gap-4"> <div>
{CHARACTER_VIEWS.map((view) => ( <label className="block text-sm font-medium text-foreground">角色图片(可选)</label>
<div key={view.type} className="rounded-xl border border-border bg-muted/20 p-3"> <p className="text-xs text-muted-foreground mt-1">上传大头照与三视图合并后的单张角色参考图。</p>
<div className="flex items-center justify-between mb-2"> </div>
<span className="text-sm font-medium text-foreground">{view.label}</span> {imagePreviews.front && (
<span className="text-[11px] text-muted-foreground">{view.shortLabel}视图</span> <button onClick={() => clearImage("front")} className="text-xs text-destructive hover:underline">
</div> 移除
<div </button>
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()} </div>
> <div
{imagePreviews[view.type] ? ( 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"
<img src={imagePreviews[view.type] ?? ""} alt={`${view.label}预览`} className="w-full h-full object-cover" /> onClick={() => fileInputRefs.current.front?.click()}
) : ( >
<div className="text-center px-4"> {imagePreviews.front ? (
<Upload className="w-5 h-5 text-muted-foreground mx-auto mb-2" /> <img src={imagePreviews.front} alt="角色图片预览" className="w-full h-full object-contain" />
<span className="text-xs text-muted-foreground">点击上传{view.label}</span> ) : (
</div> <div className="text-center px-4">
)} <Upload className="w-6 h-6 text-muted-foreground mx-auto mb-2" />
</div> <span className="text-sm text-muted-foreground">点击上传角色图片</span>
<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> </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> </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>
<div> <div>
......
...@@ -20,7 +20,7 @@ function ProjectCover({ url, name, className = "" }: { url?: string; name: strin ...@@ -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 <img src={url} alt={name} className={`w-full h-full object-cover ${className}`} />;
} }
return ( 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"> <div className="relative text-white">
<Film className="h-8 w-8 stroke-[2.2]" /> <Film className="h-8 w-8 stroke-[2.2]" />
<Sparkles className="absolute -right-2 bottom-0 h-4 w-4 fill-white stroke-white" /> <Sparkles className="absolute -right-2 bottom-0 h-4 w-4 fill-white stroke-white" />
...@@ -299,17 +299,17 @@ export function Dashboard() { ...@@ -299,17 +299,17 @@ export function Dashboard() {
}, [page, totalPages]); }, [page, totalPages]);
return ( return (
<div className="h-full overflow-auto bg-[#f6f7fb] p-5 text-[#0f172a]"> <div className="h-full overflow-auto bg-[#f8f9fa] p-6 text-[#15131f]">
<div className="mx-auto max-w-[1440px]"> <div className="mx-auto max-w-[1440px]">
{/* Header */} {/* Header */}
<div className="mb-5 flex items-center justify-between"> <div className="mb-5 flex items-center justify-between">
<div> <div>
<h1 className="mb-1 text-2xl font-semibold text-[#111827]">剧本生短剧</h1> <h1 className="mb-1 text-2xl font-semibold text-[#15131f]">剧本生短剧</h1>
</div> </div>
<Link <Link
to="/new-project" 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" /> <Plus className="w-4 h-4" />
新建项目 新建项目
...@@ -317,54 +317,54 @@ export function Dashboard() { ...@@ -317,54 +317,54 @@ export function Dashboard() {
</div> </div>
{/* Search & Filters */} {/* 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 className="grid grid-cols-1 gap-3 lg:grid-cols-[minmax(320px,1fr)_minmax(260px,340px)_minmax(140px,160px)_auto]">
<div> <div>
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#8a94a6]" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#8d86a2]" />
<input <input
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索短剧项目名称" 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> </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"> <div className="relative min-w-0 flex-1">
<input <input
type="date" type="date"
value={dateRange.start} value={dateRange.start}
onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })} onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })}
className="h-7 w-full bg-transparent text-sm text-[#111827] outline-none" className="h-7 w-full bg-transparent text-sm text-[#15131f] outline-none"
/> />
</div> </div>
<span className="px-2 text-sm text-[#98a2b3]">-</span> <span className="px-2 text-sm text-[#9b94ad]">-</span>
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
<input <input
type="date" type="date"
value={dateRange.end} value={dateRange.end}
onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })} onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })}
className="h-7 w-full bg-transparent text-sm text-[#111827] outline-none" className="h-7 w-full bg-transparent text-sm text-[#15131f] outline-none"
/> />
</div> </div>
<Calendar className="ml-2 h-4 w-4 text-[#667085]" /> <Calendar className="ml-2 h-4 w-4 text-[#7a748c]" />
</div> </div>
<div> <div>
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowMemberFilter(!showMemberFilter)} 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"> <span className="flex items-center gap-2">
<Filter className="h-4 w-4 text-[#8a94a6]" /> <Filter className="h-4 w-4 text-[#8d86a2]" />
{selectedMembers.length > 0 ? `${selectedMembers.length}个成员` : "选择成员"} {selectedMembers.length > 0 ? `${selectedMembers.length}个成员` : "选择成员"}
</span> </span>
<span className="text-xs text-[#8a94a6]">{showMemberFilter ? "▲" : "▼"}</span> <span className="text-xs text-[#8d86a2]">{showMemberFilter ? "▲" : "▼"}</span>
</button> </button>
{showMemberFilter && ( {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"> <div className="flex flex-wrap gap-2">
{allMembers.map((member) => ( {allMembers.map((member) => (
<button <button
...@@ -373,7 +373,7 @@ export function Dashboard() { ...@@ -373,7 +373,7 @@ export function Dashboard() {
prev.includes(member) ? prev.filter((m) => m !== member) : [...prev, member] prev.includes(member) ? prev.filter((m) => m !== member) : [...prev, member]
)} )}
className={`rounded-md px-3 py-1.5 text-xs transition-colors ${ 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} {member}
...@@ -391,7 +391,7 @@ export function Dashboard() { ...@@ -391,7 +391,7 @@ export function Dashboard() {
</div> </div>
<Link <Link
to="/new-project" 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" /> <Plus className="h-4 w-4" />
创建项目 创建项目
...@@ -411,7 +411,7 @@ export function Dashboard() { ...@@ -411,7 +411,7 @@ export function Dashboard() {
<div key={project.id} className="group relative"> <div key={project.id} className="group relative">
{/* Cover */} {/* 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 <ProjectCover
url={project.coverUrl} url={project.coverUrl}
name={project.title} name={project.title}
...@@ -423,26 +423,26 @@ export function Dashboard() { ...@@ -423,26 +423,26 @@ export function Dashboard() {
<div className="relative pt-3"> <div className="relative pt-3">
<Link to={`/project/${project.id}`} className="block pr-9"> <Link to={`/project/${project.id}`} className="block pr-9">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<h3 className="line-clamp-1 font-medium text-[#111827]">{project.title}</h3> <h3 className="line-clamp-1 font-medium text-[#15131f]">{project.title}</h3>
<span className="shrink-0 rounded bg-[#ecebff] px-1.5 py-0.5 text-xs text-[#5d5bff]"> <span className="shrink-0 rounded bg-[#f3eefe] px-1.5 py-0.5 text-xs text-[#5337c7]">
{project.progress === 100 ? "已完成" : "创作中"} {project.progress === 100 ? "已完成" : "创作中"}
</span> </span>
</div> </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)} 创建` : "—"} {project.createdAt ? `${project.createdAt.replace("T", " ").slice(0, 19)} 创建` : "—"}
</div> </div>
</Link> </Link>
<div className="absolute right-0 top-3 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100"> <div className="absolute right-0 top-3 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button <button
onClick={(e) => { e.preventDefault(); setEditingProject(project.raw); }} 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="编辑项目" title="编辑项目"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</button> </button>
<button <button
onClick={(e) => { e.preventDefault(); setDeletingProject(project.raw); }} 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="删除项目" title="删除项目"
> >
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
...@@ -452,29 +452,29 @@ export function Dashboard() { ...@@ -452,29 +452,29 @@ export function Dashboard() {
</div> </div>
))} ))}
</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> <span className="mr-1">{filtered.length}</span>
<button <button
onClick={() => setPage((value) => Math.max(1, value - 1))} onClick={() => setPage((value) => Math.max(1, value - 1))}
disabled={safePage === 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> </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} {safePage}
</span> </span>
<button <button
onClick={() => setPage((value) => Math.min(totalPages, value + 1))} onClick={() => setPage((value) => Math.min(totalPages, value + 1))}
disabled={safePage === totalPages} 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> </button>
<select <select
value={pageSize} value={pageSize}
onChange={(e) => setPageSize(Number(e.target.value))} 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) => ( {[10, 20, 50].map((size) => (
<option key={size} value={size}>{size} 条/页</option> <option key={size} value={size}>{size} 条/页</option>
...@@ -487,30 +487,30 @@ export function Dashboard() { ...@@ -487,30 +487,30 @@ export function Dashboard() {
const value = Number(e.target.value); const value = Number(e.target.value);
if (Number.isFinite(value)) setPage(Math.min(Math.max(1, value), totalPages)); 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> <span></span>
</div> </div>
</> </>
) : projects.length === 0 ? ( ) : projects.length === 0 ? (
<div className="rounded-lg border border-dashed border-[#cfd6e4] bg-white py-16 text-center"> <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-[#98a2b3]" /> <Film className="mx-auto mb-4 h-14 w-14 text-[#9b94ad]" />
<p className="mb-4 text-[#667085]">还没有项目,开始创建第一个吧</p> <p className="mb-4 text-[#7a748c]">还没有项目,开始创建第一个吧</p>
<Link <Link
to="/new-project" 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" /> <Plus className="h-4 w-4" />
新建项目 新建项目
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="rounded-lg border border-dashed border-[#cfd6e4] bg-white py-16 text-center"> <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-[#98a2b3]" /> <Film className="mx-auto mb-4 h-14 w-14 text-[#9b94ad]" />
<p className="text-[#667085]">没有找到符合条件的项目</p> <p className="text-[#7a748c]">没有找到符合条件的项目</p>
<button <button
onClick={() => { setSearchTerm(""); setDateRange({ start: "", end: "" }); setSelectedMembers([]); }} 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> </button>
......
...@@ -143,24 +143,24 @@ export function NewProject() { ...@@ -143,24 +143,24 @@ export function NewProject() {
const activeSubmitMeta = submitStage === "idle" ? null : submitStageMeta[submitStage]; const activeSubmitMeta = submitStage === "idle" ? null : submitStageMeta[submitStage];
return ( return (
<div className="relative min-h-full overflow-auto bg-[#f6f7fb] text-[#0f172a]"> <div className="relative min-h-full overflow-auto bg-[#f8f9fa] text-[#15131f]">
{activeSubmitMeta && ( {activeSubmitMeta && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 p-6 backdrop-blur-sm"> <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="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-gradient-to-br from-[#1677ff] to-[#7c2dff]"> <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" /> <Loader2 className="h-7 w-7 animate-spin text-white" />
</div> </div>
<h2 className="mb-2 text-xl font-semibold text-[#0f172a]">{activeSubmitMeta.title}</h2> <h2 className="mb-2 text-xl font-semibold text-[#15131f]">{activeSubmitMeta.title}</h2>
<p className="mb-5 text-sm leading-relaxed text-[#667085]"> <p className="mb-5 text-sm leading-relaxed text-[#7a748c]">
{activeSubmitMeta.description} {activeSubmitMeta.description}
</p> </p>
<Progress value={activeSubmitMeta.progress} className="mb-4 h-2.5" /> <Progress value={activeSubmitMeta.progress} className="mb-4 h-2.5" />
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex items-center justify-between text-[#202b3d]"> <div className="flex items-center justify-between text-[#262238]">
<span>创建项目</span> <span>创建项目</span>
<span>{submitStage === "creating" ? "进行中" : "已完成"}</span> <span>{submitStage === "creating" ? "进行中" : "已完成"}</span>
</div> </div>
<div className="flex items-center justify-between text-[#202b3d]"> <div className="flex items-center justify-between text-[#262238]">
<span>上传剧本</span> <span>上传剧本</span>
<span> <span>
{!hasUpload {!hasUpload
...@@ -172,28 +172,28 @@ export function NewProject() { ...@@ -172,28 +172,28 @@ export function NewProject() {
: "等待中"} : "等待中"}
</span> </span>
</div> </div>
<div className="flex items-center justify-between text-[#202b3d]"> <div className="flex items-center justify-between text-[#262238]">
<span>进入大纲页</span> <span>进入大纲页</span>
<span>{submitStage === "redirecting" ? "进行中" : "等待中"}</span> <span>{submitStage === "redirecting" ? "进行中" : "等待中"}</span>
</div> </div>
</div> </div>
<p className="mt-4 text-xs text-[#8a94a6]">处理过程中按钮已锁定,避免重复创建项目。</p> <p className="mt-4 text-xs text-[#7a748c]">处理过程中按钮已锁定,避免重复创建项目。</p>
</div> </div>
</div> </div>
)} )}
<div className="flex min-h-full flex-col"> <div className="flex min-h-full flex-col">
<div className="flex h-12 items-center justify-center gap-2 border-b border-[#e7ebf3] bg-[#f0f3ff] px-6 text-sm text-[#111827]"> <div className="flex h-12 items-center justify-center gap-2 border-b border-[#e6eaed] bg-[#f3eefe] px-6 text-sm text-[#262238]">
<Megaphone className="h-4 w-4 text-[#1764ff]" /> <Megaphone className="h-4 w-4 text-[#5337c7]" />
<span> <span>
公告:视频生成在使用视频素材作为输入时,积分计算逻辑已更新。针对不同的视频输出时长,现设有最低积分消耗限制,详情请参见 公告:视频生成在使用视频素材作为输入时,积分计算逻辑已更新。针对不同的视频输出时长,现设有最低积分消耗限制,详情请参见
</span> </span>
<span className="font-medium text-[#1764ff]">视频生成计费说明。</span> <span className="font-medium text-[#5337c7]">视频生成计费说明。</span>
</div> </div>
<main className="mx-auto flex w-full max-w-[1280px] flex-1 flex-col items-center px-8 pb-10 pt-14"> <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"> <h1 className="text-center text-[40px] font-semibold leading-tight tracking-normal text-black">
翻开剧本,创作<span className="text-[#5b55ff]">精品短剧</span> 翻开剧本,创作<span className="text-[#5337c7]">精品短剧</span>
</h1> </h1>
{submitError && ( {submitError && (
...@@ -203,13 +203,13 @@ export function NewProject() { ...@@ -203,13 +203,13 @@ export function NewProject() {
</div> </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 <label
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
className={`flex min-h-[228px] items-center justify-center rounded-2xl border border-dashed border-[#d9e2f1] bg-[#f8faff] transition-colors ${ className={`flex min-h-[228px] items-center justify-center rounded-xl border border-dashed border-[#d6cfea] bg-[#fbfaff] transition-colors ${
mode === "agent" && !isSubmitting mode === "agent" && !isSubmitting
? "cursor-pointer hover:border-[#7b8cff] hover:bg-[#f5f7ff]" ? "cursor-pointer hover:border-[#a58bff] hover:bg-[#f7f3ff]"
: "cursor-default" : "cursor-default"
}`} }`}
> >
...@@ -226,11 +226,11 @@ export function NewProject() { ...@@ -226,11 +226,11 @@ export function NewProject() {
{mode === "agent" ? ( {mode === "agent" ? (
scriptFile ? ( scriptFile ? (
<div className="flex flex-col items-center text-center"> <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"> <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-[#5b55ff]" /> <FileText className="h-8 w-8 text-[#5337c7]" />
</div> </div>
<div className="text-sm font-medium text-[#111827]">{scriptFile.name}</div> <div className="text-sm font-medium text-[#15131f]">{scriptFile.name}</div>
<div className="mt-1 text-xs text-[#8a94a6]">{formatFileSize(scriptFile.size)}</div> <div className="mt-1 text-xs text-[#7a748c]">{formatFileSize(scriptFile.size)}</div>
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
...@@ -238,7 +238,7 @@ export function NewProject() { ...@@ -238,7 +238,7 @@ export function NewProject() {
if (!isSubmitting) setScriptFile(null); if (!isSubmitting) setScriptFile(null);
}} }}
disabled={isSubmitting} 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-[#f5f7ff] 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-[#f7f3ff] disabled:cursor-not-allowed disabled:opacity-60"
> >
重新上传 重新上传
</button> </button>
...@@ -246,34 +246,34 @@ export function NewProject() { ...@@ -246,34 +246,34 @@ export function NewProject() {
) : ( ) : (
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="mb-6 flex items-center justify-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" /> <FileText className="h-5 w-5" />
</div> </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" /> <BookOpen className="h-6 w-6" />
</div> </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" /> <FileText className="h-5 w-5" />
</div> </div>
</div> </div>
<div className="text-sm text-[#8a94a6]"> <div className="text-sm text-[#7a748c]">
点击或拖拽剧本至此,支持 doc、docx、txt、pdf 和 md 格式,文件大小不超过 20M 点击或拖拽剧本至此,支持 doc、docx、txt、pdf 和 md 格式,文件大小不超过 20M
</div> </div>
</div> </div>
) )
) : ( ) : (
<div className="flex flex-col items-center text-center"> <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"> <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-[#5b55ff]" /> <Wand2 className="h-8 w-8 text-[#5337c7]" />
</div> </div>
<div className="text-sm font-medium text-[#111827]">人工模式将在下一页手动输入剧本和大纲</div> <div className="text-sm font-medium text-[#15131f]">人工模式将在下一页手动输入剧本和大纲</div>
<div className="mt-2 text-sm text-[#8a94a6]">这里不需要填写项目名称,也不需要上传图片。</div> <div className="mt-2 text-sm text-[#7a748c]">这里不需要填写项目名称,也不需要上传图片。</div>
</div> </div>
)} )}
</label> </label>
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <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 <button
type="button" type="button"
onClick={() => { onClick={() => {
...@@ -283,8 +283,8 @@ export function NewProject() { ...@@ -283,8 +283,8 @@ export function NewProject() {
disabled={isSubmitting} disabled={isSubmitting}
className={`h-9 rounded-md px-4 text-sm transition-colors ${ className={`h-9 rounded-md px-4 text-sm transition-colors ${
mode === "agent" mode === "agent"
? "bg-[#eef0ff] text-[#5b55ff]" ? "bg-[#f3eefe] text-[#5337c7]"
: "text-[#111827] hover:bg-[#f6f7fb]" : "text-[#15131f] hover:bg-[#f7f5fb]"
} disabled:cursor-not-allowed disabled:opacity-60`} } disabled:cursor-not-allowed disabled:opacity-60`}
> >
Agent模式 Agent模式
...@@ -298,8 +298,8 @@ export function NewProject() { ...@@ -298,8 +298,8 @@ export function NewProject() {
disabled={isSubmitting} disabled={isSubmitting}
className={`h-9 rounded-md px-4 text-sm transition-colors ${ className={`h-9 rounded-md px-4 text-sm transition-colors ${
mode === "manual" mode === "manual"
? "bg-[#eef0ff] text-[#5b55ff]" ? "bg-[#f3eefe] text-[#5337c7]"
: "text-[#111827] hover:bg-[#f6f7fb]" : "text-[#15131f] hover:bg-[#f7f5fb]"
} disabled:cursor-not-allowed disabled:opacity-60`} } disabled:cursor-not-allowed disabled:opacity-60`}
> >
人工模式 人工模式
...@@ -308,12 +308,12 @@ export function NewProject() { ...@@ -308,12 +308,12 @@ export function NewProject() {
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<label className="relative"> <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 <select
value={aspectRatio} value={aspectRatio}
onChange={(e) => setAspectRatio(e.target.value)} onChange={(e) => setAspectRatio(e.target.value)}
disabled={isSubmitting} 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) => ( {aspectRatioOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
...@@ -327,7 +327,7 @@ export function NewProject() { ...@@ -327,7 +327,7 @@ export function NewProject() {
value={resolution} value={resolution}
onChange={(e) => setResolution(e.target.value)} onChange={(e) => setResolution(e.target.value)}
disabled={isSubmitting} 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) => ( {resolutionOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
...@@ -340,7 +340,7 @@ export function NewProject() { ...@@ -340,7 +340,7 @@ export function NewProject() {
value={style} value={style}
onChange={(e) => setStyle(e.target.value)} onChange={(e) => setStyle(e.target.value)}
disabled={isSubmitting} 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) => ( {projectStyleOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
...@@ -356,13 +356,13 @@ export function NewProject() { ...@@ -356,13 +356,13 @@ export function NewProject() {
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} 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" />} {isSubmitting ? <Loader2 className="h-5 w-5 animate-spin" /> : <Sparkles className="h-5 w-5" />}
<span>{isSubmitting ? activeSubmitMeta?.title ?? "处理中..." : "创建短剧项目"}</span> <span>{isSubmitting ? activeSubmitMeta?.title ?? "处理中..." : "创建短剧项目"}</span>
</button> </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" /> <CheckCircle2 className="h-4 w-4" />
<span>平台内容均由人工智能模型生成,不代表平台立场</span> <span>平台内容均由人工智能模型生成,不代表平台立场</span>
</div> </div>
......
...@@ -59,6 +59,17 @@ export function ProjectDetail() { ...@@ -59,6 +59,17 @@ export function ProjectDetail() {
} }
return ( 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"> <div className="h-full overflow-auto bg-background relative">
{/* Header */} {/* Header */}
<div className="border-b border-border bg-card px-6 py-3"> <div className="border-b border-border bg-card px-6 py-3">
...@@ -169,6 +180,180 @@ export function ProjectDetail() { ...@@ -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 ---- // ---- Outline Tab ----
function OutlineTab({ function OutlineTab({
outline, outline,
......
...@@ -28,15 +28,15 @@ import { ...@@ -28,15 +28,15 @@ import {
import type { Storyboard, Character, CharacterState, Scene, Episode, AiTask } from "../../lib/api/ai"; import type { Storyboard, Character, CharacterState, Scene, Episode, AiTask } from "../../lib/api/ai";
import { useOperationCost } from "../../hooks/useUsage"; import { useOperationCost } from "../../hooks/useUsage";
const DURATION_OPTIONS = [5, 10, 15, 20, 30]; const DURATION_OPTIONS = [5, 10, 11, 15, 20, 30];
const RATIO_OPTIONS = [ const RATIO_OPTIONS = [
{ value: "16:9", label: "16:9 横屏" }, { value: "16:9", label: "16:9 横屏" },
{ value: "9:16", label: "9:16 竖屏" }, { value: "9:16", label: "9:16 竖屏" },
{ value: "1:1", label: "1:1 方形" }, { value: "1:1", label: "1:1 方形" },
]; ];
const VIDEO_MODEL_OPTIONS = [ const VIDEO_MODEL_OPTIONS = [
{ value: "doubao-seedance-2-0-fast-260128", label: "doubao-seedance-2-0-fast" }, { value: "doubao-seedance-2-0-fast-260128", label: "Doubao-Seedance-2-0" },
{ value: "doubao-seedance-2-0-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 DEFAULT_VIDEO_MODEL = VIDEO_MODEL_OPTIONS[0].value;
const MAX_CHARACTERS = 5; const MAX_CHARACTERS = 5;
...@@ -300,7 +300,7 @@ export function StoryboardWorkspace() { ...@@ -300,7 +300,7 @@ export function StoryboardWorkspace() {
const [styleKey] = useState<string>(""); // reserved; not exposed in this UI const [styleKey] = useState<string>(""); // reserved; not exposed in this UI
const [shortDescription, setShortDescription] = useState<string>(""); const [shortDescription, setShortDescription] = useState<string>("");
const [freeformPrompt, setFreeformPrompt] = 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 [selectedRatio, setSelectedRatio] = useState<string>("16:9");
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL); const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL);
const [audioOn, setAudioOn] = useState(true); const [audioOn, setAudioOn] = useState(true);
...@@ -372,11 +372,11 @@ export function StoryboardWorkspace() { ...@@ -372,11 +372,11 @@ export function StoryboardWorkspace() {
// Auto-expand first storyboard on load // Auto-expand first storyboard on load
useEffect(() => { useEffect(() => {
if (storyboards.length > 0 && !expandedId) { if (storyboards.length > 0 && (!expandedId || !storyboards.some((sb) => sb.id === expandedId))) {
setExpandedId(storyboards[0].id); setExpandedId(storyboards[0].id);
setActiveVideoSbId(storyboards[0].id); setActiveVideoSbId(storyboards[0].id);
} }
}, [storyboards.length]); }, [storyboards, expandedId]);
useEffect(() => { useEffect(() => {
if (!expanded) return; if (!expanded) return;
...@@ -610,7 +610,8 @@ export function StoryboardWorkspace() { ...@@ -610,7 +610,8 @@ export function StoryboardWorkspace() {
() => storyboards.find((s) => s.id === activeVideoSbId) ?? expanded, () => storyboards.find((s) => s.id === activeVideoSbId) ?? expanded,
[storyboards, 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 promptPreviewText = freeformPrompt.trim();
const sceneRefDisplay = expanded const sceneRefDisplay = expanded
...@@ -630,13 +631,20 @@ export function StoryboardWorkspace() { ...@@ -630,13 +631,20 @@ export function StoryboardWorkspace() {
{ label: "镜头运动", value: motionScriptDisplay }, { label: "镜头运动", value: motionScriptDisplay },
{ label: "视频提示词", value: videoPromptDisplay, multiline: true }, { 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 ( return (
<div className="h-full flex overflow-hidden bg-[#f4f7fb] text-foreground"> <div className="h-full flex overflow-hidden bg-[#f7f8fb] text-[#111827]">
{/* ─── Far Left: Episode list strip ─── */} {/* ─── 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 }}> <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="font-semibold mb-5" style={{ color: "#7c8598", fontSize: 16, letterSpacing: 1 }}> <span className="mb-3 text-[13px] font-medium text-[#475467]">
集数 集数
</span> </span>
<div <div
...@@ -648,8 +656,8 @@ export function StoryboardWorkspace() { ...@@ -648,8 +656,8 @@ export function StoryboardWorkspace() {
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" /> <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
</div> </div>
) : episodes.length === 0 ? ( ) : episodes.length === 0 ? (
<div className="px-1 pt-4 text-center"> <div className="px-2 pt-4 text-center text-[11px] leading-4 text-[#98a2b3]">
<span className="text-[12px] text-muted-foreground leading-tight">暂无集数</span> 暂无分集
</div> </div>
) : ( ) : (
episodes.map((ep) => { episodes.map((ep) => {
...@@ -663,14 +671,14 @@ export function StoryboardWorkspace() { ...@@ -663,14 +671,14 @@ export function StoryboardWorkspace() {
active ? "" : "hover:bg-[#f0f4fb] hover:text-[#475569] hover:shadow-sm" active ? "" : "hover:bg-[#f0f4fb] hover:text-[#475569] hover:shadow-sm"
}`} }`}
style={{ style={{
width: 44, width: 34,
height: 50, height: 34,
borderRadius: 14, borderRadius: 6,
fontSize: 16, fontSize: 14,
backgroundColor: active ? "#1c64ff" : "#ffffff", backgroundColor: active ? "#7657ff" : "#ffffff",
color: active ? "#ffffff" : "#8b95a7", color: active ? "#ffffff" : "#8b95a7",
boxShadow: active ? "0 12px 24px rgba(28, 100, 255, 0.28)" : "0 1px 2px rgba(15, 23, 42, 0.05)", 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(28, 100, 255, 0.35)" : "1px solid rgba(148, 163, 184, 0.22)", border: active ? "1px solid rgba(118, 87, 255, 0.45)" : "1px solid rgba(148, 163, 184, 0.25)",
}} }}
> >
{ep.episodeNumber} {ep.episodeNumber}
...@@ -681,17 +689,63 @@ export function StoryboardWorkspace() { ...@@ -681,17 +689,63 @@ export function StoryboardWorkspace() {
</div> </div>
</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) ─── */} {/* ─── 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: 分镜表 / 备注描述 */} {/* 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)]"> <div className="px-3 py-3 border-b border-[#edf0f5] bg-white flex-shrink-0 relative">
<span className="absolute left-0 top-3 bottom-3 w-[3px] bg-[#1c64ff]/55 rounded-r-full" />
<div className="flex items-center justify-between mb-1.5"> <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) && ( {(promptPreviewText || expanded) && (
<button <button
onClick={() => setShowPromptDetails(true)} 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="查看完整内容" title="查看完整内容"
> >
<Info className="w-3 h-3" /> <Info className="w-3 h-3" />
...@@ -699,21 +753,18 @@ export function StoryboardWorkspace() { ...@@ -699,21 +753,18 @@ export function StoryboardWorkspace() {
</button> </button>
)} )}
</div> </div>
<p className={`text-[11px] leading-relaxed line-clamp-2 ${promptPreviewText ? "text-foreground/85" : "text-muted-foreground italic"}`}> <div className="mb-2 text-xs font-medium text-[#344054]">分镜描述</div>
{promptPreviewText <p className={`min-h-[96px] rounded-md border border-[#d9dee8] bg-white px-3 py-2 text-xs leading-relaxed ${shortDescription ? "text-[#111827]" : "text-[#98a2b3]"}`}>
? (promptPreviewText.length > PROMPT_PREVIEW_LIMIT {shortDescription || "暂无分镜描述"}
? promptPreviewText.slice(0, PROMPT_PREVIEW_LIMIT) + "…"
: promptPreviewText)
: "选中分镜后,这里会显示已填写的视频提示词内容"}
</p> </p>
</div> </div>
{/* Action row: new + AI generate */} {/* Action row: new + AI generate */}
<div className="h-12 px-3 border-b border-slate-200/80 flex items-center gap-2 flex-shrink-0 bg-white/95"> <div className="h-10 px-3 border-b border-[#edf0f5] flex items-center gap-2 flex-shrink-0 bg-white">
<button <button
onClick={handleCreate} onClick={handleCreate}
disabled={createSb.isPending || !eid} 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" />} {createSb.isPending ? <Loader2 className="w-3 h-3 animate-spin" /> : <Plus className="w-3 h-3" />}
新建分镜 新建分镜
...@@ -722,25 +773,29 @@ export function StoryboardWorkspace() { ...@@ -722,25 +773,29 @@ export function StoryboardWorkspace() {
onClick={handleGenerateAll} onClick={handleGenerateAll}
disabled={generateSbs.isPending || !eid} disabled={generateSbs.isPending || !eid}
title="AI批量生成分镜" title="AI批量生成分镜"
className={`p-2 rounded-lg text-muted-foreground hover:bg-[#f6f9ff] hover:border-[#1c64ff]/30 hover:text-[#1c64ff] transition-colors disabled:opacity-50 ${CONTROL_SURFACE}`} className={`p-2 rounded-md text-[#475467] hover:bg-[#f6f9ff] hover:border-[#7657ff]/30 hover:text-[#7657ff] transition-colors disabled:opacity-50 ${CONTROL_SURFACE}`}
> >
{generateSbs.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />} {generateSbs.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Wand2 className="w-4 h-4" />}
</button> </button>
</div> </div>
{/* Cards (accordion) */} {/* 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 ? ( {!eid ? (
<div className="px-3 py-8 text-center"> <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">
<p className="text-xs text-muted-foreground">请从左侧选择集数</p> <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> </div>
) : isLoading ? ( ) : isLoading ? (
<div className="flex items-center justify-center py-10"> <div className="flex items-center justify-center py-10">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /> <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div> </div>
) : storyboards.length === 0 ? ( ) : storyboards.length === 0 ? (
<div className="px-3 py-8 text-center"> <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">
<p className="text-xs text-muted-foreground">点击「AI生成」或「新建分镜」开始</p> <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> </div>
) : ( ) : (
storyboards.map((sb) => ( storyboards.map((sb) => (
...@@ -776,9 +831,9 @@ export function StoryboardWorkspace() { ...@@ -776,9 +831,9 @@ export function StoryboardWorkspace() {
</div> </div>
{/* ─── Column 3: Video generation panel (5-section prompt + duration + button) ─── */} {/* ─── 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="ml-3 flex flex-col flex-shrink-0 overflow-hidden rounded-lg border border-[#eef1f6] bg-white" style={{ width: 407 }}>
<div className="h-14 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)]"> <div className="h-12 px-4 flex items-center flex-shrink-0 bg-white">
<span className="text-[13px] font-semibold text-foreground tracking-tight">分镜视频生成</span> <span className="text-sm font-semibold text-[#111827] tracking-tight">分镜视频生成</span>
{expanded && ( {expanded && (
<span className="ml-2 text-[11px] text-muted-foreground tabular-nums"> <span className="ml-2 text-[11px] text-muted-foreground tabular-nums">
· 分镜 {String(expanded.sequenceNum).padStart(3, "0")} · 分镜 {String(expanded.sequenceNum).padStart(3, "0")}
...@@ -788,12 +843,16 @@ export function StoryboardWorkspace() { ...@@ -788,12 +843,16 @@ export function StoryboardWorkspace() {
<div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3"> <div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3">
{!expanded ? ( {!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)]"> <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" /> <Plus className="w-3.5 h-3.5 text-[#98a2b3] mt-0.5 flex-shrink-0" />
<p className="text-[11px] text-[#1c64ff] leading-relaxed"> <p className="text-[11px] text-[#98a2b3] leading-relaxed">
{referenceCount > 0 {referenceCount > 0
? `已预选 ${referenceCount} 个参考素材,可继续手工调整` ? `已预选 ${referenceCount} 个参考素材,可继续手工调整`
: "可手工选择参考素材;AI 批量生成会自动预选可用角色/场景"} : "可手工选择参考素材;AI 批量生成会自动预选可用角色/场景"}
...@@ -803,13 +862,13 @@ export function StoryboardWorkspace() { ...@@ -803,13 +862,13 @@ export function StoryboardWorkspace() {
value={freeformPrompt} value={freeformPrompt}
onChange={(e) => setFreeformPrompt(e.target.value)} onChange={(e) => setFreeformPrompt(e.target.value)}
placeholder={FREEFORM_PLACEHOLDER} placeholder={FREEFORM_PLACEHOLDER}
className={`flex-1 min-h-[280px] 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-[420px] 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>
<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="flex items-center gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<select <select
...@@ -817,7 +876,7 @@ export function StoryboardWorkspace() { ...@@ -817,7 +876,7 @@ export function StoryboardWorkspace() {
onChange={(e) => setSelectedModel(e.target.value)} onChange={(e) => setSelectedModel(e.target.value)}
disabled={!expanded} disabled={!expanded}
title="视频生成模型" 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) => ( {VIDEO_MODEL_OPTIONS.map((m) => (
<option key={m.value} value={m.value}>{m.label}</option> <option key={m.value} value={m.value}>{m.label}</option>
...@@ -830,7 +889,7 @@ export function StoryboardWorkspace() { ...@@ -830,7 +889,7 @@ export function StoryboardWorkspace() {
value={selectedRatio} value={selectedRatio}
onChange={(e) => setSelectedRatio(e.target.value)} onChange={(e) => setSelectedRatio(e.target.value)}
disabled={!expanded} 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) => ( {RATIO_OPTIONS.map((r) => (
<option key={r.value} value={r.value}>{r.label}</option> <option key={r.value} value={r.value}>{r.label}</option>
...@@ -843,7 +902,7 @@ export function StoryboardWorkspace() { ...@@ -843,7 +902,7 @@ export function StoryboardWorkspace() {
value={selectedDuration} value={selectedDuration}
onChange={(e) => setSelectedDuration(Number(e.target.value))} onChange={(e) => setSelectedDuration(Number(e.target.value))}
disabled={!expanded} 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) => ( {DURATION_OPTIONS.map((d) => (
<option key={d} value={d}>{d}</option> <option key={d} value={d}>{d}</option>
...@@ -869,7 +928,7 @@ export function StoryboardWorkspace() { ...@@ -869,7 +928,7 @@ export function StoryboardWorkspace() {
onClick={handleGenerate} onClick={handleGenerate}
disabled={!canGenerate || generatingVideoId === expanded?.id} disabled={!canGenerate || generatingVideoId === expanded?.id}
title={!canGenerate ? "需要:至少 1 张参考图(角色/场景/道具任一)+ 提示词" : ""} 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 ? ( {generatingVideoId === expanded?.id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-3.5 h-3.5 animate-spin" />
...@@ -882,13 +941,13 @@ export function StoryboardWorkspace() { ...@@ -882,13 +941,13 @@ export function StoryboardWorkspace() {
</div> </div>
{/* ─── Column 4: Big video preview + thumbnail timeline ─── */} {/* ─── Column 4: Big video preview + thumbnail timeline ─── */}
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-white/90"> <div className="ml-3 flex-1 flex flex-col overflow-hidden min-w-0 rounded-lg bg-white">
<div className="h-14 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="h-12 px-5 flex items-center justify-between flex-shrink-0 bg-white">
<span className="text-[13px] font-semibold text-foreground tracking-tight"> <span className="text-[13px] font-semibold text-foreground tracking-tight">
视频预览 视频预览
{previewSb && ( {previewDisplaySb && (
<span className="ml-2 text-[11px] font-normal text-muted-foreground tabular-nums"> <span className="ml-2 text-[11px] font-normal text-muted-foreground tabular-nums">
· 分镜 {String(previewSb.sequenceNum).padStart(3, "0")} · 分镜信息{String(previewDisplaySb.sequenceNum).padStart(3, "0")}
</span> </span>
)} )}
</span> </span>
...@@ -931,20 +990,24 @@ export function StoryboardWorkspace() { ...@@ -931,20 +990,24 @@ export function StoryboardWorkspace() {
</div> </div>
</div> </div>
<div className="flex-1 overflow-y-auto p-5 min-h-0 bg-[#f4f7fb]"> <div className="flex-1 min-h-0 overflow-hidden bg-white px-5 pb-4 pt-2">
{!previewSb ? ( {!previewDisplaySb ? (
<div className="h-full flex items-center justify-center"> <div className="h-full flex flex-col items-center justify-center gap-4">
<p className="text-sm text-muted-foreground">展开任意分镜后查看视频</p> <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> </div>
) : previewTask?.status === "succeeded" && previewTask.resultVideoUrl ? ( ) : previewTask?.status === "succeeded" && previewTask.resultVideoUrl ? (
<div className="space-y-3"> <div className="flex h-full flex-col justify-center gap-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="overflow-hidden rounded-lg bg-black shadow-[0_18px_45px_rgba(15,23,42,0.16)] ring-1 ring-black/10">
<video <video
key={previewTask.resultVideoUrl} key={previewTask.resultVideoUrl}
src={previewTask.resultVideoUrl} src={previewTask.resultVideoUrl}
controls controls
autoPlay={false} autoPlay={false}
className="w-full h-full object-contain" className="aspect-video w-full object-contain"
/> />
</div> </div>
<a <a
...@@ -976,10 +1039,27 @@ export function StoryboardWorkspace() { ...@@ -976,10 +1039,27 @@ export function StoryboardWorkspace() {
</div> </div>
) : ( ) : (
<div className="h-full flex flex-col items-center justify-center gap-4"> <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"> <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)]">
<Play className="w-12 h-12 text-muted-foreground/30" /> {selectedScene?.imageUrl ? (
<p className="text-sm text-muted-foreground">尚未生成视频</p> <img src={selectedScene.imageUrl} alt={selectedScene.name} className="h-full w-full object-cover" />
<p className="text-xs text-muted-foreground/70">在左侧填好素材+提示词后生成</p> ) : (
<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>
</div> </div>
)} )}
...@@ -987,11 +1067,11 @@ export function StoryboardWorkspace() { ...@@ -987,11 +1067,11 @@ export function StoryboardWorkspace() {
{/* Thumbnail timeline */} {/* Thumbnail timeline */}
{storyboards.length > 0 && ( {storyboards.length > 0 && (
<div className="border-t border-slate-200/80 bg-white/95 flex-shrink-0 shadow-[0_-1px_0_rgba(15,23,42,0.03)]"> <div className="bg-white flex-shrink-0">
<div className="px-3 py-3 flex items-center gap-2 overflow-x-auto"> <div className="flex items-center justify-start gap-3 overflow-x-auto px-3 py-3">
{storyboards.map((sb) => { {storyboards.map((sb) => {
const t = getTaskForSb(sb); const t = getTaskForSb(sb);
const isActive = activeVideoSbId === sb.id; const isActive = activeVideoSbId === sb.id || (!activeVideoSbId && previewDisplaySb?.id === sb.id);
const url = t?.resultVideoUrl; const url = t?.resultVideoUrl;
return ( return (
<button <button
...@@ -1001,10 +1081,10 @@ export function StoryboardWorkspace() { ...@@ -1001,10 +1081,10 @@ export function StoryboardWorkspace() {
setExpandedId(sb.id); setExpandedId(sb.id);
}} }}
title={`分镜 ${String(sb.sequenceNum).padStart(3, "0")}`} 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 isActive
? "border-[#1c64ff] ring-2 ring-[#1c64ff]/25 shadow-[0_8px_18px_rgba(28,100,255,0.22)]" ? "border-[#2f80ff] ring-2 ring-[#2f80ff]/25 shadow-[0_8px_18px_rgba(47,128,255,0.20)]"
: "border-slate-200 hover:border-[#1c64ff]/40 hover:-translate-y-px hover:shadow-sm" : "border-slate-200 hover:border-[#2f80ff]/40 hover:-translate-y-px hover:shadow-sm"
}`} }`}
> >
{url ? ( {url ? (
...@@ -1030,6 +1110,8 @@ export function StoryboardWorkspace() { ...@@ -1030,6 +1110,8 @@ export function StoryboardWorkspace() {
</div> </div>
)} )}
</div> </div>
</div>
</div>
{/* ─── Modals ─── */} {/* ─── Modals ─── */}
{showCharacterPicker && expanded && ( {showCharacterPicker && expanded && (
...@@ -1117,23 +1199,23 @@ function StoryboardCard(props: StoryboardCardProps) { ...@@ -1117,23 +1199,23 @@ function StoryboardCard(props: StoryboardCardProps) {
const [descCollapsed, setDescCollapsed] = useState(false); const [descCollapsed, setDescCollapsed] = useState(false);
return ( return (
<div className={`my-2 rounded-2xl border transition-all duration-200 ${ <div className={`my-2 rounded-lg border transition-all duration-200 ${
isExpanded isExpanded
? "border-[#1c64ff]/35 bg-white shadow-[0_12px_30px_rgba(15,23,42,0.08)]" ? "border-[#e4e8f0] bg-white shadow-sm"
: "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-[#edf0f5] bg-white hover:bg-white hover:border-[#cfd6e4]"
}`}> }`}>
<button <button
onClick={onToggle} 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"> <span className="text-sm font-semibold text-foreground flex-1 truncate">
分镜信息{String(sb.sequenceNum).padStart(3, "0")} 分镜信息{String(sb.sequenceNum).padStart(3, "0")}
</span> </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> </button>
{isExpanded && ( {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> <div>
<button <button
...@@ -1151,7 +1233,7 @@ function StoryboardCard(props: StoryboardCardProps) { ...@@ -1151,7 +1233,7 @@ function StoryboardCard(props: StoryboardCardProps) {
value={props.shortDescription} value={props.shortDescription}
onChange={(e) => props.onShortDescriptionChange(e.target.value)} onChange={(e) => props.onShortDescriptionChange(e.target.value)}
placeholder="简要描述此分镜的剧情或画面" 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> </div>
...@@ -1214,7 +1296,7 @@ function CharacterRowList({ ...@@ -1214,7 +1296,7 @@ function CharacterRowList({
characterKeys, keyToCharacterRef, characterRefOptions, onRemove, onSwap, onAdd, characterKeys, keyToCharacterRef, characterRefOptions, onRemove, onSwap, onAdd,
}: CharacterRowListProps) { }: CharacterRowListProps) {
return ( return (
<div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70"> <div className="rounded-md bg-white p-0">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-foreground">出镜角色</span> <span className="text-xs font-medium text-foreground">出镜角色</span>
<button <button
...@@ -1281,8 +1363,8 @@ function CharacterRow({ ...@@ -1281,8 +1363,8 @@ function CharacterRow({
const options = allRefOptions.filter((option) => option.key === tosKey || !currentKeys.includes(option.key)); const options = allRefOptions.filter((option) => option.key === tosKey || !currentKeys.includes(option.key));
return ( return (
<div className="flex items-center gap-1.5 rounded-xl bg-white p-1.5 ring-1 ring-slate-200/70"> <div className="flex items-center gap-1.5 rounded-md bg-white py-1">
<div className="w-8 h-8 rounded-lg overflow-hidden border border-slate-200 bg-slate-100 flex-shrink-0"> <div className="w-10 h-10 rounded-md overflow-hidden border border-slate-200 bg-slate-100 flex-shrink-0">
{url ? ( {url ? (
<img src={url} alt={refOption?.name ?? ""} className="w-full h-full object-contain bg-white" /> <img src={url} alt={refOption?.name ?? ""} className="w-full h-full object-contain bg-white" />
) : ( ) : (
...@@ -1293,7 +1375,7 @@ function CharacterRow({ ...@@ -1293,7 +1375,7 @@ function CharacterRow({
<select <select
value={tosKey} value={tosKey}
onChange={(e) => onSwap(e.target.value)} 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>} {!refOption && <option value={tosKey}>(已失效)</option>}
{options.map((option) => ( {options.map((option) => (
...@@ -1307,7 +1389,7 @@ function CharacterRow({ ...@@ -1307,7 +1389,7 @@ function CharacterRow({
<div className="relative flex-1 min-w-0"> <div className="relative flex-1 min-w-0">
<select <select
disabled 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> <option>未配置音色</option>
</select> </select>
...@@ -1315,14 +1397,14 @@ function CharacterRow({ ...@@ -1315,14 +1397,14 @@ function CharacterRow({
</div> </div>
<button <button
onClick={onRemove} 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>
<button <button
onClick={handleRegen} onClick={handleRegen}
disabled={!refOption || isRegenerating} 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-[#1c64ff] 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-[#7657ff] disabled:opacity-40 flex items-center gap-1 transition-colors"
> >
{isRegenerating && <Loader2 className="w-3 h-3 animate-spin" />} {isRegenerating && <Loader2 className="w-3 h-3 animate-spin" />}
重绘 重绘
...@@ -1346,7 +1428,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) { ...@@ -1346,7 +1428,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
const selected = usable.find((s) => s.imageTosKey === selectedKey) ?? null; const selected = usable.find((s) => s.imageTosKey === selectedKey) ?? null;
return ( return (
<div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70"> <div className="rounded-md bg-white p-0">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-foreground">分镜场景</span> <span className="text-xs font-medium text-foreground">分镜场景</span>
{usable.length > 0 && ( {usable.length > 0 && (
...@@ -1365,7 +1447,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) { ...@@ -1365,7 +1447,7 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
type="button" type="button"
onClick={() => usable.length > 0 && setShowPicker((v) => !v)} onClick={() => usable.length > 0 && setShowPicker((v) => !v)}
disabled={usable.length === 0} 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-[#1c64ff]/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-[#7657ff]/40 hover:shadow-sm transition disabled:cursor-not-allowed"
> >
{selected?.imageUrl ? ( {selected?.imageUrl ? (
<img src={selected.imageUrl} alt={selected.name} className="w-full h-full object-cover" /> <img src={selected.imageUrl} alt={selected.name} className="w-full h-full object-cover" />
...@@ -1381,17 +1463,17 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) { ...@@ -1381,17 +1463,17 @@ function SceneSection({ scenes, selectedKey, onSelect }: SceneSectionProps) {
{/* Bottom row: scene name + role label + actions menu */} {/* Bottom row: scene name + role label + actions menu */}
<div className="flex items-center gap-1.5"> <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>} {selected?.name || <span className="text-muted-foreground">请选择场景</span>}
</div> </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>
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<button <button
onClick={() => setMenuOpen((v) => !v)} onClick={() => setMenuOpen((v) => !v)}
disabled={!selected} disabled={!selected}
className="p-1.5 rounded-lg border border-slate-200/80 bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#1c64ff] disabled:opacity-40 transition-colors" className="p-1.5 rounded-md border border-[#dfe3ee] bg-white text-muted-foreground hover:bg-[#f6f9ff] hover:text-[#7657ff] disabled:opacity-40 transition-colors"
> >
<MoreHorizontal className="w-3.5 h-3.5" /> <MoreHorizontal className="w-3.5 h-3.5" />
</button> </button>
...@@ -1483,7 +1565,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) { ...@@ -1483,7 +1565,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
const canAddMore = selectedKeys.length < MAX_PROPS; const canAddMore = selectedKeys.length < MAX_PROPS;
return ( return (
<div className="rounded-xl bg-[#f8fafc] p-3 ring-1 ring-slate-200/70"> <div className="rounded-md bg-white p-0">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-foreground">场景道具</span> <span className="text-xs font-medium text-foreground">场景道具</span>
<button <button
...@@ -1505,7 +1587,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) { ...@@ -1505,7 +1587,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
key={s.id} key={s.id}
onClick={() => onToggle(s.imageTosKey!)} onClick={() => onToggle(s.imageTosKey!)}
title={`移除 ${s.name}`} 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 ? ( {s.imageUrl ? (
<img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" /> <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) { ...@@ -1524,7 +1606,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
)} )}
{showPicker && ( {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 ? ( {available.length === 0 ? (
<p className="text-[11px] text-muted-foreground italic text-center py-2"> <p className="text-[11px] text-muted-foreground italic text-center py-2">
{allCandidates.length === 0 ? "先到「场景」页创建场景" : "已添加全部可选道具"} {allCandidates.length === 0 ? "先到「场景」页创建场景" : "已添加全部可选道具"}
...@@ -1540,7 +1622,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) { ...@@ -1540,7 +1622,7 @@ function PropGrid({ scenes, sceneKey, selectedKeys, onToggle }: PropGridProps) {
}} }}
disabled={!canAddMore} disabled={!canAddMore}
title={s.name} title={s.name}
className="relative aspect-square rounded-lg overflow-hidden border border-slate-200 bg-white hover:border-[#1c64ff]/40 hover:shadow-sm disabled:opacity-40 transition" className="relative aspect-square rounded-md overflow-hidden border border-slate-200 bg-white hover:border-[#7657ff]/40 hover:shadow-sm disabled:opacity-40 transition"
> >
{s.imageUrl ? ( {s.imageUrl ? (
<img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" /> <img src={s.imageUrl} alt={s.name} className="w-full h-full object-cover" />
......
...@@ -3,42 +3,42 @@ ...@@ -3,42 +3,42 @@
:root { :root {
--font-size: 14px; --font-size: 14px;
--background: #f8f9fa; --background: #f8f9fa;
--foreground: #1a1a1a; --foreground: #15131f;
--card: #ffffff; --card: #ffffff;
--card-foreground: #1a1a1a; --card-foreground: #15131f;
--popover: #ffffff; --popover: #ffffff;
--popover-foreground: #1a1a1a; --popover-foreground: #15131f;
--primary: #5b5ff9; --primary: #5337c7;
--primary-foreground: #ffffff; --primary-foreground: #ffffff;
--secondary: #f1f3f5; --secondary: #f3eefe;
--secondary-foreground: #1a1a1a; --secondary-foreground: #262238;
--muted: #f8f9fa; --muted: #f8f9fa;
--muted-foreground: #6c757d; --muted-foreground: #4b5864;
--accent: #e7f5ff; --accent: #f3eefe;
--accent-foreground: #1971c2; --accent-foreground: #5337c7;
--destructive: #f03e3e; --destructive: #f03e3e;
--destructive-foreground: #ffffff; --destructive-foreground: #ffffff;
--border: rgba(0, 0, 0, 0.06); --border: #e6eaed;
--input: rgba(0, 0, 0, 0.06); --input: #d8dae9;
--input-background: #ffffff; --input-background: #ffffff;
--switch-background: #dee2e6; --switch-background: #dee2e6;
--font-weight-medium: 500; --font-weight-medium: 500;
--font-weight-normal: 400; --font-weight-normal: 400;
--ring: #5b5ff9; --ring: #5337c7;
--chart-1: #5b5ff9; --chart-1: #5337c7;
--chart-2: #22b8cf; --chart-2: #36bffa;
--chart-3: #51cf66; --chart-3: #12b76a;
--chart-4: #ff6b6b; --chart-4: #f04438;
--chart-5: #fcc419; --chart-5: #fdb022;
--radius: 0.75rem; --radius: 0.625rem;
--sidebar: #ffffff; --sidebar: #ffffff;
--sidebar-foreground: #1a1a1a; --sidebar-foreground: #15131f;
--sidebar-primary: #5b5ff9; --sidebar-primary: #5337c7;
--sidebar-primary-foreground: #ffffff; --sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f8f9fa; --sidebar-accent: #f3eefe;
--sidebar-accent-foreground: #1a1a1a; --sidebar-accent-foreground: #5337c7;
--sidebar-border: rgba(0, 0, 0, 0.06); --sidebar-border: #e6eaed;
--sidebar-ring: #5b5ff9; --sidebar-ring: #5337c7;
} }
.dark { .dark {
...@@ -188,3 +188,26 @@ ...@@ -188,3 +188,26 @@
line-height: 1.5; 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);
}
}
...@@ -8,7 +8,7 @@ services: ...@@ -8,7 +8,7 @@ services:
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-yaoai123} MYSQL_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
ports: ports:
- "${MYSQL_PORT:-23306}:3306" - "${MYSQL_PORT:-13306}:3306"
command: command:
- --character-set-server=utf8mb4 - --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci - --collation-server=utf8mb4_unicode_ci
...@@ -26,7 +26,7 @@ services: ...@@ -26,7 +26,7 @@ services:
redis: redis:
image: docker.m.daocloud.io/library/redis:7.2-alpine image: docker.m.daocloud.io/library/redis:7.2-alpine
ports: ports:
- "${REDIS_PORT:-26379}:6379" - "${REDIS_PORT:-16379}:6379"
volumes: volumes:
- redis-data:/data - redis-data:/data
healthcheck: healthcheck:
...@@ -43,8 +43,7 @@ services: ...@@ -43,8 +43,7 @@ services:
environment: environment:
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod} SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
SERVER_PORT: 8081 SERVER_PORT: 8080
BILLING_MODE: ${BILLING_MODE:-STRICT}
DB_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-yaoai_comic}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8 DB_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-yaoai_comic}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
DB_USERNAME: ${MYSQL_USER:-yaoai} DB_USERNAME: ${MYSQL_USER:-yaoai}
DB_PASSWORD: ${MYSQL_PASSWORD:-yaoai123} DB_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
...@@ -66,7 +65,7 @@ services: ...@@ -66,7 +65,7 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
ports: ports:
- "${BACKEND_PORT:-28081}:8081" - "${BACKEND_PORT:-8080}:8080"
restart: unless-stopped restart: unless-stopped
frontend: frontend:
...@@ -78,19 +77,7 @@ services: ...@@ -78,19 +77,7 @@ services:
depends_on: depends_on:
- backend - backend
ports: ports:
- "${FRONTEND_PORT:-3000}:3000" - "${FRONTEND_PORT:-3000}:80"
restart: unless-stopped
admin-web:
build:
context: ./yaoai-admin-web
dockerfile: Dockerfile
environment:
ADMIN_API_UPSTREAM: http://backend:8081
depends_on:
- backend
ports:
- "${ADMIN_WEB_PORT:-3001}:3001"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
......
# 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`
- 不要把账号密码写进仓库文件。需要登录时由用户重新提供,或使用当前会话上下文。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment