Commit fbe3f0e7 authored by 黄同智's avatar 黄同智
parents 06978b88 c2bcc050
......@@ -120,7 +120,7 @@ export function Layout() {
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-[#0073ff] to-[#7b2cff]">
<Film className="h-4 w-4 text-white" />
</div>
<span className="text-base font-semibold tracking-normal text-[#111827]">Dramix</span>
<span className="text-base font-semibold tracking-normal text-[#7a5cf6]">Dramix.ai</span>
</Link>
</div>
......@@ -309,31 +309,66 @@ export function Layout() {
<section className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
{isProjectRoute && (
<div className="flex h-12 items-center gap-5 border-b border-[#dfe3ee] bg-white px-6">
<div className="flex min-w-0 items-center gap-2 border-r border-[#dfe3ee] pr-5">
<Film className="h-4 w-4 flex-shrink-0 text-[#1664ff]" />
<span className="max-w-[240px] truncate text-sm font-medium text-[#111827]">
{currentProject?.name ?? projectId}
</span>
<div className="flex h-16 items-center border-b border-[#e5e8f0] bg-[#fbfcff] px-5">
<div className="flex min-w-[310px] items-center gap-3">
<button
type="button"
onClick={() => navigate("/")}
className="flex h-7 w-7 items-center justify-center rounded-md text-[#111827] hover:bg-[#edf1f8]"
aria-label="返回项目列表"
>
<ChevronDown className="h-4 w-4 rotate-90" />
</button>
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<span className="max-w-[270px] truncate text-sm font-semibold text-[#111827]">
{currentProject?.name ?? projectId}
</span>
<button className="text-[#98a2b3] hover:text-[#6f55f2]" aria-label="编辑项目名">
<FileText className="h-3.5 w-3.5" />
</button>
</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[#667085]">
<span>9:16</span>
<span>720</span>
<span>写实电影感</span>
</div>
</div>
</div>
<div className="flex min-w-0 items-center gap-1.5 overflow-x-auto">
{projectSections.map((section) => {
<div className="mx-auto hidden items-center gap-5 xl:flex">
{projectSections.filter((section) => section.id !== "agent").map((section, index, arr) => {
const Icon = section.icon;
const active = getActiveSection() === section.id;
return (
<button
key={section.id}
onClick={() => navigate(section.path)}
className={`flex h-8 flex-shrink-0 items-center gap-2 rounded-md px-3 text-sm transition ${
active ? "bg-[#eef3ff] text-[#1664ff]" : "text-[#475467] hover:bg-[#f2f4f8]"
}`}
>
<Icon className="h-4 w-4" />
{section.label}
</button>
<div key={section.id} className="flex items-center gap-5">
<button
onClick={() => navigate(section.path)}
className={`flex h-12 min-w-[96px] flex-col items-center justify-center gap-1 rounded-full border px-6 text-xs transition ${
active
? "border-[#7657ff] bg-white text-[#7657ff] shadow-[0_6px_18px_rgba(118,87,255,0.18)]"
: "border-[#e2e6ef] bg-white text-[#111827] hover:border-[#b8c1d4]"
}`}
>
<Icon className="h-4 w-4" />
{section.label}
</button>
{index < arr.length - 1 && <span className="h-px w-8 bg-[#d9dee9]" />}
</div>
);
})}
</div>
<div className="ml-auto flex min-w-[260px] items-center justify-end gap-3">
<button className="text-xs text-[#667085] hover:text-[#6f55f2]">优化建议</button>
<button
onClick={() => navigate("/usage")}
className="flex h-8 items-center gap-1.5 rounded-md border border-[#d9d2ff] bg-[#f6f2ff] px-3 text-xs font-semibold text-[#6f55f2]"
>
<Sparkles className="h-3.5 w-3.5" />
{Number(balance?.balance ?? 7492538).toLocaleString()}
</button>
</div>
</div>
)}
......
This diff is collapsed.
This diff is collapsed.
......@@ -59,6 +59,17 @@ export function ProjectDetail() {
}
return (
<ProjectScriptView
project={project}
outline={outline}
episodes={episodes}
characters={characters}
projectId={projectId}
onNavigate={(path) => navigate(path)}
/>
);
return (
<div className="h-full overflow-auto bg-background relative">
{/* Header */}
<div className="border-b border-border bg-card px-6 py-3">
......@@ -169,6 +180,180 @@ export function ProjectDetail() {
);
}
function ProjectScriptView({
project,
outline,
episodes,
characters,
projectId,
onNavigate,
}: {
project: import("../../lib/api/projects").ProjectDTO | undefined;
outline: import("../../lib/api/ai").Outline | undefined;
episodes: import("../../lib/api/ai").Episode[];
characters: import("../../lib/api/ai").Character[];
projectId?: string;
onNavigate: (path: string) => void;
}) {
const orderedEpisodes = [...episodes].sort(
(a, b) => (a.episodeNumber || 0) - (b.episodeNumber || 0)
);
const episodeTotal = Math.max(
outline?.episodeCount ?? 0,
orderedEpisodes.length,
orderedEpisodes.length === 0 ? 60 : 0
);
const [selectedEpisodeNo, setSelectedEpisodeNo] = useState(1);
const selectedEpisode =
orderedEpisodes.find((ep) => ep.episodeNumber === selectedEpisodeNo) ?? null;
const selectedNo = selectedEpisodeNo;
const uniqueTags = Array.from(
new Set(
[outline?.genre, project?.style, "爽文", "高燃", "悬疑", "玄幻"].filter(Boolean) as string[]
)
).slice(0, 6);
const defaultSynopsis =
"一场意外让主角卷入仙门与妖族的暗潮之中。看似平凡的手艺成为破局关键,少年在一次次试炼里识破阴谋、守住同伴,也逐渐揭开自己身世与旧日神工之间的联系。";
const fallbackCharacters = [
{
id: "fallback-1",
name: "夏母",
roleType: "关键人物",
gender: "",
age: "中年",
personality: "温和坚韧,遇事有主见",
costume: "朴素家常服",
},
{
id: "fallback-2",
name: "萧医师",
roleType: "推动剧情",
gender: "",
age: "青年",
personality: "理性克制,暗藏锋芒",
costume: "青色长衫",
},
];
const displayCharacters = characters.length > 0 ? characters.slice(0, 4) : fallbackCharacters;
const nextPath = episodes.length === 0 ? `/project/${projectId}/outline` : `/project/${projectId}/characters`;
return (
<div className="relative h-full overflow-auto bg-[#f7f8fb] text-[#111827]">
<main className="mx-auto max-w-[860px] px-6 pb-32 pt-12">
<section className="flex flex-col gap-8 sm:flex-row sm:items-start">
<div className="h-[218px] w-[154px] flex-shrink-0 overflow-hidden rounded-[3px] bg-[#dfe3ea] shadow-[0_18px_48px_rgba(15,23,42,0.14)]">
{project?.coverUrl ? (
<img src={project.coverUrl} alt="项目封面" className="h-full w-full object-cover" />
) : (
<div className="flex h-full w-full flex-col items-center justify-center bg-[linear-gradient(145deg,#262b36,#10131a)] text-white">
<Film className="mb-3 h-9 w-9 opacity-70" />
<span className="px-5 text-center text-sm font-semibold leading-5">
{project?.name ?? outline?.title ?? "剧本封面"}
</span>
</div>
)}
</div>
<div className="min-w-0 flex-1 pt-1">
<h1 className="mb-4 text-[28px] font-semibold leading-[1.22] tracking-normal text-[#0b1220]">
{outline?.title ?? project?.name ?? "未命名项目"}
</h1>
<div className="mb-5 grid grid-cols-2 gap-x-8 gap-y-2 text-[13px] leading-6 text-[#6b7280] sm:grid-cols-4">
<span>集数:{outline?.episodeCount ?? episodeTotal}</span>
<span>比例:{project?.aspectRatio ?? "16:9"}</span>
<span>清晰度:{project?.resolution ?? "720p"}</span>
<span>风格:{project?.style ?? outline?.genre ?? "待生成"}</span>
</div>
<div className="flex flex-wrap gap-2">
{uniqueTags.map((tag) => (
<span
key={tag}
className="rounded-full border border-[#e4e7ef] bg-white px-3 py-1 text-xs font-medium text-[#536079]"
>
{tag}
</span>
))}
</div>
</div>
</section>
<div className="my-10 h-px bg-[#e6e8ef]" />
<section className="mb-11">
<h2 className="mb-4 text-[17px] font-semibold text-[#0b1220]">大纲</h2>
<p className="text-[14px] leading-8 text-[#384152]">
{outline?.synopsis ?? project?.description ?? defaultSynopsis}
</p>
</section>
<section className="mb-12">
<h2 className="mb-5 text-[17px] font-semibold text-[#0b1220]">人物小传</h2>
<div className="grid gap-x-12 gap-y-6 md:grid-cols-2">
{displayCharacters.map((char) => (
<article key={char.id} className="text-[14px] leading-7 text-[#384152]">
<h3 className="mb-1 font-semibold text-[#111827]">{char.name}</h3>
<p>
{char.roleType ? `${char.roleType},` : ""}
{char.gender ? `${char.gender},` : ""}
{char.age ? `${char.age}。` : ""}
{char.personality || "性格鲜明,与主线冲突和情感推进紧密相关。"}
{char.costume ? ` 服饰:${char.costume}。` : ""}
</p>
</article>
))}
</div>
</section>
<section>
<h2 className="mb-5 text-[17px] font-semibold text-[#0b1220]">分集剧情</h2>
<div className="mb-7 grid grid-cols-8 gap-3 sm:grid-cols-10 md:grid-cols-12">
{Array.from({ length: episodeTotal }, (_, index) => index + 1).map((num) => (
<button
key={num}
type="button"
onClick={() => setSelectedEpisodeNo(num)}
className={`h-9 rounded-full text-sm font-medium transition-all ${
num === selectedNo
? "bg-[#111827] text-white shadow-[0_8px_18px_rgba(17,24,39,0.2)]"
: "bg-white text-[#4b5563] ring-1 ring-[#e3e6ee] hover:bg-[#f1f3f7]"
}`}
>
{num}
</button>
))}
</div>
<article className="text-[14px] leading-8 text-[#384152]">
<h3 className="mb-2 font-semibold text-[#111827]">
{selectedNo}{selectedEpisode?.title ? `:${selectedEpisode.title}` : ""}
</h3>
<p className="mb-4">
{selectedEpisode?.summary ??
"本集承接上一段危机,主角在新的线索中发现关键异常,并由此推动人物关系、外部冲突和最终选择进一步升级。"}
</p>
<p className="whitespace-pre-wrap">
{selectedEpisode?.script ??
"镜头从喧闹的街巷切入,主角察觉到细微异动。几句试探之后,暗处的矛盾浮出水面,众人的立场开始分化。主角在压力下做出决定,为下一集的分镜和视觉呈现留下清晰的动作线。"}
</p>
</article>
</section>
</main>
<div className="pointer-events-none fixed bottom-8 left-0 right-0 z-20 flex justify-center px-6">
<div className="pointer-events-auto flex w-full max-w-[520px] items-center justify-center">
<button
type="button"
onClick={() => onNavigate(nextPath)}
className="h-12 min-w-[260px] rounded-full bg-[#0b0f17] px-10 text-[15px] font-semibold text-white shadow-[0_18px_46px_rgba(17,24,39,0.3)] transition-transform hover:scale-[1.01] active:scale-[0.99]"
>
{episodes.length === 0 ? "生成分集" : "进入下一步"}
</button>
</div>
</div>
</div>
);
}
// ---- Outline Tab ----
function OutlineTab({
outline,
......
......@@ -3,42 +3,42 @@
:root {
--font-size: 14px;
--background: #f8f9fa;
--foreground: #1a1a1a;
--foreground: #15131f;
--card: #ffffff;
--card-foreground: #1a1a1a;
--card-foreground: #15131f;
--popover: #ffffff;
--popover-foreground: #1a1a1a;
--primary: #5b5ff9;
--popover-foreground: #15131f;
--primary: #5337c7;
--primary-foreground: #ffffff;
--secondary: #f1f3f5;
--secondary-foreground: #1a1a1a;
--secondary: #f3eefe;
--secondary-foreground: #262238;
--muted: #f8f9fa;
--muted-foreground: #6c757d;
--accent: #e7f5ff;
--accent-foreground: #1971c2;
--muted-foreground: #4b5864;
--accent: #f3eefe;
--accent-foreground: #5337c7;
--destructive: #f03e3e;
--destructive-foreground: #ffffff;
--border: rgba(0, 0, 0, 0.06);
--input: rgba(0, 0, 0, 0.06);
--border: #e6eaed;
--input: #d8dae9;
--input-background: #ffffff;
--switch-background: #dee2e6;
--font-weight-medium: 500;
--font-weight-normal: 400;
--ring: #5b5ff9;
--chart-1: #5b5ff9;
--chart-2: #22b8cf;
--chart-3: #51cf66;
--chart-4: #ff6b6b;
--chart-5: #fcc419;
--radius: 0.75rem;
--ring: #5337c7;
--chart-1: #5337c7;
--chart-2: #36bffa;
--chart-3: #12b76a;
--chart-4: #f04438;
--chart-5: #fdb022;
--radius: 0.625rem;
--sidebar: #ffffff;
--sidebar-foreground: #1a1a1a;
--sidebar-primary: #5b5ff9;
--sidebar-foreground: #15131f;
--sidebar-primary: #5337c7;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f8f9fa;
--sidebar-accent-foreground: #1a1a1a;
--sidebar-border: rgba(0, 0, 0, 0.06);
--sidebar-ring: #5b5ff9;
--sidebar-accent: #f3eefe;
--sidebar-accent-foreground: #5337c7;
--sidebar-border: #e6eaed;
--sidebar-ring: #5337c7;
}
.dark {
......@@ -188,3 +188,26 @@
line-height: 1.5;
}
}
@layer components {
.dramix-surface {
background: #f8f9fa;
}
.dramix-panel {
border: 1px solid #e6eaed;
background: #ffffff;
box-shadow: 0 10px 30px rgba(37, 31, 63, 0.05);
}
.dramix-primary {
background: #8064f1;
color: #ffffff;
box-shadow: 0 10px 22px rgba(83, 55, 199, 0.18);
}
.dramix-primary:hover {
background: #7357e8;
box-shadow: 0 14px 28px rgba(83, 55, 199, 0.24);
}
}
......@@ -8,7 +8,7 @@ services:
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${MYSQL_PORT:-23306}:3306"
- "${MYSQL_PORT:-13306}:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
......@@ -26,7 +26,7 @@ services:
redis:
image: docker.m.daocloud.io/library/redis:7.2-alpine
ports:
- "${REDIS_PORT:-26379}:6379"
- "${REDIS_PORT:-16379}:6379"
volumes:
- redis-data:/data
healthcheck:
......@@ -43,8 +43,7 @@ services:
environment:
TZ: ${TZ:-Asia/Shanghai}
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
SERVER_PORT: 8081
BILLING_MODE: ${BILLING_MODE:-STRICT}
SERVER_PORT: 8080
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_PASSWORD: ${MYSQL_PASSWORD:-yaoai123}
......@@ -66,7 +65,7 @@ services:
redis:
condition: service_healthy
ports:
- "${BACKEND_PORT:-28081}:8081"
- "${BACKEND_PORT:-8080}:8080"
restart: unless-stopped
frontend:
......@@ -78,19 +77,7 @@ services:
depends_on:
- backend
ports:
- "${FRONTEND_PORT:-3000}:3000"
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"
- "${FRONTEND_PORT:-3000}:80"
restart: unless-stopped
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