diff --git a/CHANGELOG.md b/CHANGELOG.md index c636c8aaf..39a693846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v1.6.8.2 + +- 压缩创作首页首屏,一行展示创建画布与最近更新的 4 个画布,优化窄屏浏览与加载状态。 +- 将灵感大厅合并到创作首页,移除独立灵感页面与导航,保留搜索、筛选、收藏、编辑和导入导出。 +- 优化灵感卡片、封面失败占位、主题适配与键盘/触摸交互;使用灵感后回填提示词并返回输入区。 +- 修复图片参数空切片兼容、局部重绘默认模型同步、远端参考素材选择与跨域图片工具导出问题。 +- 升级注意:本版本无数据库结构变更。 + ## v1.6.8.1 - 新增千问 AI(QianWenAI MAAS)图像协议插件,补齐协议清单、接口文档和回归测试。 diff --git a/VERSION b/VERSION index aae2d18dc..835747a01 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.6.8.1 +v1.6.8.2 diff --git a/docs/content/docs/overview/features.mdx b/docs/content/docs/overview/features.mdx index 1d111a0e8..ed43b77f5 100644 --- a/docs/content/docs/overview/features.mdx +++ b/docs/content/docs/overview/features.mdx @@ -485,28 +485,24 @@ SEO 标题、描述、可选关键词会投影到浏览器 metadata、Open Graph 开启时一级分类也可在后台新增、改名、排序、停用和选择图示,未归入技能仍有默认入口。 -## 创作首页纵深画廊与灵感库 +## 创作首页、最近画布与灵感大厅 -创作页空态是直线纵深图片画廊:11 张横版卡片沿 Z 轴前后排队,当前一张在最前、最大,其余依次后退缩小并左右对称。滚轮、两侧按钮和自动轮播都能切换,切换只动 `transform` 与 `opacity`。卡片带玻璃描边、圆角和投影,底部是「行动点 / 标题 / 描述」三段信息,只有最前那张露出。队列跟随输入框的创作类型:选图片只排图片灵感,选视频只排视频灵感,选文本用本项目原创条目。每次进场和每次换类型都重新随机抽一批,并记住最近出现过哪些,走完一轮自动换批。 +根路径 `/` 与 `/create` 共用创作首页。空态首屏提供紧凑标题、文本/图片/视频/Agent 模式、真实提示词输入区和快捷创作入口。下方一行展示创建画布与最近更新的 4 个画布,支持打开、查看全部和窄屏横向浏览;读取失败提供重试,创建后明确区分本机保存与云端同步。 -工作区左侧「创作」下方新增灵感页 `/inspirations`,展示全部提示词,按来源、分类和创作类型筛选,支持搜索、收藏、视图切换和分页;自建灵感支持新建、编辑、删除和 JSON 导入导出,改动只存在本地。 +灵感大厅合并到创作首页下方,不再提供独立灵感导航或 `/inspirations` 页面路由。类型与搜索常驻,来源、分类、收藏/隐藏和排序收拢在更多筛选中;默认每页 12 条。新建、编辑、隐藏/恢复、收藏与 JSON 导入导出继续保留,本地改动按账号 scope 持久化。使用灵感会关闭详情、回填首页提示词并返回输入区,不自动生成。 -内置灵感的提示词统一提供中文正文:详情阅读、复制、编辑和一键带入创作使用同一份中文内容,首页画廊保持一致。已有中文来源直接采用中文版本,其余外文正文翻译后随静态目录发布,不需要客户等待在线翻译。模型名、引用标记和模板参数保留原有用途。上游原页仍可从来源链接访问。 +内置提示词统一提供中文正文,详情阅读、复制、编辑和回填创作使用同一份中文内容。已有中文来源直接采用中文版本,其余外文正文翻译后随静态目录发布;模型名、引用标记和模板参数保留原有用途。来源及原页链接显示遵循后台来源展示开关。 -译文维护在 `web/scripts/data/inspiration-prompts-zh.json`,记录原文 SHA-256;导入脚本在写入目录前校验原文版本。上游正文更新或新增条目时需补齐匹配的译文,缺译会停止导入,避免重新发布英文提示词。 +译文维护在 `web/scripts/data/inspiration-prompts-zh.json`,记录原文 SHA-256;导入脚本写入目录前校验原文版本,缺译会停止导入。 -生产 Nginx 对 `/inspirations` 与 `/inspirations/` 精确返回 SPA 入口,支持直接打开和刷新;`/inspirations/*.json` 继续提供静态目录数据,避免页面路径与同名目录冲突而跳转到容器端口。 - -外部提示词由 `web/scripts/import-inspirations.mjs` 同步:`awesome-seedance`、好好学 AI 提示词库、YouMind 的两个姊妹库。脚本同时为画廊池生成本地缩略图(560px、webp、约 35KB 一张)。上游封面平均 400KB~700KB,直接引用一屏 11 张要拉 5MB 以上,换成本地缩略图后约 400KB。 - -画廊池走 `public/inspirations/gallery-pool.json` 运行时拉取,不打进首屏 JS:几百条提示词正文压在包里是白背的,而这份数据只服务画廊一处。池子按来源等距抽取以横跨各分类,并且只收描述非空的条目,保证卡片三行版式不缺角;这些条目仍留在全量目录里供灵感页展示。 +外部提示词由 `web/scripts/import-inspirations.mjs` 同步。`web/public/inspirations/*.json` 继续提供静态目录数据,灵感大厅进入可视区域附近后才按需加载全量目录,浏览器缓存与会话内缓存避免重复请求;封面保持稳定比例,加载失败显示占位内容。 ### 相关实现 -- `web/src/pages/create/creation-inspiration-tunnel.tsx`、`creation-inspiration-tunnel.css`:纵深画廊组件与样式。 -- `web/src/pages/inspirations/`:灵感页、详情弹窗、编辑弹窗与样式。 -- `web/src/lib/inspirations/`:来源登记表、条目类型与解析、原创条目、上游版本号(`catalog.ts`、`resolve.ts`、`source-revisions.ts`)。 -- `web/src/services/inspiration-catalog.ts`:全量目录与画廊池的运行时加载和内存缓存。 +- `web/src/pages/create/index.tsx`、`creation-product.css`:首页组合、提示词回填与紧凑布局。 +- `web/src/pages/create/creation-recent-canvases.tsx`:最近画布查询、创建与快捷入口。 +- `web/src/pages/inspirations/`:内嵌灵感大厅、详情/编辑弹窗与样式。 +- `web/src/lib/inspirations/`:来源登记、条目类型、解析、原创条目与上游版本。 +- `web/src/services/inspiration-catalog.ts`:静态目录的运行时加载与内存缓存。 - `web/src/stores/use-inspiration-store.ts`:自建、覆盖、隐藏与收藏的本地持久化。 -- `web/scripts/import-inspirations.mjs`:外部来源同步、缩略图生成与画廊池落盘。 -- `web/public/inspirations/`、`web/public/inspiration-thumbs/`:全量目录、画廊池与本地缩略图。 +- `web/scripts/import-inspirations.mjs`:外部来源同步、缩略图生成与目录落盘。 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 11a0ce73e..f56f6613d 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -92,13 +92,15 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - [ ] 在本地未缓存云端素材的浏览器中选择图片、视频和音频,确认支持的条目可选并能添加;跨页选择后确认应保留选择顺序,再次打开弹窗应保留已选状态。 - [ ] 切换到图片创作及不同视频模型,确认不支持的媒体类型保持禁用,参考数量仍受模型上限约束;云端素材读取失败时保留弹窗并显示错误。 -## 灵感页路由与导航 +## 创作首页与灵感大厅整合 -- 已修复:Nginx 对 `/inspirations` 与 `/inspirations/` 精确返回 SPA 入口,避免同名静态目录触发带容器端口的 301;目录下的 JSON 仍按静态文件提供。 -- 已修复:顶部面包屑补齐灵感路由标题,显示「站点名称 / 灵感」,与侧栏一致,不再回退到站点名称。 -- [x] 使用生产同版本 Nginx 容器复现旧配置的 301;修改后配置检查通过,两个页面路径及查询参数返回入口文件且无重定向,五个真实 JSON 文件返回原内容和正确类型。该验证只覆盖网关,不替代真实页面验收。 -- [ ] 更新前端镜像后,通过实际域名直接打开和刷新两个路径,确认不跳转到 `:3000`、不返回 403,且灵感目录正常加载。 -- [ ] 通过侧栏进入、直接打开及刷新灵感页,确认面包屑显示「灵感」。 +- 已实现:首页首屏压缩为标题、创作模式、提示词区与快捷入口;无附件时参考内容入口合并到输入工具栏。下方一行展示创建入口和最近更新的 4 个画布,保留查看全部、加载失败重试和窄屏横向浏览。 +- 已实现:灵感大厅迁入创作首页,移除独立页面路由、侧栏入口与预加载;保留搜索、类型/来源/分类/收藏隐藏筛选、分页、详情、编辑、导入导出。使用灵感会关闭详情、回填提示词并返回输入区,不自动生成。 +- 已实现:来源展示开关同时作用于卡片和详情;封面失败显示占位,触摸操作常显,键盘焦点与减少动效可用。根路径及 `/create` 的页面壳和侧栏高亮一致。 +- 已验证:前端生产构建、最终类型检查、UI lint 与补丁格式检查通过;已有路由、游客访问与灵感目录专项测试 13 项通过。真实 Chrome 游客首页 11 项断言通过:1440/2048/768/390px 无页面横向溢出、真实主题按钮切换暗色、创建入口打开登录弹窗、灵感详情回填真实输入框、独立灵感导航移除、`/create` 高亮与无未捕获运行时异常。已查看实际页面截图;未提交生成任务或用真实账号创建画布。 +- 已验证:与 GitHub Actions 同命令的冻结依赖安装、变更文件格式检查、Typecheck 和 UI lint 通过;前端全量测试 2302 项通过、0 失败,pretest 通过;预演 P0 Chrome E2E 65 项断言通过。修复灵感大厅、详情、样式和最近画布组件的四处 CI 格式问题;发布版本与更新日志对齐 `v1.6.8.2`。 +- [ ] 用户验收:登录态最近画布排序、创建并进入画布、云端同步失败提示、查看全部;切换账号后列表不串数据。 +- [ ] 用户验收:明暗主题与 390/768/1440px 布局、滚动、弹窗、空态和完整长提示词编辑;收藏、新建/编辑、隐藏恢复与 JSON 导入导出正常,来源开关显示一致。 ## 画布素材缺失时的本地强制覆盖 diff --git a/web/src/components/layout/app-top-nav.tsx b/web/src/components/layout/app-top-nav.tsx index 291a177a8..1e517ca3a 100644 --- a/web/src/components/layout/app-top-nav.tsx +++ b/web/src/components/layout/app-top-nav.tsx @@ -23,7 +23,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) { const hideChrome = pathname.startsWith("/admin") || /^\/canvas\/[^/]+/.test(pathname); const showGlobalTopBar = !hideChrome; const spatialWorkbench = isSpatialWorkbenchPath(pathname); - const creationWorkspace = pathname === "/"; + const creationWorkspace = pathname === "/" || pathname === "/create"; const isMobileViewport = () => window.innerWidth < 1024; diff --git a/web/src/components/layout/workspace-sidebar-nav.tsx b/web/src/components/layout/workspace-sidebar-nav.tsx index 90215cbe5..7b34f2eda 100644 --- a/web/src/components/layout/workspace-sidebar-nav.tsx +++ b/web/src/components/layout/workspace-sidebar-nav.tsx @@ -45,7 +45,6 @@ function buildNav(features: FeatureAvailability): { groups: WorkspaceNavGroup[]; { items: [ { ...toolItem("create", "/"), id: "home", title: "创作" }, - { ...toolItem("inspirations", "/inspirations"), title: "灵感" }, { ...toolItem("projects", "/projects"), title: "短剧制作" }, { ...toolItem("canvas", "/canvas"), title: "自由画布" }, ], @@ -338,6 +337,7 @@ export function workspaceNavActiveId(pathname: string, section?: string | null) const segments = pathname.split("/").filter(Boolean); if (segments[0] === "toolbox" && segments[1]) return `toolbox-${segments[1]}`; const slug = segments[0] || "home"; + if (slug === "create") return "home"; return slug === "settings" && section ? `settings:${section}` : slug; } diff --git a/web/src/components/layout/workspace-top-bar.tsx b/web/src/components/layout/workspace-top-bar.tsx index 49c9d1a5c..b2c7716ed 100644 --- a/web/src/components/layout/workspace-top-bar.tsx +++ b/web/src/components/layout/workspace-top-bar.tsx @@ -15,7 +15,6 @@ import { openWorkspaceWallet } from "@/lib/workspace-wallet"; const PAGE_TITLES: Record = { home: "创作", create: "创作", - inspirations: "灵感", projects: "短剧制作", canvas: "自由画布", tasks: "创作历史", diff --git a/web/src/constant/navigation-tools.ts b/web/src/constant/navigation-tools.ts index 6c8ca1a66..92d2e862c 100644 --- a/web/src/constant/navigation-tools.ts +++ b/web/src/constant/navigation-tools.ts @@ -1,4 +1,4 @@ -import { Blocks, CircleDollarSign, Clapperboard, GalleryVerticalEnd, Images, LibraryBig, ListTodo, PanelsTopLeft, Settings, WandSparkles } from "lucide-react"; +import { Blocks, CircleDollarSign, Clapperboard, Images, LibraryBig, ListTodo, PanelsTopLeft, Settings, WandSparkles } from "lucide-react"; export const navigationTools = [ { @@ -7,12 +7,6 @@ export const navigationTools = [ icon: WandSparkles, section: "创作空间", }, - { - slug: "inspirations", - label: "灵感", - icon: GalleryVerticalEnd, - section: "创作空间", - }, { slug: "projects", label: "短剧创作", diff --git a/web/src/lib/workspace-route-modules.ts b/web/src/lib/workspace-route-modules.ts index 1947113d8..d438e050c 100644 --- a/web/src/lib/workspace-route-modules.ts +++ b/web/src/lib/workspace-route-modules.ts @@ -2,7 +2,6 @@ const workspaceRouteLoaders = { assets: () => import("@/pages/assets"), canvas: () => import("@/pages/canvas"), create: () => import("@/pages/create"), - inspirations: () => import("@/pages/inspirations"), projects: () => import("@/pages/projects"), projectDetail: () => import("@/pages/projects/detail"), }; @@ -11,7 +10,6 @@ export const loadAssetsPage = workspaceRouteLoaders.assets; export const loadCanvasPage = workspaceRouteLoaders.canvas; export const loadCanvasProjectPage = () => import("@/pages/canvas/project"); export const loadCreatePage = workspaceRouteLoaders.create; -export const loadInspirationsPage = workspaceRouteLoaders.inspirations; export const loadProjectDetailPage = workspaceRouteLoaders.projectDetail; export const loadProjectsPage = workspaceRouteLoaders.projects; diff --git a/web/src/pages/create/creation-login-dialog.tsx b/web/src/pages/create/creation-login-dialog.tsx index 1ec4d1626..33b43640e 100644 --- a/web/src/pages/create/creation-login-dialog.tsx +++ b/web/src/pages/create/creation-login-dialog.tsx @@ -28,7 +28,7 @@ export function CreationLoginDialog({ open, onClose }: { open: boolean; onClose: await login({ username: username.trim(), password }); const { applyUserSession } = await import("@/lib/user-session"); await applyUserSession(await getAuthSession()); - message.success("登录成功,请再次点击生成"); + message.success("登录成功,可以继续创作了"); onClose(); } catch (error) { message.error(error instanceof Error ? error.message : "登录失败"); @@ -40,7 +40,7 @@ export function CreationLoginDialog({ open, onClose }: { open: boolean; onClose: return (
-

