From 2088cf78cc66acbb31d582f6fc80ee2f7ecc73cb Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 22 Sep 2026 15:48:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(canvas):=20=E5=88=9B=E4=BD=9C=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=20-=20=E7=AA=81=E5=87=BA=20Agent=20=E4=B8=BB=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E5=B9=B6=E4=BC=98=E5=8C=96=E5=93=8D=E5=BA=94=E5=BC=8F?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/progress/pending-test.mdx | 7 + .../canvas/canvas-short-drama-entry.css | 184 ++++++++++++++++++ .../canvas/canvas-short-drama-entry.tsx | 137 ++++++------- 3 files changed, 250 insertions(+), 78 deletions(-) create mode 100644 web/src/components/canvas/canvas-short-drama-entry.css diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 147e893ef..72c2ed6bc 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -38,6 +38,13 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - [ ] 更新并重启后端后,使用已声明图片能力的实际渠道模型,重新询问已上传图片节点的主体和背景;确认不再因 `unknown` 标记拒绝看图。真实模型调用会消耗额度,尚未执行。 - [ ] 实际部署验证 OSS 图片、同轮多图比较、跨轮换图与读取失败提示;检查其他依赖公网 URL 的生成协议配置,不能将本次看图修复视为存储公网地址已修正。 +## 画布创作入口主次调整 + +- 前端类型检查和 4 项画布起点状态测试通过。 +- [x] 真实画布路由使用隔离的身份与空画布测试数据完成局部浏览器验收:1440 × 900 明暗主题、390 × 844 窄屏、390 × 320 低高度。入口不横向溢出,点击目标至少 44px;Tab 顺序与焦点样式正常,入口滚轮不改变画布缩放,减少动态效果设置生效。 +- [x] 入口改为左对齐分层标题、主题适配的金属质感片头符号和细分隔线,无卡片或实底大按钮。Agent 使用文字与小箭头入口、悬停下划线反馈;次级入口和素材操作分层展示。点击「交给 Agent」可展开原有 Agent 工作台。 +- [ ] 用户确认视觉效果;使用真实登录账号验收全部入口、上传和 Agent 对话。本轮浏览器验收拦截了写请求,未修改真实项目;未模拟的辅助接口返回错误,因此局部视觉与面板展开检查不代表后端联调、保存或 Agent 执行通过。 + ## 技能文件包迁移元数据长度 - [x] 启动迁移对旧技能名称、简介的回填元数据按字符数限制长度,省略号计入上限;不改写原始名称、简介和正文,不放宽非迁移入口的校验。 diff --git a/web/src/components/canvas/canvas-short-drama-entry.css b/web/src/components/canvas/canvas-short-drama-entry.css new file mode 100644 index 000000000..1566993c4 --- /dev/null +++ b/web/src/components/canvas/canvas-short-drama-entry.css @@ -0,0 +1,184 @@ +.canvas-entry-stage { + --canvas-entry-hairline: color-mix(in srgb, var(--canvas-entry-text) 18%, transparent); + --canvas-entry-metal-highlight: var(--canvas-entry-text); + --canvas-entry-metal-mid: color-mix(in srgb, var(--canvas-entry-text) 52%, var(--canvas-entry-muted)); + --canvas-entry-metal-shadow: color-mix(in srgb, var(--canvas-entry-muted) 32%, transparent); + position: absolute; + inset: 0; + z-index: var(--z-node-active); + display: grid; + place-items: center; + padding: var(--space-20) var(--space-6); + color: var(--canvas-entry-text); + pointer-events: none; +} + +.canvas-entry-shell { + width: min(100%, 520px); + max-height: 100%; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: var(--space-2); + text-align: left; + pointer-events: auto; +} + +.canvas-entry-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); +} + +.canvas-entry-heading-copy { min-width: 0; } + +.canvas-entry-header h2 { + display: flex; + flex-direction: column; + gap: var(--space-1); + margin: 0; + font-size: var(--typ-display-md); + font-weight: 400; + letter-spacing: -0.035em; + line-height: 1.45; +} + +.canvas-entry-header h2 > span { color: var(--canvas-entry-muted); } +.canvas-entry-header h2 > strong { font-weight: 600; } + +.canvas-entry-header p { + margin: var(--space-4) 0 0; + color: var(--canvas-entry-muted); + font-size: var(--fs-caption); + line-height: 1.8; + text-wrap: balance; +} + +.canvas-entry-film-mark { + width: calc(var(--space-24) + var(--space-4)); + height: calc(var(--space-24) + var(--space-4)); + flex-shrink: 0; + margin-right: var(--space-2); +} + +.canvas-entry-shell button { + display: inline-flex; + min-height: calc(var(--space-10) + var(--space-1)); + align-items: center; + gap: var(--space-2); + padding: 0; + border: 0; + border-radius: var(--r-sm); + background: transparent; + cursor: pointer; +} + +.canvas-entry-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--space-3) var(--space-8); + margin-top: var(--space-10); +} + +.canvas-entry-shell .canvas-entry-agent { + gap: var(--space-4); + color: var(--canvas-entry-text); + font-size: var(--fs-heading-lg); + font-weight: 600; +} + +.canvas-entry-agent-label { position: relative; } +.canvas-entry-agent-label::after { + content: ""; + position: absolute; + inset: auto 0 calc(-1 * var(--space-1)); + height: var(--stroke-1); + background: currentColor; + transform: scaleX(0); + transform-origin: left; + transition: transform var(--motion-state) var(--motion-ease-out); +} + +.canvas-entry-agent-arrow { + display: grid; + width: var(--space-8); + height: var(--space-8); + place-items: center; + border: var(--stroke-1) solid var(--canvas-entry-hairline); + border-radius: var(--r-full); + transition: border-color var(--motion-state) ease, transform var(--motion-state) var(--motion-ease-out); +} + +.canvas-entry-agent-arrow svg { width: var(--space-4); height: var(--space-4); } +.canvas-entry-agent:is(:hover, :focus-visible) .canvas-entry-agent-label::after { transform: scaleX(1); } +.canvas-entry-agent:is(:hover, :focus-visible) .canvas-entry-agent-arrow { + border-color: var(--canvas-entry-muted); + transform: translate(var(--space-half), calc(-1 * var(--space-half))); +} +.canvas-entry-agent:active .canvas-entry-agent-arrow { transform: none; } + +.canvas-entry-secondary-actions, +.canvas-entry-quick-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-4); +} + +.canvas-entry-secondary-actions button, +.canvas-entry-quick-actions button { + color: var(--canvas-entry-muted); + font-size: var(--fs-caption); + font-weight: 400; + transition: color var(--motion-state) ease; +} + +.canvas-entry-secondary-actions svg { width: var(--space-3); height: var(--space-3); opacity: 0.6; } +.canvas-entry-secondary-actions button:hover, +.canvas-entry-quick-actions button:hover { color: var(--canvas-entry-text); } + +.canvas-entry-shell button:focus-visible { + outline: var(--stroke-2) solid var(--canvas-entry-accent); + outline-offset: var(--space-1); +} + +.canvas-entry-footer { + position: relative; + margin-top: var(--space-6); + padding-top: var(--space-4); +} + +.canvas-entry-footer::before { + content: ""; + position: absolute; + inset: 0 0 auto; + height: var(--stroke-1); + background: linear-gradient(90deg, var(--canvas-entry-hairline), transparent); +} + +.canvas-entry-quick-actions { gap: var(--space-6); } +.canvas-entry-quick-actions svg { width: var(--space-3-half); height: var(--space-3-half); stroke-width: 1.5; } + +@media (max-width: 520px) { + .canvas-entry-stage { padding-inline: var(--space-5); } + .canvas-entry-header h2 { font-size: var(--typ-display-sm); } + .canvas-entry-film-mark { width: var(--space-20); height: var(--space-20); margin-right: 0; } + .canvas-entry-actions { column-gap: var(--space-4); margin-top: var(--space-8); } + .canvas-entry-quick-actions { gap: var(--space-4); } +} + +@media (max-height: 560px) { + .canvas-entry-stage { padding-block: var(--space-4); } + .canvas-entry-actions { margin-top: var(--space-5); } + .canvas-entry-footer { margin-top: var(--space-4); } +} + +@media (prefers-reduced-motion: reduce) { + .canvas-entry-shell button, + .canvas-entry-agent-label::after, + .canvas-entry-agent-arrow { transition: none; } + .canvas-entry-agent:is(:hover, :focus-visible) .canvas-entry-agent-arrow { transform: none; } +} diff --git a/web/src/components/canvas/canvas-short-drama-entry.tsx b/web/src/components/canvas/canvas-short-drama-entry.tsx index b3b245084..8e75a1ba7 100644 --- a/web/src/components/canvas/canvas-short-drama-entry.tsx +++ b/web/src/components/canvas/canvas-short-drama-entry.tsx @@ -1,6 +1,6 @@ -import { Fragment, useState, type CSSProperties, type ReactNode } from "react"; -import { Dropdown, Popover } from "antd"; -import { AlignLeft, ArrowRight, Bot, Check, ChevronDown, ChevronUp, Clapperboard, FolderKanban, Images, MoreHorizontal, Palette, Pencil, Plus, Sparkles, Type, Upload, X } from "lucide-react"; +import { Fragment, useId, useState, type CSSProperties } from "react"; +import { Popover } from "antd"; +import { AlignLeft, ArrowUpRight, Check, ChevronUp, Clapperboard, FolderKanban, Images, Palette, Pencil, Plus, Sparkles, Type, Upload, X } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu"; @@ -8,6 +8,8 @@ import type { CanvasShortDramaProgress, CanvasShortDramaStepId } from "@/lib/can import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import type { CanvasNodeData } from "@/types/canvas"; +import "./canvas-short-drama-entry.css"; + export function CanvasLinkedProjectEmptyState({ projectName, hasChapter, onAddFirstChapter, onOpenAssets, onAddText }: { projectName: string; hasChapter: boolean; onAddFirstChapter: () => void; onOpenAssets: () => void; onAddText: () => void }) { const theme = canvasThemes[useActiveTheme()]; return ( @@ -33,67 +35,68 @@ export function CanvasShortDramaEmptyState({ onCreatePipeline, onOpenAgent, onSt onAddScript: () => void; }) { const theme = canvasThemes[useActiveTheme()]; - const focusStyle = { "--tw-ring-color": theme.accent.primary } as CSSProperties; + const entryStyle = { + "--canvas-entry-text": theme.node.text, + "--canvas-entry-muted": theme.node.muted, + "--canvas-entry-accent": theme.accent.primary, + } as CSSProperties; return ( -
-
-
-

