From 7ce4a3a9c63358697af37cbd381a925f4f7d8c17 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 14 Sep 2026 17:07:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E7=B4=A0=E6=9D=90=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=BB=8E=E3=80=8C=E7=BC=96=E8=BE=91=E7=B4=A0=E6=9D=90=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E3=80=8D=E9=9D=A2=E6=9D=BF=E6=8B=86=E6=88=90=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ResourceTypePanel(src/view/project-development/ResourceTypePanel.tsx):类型选一项即落盘,category 传用户选中值、tags 逐字回传落盘原值,保存在飞时沿用 closeOnBackdrop/closeOnEscape = !saving - 新增 resourceAssetDisplayName.ts:两块面板共用同一个素材名副标题口径(localPath basename),不再各写一份 - 新增 resourceTypePanel.css:类型面板的弹窗骨架与信息浮层「分类」行入口样式,按 AGC 内独立文件约定,不动 packages/** 与 styles.css - ResourceClassificationPanel 删掉类型 chip 与 categoryChoice:保存时恒回传 gameCreationAppAssetPersistedCategory(asset),「只改标签不动分类」由结构保证 - index.tsx 新增 resourceTypeAssetId 与 resourceTypeAsset;工具条在「编辑标签」旁新增「素材类型」按钮(Shapes 图标) - index.tsx 用合并后的 resourceClassificationOverlayOpen 纳入点外部/Esc 浮层遮挡判据,并同步 useEffect 依赖项 - index.tsx 渲染新面板:保存成功后由宿主收起面板并沿用 manifest 重载路径,让卡片立刻落到新栏目 - 信息浮层「分类」行接出第二入口:按钮渲染在 dd 之外,不污染 dt/dd 字段读取口径;非 manifest 资产不给入口 - resourceCanvasInfoModel 导出 RESOURCE_INFO_CATEGORY_FIELD_LABEL,模型与入口共用同一行标识 - 新增 tests/resourceTypePanel.test.tsx:选中即落盘、tags 逐字回传、没碰过不写盘、显式待归类、失败回退、saving 锁、Esc/遮罩、面板骨架共 11 条 - 迁移「没碰过分类就回传落盘原值」对照用例并保留两条(标签面板侧),chip 用例搬到新面板文件 - 集成用例改走新入口:选中即落盘并自动关窗,另补 Esc/点外部不串台、只改标签后 manifest category 逐字不变两条宿主级用例 - appSurface 工具条清单补上「素材类型」 --- .../project-workspace/resourceTypePanel.css | 69 +++ .../resourceCanvasInfoModel.ts | 13 +- .../ResourceClassificationPanel.tsx | 72 +-- .../ResourceInfoPanelView.tsx | 29 +- .../project-development/ResourceTypePanel.tsx | 181 ++++++++ .../src/view/project-development/index.tsx | 86 +++- .../resourceAssetDisplayName.ts | 12 + .../appSurface/project-development.suite.ts | 8 +- .../projectResourceLiveIntegration.test.tsx | 160 ++++++- .../resourceClassificationPanel.test.tsx | 186 ++------ .../tests/resourceTypePanel.test.tsx | 422 ++++++++++++++++++ 11 files changed, 1011 insertions(+), 227 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css create mode 100644 apps/ai-game-creator-shell/src/view/project-development/ResourceTypePanel.tsx create mode 100644 apps/ai-game-creator-shell/src/view/project-development/resourceAssetDisplayName.ts create mode 100644 apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css b/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css new file mode 100644 index 000000000..f582f3885 --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css @@ -0,0 +1,69 @@ +/* + * 「设置素材类型」面板的弹窗骨架与信息浮层的类型入口。 + * + * 单独一个文件而不是塞进 styles.css:与「编辑素材标签」面板当初同样的理由 —— + * 这份样式只服务本次的素材类型入口,与工作台其它区块没有共享选择器,独立文件让改动 + * 边界更清楚,也不会与同一时段其它 Agent 在 styles.css 里的编辑互相踩。 + * + * 骨架沿用「编辑素材标签」那套三段式契约(`auto / minmax(0, 1fr)`):标题常驻、 + * 中间一行可压缩可滚动、`max-height` 兜住上界。类型面板没有底部按钮,所以只有两行。 + */ +.game-resource-type-dialog { + width: min(480px, 100%); + max-height: min(720px, calc(100dvh - 40px)); + grid-template-rows: auto minmax(0, 1fr); +} + +/* + * 滚动落在 body 这一行:`min-height: 0` 是网格项能被 `1fr` 压缩的前提, + * 否则内容高度会顶回轨道、`overflow-y` 永远不触发。 + */ +.game-resource-type-body { + display: grid; + gap: 10px; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +/* + * 类型选项之上的一句短提示。只说这一屏要选什么,不写规则说明或开发解释。 + */ +.game-resource-type-hint { + margin: 0; + color: var(--platform-text-base); + font-size: 12px; +} + +.game-resource-type-error { + margin: 0; + color: #b3261e; + font-size: 11px; +} + +/* + * 第二入口:信息浮层「分类」行右侧的入口按钮。 + * + * 放在 `dd` **外面**:信息字段的读取口径(`dt` / `dd` 文本逐行比对)在两处共用, + * 把按钮塞进 `dd` 会让分类值变成「角色与对象设置」这类拼接文案。 + */ +.game-resource-info-field-action { + align-self: start; + margin-left: auto; + padding: 0 6px; + border: 1px solid var(--platform-subpanel-border); + border-radius: 8px; + background: transparent; + color: var(--platform-text-base); + font-size: 11px; + line-height: 20px; + cursor: pointer; +} + +.game-resource-info-field-action:hover, +.game-resource-info-field-action:focus-visible { + border-color: var(--platform-surface-hover-border); + background: var(--platform-warm-bg); + color: var(--platform-text-strong); +} diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasInfoModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasInfoModel.ts index 9370975b7..09b4ce29a 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasInfoModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasInfoModel.ts @@ -19,6 +19,14 @@ export type ResourceInfoFieldRow = { const EMPTY_TAGS_TEXT = '暂无标签'; +/** + * 「分类」字段的行标识。 + * + * 画布上的信息浮层用这一行接出素材类型设置入口(运行页签的「信息展示」不带入口, + * 同一份字段清单在两处渲染);把字面量放在这里,模型与入口两侧不会各写一个。 + */ +export const RESOURCE_INFO_CATEGORY_FIELD_LABEL = '分类'; + /** * 栏目文案与资源筛选同源;`version` 不是跨端资源分类,和画布栏目一样单独给文案。 */ @@ -43,7 +51,10 @@ export function resolveResourceInfoFieldRows( { label: '名称', value: resource.label }, { label: '路径', value: resource.path }, { label: '类型', value: resource.mediaType }, - { label: '分类', value: resourceCategoryLabel(resource.category) }, + { + label: RESOURCE_INFO_CATEGORY_FIELD_LABEL, + value: resourceCategoryLabel(resource.category), + }, { label: '标签', value: tags.length > 0 ? tags.join('、') : EMPTY_TAGS_TEXT, diff --git a/apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx b/apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx index 0f209158c..1bd9ed092 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx @@ -5,38 +5,22 @@ import { useState } from 'react'; import { PlatformActionButton } from '../../../../../packages/shared/src/components/PlatformActionButton'; import { PlatformPillBadge } from '../../../../../packages/shared/src/components/PlatformPillBadge'; -import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs'; import { PlatformTextField } from '../../../../../packages/shared/src/components/PlatformTextField'; import { - GAME_CREATION_APP_ASSET_CATEGORIES, - type GameCreationAppAssetCategory, - gameCreationAppAssetCategory, type GameCreationAppAssetManifestEntry, gameCreationAppAssetPersistedCategory, gameCreationAppAssetTags, normalizeGameCreationAppAssetTags, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { ThemedModal } from '../../components/modal/ThemedModal'; -import { resourceReferenceCategoryLabel } from '../../features/project-workspace/resourceReferences'; import { projectAssetCommandErrorMessage } from './projectAssetCommandErrorMessage'; +import { resourceAssetDisplayName } from './resourceAssetDisplayName'; type UpdateLocalProjectResourceClassificationResult = { asset: GameCreationAppAssetManifestEntry; committedProjectRevision: number; }; -/** - * 素材类型(功能分类)选项 = 合法分类枚举 × 既有中文展示名。 - * - * 展示名只从 `resourceReferenceCategoryLabel`(筛选与栏目的同一份口径)取, - * 不在业务页另写一张译名表 —— 面板说「角色与对象」而栏目说别的,用户会以为是两个东西。 - */ -const RESOURCE_CLASSIFICATION_CATEGORY_OPTIONS = - GAME_CREATION_APP_ASSET_CATEGORIES.map((category) => ({ - id: category, - label: resourceReferenceCategoryLabel(category), - })); - /** * 标签草稿沿用写入路径的归一化边界,只按中英文逗号、顿号与换行切分。 * 与输入框旧的"整段逗号分隔文本"口径完全一致,改动只是把结果换成逐个可删的 pill。 @@ -57,16 +41,6 @@ function mergeResourceClassificationTagDraft( return next; } -/** - * 素材名取 `localPath` 的 basename:manifest 资产没有独立的显示名字段, - * 与资源卡、`@` 面板的显示口径一致。 - */ -function resourceAssetDisplayName(localPath: string) { - const normalized = localPath.replaceAll('\\', '/'); - const segments = normalized.split('/'); - return segments[segments.length - 1] || localPath; -} - function resourceClassificationErrorMessage(error: unknown) { // 项目身份 / 版本 CAS 拒绝翻成用户可读中文,其余原样透出; // 与重命名、删除共用同一份映射。 @@ -89,26 +63,17 @@ export function ResourceClassificationPanel({ onSaved, }: ResourceClassificationPanelProps) { /** - * 素材类型(功能分类)从本面板设置,与标签同一次保存、同一条写入路径。 + * 本面板只编辑标签:素材类型(功能分类)在「设置素材类型」面板里单独设置。 * - * **选择器读的是「显示口径」** `gameCreationAppAssetCategory`:它与资源画布栏目 - * (`projectResourceAssetCategory` / `projectResourceCanvasCategory`)同一份读数, - * 所以用户看到的选中项恰好就是他看到的那一栏,不存在「面板说 A、卡片在 B 栏」。 - * - * **写回不能用这个读数**:显示口径含读时自愈 —— 落盘 `unclassified` 而 `kind` 能派生出 - * 明确分类时,读出来的是派生值。回传它就等于用户只改标签也被静默改了分类 + * **写回必须用落盘口径** `gameCreationAppAssetPersistedCategory`,不能用读显示口径 + * `gameCreationAppAssetCategory`:显示口径含读时自愈 —— 落盘 `unclassified` 而 `kind` + * 能派生出明确分类时,读出来的是派生值。回传它就等于用户只改标签也被静默改了分类 * (真机上同一条 `kind:"ui"` 资产同时出现过 `unclassified` 与 `ui-interaction` 两种落盘值)。 * - * 因此用 `categoryChoice` 表达「用户是否主动选过」: - * - `null`(没碰过分类控件)→ 回传 `gameCreationAppAssetPersistedCategory` 的落盘原值; - * - 用户选过 → 回传用户选的那个值。 - * 这条分叉是本次改动的核心不变量,两个方向都由 - * `tests/resourceClassificationPanel.test.tsx` 的对照用例钉住。 + * 拆出类型面板后这条不变量不再依赖"用户是否碰过控件",而是结构性的: + * 本面板没有类型控件,`category` 恒为落盘原值。 + * `tests/resourceClassificationPanel.test.tsx` 两个方向各有用例钉住它。 */ - const [categoryChoice, setCategoryChoice] = - useState(null); - const displayedCategory = - categoryChoice ?? gameCreationAppAssetCategory(asset); const [tags, setTags] = useState(() => gameCreationAppAssetTags(asset), ); @@ -159,9 +124,8 @@ export function ResourceClassificationPanel({ expectedProjectId: projectId, expectedProjectRevision: status.revision, assetId: asset.id, - // 用户没主动选类型就原样回传落盘值(不含读时自愈),选了就写用户选的那个。 - category: - categoryChoice ?? gameCreationAppAssetPersistedCategory(asset), + // 分类不由本面板编辑:恒回传落盘原值(不含读时自愈)。 + category: gameCreationAppAssetPersistedCategory(asset), tags: normalizeGameCreationAppAssetTags(tagsToSave), }, }, @@ -214,20 +178,10 @@ export function ResourceClassificationPanel({
{/* - 素材类型选择器:选中的那一项就是这张卡当前所在的画布栏目。 - 点任意一项即视为用户主动改类型(即便点的是当前已选中的那一项), - 与「没碰过就回传落盘原值」的分叉保持同一条判据,不做隐式 no-op。 + 本面板没有素材类型控件:类型是「设置素材类型」面板的编辑对象,入口在资源卡选中 + 工具条上。曾长在这里的类型 chip 只改本地 state、不落盘,保存又只能借道标签的 + 「添加」,导致"改了类型没生效"。 */} - {tags.length > 0 ? (
    {tags.map((tag) => ( diff --git a/apps/ai-game-creator-shell/src/view/project-development/ResourceInfoPanelView.tsx b/apps/ai-game-creator-shell/src/view/project-development/ResourceInfoPanelView.tsx index 94eadd92e..6134941e9 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/ResourceInfoPanelView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/ResourceInfoPanelView.tsx @@ -3,6 +3,7 @@ import { Info, X } from 'lucide-react'; import { resolveResourceInfoFieldRows, resolveResourceInfoPanelStyle, + RESOURCE_INFO_CATEGORY_FIELD_LABEL, type ResourceInfoPanelAnchor, } from '../../features/resource-canvas/resourceCanvasInfoModel'; import type { ProjectResource } from './resourceProjectionModel'; @@ -10,11 +11,17 @@ import type { ProjectResource } from './resourceProjectionModel'; /** * 只读资源信息字段。运行页签的「信息展示」与画布上的信息浮层共用这一份, * 字段清单只在 `resolveResourceInfoFieldRows` 里定义,两处不会各说一套。 + * + * `onEditCategory` 是「分类」行的可选入口(只有画布浮层传):分类值本身仍然只读展示, + * 入口按钮渲染在 `dd` **外面** —— 字段值的读取口径是 `dt` / `dd` 的文本, + * 把按钮塞进 `dd` 会让分类值变成「角色与对象设置」这类拼接文案。 */ export function ResourceInfoFieldsView({ resource, + onEditCategory, }: { resource: ProjectResource; + onEditCategory?: () => void; }) { return (
    @@ -22,6 +29,18 @@ export function ResourceInfoFieldsView({
    {row.label}
    {row.value}
    + {onEditCategory && + row.label === RESOURCE_INFO_CATEGORY_FIELD_LABEL ? ( + + ) : null}
    ))}
    @@ -30,6 +49,10 @@ export function ResourceInfoFieldsView({ export type ResourceInfoPanelViewProps = ResourceInfoPanelAnchor & { resource: ProjectResource; + /** + * 「分类」行的类型设置入口;不传就没有入口(资源不是 manifest 资产时宿主不传)。 + */ + onEditCategory?: () => void; onClose: () => void; }; @@ -44,6 +67,7 @@ export function ResourceInfoPanelView({ sourceLayer, viewport, canvasSize, + onEditCategory, onClose, }: ResourceInfoPanelViewProps) { const style = resolveResourceInfoPanelStyle({ @@ -75,7 +99,10 @@ export function ResourceInfoPanelView({