diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 93ddf685..d5a01ba1 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,12 +16,37 @@ --- -## 2026-05-27 生成页总进度圆弧锁定固定画布 +## 2026-05-30 创作流程统一化门禁扩展为跨玩法矩阵 -- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题,且圆环还会随着容器宽度伸缩,导致 UI 看起来时大时小、位置漂移。 -- 决策:共用 `GenerationProgressHero` 的 SVG 圆弧起始角固定为 `135deg`,轨道和橘黄色填充都从同一个对称起点 `rotate(135 200 200)` 出发;`270deg` 扫描角配合正下方 `90deg` 留空,圆环本体改为固定 `400x400` 画布,不再跟随页面宽度缩放,外层布局只负责定位,不负责改动圆环样式。 +- 背景:统一创作 / 统一生成门禁已经足够覆盖 Phase 2 的入口与壳层,但当前总计划已经推进到 Phase 3-6,继续只保留单页门禁会让 Phase 4 的特殊工作台、Phase 5 的结果页 / 作品架 / 公开详情和 Phase 6 的冻结验收没有统一入口。 +- 决策:`quality-gates/README.md` 继续保留单页门禁与 `dev-stack` 门禁,同时新增跨玩法回归 / 冒烟门禁,按 Phase 2 到 Phase 5 的最小验证集合分层执行;Phase 6 冻结前以这份矩阵为主,不再另外拆新波次。涉及入口配置、统一字段 spec、普通工作台、RPG / Bark Battle / 视觉小说特殊边界、发布 / 公开 / runtime 或本地 smoke 的变更,优先对照这份矩阵补齐验收命令。 +- 影响范围:`quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`、后续 Phase 2-6 玩法接入与冻结流程。 +- 验证方式:按矩阵执行 `npm run check:encoding`、`npm run typecheck`、`npm run admin-web:typecheck`、对应分期 `npm run test`、`npm run check:visual-novel-vn11`,以及需要时的 `npm run dev:api-server` + `/healthz` smoke。 +- 关联文档:`quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 + +## 2026-05-30 跳一跳结果页直达必须优先恢复作品而不是白屏 + +- 背景:跳一跳结果页已经接入统一壳,但如果用户直接打开 `/creation/jump-hop/result`,旧路径容易因为缺少 `draft` 恢复信息而看起来像白屏,误导成结果页坏了。 +- 决策:`PlatformEntryFlowShellImpl` 的跳一跳恢复顺序固定为 `profileId -> getWorkDetail`,再 `sessionId -> getSession`;两者都拿不到时必须展示 `跳一跳草稿未恢复` 恢复面板和 `返回创作`,不能继续留空白结果页。进入结果页的 smoke 允许恢复面板,但不允许纯空白。 +- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 +- 验证方式:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`;手测 `/creation/jump-hop/result` 和 `/creation/jump-hop/result?profileId=`。 +- 关联文档:`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-29 一期统一创作页必须提供可见统一外壳 + +- 背景:`UnifiedCreationPage` 首版只暴露隐藏 spec 元数据并包裹旧玩法工作台,用户打开拼图创作页时仍只能看到旧工作台外观,无法验收“统一创作页”。 +- 决策:一期统一创作页(拼图、抓大鹅、敲木鱼)必须由 `UnifiedCreationPage` 提供统一标题栏、内容区和隐藏字段契约;字段元信息只留给测试和代码,不再额外作为可见 chip 占用首屏。玩法工作台只承载具体输入控件、上传、历史素材、校验和提交,不再各自渲染巨大入口标题。拼图继续复用 `PuzzleAgentWorkspace` 的上传、裁剪、历史图、AI 重绘和提交逻辑,抓大鹅继续复用 `Match3DAgentWorkspace` 的题材与难度表单逻辑;二者在统一壳内启用 `unifiedChrome`,收起旧标题与外层壳。敲木鱼右侧音效和功德面板不得再套内部滚动容器,移动端应自然跟随页面滚动。 +- 追加决策:`UnifiedCreationPage` 不创建自己的纵向滚动窗;拼图、抓大鹅和敲木鱼三个统一创作入口由平台 stage 承担整页滚动,竖屏移动端必须能从统一标题、表单控件一路滑到提交按钮,避免工作台内部或右侧面板形成套滚动。 +- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx`、`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`、`src/components/wooden-fish-creation/WoodenFishWorkspace.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、玩法链路文档。 +- 验证方式:`UnifiedCreationPage` 测试应断言隐藏契约仍在但 UI 不再出现字段 chip;拼图和抓大鹅工作台测试应断言 `unifiedChrome=true` 时不再渲染旧巨大标题且仍保留表单输入;木鱼工作台测试或手测应确认敲击音效和功德词条不再停留在独立滚动窗内。 +- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + +## 2026-05-27 生成页总进度圆弧锁定固定 SVG 坐标系 + +- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题;后来窄屏验收又发现固定 `400px` 外层宽度会让等待页右侧被裁切。 +- 决策:共用 `GenerationProgressHero` 的 SVG 圆弧起始角固定为 `135deg`,轨道和橘黄色填充都从同一个对称起点 `rotate(135 200 200)` 出发;`270deg` 扫描角配合正下方 `90deg` 留空。SVG 内部坐标系固定为 `400x400`,圆弧使用 `r=166` 和 `strokeWidth=18`;外层显示宽度以 `400px` 为上限,窄屏按 `min(400px, calc(100vw - 2.5rem))` 等比收缩。预计等待 / 已耗时信息卡在窄屏下落到圆环下方两列,`sm` 及以上再回到左右悬浮。 - 影响范围:`src/components/GenerationProgressHero.tsx`、共用 `CustomWorldGenerationView`、汪汪声浪 `BarkBattleGeneratingView` 以及生成页圆环布局文档。 -- 验证方式:`CustomWorldGenerationView` 和 `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`、`data-ring-fill-start-degrees=135`,且圆环容器固定为 `h-[400px] w-[400px]`,track / fill transform 都是 `rotate(135 200 200)`。 +- 验证方式:`CustomWorldGenerationView` 和 `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`、`data-ring-fill-start-degrees=135`,且圆环容器包含 `w-[min(400px,calc(100vw-2.5rem))]`、`max-w-full` 与 `aspect-square`,track / fill transform 都是 `rotate(135 200 200)`;竖屏 smoke 至少覆盖 `280px / 320px / 360px / 390px` 宽度。 - 关联文档:`docs/【玩法创作】生成页圆环布局口径-2026-05-23.md`。 ## 2026-05-26 平台跨流程错误统一用可复制来源弹窗展示 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 23e61f20..6bafa510 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -256,7 +256,7 @@ npm run check:server-rs-ddd ## 提交前建议让 Hermes 执行 -涉及拼图、抓大鹅、敲木鱼统一创作 / 生成链路或本地 dev 栈时,先按 `quality-gates/README.md` 和对应门禁文档执行自动脚本与体验检查。 +涉及拼图、抓大鹅、敲木鱼统一创作 / 生成链路、Phase 2 之后的跨玩法回归或本地 dev 栈时,先按 `quality-gates/README.md`、`quality-gates/【玩法创作】跨玩法回归与冒烟门禁-2026-05-30.md` 和对应单项门禁文档执行自动脚本与体验检查。 ```text 请检查当前 git diff,指出: diff --git a/.hermes/shared-memory/document-map.md b/.hermes/shared-memory/document-map.md index 512c5949..62a5538f 100644 --- a/.hermes/shared-memory/document-map.md +++ b/.hermes/shared-memory/document-map.md @@ -11,6 +11,7 @@ | 产品、命名、UI、协作和废弃路线 | `docs/【项目基线】当前产品与工程约束-2026-05-15.md` | | 后端、DDD、API、SpacetimeDB schema 和表目录 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` | | 创作入口、草稿架和玩法链路 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` | +| 创作流程统一阶段计划 | `docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md` | | 本地启动、验证、部署、埋点和运营查询 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` | | UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` | @@ -32,8 +33,9 @@ 玩法 / 创作入口 / 运行态: 1. `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` -2. `docs/【项目基线】当前产品与工程约束-2026-05-15.md` -3. 相关前端组件、service、shared contract 和后端 module +2. 若任务涉及跨玩法创作流程统一,读取 `docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md` +3. `docs/【项目基线】当前产品与工程约束-2026-05-15.md` +4. 相关前端组件、service、shared contract 和后端 module 生产部署 / 服务器 / Jenkins: diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 3dea8cd4..caca24d6 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -1537,6 +1537,13 @@ - 现象:移动端创作 Tab 里进入汪汪声浪表单后,页面右侧出现不自然的内层滚动条,最后的形象描述输入框容易被“生成草稿”按钮、键盘或底部 TabBar 挤压 / 遮挡;顶部玩法卡首尾也可能贴边显得被裁。 - 原因:外层 `.platform-tab-panel` 已经是纵向滚动容器,创作页中间又有多层 `overflow-hidden`,旧的 `BarkBattleConfigEditor` 根节点再加 `overflow-y-auto`,形成外层 Tab 面板 + 内层表单的套滚动;底部按钮只预留 safe-area,不预留真实操作区距离;顶部玩法卡横向滚动条隐藏且首尾没有 scroll padding。 - 处理:移动端让 Bark Battle 表单跟随父级滚动,`lg` 以上才恢复表单内滚动;创作页容器移动端使用 `overflow-visible` 和 safe-area 底部 padding;顶部模板 tablist 加 `scroll-px-3` / 横向 padding,移动端卡片宽度收窄,避免首尾 ring 和圆角贴边裁切。 + +## 统一创作页不要把竖屏滚动锁进内部内容区 + +- 现象:竖屏打开拼图、抓大鹅或敲木鱼创作页时,浏览器页面本身无法滚动,生成按钮或右侧表单面板落到视口外;木鱼的敲击音效和功德词条看起来像被塞进单独滑动窗口。 +- 原因:平台根壳固定一屏并隐藏溢出,`UnifiedCreationPage` 又使用 `h-full min-h-0 overflow-hidden` 和内容区 `overflow-y-auto`,导致滚动责任落到内部内容窗,而不是整个创作 stage。 +- 处理:`UnifiedCreationPage` 只保留统一标题、隐藏字段契约和内容包装,不再设置内部纵向滚动;拼图、抓大鹅和敲木鱼三个统一创作入口的 `motion.div` stage 负责 `overflow-y-auto overflow-x-hidden`。拼图和抓大鹅在 `unifiedChrome` 下收起旧 `h-full overflow-hidden` 外壳,让表单主体跟随 stage 滚动。 +- 验证:用竖屏浏览器视口打开 `/creation/wooden-fish`、`/creation/puzzle` 和 `/creation/match3d`,页面级 stage 应可滚动到生成按钮;`.unified-creation-page__content` 不应包含 `overflow-y-auto`,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。 - 验证:`npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"`、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。 - 关联:`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 @@ -1596,6 +1603,14 @@ - 验证:移动端视口检查视频 `rect` 应覆盖整个视口,`paused` 应最终变为 `false`,`currentTime` 应持续前进。 - 关联:`src/components/GenerationProgressHero.tsx`、`docs/【玩法创作】生成页圆环布局口径-2026-05-23.md`。 +## 跳一跳结果页直达时不要把恢复面板当成空白页 + +- 现象:浏览器直接打开 `/creation/jump-hop/result`,如果没有 `sessionId`、`profileId`、`draftId` 或 `workId`,页面以前会看起来像空白,容易误判成结果页坏了。 +- 原因:跳一跳结果页恢复原先只盯 `jumpHopSession.draft`,没有把“缺恢复信息”明确兜成可见恢复面板;直达结果页时也没有优先用 `profileId -> getWorkDetail` 补回完整作品。 +- 处理:`PlatformEntryFlowShellImpl` 的跳一跳恢复逻辑改成先尝试 `profileId -> getWorkDetail`,再尝试 `sessionId -> getSession`;两者都没有时显示 `跳一跳草稿未恢复` 和 `返回创作`,不再留空白页。 +- 验证:`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"`,并手测 `/creation/jump-hop/result` 与 `/creation/jump-hop/result?profileId=` 两种情况。 +- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx`、`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md`。 + 2026-05-24 补充:`GenerationPageBackdrop` 不要通过 portal 挂到 `document.body`。body 级 fixed 背景会逃离生成页自己的 stacking context,即使业务内容有局部 `z-10`,真实浏览器里也可能把整页 UI 压住。背景视频应作为生成页根容器子节点保留 `fixed inset-0 z-0`,生成页内容保持 `relative z-10`;相关测试应同时断言背景容器低层级、生成页根容器高层级,以及视频节点仍在生成页 DOM 内部。视觉调整时还要记住:空心圆环的中心块要抽掉,时间卡与总进度标题都应缩小,不要让生成页再回到“纯色底 + 大字号说明卡”的状态。顶部返回和右上状态也不能沿用 `text-lg` / `sm:text-2xl` 这类展示级字号;当前步骤名、步骤状态和底部玩法信息标题要维持普通 UI 字号档位,优先保持 `text-xs` 到 `text-sm` 区间。 2026-05-24 补充:生成页“预计等待 / 已耗时”卡片本身已经有标签,传给 `GenerationProgressHero` 的值只能是纯时间,例如 `4 分钟`、`1 分 15 秒`,不要再拼接“预计还需”或“已耗时”;两张时间卡也要和当前步骤卡一样保持半透明。拼图总进度初始帧必须允许显示 `0%`,不要再用 `Math.max(1, nextProgress)` 之类的保护把启动态抬到 `1%`。 diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 6bcb7c11..3ba26abc 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -161,6 +161,7 @@ export interface AdminCreationEntryTypeConfigPayload { categoryLabel: string; categorySortOrder: number; updatedAtMicros: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; } export interface AdminUpsertCreationEntryTypeConfigRequest { @@ -175,6 +176,23 @@ export interface AdminUpsertCreationEntryTypeConfigRequest { categoryId: string; categoryLabel: string; categorySortOrder: number; + unifiedCreationSpec?: UnifiedCreationSpecPayload | null; +} + +export interface UnifiedCreationSpecPayload { + playId: string; + title: string; + workspaceStage: string; + generationStage: string; + resultStage: string; + fields: UnifiedCreationFieldPayload[]; +} + +export interface UnifiedCreationFieldPayload { + id: string; + kind: 'text' | 'select' | 'image' | 'audio'; + label: string; + required: boolean; } export interface AdminWorkVisibilityEntryPayload { diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx new file mode 100644 index 00000000..75c84504 --- /dev/null +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx @@ -0,0 +1,112 @@ +/* @vitest-environment jsdom */ + +import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {beforeEach, expect, test, vi} from 'vitest'; + +import { + getAdminCreationEntryConfig, + upsertAdminCreationEntryConfig, +} from '../api/adminApiClient'; +import type { + AdminCreationEntryConfigResponse, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; +import {AdminCreationEntrySwitchPage} from './AdminCreationEntrySwitchPage'; + +vi.mock('../api/adminApiClient', () => ({ + formatAdminApiError: vi.fn((error: unknown) => + error instanceof Error ? error.message : '请求失败', + ), + getAdminCreationEntryConfig: vi.fn(), + isAdminApiError: vi.fn(() => false), + upsertAdminCreationEntryConfig: vi.fn(), +})); + +const puzzleSpec: UnifiedCreationSpecPayload = { + playId: 'puzzle', + title: '想做个什么玩法?', + workspaceStage: 'puzzle-agent-workspace', + generationStage: 'puzzle-generating', + resultStage: 'puzzle-result', + fields: [ + { + id: 'pictureDescription', + kind: 'text', + label: '画面描述', + required: true, + }, + ], +}; + +const configResponse: AdminCreationEntryConfigResponse = { + entries: [ + { + id: 'puzzle', + title: '拼图', + subtitle: '拼图关卡创作', + badge: '可创建', + imageSrc: '/creation-type-references/puzzle.webp', + visible: true, + open: true, + sortOrder: 30, + categoryId: 'recent', + categoryLabel: '最近创作', + categorySortOrder: 10, + updatedAtMicros: 1, + unifiedCreationSpec: puzzleSpec, + }, + ], +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse); + vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse); +}); + +test('创作入口后台展示并保存统一创作契约', async () => { + const user = userEvent.setup(); + const {container} = render( + , + ); + + await screen.findByText('pictureDescription'); + expect(container.querySelector('.admin-subsection .admin-info-list')).not.toBeNull(); + expect(container.querySelector('.admin-panel .admin-panel')).toBeNull(); + expect(container.querySelector('.admin-muted')).toBeNull(); + + await user.click(screen.getByRole('button', {name: '保存入库'})); + await user.click(screen.getByRole('button', {name: '确认'})); + + await waitFor(() => { + expect(upsertAdminCreationEntryConfig).toHaveBeenCalledWith( + 'admin-token', + expect.objectContaining({ + id: 'puzzle', + unifiedCreationSpec: puzzleSpec, + }), + ); + }); +}); + +test('创作入口后台拒绝 playId 不一致的统一创作契约', async () => { + const user = userEvent.setup(); + render( + , + ); + + const textarea = await screen.findByLabelText('契约 JSON'); + fireEvent.change(textarea, { + target: { + value: JSON.stringify({ + ...puzzleSpec, + playId: 'match3d', + }), + }, + }); + await user.click(screen.getByRole('button', {name: '保存入库'})); + + expect(await screen.findByText('统一创作契约 playId 必须与入口 ID 一致')).toBeTruthy(); + expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled(); +}); diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx index fb817c65..4a06ddd9 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx @@ -5,7 +5,11 @@ import { getAdminCreationEntryConfig, upsertAdminCreationEntryConfig, } from '../api/adminApiClient'; -import type {AdminCreationEntryTypeConfigPayload} from '../api/adminApiTypes'; +import type { + AdminCreationEntryTypeConfigPayload, + UnifiedCreationFieldPayload, + UnifiedCreationSpecPayload, +} from '../api/adminApiTypes'; import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; import {handlePageError} from './pageUtils'; @@ -30,6 +34,7 @@ export function AdminCreationEntrySwitchPage({ const [categoryId, setCategoryId] = useState('recent'); const [categoryLabel, setCategoryLabel] = useState('最近创作'); const [categorySortOrder, setCategorySortOrder] = useState('10'); + const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [listErrorMessage, setListErrorMessage] = useState(''); @@ -66,6 +71,14 @@ export function AdminCreationEntrySwitchPage({ const targetId = selectedId.trim(); setErrorMessage(''); + const unifiedCreationSpecResult = parseUnifiedCreationSpecJson( + targetId, + unifiedCreationSpecJson, + ); + if (!unifiedCreationSpecResult.ok) { + setErrorMessage(unifiedCreationSpecResult.message); + return; + } const confirmed = await confirmWrite({ action: '保存创作入口开关', target: targetId, @@ -88,6 +101,7 @@ export function AdminCreationEntrySwitchPage({ categoryId: categoryId.trim(), categoryLabel: categoryLabel.trim(), categorySortOrder: parseInteger(categorySortOrder), + unifiedCreationSpec: unifiedCreationSpecResult.spec, }); const nextEntries = sortEntries(response.entries); setEntries(nextEntries); @@ -114,6 +128,7 @@ export function AdminCreationEntrySwitchPage({ setCategoryId(entry.categoryId); setCategoryLabel(entry.categoryLabel); setCategorySortOrder(String(entry.categorySortOrder)); + setUnifiedCreationSpecJson(formatUnifiedCreationSpecJson(entry.unifiedCreationSpec)); } return ( @@ -224,6 +239,26 @@ export function AdminCreationEntrySwitchPage({ /> +
+
+ 统一创作契约 + {unifiedCreationSpecJson.trim() ? '已配置' : '未配置'} +
+ {unifiedCreationSpecJson.trim() ? ( + + ) : ( +
未配置统一创作页契约
+ )} +