从哪里开始?

-

选择一条主路径,之后仍可随时切换。

+
+
+
+
+

让灵感,开始成片。

+

从一句想法开始,和 Agent 一起创作。

+
+ +
+
+ +
+ + +
-
- } - title="自己创作" - description="搭好短剧骨架,再逐镜头编辑和生成。" - action="创建短剧流水线" - accent={theme.accent.primary} - theme={theme} - focusStyle={focusStyle} - onClick={onCreatePipeline} - /> - } - title="交给 Agent" - description="用一句话描述题材、角色和核心冲突。" - action="一句话生成影视项目" - accent={theme.node.activeStroke} - theme={theme} - focusStyle={focusStyle} - onClick={onOpenAgent} - /> - } - title="自由空白画布" - description="不预设流程,自由添加文本、图片、音频和视频。" - action="从空白画布开始" - accent={theme.node.muted} - theme={theme} - focusStyle={focusStyle} - onClick={onStartFreeform} - /> -
-
- , label: "导入素材", onClick: onUpload }, - { key: "text", icon: , label: "新建文本", onClick: onAddText }, - { key: "storyboard", icon: , label: "新建空白分镜", onClick: onAddScript }, - ], - }} - > - - +
+
+ + + +
); } +function CanvasEntryFilmMark() { + const id = useId(); + return ( + + ); +} + export function CanvasFreeformEmptyState({ commands }: { commands: CanvasCreateCommand[] }) { const theme = canvasThemes[useActiveTheme()]; const [createOpen, setCreateOpen] = useState(false); @@ -132,28 +135,6 @@ export function CanvasFreeformEmptyState({ commands }: { commands: CanvasCreateC ); } -function PathCard({ icon, title, description, action, accent, theme, focusStyle, onClick }: { - icon: ReactNode; - title: string; - description: string; - action: string; - accent: string; - theme: (typeof canvasThemes)[keyof typeof canvasThemes]; - focusStyle: CSSProperties; - onClick: () => void; -}) { - return ( -
- {icon} -
{title}
-

{description}

- -
- ); -} - export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, onStepClick }: { progress: CanvasShortDramaProgress; collapsed: boolean;