把运行页签的资源信息搬到画布选中工具条,两处共用一份只读字段
- 新增「信息」动作:排在「@引用」之后、「编辑标签」之前,点开非模态信息浮层,再点一次即收起(aria-pressed 反映开合) - 新增只读字段组件 ResourceInfoFieldsView(ResourceInfoPanelView.tsx),运行页签的「信息展示」与画布浮层共用,字段清单只在 resolveResourceInfoFieldRows 单点定义 - 字段口径:名称 / 路径 / 类型 + 只读分类与标签(分类取画布栏目口径,标签为空时给「暂无标签」),刻意不含来源任务、资产 ID 等 manifest 内部标识 - 浮层 chrome 与快速编辑浮层同族(同一锚点、同档圆角与阴影),样式落在 resourceCanvasChrome.css;关闭入口三处:动作按钮、面板关闭按钮、画布浮层统一的 Esc / 点边界外 - 关闭判据收口:resolveResourceCanvasQuickEditDismissOpen 更名为 resolveResourceCanvasFloatingPanelDismissOpen、参数改为 isFloatingPanelOpen,宿主把「快速编辑开」与「信息开」合成一个布尔 - .game-resource-info-panel 补进 RESOURCE_CANVAS_INTERACTION_SELECTOR,否则点在只读字段值上会被判成点画布空白而清掉选中 - 换选 / 清空选中 / 切项目时信息浮层自动收起:在选中派生处按 selectedResourceId 收口,不在每个重置点各补一行 - 测试:新增 resourceCanvasInfoModel 三条与「信息面板在画布浮层与运行页签里渲染同一份只读字段」(工具条顺序、开合、关闭按钮只收面板、点画布以外收起、Esc 连工具条一起收、运行页签字段逐行相同);resourceCanvasFloatingDismiss 的判据用例与交互选择器夹具改为浮层通用口径 - resourceCanvasChrome.css 的字段排版收在浮层选择器下,不跨上下文改运行页签的字号与列宽 - decision-log 新增一条:记录这次对 Issue #309 C3「取消资源详情面板」的有意局部回退(只回退「看信息」,不恢复可编辑详情面板与全屏编辑路由),并登记需要同步的 C3 引用位置
This commit is contained in:
@@ -759,6 +759,90 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 信息浮层:与快速编辑浮层同一族(同一个锚点、同一档圆角与阴影),
|
||||
* 但只读、更窄,所以不复用生成 composer 的栅格。 */
|
||||
.game-resource-canvas-toolbar-host .game-resource-info-panel {
|
||||
position: absolute;
|
||||
z-index: 70;
|
||||
display: grid;
|
||||
width: min(19rem, calc(100vw - 1.5rem));
|
||||
max-height: min(24rem, calc(100% - 1.5rem));
|
||||
gap: 0.5rem;
|
||||
overflow: auto;
|
||||
border: 1px solid rgba(148, 163, 184, 0.36);
|
||||
border-radius: 1.1rem;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
padding: 0.72rem;
|
||||
color: #1f2937;
|
||||
font-size: 0.78rem;
|
||||
box-shadow: 0 22px 48px rgba(15, 23, 42, 0.22);
|
||||
transform: translateX(-50%);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-resource-info-panel__header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.38rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.game-resource-info-panel__header strong {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-resource-info-panel__close {
|
||||
display: grid;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.game-resource-info-panel__close:hover,
|
||||
.game-resource-info-panel__close:focus-visible {
|
||||
background: rgba(148, 163, 184, 0.22);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* 字段列表在信息浮层里单独排版;运行页签那处继续用 `.game-run-panels` 的既有口径,
|
||||
* 所以这里把声明全部收在浮层选择器下,不跨上下文改字号与列宽。 */
|
||||
.game-resource-info-panel .game-resource-info-fields {
|
||||
display: grid;
|
||||
gap: 0.34rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.game-resource-info-panel .game-resource-info-fields div {
|
||||
display: grid;
|
||||
grid-template-columns: 3.1rem minmax(0, 1fr);
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.game-resource-info-panel .game-resource-info-fields dt,
|
||||
.game-resource-info-panel .game-resource-info-fields dd {
|
||||
margin: 0;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.game-resource-info-panel .game-resource-info-fields dt {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.game-resource-info-panel .game-resource-info-fields dd {
|
||||
color: #1f2937;
|
||||
font-weight: 620;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.game-resource-canvas-toolbar-host
|
||||
.image-canvas-editor__floating-toolbar {
|
||||
|
||||
+17
-15
@@ -5,7 +5,7 @@
|
||||
* 的 `clearCanvasFocus()` 与 `src/components/image-editor/useImageCanvasKeyboardShortcuts.ts`
|
||||
* 的 Escape 分支),AGC 不再自带第二套口径:
|
||||
*
|
||||
* - 画布空白处左键 pointerdown → 清焦点(清空选中 + 关闭快速编辑浮层);
|
||||
* - 画布空白处左键 pointerdown → 清焦点(清空选中 + 关闭画布浮层:快速编辑与信息);
|
||||
* - 点在资源卡、交互控件或画布浮层里 → 不清焦点;
|
||||
* - 选中变化 / 切项目 / 切模式 → 由宿主既有的重置逻辑负责。
|
||||
*/
|
||||
@@ -14,8 +14,8 @@
|
||||
* 落在这些元素里的 pointerdown 不清画布焦点。
|
||||
*
|
||||
* 与美术画布「点在图层上不清焦点、点在空白/world 上才清」同口径:AGC 的资源卡
|
||||
* 等价于图层,工具条与快速编辑面板等价于浮层,其余交互控件(按钮、输入、缩放、
|
||||
* 页签)都不该顺手把选中清掉。
|
||||
* 等价于图层,工具条、快速编辑面板与信息浮层等价于浮层,其余交互控件(按钮、输入、
|
||||
* 缩放、页签)都不该顺手把选中清掉。
|
||||
*/
|
||||
export const RESOURCE_CANVAS_INTERACTION_SELECTOR = [
|
||||
'button',
|
||||
@@ -28,6 +28,7 @@ export const RESOURCE_CANVAS_INTERACTION_SELECTOR = [
|
||||
'.game-resource-book-zoom',
|
||||
'.image-canvas-editor__floating-toolbar',
|
||||
'.image-canvas-editor__generation-composer',
|
||||
'.game-resource-info-panel',
|
||||
].join(', ');
|
||||
|
||||
/** 这一次画布 pointerdown 要不要清焦点。 */
|
||||
@@ -38,10 +39,10 @@ export function isResourceCanvasInteractionTarget(
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速编辑面板是否可以被清焦点顺手关掉。
|
||||
* 画布浮层是否可以被清焦点顺手关掉。
|
||||
*
|
||||
* 对齐美术画布 `closeTransientEditorPanels()`:生成中的面板不关,否则用户看不到
|
||||
* 「修改中」状态,也拿不到失败后的重试入口。
|
||||
* 「修改中」状态,也拿不到失败后的重试入口。信息浮层是只读的,没有生成态。
|
||||
*/
|
||||
export function canDismissResourceCanvasQuickEdit(
|
||||
panel: { readonly status: string } | null | undefined,
|
||||
@@ -71,26 +72,26 @@ export function isResourceCanvasHostOverlayOpen(
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速编辑浮层是否接管「点外部 / Esc 关闭」。
|
||||
* 画布浮层(快速编辑 / 信息)是否接管「点外部 / Esc 关闭」。
|
||||
*
|
||||
* 只有它自己开着时才接管,而且是唯一的取消条件:AGC 弹层开着时不接管——资源面板的
|
||||
* 只有它们自己开着时才接管,而且是唯一的取消条件:AGC 弹层开着时不接管——资源面板的
|
||||
* 多选直接读写画布那一份 `selectedResourceIds`,此刻清选中会把用户刚勾的资源清空。
|
||||
*
|
||||
* 选中工具条不在这里,它跟美术画布一样只随选中存在:单独选中一个资源卡不会因为
|
||||
* 点了一下对话区就丢选中(资源面板下载、版本绑定高亮都挂在同一份选中上)。
|
||||
*/
|
||||
export function resolveResourceCanvasQuickEditDismissOpen({
|
||||
export function resolveResourceCanvasFloatingPanelDismissOpen({
|
||||
isCanvasVisible,
|
||||
isQuickEditPanelOpen,
|
||||
isFloatingPanelOpen,
|
||||
hostOverlay,
|
||||
}: {
|
||||
isCanvasVisible: boolean;
|
||||
isQuickEditPanelOpen: boolean;
|
||||
isFloatingPanelOpen: boolean;
|
||||
hostOverlay: ResourceCanvasHostOverlayState;
|
||||
}): boolean {
|
||||
return (
|
||||
isCanvasVisible &&
|
||||
isQuickEditPanelOpen &&
|
||||
isFloatingPanelOpen &&
|
||||
!isResourceCanvasHostOverlayOpen(hostOverlay)
|
||||
);
|
||||
}
|
||||
@@ -99,21 +100,22 @@ export function resolveResourceCanvasQuickEditDismissOpen({
|
||||
* 宿主自己的 Escape 是否归资源画布。
|
||||
*
|
||||
* 对齐美术画布:Escape 走 `closeTransientEditorPanels()` + `clearCanvasSelection()`,
|
||||
* 既收浮层也清选中。AGC 自带弹窗开着时让位给弹窗自己(Esc 只关弹窗)。
|
||||
* 既收浮层也清选中。AGC 自带弹窗开着时让位给弹窗自己(Esc 只关弹窗);画布浮层开着时
|
||||
* 由浮层在 document 上截断(见 `resolveResourceCanvasFloatingPanelDismissOpen`)。
|
||||
*/
|
||||
export function resolveResourceCanvasFocusEscapeActive({
|
||||
isCanvasVisible,
|
||||
hasSelectedResource,
|
||||
isQuickEditPanelOpen,
|
||||
isFloatingPanelOpen,
|
||||
hostOverlay,
|
||||
}: {
|
||||
isCanvasVisible: boolean;
|
||||
hasSelectedResource: boolean;
|
||||
isQuickEditPanelOpen: boolean;
|
||||
isFloatingPanelOpen: boolean;
|
||||
hostOverlay: ResourceCanvasHostOverlayState;
|
||||
}): boolean {
|
||||
if (!isCanvasVisible || isResourceCanvasHostOverlayOpen(hostOverlay)) {
|
||||
return false;
|
||||
}
|
||||
return hasSelectedResource || isQuickEditPanelOpen;
|
||||
return hasSelectedResource || isFloatingPanelOpen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
CanvasLayer,
|
||||
CanvasOverlayStyle,
|
||||
CanvasViewport,
|
||||
QuickEditPanelState,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import { resolveQuickEditPanelStyle } from '@genarrative/image-canvas-core';
|
||||
|
||||
import type {
|
||||
ProjectResource,
|
||||
ProjectResourceCategory,
|
||||
} from '../../view/project-development/resourceProjectionModel';
|
||||
import { resourceReferenceCategoryLabel } from '../project-workspace/resourceReferences';
|
||||
|
||||
export type ResourceInfoFieldRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const EMPTY_TAGS_TEXT = '暂无标签';
|
||||
|
||||
/**
|
||||
* 栏目文案与资源筛选同源;`version` 不是跨端资源分类,和画布栏目一样单独给文案。
|
||||
*/
|
||||
function resourceCategoryLabel(category: ProjectResourceCategory) {
|
||||
return category === 'version'
|
||||
? '项目版本'
|
||||
: resourceReferenceCategoryLabel(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源信息面板的字段清单:名称 / 路径 / 类型 / 分类 / 标签。
|
||||
*
|
||||
* 全部只读,且刻意不含「来源任务」「资产 ID」等 manifest 内部标识——面板是给创作者看的
|
||||
* 事实卡,不是调试视图。分类取画布栏目口径(与资源卡类型徽标、资源书栏目一致),
|
||||
* 标签只读展示 manifest `tags`,没有标签时给明确文案而不是留空。
|
||||
*/
|
||||
export function resolveResourceInfoFieldRows(
|
||||
resource: ProjectResource,
|
||||
): ResourceInfoFieldRow[] {
|
||||
const tags = resource.assetTags ?? [];
|
||||
return [
|
||||
{ label: '名称', value: resource.label },
|
||||
{ label: '路径', value: resource.path },
|
||||
{ label: '类型', value: resource.mediaType },
|
||||
{ label: '分类', value: resourceCategoryLabel(resource.category) },
|
||||
{
|
||||
label: '标签',
|
||||
value: tags.length > 0 ? tags.join('、') : EMPTY_TAGS_TEXT,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息浮层锚点:与快速编辑浮层共用同一条几何口径(贴着选中资源卡下沿居中),
|
||||
* 所以直接复用 `resolveQuickEditPanelStyle`,不在这里另写一份锚点算法。
|
||||
*
|
||||
* 该函数只读 `panel` 判空,这里传一个固定探针:信息浮层没有提示词之类的面板状态,
|
||||
* 不该为了复用几何被迫持有一份快速编辑专属字段。
|
||||
*/
|
||||
const INFO_PANEL_ANCHOR_PROBE: QuickEditPanelState = {
|
||||
sourceLayerId: '',
|
||||
prompt: '',
|
||||
size: '',
|
||||
model: '',
|
||||
status: 'idle',
|
||||
};
|
||||
|
||||
/** 信息浮层的锚点输入:选中图层 + 画布视口/尺寸。 */
|
||||
export type ResourceInfoPanelAnchor = {
|
||||
sourceLayer: CanvasLayer | null;
|
||||
viewport: CanvasViewport;
|
||||
canvasSize: { width: number; height: number };
|
||||
};
|
||||
|
||||
export function resolveResourceInfoPanelStyle({
|
||||
sourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
}: ResourceInfoPanelAnchor): CanvasOverlayStyle | null {
|
||||
return resolveQuickEditPanelStyle({
|
||||
panel: INFO_PANEL_ANCHOR_PROBE,
|
||||
sourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Info, X } from 'lucide-react';
|
||||
|
||||
import {
|
||||
resolveResourceInfoFieldRows,
|
||||
resolveResourceInfoPanelStyle,
|
||||
type ResourceInfoPanelAnchor,
|
||||
} from '../../features/resource-canvas/resourceCanvasInfoModel';
|
||||
import type { ProjectResource } from './resourceProjectionModel';
|
||||
|
||||
/**
|
||||
* 只读资源信息字段。运行页签的「信息展示」与画布上的信息浮层共用这一份,
|
||||
* 字段清单只在 `resolveResourceInfoFieldRows` 里定义,两处不会各说一套。
|
||||
*/
|
||||
export function ResourceInfoFieldsView({
|
||||
resource,
|
||||
}: {
|
||||
resource: ProjectResource;
|
||||
}) {
|
||||
return (
|
||||
<dl className="game-resource-info-fields">
|
||||
{resolveResourceInfoFieldRows(resource).map((row) => (
|
||||
<div key={row.label}>
|
||||
<dt>{row.label}</dt>
|
||||
<dd>{row.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
export type ResourceInfoPanelViewProps = ResourceInfoPanelAnchor & {
|
||||
resource: ProjectResource;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 画布上的信息浮层:贴着选中资源卡,非模态。
|
||||
*
|
||||
* 关闭时机(Esc / 点边界外)由宿主复用 `useImageCanvasFloatingOptionDismiss` 接管,
|
||||
* 这里只负责渲染,所以面板本身不注册任何 document 监听。
|
||||
*/
|
||||
export function ResourceInfoPanelView({
|
||||
resource,
|
||||
sourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
onClose,
|
||||
}: ResourceInfoPanelViewProps) {
|
||||
const style = resolveResourceInfoPanelStyle({
|
||||
sourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
if (!style) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className="game-resource-info-panel"
|
||||
role="dialog"
|
||||
aria-label="资源信息"
|
||||
style={style}
|
||||
>
|
||||
<header className="game-resource-info-panel__header">
|
||||
<Info size={14} aria-hidden="true" />
|
||||
<strong>信息</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-info-panel__close"
|
||||
aria-label="关闭信息面板"
|
||||
title="关闭"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<ResourceInfoFieldsView resource={resource} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -97,8 +97,8 @@ import {
|
||||
import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasInteractionTarget,
|
||||
resolveResourceCanvasFloatingPanelDismissOpen,
|
||||
resolveResourceCanvasFocusEscapeActive,
|
||||
resolveResourceCanvasQuickEditDismissOpen,
|
||||
} from '../../features/resource-canvas/resourceCanvasFocusModel';
|
||||
import {
|
||||
isResourceCanvasGenerationAvailable,
|
||||
@@ -216,6 +216,10 @@ import {
|
||||
canonicalProjectedResourceMediaType,
|
||||
defaultDerivedResourceName,
|
||||
} from './resourceEditModel';
|
||||
import {
|
||||
ResourceInfoFieldsView,
|
||||
ResourceInfoPanelView,
|
||||
} from './ResourceInfoPanelView';
|
||||
import {
|
||||
type ProjectAgentResultSummary,
|
||||
type ProjectAttachmentResult,
|
||||
@@ -1366,6 +1370,8 @@ export default function ProjectDevelopmentView({
|
||||
useState<CanvasLayer | null>(null);
|
||||
const [quickEditPanel, setQuickEditPanel] =
|
||||
useState<QuickEditPanelState | null>(null);
|
||||
/** 画布上的只读信息浮层(「信息」动作的落点),与运行页签的信息面板同源。 */
|
||||
const [resourceInfoPanelOpen, setResourceInfoPanelOpen] = useState(false);
|
||||
const [resourceCanvasMarquee, setResourceCanvasMarquee] =
|
||||
useState<CanvasMarqueeState | null>(null);
|
||||
/** 资源卡组织操作历史:只回滚布局坐标,不回滚素材。 */
|
||||
@@ -1502,12 +1508,14 @@ export default function ProjectDevelopmentView({
|
||||
* 清资源画布焦点:对齐美术画布 `clearCanvasFocus()`。
|
||||
*
|
||||
* 美术画布在画布空白 pointerdown 与 Escape 两处都会走这一步——清空选中让选中工具条
|
||||
* 收起,并关掉快速编辑浮层。资源画布保持同一套时机:点在资源卡或浮层里不清,
|
||||
* 点画布空白、按 Esc 才清。生成中的面板不关,与美术画布
|
||||
* `closeTransientEditorPanels()` 的 `status === 'generating'` 判断一致。
|
||||
* 收起,并关掉画布浮层(快速编辑与信息)。资源画布保持同一套时机:点在资源卡或浮层里
|
||||
* 不清,点画布空白、按 Esc 才清。生成中的快速编辑面板不关,与美术画布
|
||||
* `closeTransientEditorPanels()` 的 `status === 'generating'` 判断一致;信息浮层只读,
|
||||
* 没有生成态,随焦点一起收起。
|
||||
*/
|
||||
const clearResourceCanvasFocus = useCallback(() => {
|
||||
setSelectedResourceIds([]);
|
||||
setResourceInfoPanelOpen(false);
|
||||
if (!canDismissResourceCanvasQuickEdit(quickEditPanelRef.current)) {
|
||||
return;
|
||||
}
|
||||
@@ -1515,19 +1523,19 @@ export default function ProjectDevelopmentView({
|
||||
}, [closeResourceQuickEditPanel]);
|
||||
|
||||
/**
|
||||
* 快速编辑浮层的「点外部 / Esc 关闭」。
|
||||
* 画布浮层的「点外部 / Esc 关闭」。
|
||||
*
|
||||
* 直接复用美术画布的 `useImageCanvasFloatingOptionDismiss`,不另写判据:它在
|
||||
* document 上同时接管 click 与 Escape,点在边界外即关、点在边界内(含
|
||||
* `.image-canvas-editor__portal-menu` 弹出层,例如模型 / 比例下拉)保留。
|
||||
*
|
||||
* 边界取整个资源画布管理器:面板内部的点(改提示词、切选项)不算外部;点到画布
|
||||
* 以外(顶部工具条、对话区、左侧导航)立即收起。
|
||||
* 边界取整个资源画布管理器:面板内部的点(改提示词、切选项、看信息)不算外部;
|
||||
* 点到画布以外(顶部工具条、对话区、左侧导航)立即收起。
|
||||
*/
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: resolveResourceCanvasQuickEditDismissOpen({
|
||||
isOpen: resolveResourceCanvasFloatingPanelDismissOpen({
|
||||
isCanvasVisible: mode === 'resources' && !uiEditorRoute,
|
||||
isQuickEditPanelOpen: quickEditPanel !== null,
|
||||
isFloatingPanelOpen: quickEditPanel !== null || resourceInfoPanelOpen,
|
||||
hostOverlay: {
|
||||
isResourcePanelOpen: resourcePanelOpen,
|
||||
isGenerationPanelOpen: resourceGenerationOpen,
|
||||
@@ -1553,7 +1561,7 @@ export default function ProjectDevelopmentView({
|
||||
!resolveResourceCanvasFocusEscapeActive({
|
||||
isCanvasVisible: mode === 'resources' && !uiEditorRoute,
|
||||
hasSelectedResource: selectedResourceIds.length > 0,
|
||||
isQuickEditPanelOpen: quickEditPanel !== null,
|
||||
isFloatingPanelOpen: quickEditPanel !== null || resourceInfoPanelOpen,
|
||||
hostOverlay: {
|
||||
isResourcePanelOpen: resourcePanelOpen,
|
||||
isGenerationPanelOpen: resourceGenerationOpen,
|
||||
@@ -1578,12 +1586,14 @@ export default function ProjectDevelopmentView({
|
||||
quickEditPanel,
|
||||
resourceClassificationAssetId,
|
||||
resourceGenerationOpen,
|
||||
resourceInfoPanelOpen,
|
||||
resourcePanelOpen,
|
||||
resourceRecoveryPanelOpen,
|
||||
resourceRenameAssetId,
|
||||
selectedResourceIds,
|
||||
uiEditorRoute,
|
||||
]);
|
||||
|
||||
const resourceSectionScrollPositionsRef = useRef(
|
||||
new Map<string, { left: number; top: number }>(),
|
||||
);
|
||||
@@ -1883,6 +1893,14 @@ export default function ProjectDevelopmentView({
|
||||
const selectedResource =
|
||||
canvasResources.find((resource) => resource.id === selectedResourceId) ??
|
||||
null;
|
||||
/**
|
||||
* 信息浮层只对打开它的那次选中有效:换选、清空选中、切项目都会关掉它,
|
||||
* 面板里不会留下上一张资源的陈旧信息。放在选中派生处而不是逐个重置点补一行,
|
||||
* 是因为选中本身有多个清空入口(清焦点、切视图、切项目)。
|
||||
*/
|
||||
useEffect(() => {
|
||||
setResourceInfoPanelOpen(false);
|
||||
}, [selectedResourceId]);
|
||||
const projectVersions = useMemo(
|
||||
() => manifest.versions ?? [],
|
||||
[manifest.versions],
|
||||
@@ -5746,6 +5764,20 @@ export default function ProjectDevelopmentView({
|
||||
<span>@引用</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedResource ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
label="信息"
|
||||
title="信息"
|
||||
pressed={resourceInfoPanelOpen}
|
||||
icon={<Info className="h-4 w-4" />}
|
||||
onClick={() =>
|
||||
setResourceInfoPanelOpen((open) => !open)
|
||||
}
|
||||
>
|
||||
<span>信息</span>
|
||||
</CanvasChromeButton>
|
||||
) : null}
|
||||
{selectedResource?.manifestAssetId ? (
|
||||
<CanvasChromeButton
|
||||
className="image-canvas-editor__floating-toolbar-text-button"
|
||||
@@ -5824,6 +5856,17 @@ export default function ProjectDevelopmentView({
|
||||
);
|
||||
})()
|
||||
: null}
|
||||
{resourceInfoPanelOpen &&
|
||||
selectedResource &&
|
||||
selectedResourceLayer ? (
|
||||
<ResourceInfoPanelView
|
||||
resource={selectedResource}
|
||||
sourceLayer={selectedResourceLayer}
|
||||
viewport={resourceCanvasSceneViewportRef.current}
|
||||
canvasSize={resourceBookSceneSize}
|
||||
onClose={() => setResourceInfoPanelOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6241,20 +6284,7 @@ export default function ProjectDevelopmentView({
|
||||
信息展示
|
||||
</header>
|
||||
{selectedResource ? (
|
||||
<dl>
|
||||
<div>
|
||||
<dt>名称</dt>
|
||||
<dd>{selectedResource.label}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>路径</dt>
|
||||
<dd>{selectedResource.path}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>类型</dt>
|
||||
<dd>{selectedResource.mediaType}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<ResourceInfoFieldsView resource={selectedResource} />
|
||||
) : null}
|
||||
</section>
|
||||
<section aria-label="数值微调面板">
|
||||
|
||||
@@ -215,6 +215,17 @@ function styleNumber(body: string, property: string) {
|
||||
return Number(new RegExp(`${property}\\s*:\\s*(\\d+)`).exec(body)?.[1]);
|
||||
}
|
||||
|
||||
/** 按渲染顺序读出只读信息字段,用来比对画布浮层与运行页签两处是否同一份字段。 */
|
||||
function resourceInfoFieldRows(scope: HTMLElement) {
|
||||
const labels = Array.from(scope.querySelectorAll('dt')).map(
|
||||
(node) => node.textContent,
|
||||
);
|
||||
const values = Array.from(scope.querySelectorAll('dd')).map(
|
||||
(node) => node.textContent,
|
||||
);
|
||||
return labels.map((label, index) => [label, values[index]]);
|
||||
}
|
||||
|
||||
function colorAlpha(value: string) {
|
||||
const match = /rgba?\(([^)]*)\)/u.exec(value);
|
||||
if (!match) {
|
||||
@@ -3082,6 +3093,169 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('信息面板在画布浮层与运行页签里渲染同一份只读字段', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-resource-info',
|
||||
'资源信息测试',
|
||||
);
|
||||
const codePrototype = manifest.tasks.find(
|
||||
(task) => task.id === 'code-prototype',
|
||||
);
|
||||
if (!codePrototype) {
|
||||
throw new Error('missing code-prototype seed task');
|
||||
}
|
||||
codePrototype.status = 'completed';
|
||||
manifest.preview = {
|
||||
status: 'running',
|
||||
url: 'http://127.0.0.1:4173',
|
||||
port: 4173,
|
||||
};
|
||||
manifest.assets.push({
|
||||
id: 'character-hero',
|
||||
kind: 'character',
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/hero.png',
|
||||
tags: ['主角', '待定稿'],
|
||||
source: { kind: 'generated' },
|
||||
});
|
||||
let layoutRevision = 0;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_project_resource_graph') {
|
||||
return resourceGraphForInputs(args);
|
||||
}
|
||||
if (command === 'read_local_project_resource_canvas_layout') {
|
||||
return {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: manifest.projectId,
|
||||
mode: args?.mode,
|
||||
revision: layoutRevision,
|
||||
positions: [],
|
||||
updatedAt: 0,
|
||||
};
|
||||
}
|
||||
if (command === 'update_local_project_resource_canvas_layout') {
|
||||
layoutRevision += 1;
|
||||
return {
|
||||
status: 'updated',
|
||||
layout: {
|
||||
schemaVersion: 'game-creator-resource-layout.v1',
|
||||
projectId: manifest.projectId,
|
||||
mode: args?.mode,
|
||||
revision: layoutRevision,
|
||||
positions: args?.positions,
|
||||
updatedAt: layoutRevision,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (command === 'read_local_project_media_preview') {
|
||||
return {
|
||||
path: 'assets/hero.png',
|
||||
mediaType: 'image/png',
|
||||
byteLen: 48,
|
||||
dataUrl: 'data:image/png;base64,iVBORw0KGgo=',
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
|
||||
render(
|
||||
React.createElement(ProjectDevelopmentView, {
|
||||
projectName: '资源信息测试',
|
||||
projectPath: '/tmp/workbench-resource-info',
|
||||
manifest,
|
||||
attachments: [],
|
||||
recentRunStatus: 'completed',
|
||||
recentRunStopReason: null,
|
||||
supervisor: React.createElement('div', null, '项目总控'),
|
||||
onHomeOpen: vi.fn(),
|
||||
onProjectsOpen: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
// 只读字段与运行页签同源:名称 / 路径 / 类型 + 只读的分类与标签,
|
||||
// 不含「来源任务」「资产 ID」这类 manifest 内部标识。
|
||||
const expectedRows = [
|
||||
['名称', 'hero.png'],
|
||||
['路径', 'assets/hero.png'],
|
||||
['类型', 'image/png'],
|
||||
['分类', '角色与对象'],
|
||||
['标签', '主角、待定稿'],
|
||||
];
|
||||
|
||||
await openResourceBookCategory('角色与对象');
|
||||
fireEvent.click(await findResourceSelectButton('hero.png'));
|
||||
const toolbar = await screen.findByRole('toolbar', { name: '图片工具栏' });
|
||||
const toolbarLabels = within(toolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label') ?? '');
|
||||
const infoButton = within(toolbar).getByRole('button', { name: '信息' });
|
||||
// 位置固定在「@引用」之后、「编辑标签」之前:工具条上的顺序即功能顺序。
|
||||
expect(toolbarLabels.indexOf('信息')).toBeGreaterThan(
|
||||
toolbarLabels.indexOf('引用资源 hero.png'),
|
||||
);
|
||||
expect(toolbarLabels.indexOf('信息')).toBeLessThan(
|
||||
toolbarLabels.indexOf('编辑标签'),
|
||||
);
|
||||
expect(infoButton.getAttribute('aria-pressed')).toBe('false');
|
||||
|
||||
fireEvent.click(infoButton);
|
||||
const canvasPanel = await screen.findByRole('dialog', {
|
||||
name: '资源信息',
|
||||
});
|
||||
expect(resourceInfoFieldRows(canvasPanel)).toEqual(expectedRows);
|
||||
expect(infoButton.getAttribute('aria-pressed')).toBe('true');
|
||||
// 再点一次同一个动作即收起,不用去别处找关闭入口。
|
||||
fireEvent.click(infoButton);
|
||||
expect(screen.queryByRole('dialog', { name: '资源信息' })).toBeNull();
|
||||
|
||||
// 关闭按钮只收面板:选中与工具条留着,方便接着看别的动作。
|
||||
fireEvent.click(infoButton);
|
||||
fireEvent.click(
|
||||
within(await screen.findByRole('dialog', { name: '资源信息' })).getByRole(
|
||||
'button',
|
||||
{ name: '关闭信息面板' },
|
||||
),
|
||||
);
|
||||
expect(screen.queryByRole('dialog', { name: '资源信息' })).toBeNull();
|
||||
expect(screen.getByRole('toolbar', { name: '图片工具栏' })).not.toBeNull();
|
||||
|
||||
// 画布浮层的统一关闭时机:点画布以外立即收起。
|
||||
fireEvent.click(infoButton);
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '资源信息' }),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(document.body);
|
||||
expect(screen.queryByRole('dialog', { name: '资源信息' })).toBeNull();
|
||||
|
||||
// Esc 与快速编辑浮层同一口径:既收浮层也清选中,整个工具条一起收起。
|
||||
fireEvent.click(await findResourceSelectButton('hero.png'));
|
||||
const reopenedToolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(reopenedToolbar).getByRole('button', { name: '信息' }),
|
||||
);
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '资源信息' }),
|
||||
).not.toBeNull();
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(screen.queryByRole('dialog', { name: '资源信息' })).toBeNull();
|
||||
expect(screen.queryByRole('toolbar', { name: '图片工具栏' })).toBeNull();
|
||||
|
||||
// 运行页签的「信息展示」渲染的是同一份字段,不是第二套口径。
|
||||
fireEvent.click(await findResourceSelectButton('hero.png'));
|
||||
const runTab = screen.getByRole('tab', { name: '运行' });
|
||||
fireEvent.click(runTab);
|
||||
const runInfoPanel = screen.getByLabelText('资源信息面板');
|
||||
expect(resourceInfoFieldRows(runInfoPanel)).toEqual(expectedRows);
|
||||
// 画布浮层只属于画布:切到运行视图后不再渲染。
|
||||
expect(screen.queryByRole('dialog', { name: '资源信息' })).toBeNull();
|
||||
});
|
||||
|
||||
it('loads registered documents, art media, video, and audio with safe failure states inside central focus', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'workbench-resource-media',
|
||||
@@ -3272,9 +3446,9 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
).toBe(false);
|
||||
// 音频资源的选中工具条复用美术画布的音频分支(aria-label「素材工具栏」),
|
||||
// 并且只渲染宿主编排层真实接通的动作:「@引用」(从卡片挪进工具条的引用入口,
|
||||
// 资源卡上的圆钮已删除)「编辑标签」(面板只编辑 manifest `assets[].tags`,
|
||||
// 分类不再有手动入口)「重命名」已接面板,「下载按钮」复用资源面板同一条落盘
|
||||
// 链路,「改造」在宿主编排层仍是空回调,不能再渲染成点了没反应的按钮。
|
||||
// 资源卡上的圆钮已删除)「信息」(只读信息浮层)「编辑标签」(面板只编辑 manifest
|
||||
// `assets[].tags`,分类不再有手动入口)「重命名」已接面板,「下载按钮」复用资源
|
||||
// 面板同一条落盘链路,「改造」在宿主编排层仍是空回调,不能再渲染成点了没反应的按钮。
|
||||
const audioToolbar = screen.getByRole('toolbar', {
|
||||
name: '素材工具栏',
|
||||
});
|
||||
@@ -3285,7 +3459,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
within(audioToolbar)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['引用资源 bgm.mp3', '编辑标签', '重命名', '下载按钮']);
|
||||
).toEqual(['引用资源 bgm.mp3', '信息', '编辑标签', '重命名', '下载按钮']);
|
||||
|
||||
// 工具条的「下载按钮」必须真的走通落盘链路:原生保存对话框 + Rust 分块复制,
|
||||
// 而不是只渲染一个按钮。原生对话框由入口文件 mock 成"用户选了
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasHostOverlayOpen,
|
||||
isResourceCanvasInteractionTarget,
|
||||
resolveResourceCanvasFloatingPanelDismissOpen,
|
||||
resolveResourceCanvasFocusEscapeActive,
|
||||
resolveResourceCanvasQuickEditDismissOpen,
|
||||
} from '../src/features/resource-canvas/resourceCanvasFocusModel';
|
||||
import { createResourceQuickEditPanelDraft } from '../src/features/resource-canvas/resourceCanvasQuickEditModel';
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('resourceCanvasFocusModel', () => {
|
||||
<div class="game-resource-canvas-content"><span id="blank">空白</span></div>
|
||||
<div class="image-canvas-editor__floating-toolbar"><button type="button">快速编辑</button></div>
|
||||
<div class="image-canvas-editor__generation-composer"><div id="prompt">提示词</div></div>
|
||||
<div class="game-resource-info-panel"><div id="info-value">封面图</div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -82,6 +83,11 @@ describe('resourceCanvasFocusModel', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isResourceCanvasInteractionTarget(query('#prompt'))).toBe(true);
|
||||
// 信息浮层是只读的,主体不是按钮:点在字段值上也必须不算「画布空白」。
|
||||
expect(
|
||||
isResourceCanvasInteractionTarget(query('.game-resource-info-panel')),
|
||||
).toBe(true);
|
||||
expect(isResourceCanvasInteractionTarget(query('#info-value'))).toBe(true);
|
||||
expect(isResourceCanvasInteractionTarget(null)).toBe(false);
|
||||
|
||||
document.body.innerHTML = '';
|
||||
@@ -112,7 +118,7 @@ describe('resourceCanvasFocusModel', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('画布可见且快速编辑浮层打开时才接管点外部关闭', () => {
|
||||
test('画布可见且画布浮层打开时才接管点外部关闭', () => {
|
||||
const closed = {
|
||||
isResourcePanelOpen: false,
|
||||
isGenerationPanelOpen: false,
|
||||
@@ -122,25 +128,26 @@ describe('resourceCanvasFocusModel', () => {
|
||||
};
|
||||
const base = {
|
||||
isCanvasVisible: true,
|
||||
isQuickEditPanelOpen: true,
|
||||
isFloatingPanelOpen: true,
|
||||
hostOverlay: closed,
|
||||
};
|
||||
|
||||
expect(resolveResourceCanvasQuickEditDismissOpen(base)).toBe(true);
|
||||
// 判据对浮层种类无差别:快速编辑与信息浮层共用这一条(宿主把两者合成一个布尔)。
|
||||
expect(resolveResourceCanvasFloatingPanelDismissOpen(base)).toBe(true);
|
||||
expect(
|
||||
resolveResourceCanvasQuickEditDismissOpen({
|
||||
resolveResourceCanvasFloatingPanelDismissOpen({
|
||||
...base,
|
||||
isQuickEditPanelOpen: false,
|
||||
isFloatingPanelOpen: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveResourceCanvasQuickEditDismissOpen({
|
||||
resolveResourceCanvasFloatingPanelDismissOpen({
|
||||
...base,
|
||||
isCanvasVisible: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveResourceCanvasQuickEditDismissOpen({
|
||||
resolveResourceCanvasFloatingPanelDismissOpen({
|
||||
...base,
|
||||
hostOverlay: { ...closed, isResourcePanelOpen: true },
|
||||
}),
|
||||
@@ -158,7 +165,7 @@ describe('resourceCanvasFocusModel', () => {
|
||||
const base = {
|
||||
isCanvasVisible: true,
|
||||
hasSelectedResource: false,
|
||||
isQuickEditPanelOpen: true,
|
||||
isFloatingPanelOpen: true,
|
||||
hostOverlay: closed,
|
||||
};
|
||||
|
||||
@@ -167,14 +174,14 @@ describe('resourceCanvasFocusModel', () => {
|
||||
expect(
|
||||
resolveResourceCanvasFocusEscapeActive({
|
||||
...base,
|
||||
isQuickEditPanelOpen: false,
|
||||
isFloatingPanelOpen: false,
|
||||
hasSelectedResource: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveResourceCanvasFocusEscapeActive({
|
||||
...base,
|
||||
isQuickEditPanelOpen: false,
|
||||
isFloatingPanelOpen: false,
|
||||
hasSelectedResource: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveResourceInfoFieldRows,
|
||||
resolveResourceInfoPanelStyle,
|
||||
} from '../src/features/resource-canvas/resourceCanvasInfoModel';
|
||||
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
||||
|
||||
function resourceFixture(overrides: Partial<ProjectResource>): ProjectResource {
|
||||
return {
|
||||
id: 'asset:hero',
|
||||
category: 'character',
|
||||
subtype: 'character',
|
||||
label: 'hero.png',
|
||||
path: 'assets/hero.png',
|
||||
mediaType: 'image/png',
|
||||
sourceLabel: 'Agent 生成',
|
||||
taskTitle: null,
|
||||
manifestAssetId: 'hero',
|
||||
producerTaskId: null,
|
||||
externalResourceId: null,
|
||||
referenceResourceIds: [],
|
||||
dependencies: [],
|
||||
dependencyDepth: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const SOURCE_LAYER = {
|
||||
id: 'asset:hero',
|
||||
resourceId: 'asset:hero',
|
||||
title: 'hero.png',
|
||||
src: 'blob:hero',
|
||||
x: 100,
|
||||
y: 40,
|
||||
width: 200,
|
||||
height: 120,
|
||||
originalWidth: 200,
|
||||
originalHeight: 120,
|
||||
zIndex: 1,
|
||||
sourceType: 'generated' as const,
|
||||
};
|
||||
|
||||
describe('resourceCanvasInfoModel', () => {
|
||||
test('字段固定为名称 / 路径 / 类型 + 只读分类与标签', () => {
|
||||
expect(resolveResourceInfoFieldRows(resourceFixture({}))).toEqual([
|
||||
{ label: '名称', value: 'hero.png' },
|
||||
{ label: '路径', value: 'assets/hero.png' },
|
||||
{ label: '类型', value: 'image/png' },
|
||||
{ label: '分类', value: '角色与对象' },
|
||||
{ label: '标签', value: '暂无标签' },
|
||||
]);
|
||||
|
||||
// 面板是给用户看的事实卡:manifest 内部标识(来源任务、资产 ID、producer)不上桌。
|
||||
const labels = resolveResourceInfoFieldRows(resourceFixture({})).map(
|
||||
(row) => row.label,
|
||||
);
|
||||
for (const forbidden of ['来源任务', '资产 ID', 'ID', '来源']) {
|
||||
expect(labels).not.toContain(forbidden);
|
||||
}
|
||||
});
|
||||
|
||||
test('分类跟随画布栏目口径,标签只读拼接', () => {
|
||||
expect(
|
||||
resolveResourceInfoFieldRows(
|
||||
resourceFixture({ category: 'version', assetCategory: undefined }),
|
||||
).find((row) => row.label === '分类')?.value,
|
||||
).toBe('项目版本');
|
||||
expect(
|
||||
resolveResourceInfoFieldRows(
|
||||
resourceFixture({ category: 'unclassified', assetCategory: undefined }),
|
||||
).find((row) => row.label === '分类')?.value,
|
||||
).toBe('待归类');
|
||||
expect(
|
||||
resolveResourceInfoFieldRows(
|
||||
resourceFixture({ assetTags: ['主角', '待定稿'] }),
|
||||
).find((row) => row.label === '标签')?.value,
|
||||
).toBe('主角、待定稿');
|
||||
});
|
||||
|
||||
test('浮层锚点与快速编辑浮层同一条几何:没有选中图层就没有面板', () => {
|
||||
const anchor = {
|
||||
sourceLayer: SOURCE_LAYER,
|
||||
viewport: { scale: 1, x: 0, y: 0 },
|
||||
canvasSize: { width: 900, height: 600 },
|
||||
};
|
||||
// 锚在图层下沿居中 + 12px 间距(与 `resolveQuickEditPanelStyle` 同口径)。
|
||||
expect(resolveResourceInfoPanelStyle(anchor)).toEqual({
|
||||
left: 200,
|
||||
top: 172,
|
||||
});
|
||||
expect(
|
||||
resolveResourceInfoPanelStyle({ ...anchor, sourceLayer: null }),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -8364,3 +8364,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 不做什么:不新增替代导航、不做栏目下拉/分段控件、不改 `resourceBookModel` 的 `main / child + category` 目标模型、不改每「排序模式 + 栏目」独立 viewport 的既有约定、不动 sidecar schema 与跨端契约。
|
||||
- 验证方式:新增反向守卫用例(`资源栏目大纲` 标签、`.game-resource-outline` 类名、对应 `<nav>` 都不再渲染)与切栏目链路用例(总览卡片进空栏目 → 下一页切下一个 → 标题栏「资源总览」返回 → 「所有资源」入口往返);变异验证:把 `<nav aria-label="资源栏目大纲">` 加回去 → 守卫红灯,只加回类名 → 守卫同样红灯(均已实测)。既有断言未放宽:栏目顺序断言改钉在总览缩略卡片的 `aria-label` 序列上(第 0 张「所有资源」+ 固定七栏目),只服务导航的断言按「四不写」直接删除。
|
||||
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`(:56 / :512)、`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`(:5 / :33 / :53 / :54 / :57 / :102 / :103)、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
|
||||
## 2026-09-11 只读资源信息回归画布:把运行页签的「信息展示」搬到选中工具条,不恢复 C3 的可编辑详情面板
|
||||
|
||||
- 背景:用户提「信息需要迁移」。Issue #309 的 **C3「取消资源详情面板与全屏编辑路由」保持有效**,但它取消的是**可编辑详情面板 + 全屏编辑路由**;被取消时一并失去的是「选中资源后就地看一眼它是什么」的能力——`ProjectDevelopmentView` 的「信息展示」区块至今只挂在**运行页签**(`game-run-panels` 的左格),用户在资源画布上选中一张卡时看不到任何只读信息。本次是**用户直接要求下的一次有意局部回退**:只把「看信息」这条能力搬回画布,编辑能力仍不回来。
|
||||
- 决策一:**只读信息浮层**(`ResourceInfoPanelView`)挂在选中资源工具条的 `extraActions`,位置固定在「@引用」之后、「编辑标签」之前;非模态 `role="dialog" aria-label="资源信息"`,chrome 与快速编辑浮层同族(同一锚点、同一档圆角与阴影,样式在 `resourceCanvasChrome.css`),关闭入口三处:工具条「信息」按钮再点一次(`aria-pressed` 反映开合)、面板右上角「关闭信息面板」、以及画布浮层统一的 Esc / 点边界外。**不新建全屏路由、不新建编辑表单、不新增 IPC / Tauri 命令**。
|
||||
- 决策二:**一处字段、两处渲染**。只读字段组件 `ResourceInfoFieldsView`(`apps/ai-game-creator-shell/src/view/project-development/ResourceInfoPanelView.tsx`)同时供运行页签的「信息展示」与画布浮层使用,字段清单只在 `resolveResourceInfoFieldRows`(`features/resource-canvas/resourceCanvasInfoModel.ts`)定义:**名称 / 路径 / 类型 + 只读的分类与标签**。分类取画布栏目口径(与卡片类型徽标、资源书栏目一致,`version` 单独给「项目版本」),标签只读拼接 manifest `tags`、为空时给「暂无标签」而不是留白。**刻意不含「来源任务」「资产 ID」「producer」等 manifest 内部标识**——面板是给创作者看的事实卡,不是调试视图;要加字段必须先改这条口径。
|
||||
- 语义变更(用户已确认接受):画布上多出一个只读浮层;运行页签的「信息展示」仍在(不制造信息盲区),只是换了共用组件渲染,**字段从 3 项变成 5 项**(多出只读的分类与标签)。
|
||||
- 关闭时机沿用既有画布口径,不新造第二套:`resolveResourceCanvasQuickEditDismissOpen` 更名为 `resolveResourceCanvasFloatingPanelDismissOpen`、判据参数更名为 `isFloatingPanelOpen`(宿主把「快速编辑开」与「信息开」合成一个布尔),Esc / 点画布以外既收浮层也清选中工具条;`.game-resource-info-panel` 补进 `RESOURCE_CANVAS_INTERACTION_SELECTOR`,否则点在只读字段值上会被判成「点画布空白」而清掉选中。换选 / 清空选中 / 切项目时信息浮层自动收起(在选中派生处按 `selectedResourceId` 收口,不在每个重置点各补一行)。
|
||||
- 不做什么:不恢复 C3 取消的可编辑详情面板与全屏编辑路由;不加「打开详情」按钮;不在浮层里做重命名 / 改分类 / 改标签(这些仍是工具条 `编辑标签` / `重命名` 与分类标签面板的职责);不引入第三套筛选或分类文案。
|
||||
- 验证方式:新增 `resourceCanvasInfoModel` 三条(字段固定 5 项且不含内部标识、栏目口径与标签拼接、锚点与快速编辑浮层同一条几何);AGC 子集新增「信息面板在画布浮层与运行页签里渲染同一份只读字段」(工具条顺序、开合、X 只收面板、点画布以外收起、Esc 连工具条一起收、运行页签字段逐行相同),并把 `resourceCanvasFloatingDismiss` 的判据用例与交互选择器夹具改成浮层通用口径。变异验证:把信息浮层从关闭判据里摘掉 → 「点画布以外」红灯;把运行页签换回三字段内联 `<dl>` → 两处字段比对红灯(均已实测)。
|
||||
- 需要同步的既有文档(**本次只报告,不改远程 Issue**):`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md` :51(S9 行「按 #309 C3 判…真正入口是 `选中资源:<栏目> <名>`」)与 :248(差异表同行)需要补一句「只读信息浮层已于 2026-09-11 回归,C3 取消的仍只是可编辑详情面板与全屏编辑路由」;`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` :61–62 的「打开详情」按钮口径仍与 #309 C3 冲突,本次未动。
|
||||
- 关联:Issue #309(C3)、`apps/ai-game-creator-shell/src/view/project-development/{index.tsx,ResourceInfoPanelView.tsx}`、`apps/ai-game-creator-shell/src/features/resource-canvas/{resourceCanvasInfoModel.ts,resourceCanvasFocusModel.ts,resourceCanvasChrome.css}`。
|
||||
|
||||
Reference in New Issue
Block a user