你的创作内容会保留。登录成功后,请再次点击生成。

+

你的创作内容会保留。登录成功后,即可继续创作或创建画布。

void submit(event)}> * { width: 100%; max-width: 1280px; margin-inline: auto; } +.creation-home-scroll > .creation-launchpad { width: min(960px, 100%); margin-inline: auto; } +.creation-start-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); } +.creation-start-heading h1 { margin: 0; color: var(--user-ink); font-size: var(--typ-display-sm); font-weight: 650; letter-spacing: -.025em; line-height: 1.3; } +.creation-start-heading p { margin: var(--space-1) 0 0; color: var(--user-ink-muted); font-size: var(--fs-caption); line-height: 1.6; } +.creation-history-entry { display: inline-flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); min-height: var(--user-control-height); border: 0; border-radius: var(--user-radius-control); padding: var(--space-2) var(--space-3); background: var(--user-surface-muted); color: var(--user-ink-muted); font-size: var(--fs-caption); cursor: pointer; } +.creation-history-entry:hover { background: var(--user-surface-hover); color: var(--user-ink); } +.creation-history-entry svg { width: var(--user-icon-size); height: var(--user-icon-size); } +.creation-recent-canvases { margin-top: var(--space-6); } +.creation-section-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); } +.creation-section-heading h2 { display: inline-flex; align-items: center; gap: var(--space-2); margin: 0; color: var(--user-ink); font-size: var(--fs-body); font-weight: 600; } +.creation-section-heading h2 svg { width: var(--user-icon-size); height: var(--user-icon-size); color: var(--user-ink-muted); } +.creation-section-link { display: inline-flex; align-items: center; gap: var(--space-1); color: var(--user-ink-muted); font-size: var(--fs-caption); } +.creation-section-link:hover { color: var(--user-ink); } +.creation-section-link svg { width: var(--user-icon-size); height: var(--user-icon-size); } +.creation-canvas-strip { display: flex; gap: var(--space-3); overflow-x: auto; padding: var(--space-1) var(--space-1) var(--space-2); margin-inline: calc(-1 * var(--space-1)); scroll-snap-type: x proximity; } +.creation-canvas-strip-item { min-width: 200px; flex: 1 0 200px; scroll-snap-align: start; } +.creation-canvas-tile { display: flex; align-items: center; gap: var(--space-3); width: 100%; min-width: 0; height: 88px; border: 0; border-radius: var(--user-radius-card); padding: var(--space-3); background: var(--library-surface); color: var(--user-ink); text-align: left; cursor: pointer; transition: background var(--user-motion), transform var(--user-motion); } +.creation-canvas-tile:hover { background: var(--library-surface-hover); transform: translateY(-2px); } +.creation-canvas-tile:disabled { cursor: wait; opacity: .5; transform: none; } +.creation-canvas-thumbnail { display: block; width: 56px; height: 56px; flex: 0 0 56px; overflow: hidden; border-radius: var(--user-radius-control); background: var(--library-cover-surface); } +.creation-canvas-thumbnail.is-create { display: grid; place-items: center; background: var(--library-icon-surface); color: var(--user-ink-muted); } +.creation-canvas-thumbnail.is-create svg { width: var(--space-6); height: var(--space-6); } +.creation-canvas-thumbnail .canvas-project-empty { background: var(--library-cover-surface); } +.creation-canvas-thumbnail .canvas-project-empty-icon { width: var(--space-5); height: var(--space-5); margin: 0; } +.creation-canvas-thumbnail .canvas-project-empty :is(span, small) { display: none; } +.creation-canvas-copy { display: flex; min-width: 0; flex-direction: column; gap: var(--space-1-half); } +.creation-canvas-copy strong { overflow: hidden; font-size: var(--fs-caption); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } +.creation-canvas-copy small { overflow: hidden; color: var(--user-ink-muted); font-size: var(--fs-label); text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; } +.creation-canvas-strip-state { display: flex; min-width: 0; flex: 4 0 240px; align-items: center; gap: var(--space-3); border-radius: var(--user-radius-card); padding: var(--space-4); color: var(--user-ink-muted); background: var(--library-surface); font-size: var(--fs-caption); } +.creation-canvas-strip-state > svg { width: var(--space-5); height: var(--space-5); flex-shrink: 0; } +.creation-canvas-strip-state button { flex: 0 0 auto; margin-left: auto; border: 0; border-radius: var(--user-radius-control); padding: var(--space-2) var(--space-3); background: var(--library-icon-surface); color: var(--user-ink); cursor: pointer; } +.creation-canvas-skeleton { display: flex; height: 88px; align-items: center; gap: var(--space-3); border-radius: var(--user-radius-card); padding: var(--space-3); background: var(--library-surface); } +.creation-canvas-skeleton > span { width: 56px; height: 56px; flex: 0 0 56px; border-radius: var(--user-radius-control); background: var(--library-cover-surface); } +.creation-canvas-skeleton > span:last-child { width: 60%; height: var(--space-5); flex: 1; } +.creation-hall-loading { margin-top: var(--space-8); padding: var(--space-6); border-radius: var(--user-radius-card); background: var(--library-surface); color: var(--user-ink-muted); font-size: var(--fs-caption); } +.creation-source-notice { margin-top: var(--space-5); color: var(--user-ink-muted); font-size: var(--fs-caption); line-height: 1.7; } +.creation-source-notice summary { width: fit-content; padding-block: var(--space-2); cursor: pointer; } +.creation-source-notice p { margin: var(--space-2) 0; } +.creation-source-notice a { color: var(--user-ink); text-decoration: underline; text-underline-offset: var(--space-half); } +:is(.creation-history-entry, .creation-section-link, .creation-canvas-tile, .creation-canvas-strip-state button):focus-visible { outline: 2px solid var(--control-focus-ring); outline-offset: 2px; } +@media (max-width: 720px) { + .creation-home-scroll { padding: var(--space-4) var(--space-4) var(--space-8); } + .creation-start-heading { align-items: flex-start; margin-bottom: var(--space-4); gap: var(--space-2); } + .creation-start-heading h1 { font-size: var(--fs-title); } + .creation-history-entry { padding: var(--space-2); } + .creation-history-entry span { display: none; } + .creation-canvas-strip-item { min-width: 220px; flex-basis: 220px; } +} +@media (prefers-reduced-motion: reduce) { + .creation-canvas-tile { transition: none; } + .creation-canvas-tile:hover { transform: none; } +} .creation-mode-tabs { position: relative; isolation: isolate; display: grid; width: min(456px, 100%); margin: 0 auto 20px; padding: 4px; border-radius: 18px; background: var(--user-surface-muted); } .creation-mode-indicator { position: absolute; z-index: -1; inset: 0; border-radius: 14px; background: var(--user-surface); box-shadow: 0 2px 8px color-mix(in srgb, var(--user-ink) 5%, transparent); } .creation-mode-button { position: relative; isolation: isolate; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 0; height: 44px; padding: 0 12px; border: 0; border-radius: 14px; background: transparent; color: var(--user-ink-muted); font-size: 14px; font-weight: 600; cursor: pointer; transition: color var(--user-motion), transform var(--user-motion); } diff --git a/web/src/pages/create/creation-recent-canvases.tsx b/web/src/pages/create/creation-recent-canvases.tsx new file mode 100644 index 000000000..3fc008b17 --- /dev/null +++ b/web/src/pages/create/creation-recent-canvases.tsx @@ -0,0 +1,135 @@ +import { useMemo, useRef, useState } from "react"; +import { App } from "antd"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowUpRight, Clock3, LoaderCircle, PanelsTopLeft, Plus } from "lucide-react"; +import { Link, useNavigate } from "react-router"; + +import { ProjectPreview } from "@/components/canvas/canvas-project-card"; +import { getActiveUserScope } from "@/lib/user-scope"; +import { loadCanvasProjectPage } from "@/lib/workspace-route-modules"; +import { listRemoteCanvasProjectsPage, type CanvasLibrarySummary } from "@/services/api/user-data"; +import { createCanvasProjectWithRemoteSync } from "@/services/user-data-sync"; +import { flushCanvasStorePersistence, useCanvasStore } from "@/stores/canvas/use-canvas-store"; +import { useUserStore } from "@/stores/use-user-store"; + +const RECENT_COUNT = 4; +const updatedFormatter = new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }); + +export function CreationRecentCanvases({ onRequireLogin }: { onRequireLogin: () => void }) { + const { message } = App.useApp(); + const navigate = useNavigate(); + const userId = useUserStore((state) => state.user?.id); + const sessionHydrated = useUserStore((state) => state.hydrated); + const canvasHydrated = useCanvasStore((state) => state.hydrated); + const localProjects = useCanvasStore((state) => state.projects); + const [creating, setCreating] = useState(false); + const creatingRef = useRef(false); + const recentQuery = useQuery({ + queryKey: ["canvas-library", userId, "recent"], + queryFn: ({ signal }) => listRemoteCanvasProjectsPage({ page: 1, pageSize: RECENT_COUNT, sort: "updated", signal }), + enabled: Boolean(userId) && sessionHydrated, + }); + const recentProjects = useMemo( + () => + userId + ? recentQuery.data?.projects || [] + : [...localProjects] + .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt)) + .slice(0, RECENT_COUNT) + .map((project) => ({ ...project, nodeCount: project.nodes.length, previewNodes: project.nodes.slice(0, 4) })), + [localProjects, recentQuery.data, userId], + ); + const loading = !sessionHydrated || !canvasHydrated || Boolean(userId && recentQuery.isPending); + const preload = () => { + void loadCanvasProjectPage(); + }; + const createCanvas = async () => { + if (!userId) { + onRequireLogin(); + return; + } + if (!canvasHydrated || creatingRef.current) return; + const scope = getActiveUserScope(); + creatingRef.current = true; + setCreating(true); + try { + const { id, syncError } = await createCanvasProjectWithRemoteSync("未命名画布"); + await flushCanvasStorePersistence(); + if (getActiveUserScope() !== scope || useUserStore.getState().user?.id !== userId) return; + if (syncError) message.warning("画布已保存在本机,云端同步尚未完成,将自动重试。"); + navigate(`/canvas/${id}`); + } catch (error) { + if (getActiveUserScope() === scope) message.error(error instanceof Error ? error.message : "创建画布失败,请重试"); + } finally { + creatingRef.current = false; + setCreating(false); + } + }; + + return ( +
+
+

+

+ + 查看全部 +
+
+
+ +
+ {loading ? ( + Array.from({ length: RECENT_COUNT }, (_, index) => ( +
+ + +
+ )) + ) : userId && recentQuery.isError ? ( +
+ 最近画布暂时无法读取 + +
+ ) : recentProjects.length ? ( + recentProjects.map((project) => ( +
+ + + + {project.title || "未命名画布"} + + {project.nodeCount} 个节点 · + + + +
+ )) + ) : ( +
+
+ )} +
+
+ ); +} diff --git a/web/src/pages/create/creation-workspace.tsx b/web/src/pages/create/creation-workspace.tsx index 92decc6bc..6b97d30e6 100644 --- a/web/src/pages/create/creation-workspace.tsx +++ b/web/src/pages/create/creation-workspace.tsx @@ -305,7 +305,7 @@ export function CreationComposer(props: ComposerProps) {
{ const target = props.references.find((item) => item.id === reference.id); if (target?.attachmentId) props.onReplaceReferenceFiles(target.attachmentId, files); }} /> - {props.attachments.length || referencesSupported ?
+ {props.attachments.length ?
{trackState.isExpanded ?
{([ @@ -374,6 +374,7 @@ export function CreationComposer(props: ComposerProps) {