AGC「生成任务」侧栏移到画布右上角:开关常驻、面板贴着它展开
- 新增右上角锚点 game-resource-generation-tasks-anchor,挂在 game-workbench-stage 内;资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口。 - 开关改由 ResourceCanvasAssetGenerationTasksPanelView 自己渲染(保留 data-resource-generation-task-toggle 与计数属性、可访问名唯一),收起态只剩这一枚开关;工具条上两处重复入口与宿主那份 resourceAssetGenerationInFlightCount 一并删除。 - 面板自己不再定位,改由 max-height 封顶 + 内部滚动;进场 / 退场动画位移方向跟着锚点翻到正 X。 - 开关沿用原工具条那枚次级胶囊外观,颜色 / 圆角 / 字重 / 阴影继续全部走平台 token。 - 测试:面板用例新增「折叠只剩右上角开关」「开关计数与面板头部同源」「锚点按工作面分档」;样式用例改为锚点坐标 / 运行档下移 / 面板不再自定位 / 开关走 token;dismiss 用例改为右上角入口且工具条不再有生成任务按钮。 - 文档:PRD 3.10 与更新时间、decision-log 新增 2026-09-21 条目、画布验收待办登记第四批并勾掉该条。
This commit is contained in:
+142
-110
@@ -43,6 +43,13 @@ export type ResourceCanvasAssetGenerationTasksPanelViewProps = {
|
||||
onToggleOpen: () => void;
|
||||
/** 定位到该任务产出的素材卡(宿主复用既有 `pendingResourceFocusRef` 聚焦链)。 */
|
||||
onFocusTask: (task: ResourceCanvasAssetGenerationTask) => void;
|
||||
/**
|
||||
* 锚点落在哪个工作面:资源栏目画布 / 运行表现层 / UI 编辑器。
|
||||
*
|
||||
* 三处右上角的既有 chrome 不同(运行表现层右上角是版本入口、UI 编辑器是整页编辑器壳),
|
||||
* 所以坐标由这一档在样式里分档给出;缺省按资源栏目画布算。
|
||||
*/
|
||||
placement?: 'canvas' | 'run' | 'editor';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -222,14 +229,15 @@ function resourceCanvasGenerationTaskRowNode(
|
||||
/**
|
||||
* 画布上的「生成任务」侧栏(常驻、可折叠、非模态)。
|
||||
*
|
||||
* 形态对齐网页端美术画布的任务侧栏:贴边的独立 `<aside>`,展开时是一列任务(画布照常可交互),
|
||||
* 折叠时只留一个可点的贴边把手(带在途数量角标)。它**不是**模态浮层:不铺全屏遮罩、不做焦点
|
||||
* 陷阱、不进 `isResourceCanvasFloatingPanelOpen` / `resourceCanvasHostGenerationPanelOpen` 的
|
||||
* 遮挡判据;折叠也不影响任务推进——任务活在账本与本地队列里,与这个视图的生命周期无关。
|
||||
* 形态对齐网页端美术画布的任务侧栏:**画布右上角**的一枚独立开关 + 展开时贴着它的浮动面板
|
||||
* (开关始终在,收起态只剩开关),面板是一列任务(画布照常可交互)。它**不是**模态浮层:
|
||||
* 不铺全屏遮罩、不做焦点陷阱、不进 `isResourceCanvasFloatingPanelOpen` /
|
||||
* `resourceCanvasHostGenerationPanelOpen` 的遮挡判据;折叠也不影响任务推进——任务活在账本与
|
||||
* 本地队列里,与这个视图的生命周期无关。
|
||||
*
|
||||
* 位置取舍:AGC 栏目画布的右侧是「智能创作」对话面板、底部是栏目工具栏、顶部是栏目标题栏,所以
|
||||
* 侧栏挂在**左侧、标题栏之下、工具栏之上**,并且是**覆盖式**而不是把画布挤窄(画布的视口数学与
|
||||
* 资源卡排布都不被 reflow 动到,收起即完全让出画布)。宽度 300px 落在 280–340px 区间内。
|
||||
* 位置取舍:右上角是画布上唯一「既不被对话面板占、也不和底部工具栏 / 右下角缩放 Dock 打架」的
|
||||
* 稳定空位,与美术画布同一处;锚点是**覆盖式**而不是把画布挤窄(画布的视口数学与资源卡排布都
|
||||
* 不被 reflow 动到,收起即完全让出画布,只留那一枚开关)。宽度 300px 落在 280–340px 区间内。
|
||||
*
|
||||
* 分栏、条数、封顶、折叠语义都在这里;颜色与排版在同目录的 CSS 文件里,取值全部走
|
||||
* `--platform-*` 设计 token。
|
||||
@@ -240,6 +248,7 @@ export function ResourceCanvasAssetGenerationTasksPanelView({
|
||||
open,
|
||||
onToggleOpen,
|
||||
onFocusTask,
|
||||
placement = 'canvas',
|
||||
}: ResourceCanvasAssetGenerationTasksPanelViewProps) {
|
||||
const [nowMillis, setNowMillis] = useState(() => Date.now());
|
||||
/**
|
||||
@@ -334,115 +343,138 @@ export function ResourceCanvasAssetGenerationTasksPanelView({
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [open]);
|
||||
|
||||
// 折叠态不再在画布左侧留贴边把手:开合口只剩工具条上那一枚「生成任务」按钮
|
||||
// (带在途计数),收起时侧栏完全让出画布(播完收起动画之后)。
|
||||
if (!open && phase === 'idle') {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 右上角开关:收起态只剩它(画布完全让出),展开态它是「已经开着的那一枚」。
|
||||
*
|
||||
* 在途计数只在这里算一次,开关与面板头部共用同一个值,两处不会各算一套。
|
||||
*/
|
||||
const toggle = (
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-generation-tasks-toggle"
|
||||
aria-label="生成任务"
|
||||
aria-expanded={open}
|
||||
data-resource-generation-task-toggle=""
|
||||
data-resource-generation-task-count={inFlightCount}
|
||||
onClick={onToggleOpen}
|
||||
>
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
{inFlightCount > 0 ? `生成任务 · ${inFlightCount}` : '生成任务'}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`platform-theme platform-theme--light game-resource-generation-tasks-sidebar${
|
||||
open ? '' : ' is-leaving'
|
||||
}`}
|
||||
role="region"
|
||||
aria-label="生成任务"
|
||||
data-resource-generation-task-count={rendered.inFlightCount}
|
||||
<div
|
||||
className="platform-theme platform-theme--light game-resource-generation-tasks-anchor"
|
||||
data-generation-tasks-placement={placement}
|
||||
>
|
||||
<header className="game-resource-generation-tasks-sidebar-header">
|
||||
<h2 className="game-resource-generation-tasks-sidebar-title">
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
生成任务
|
||||
<span
|
||||
className="game-resource-generation-tasks-sidebar-count"
|
||||
aria-label={`在途生成任务 ${rendered.inFlightCount}`}
|
||||
>
|
||||
{rendered.inFlightCount}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-generation-tasks-sidebar-icon-button"
|
||||
aria-label="关闭生成任务"
|
||||
onClick={onToggleOpen}
|
||||
{toggle}
|
||||
{/* 收起要先把退场动画播完(`is-leaving`)再卸载,所以 `phase` 未回到 idle 时面板还在。 */}
|
||||
{open || phase !== 'idle' ? (
|
||||
<aside
|
||||
className={`game-resource-generation-tasks-sidebar${
|
||||
open ? '' : ' is-leaving'
|
||||
}`}
|
||||
role="region"
|
||||
aria-label="生成任务"
|
||||
data-resource-generation-task-count={rendered.inFlightCount}
|
||||
>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div
|
||||
className="game-resource-generation-tasks-scroll"
|
||||
data-resource-generation-task-scroll=""
|
||||
>
|
||||
{rendered.ordered.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty" role="status">
|
||||
还没有生成任务
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<section
|
||||
className="game-resource-generation-tasks-section"
|
||||
aria-label="排队与生成中"
|
||||
<header className="game-resource-generation-tasks-sidebar-header">
|
||||
<h2 className="game-resource-generation-tasks-sidebar-title">
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
生成任务
|
||||
<span
|
||||
className="game-resource-generation-tasks-sidebar-count"
|
||||
aria-label={`在途生成任务 ${rendered.inFlightCount}`}
|
||||
>
|
||||
{rendered.inFlightCount}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="game-resource-generation-tasks-sidebar-icon-button"
|
||||
aria-label="关闭生成任务"
|
||||
onClick={onToggleOpen}
|
||||
>
|
||||
<h3 className="game-resource-generation-tasks-section-title">
|
||||
<span>排队/生成中</span>
|
||||
<span className="game-resource-generation-tasks-section-title-count">
|
||||
{rendered.active.length}
|
||||
</span>
|
||||
</h3>
|
||||
{rendered.active.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty">
|
||||
没有进行中的任务
|
||||
</p>
|
||||
) : (
|
||||
<ul className="game-resource-generation-tasks-list">
|
||||
{rendered.active.map((row) =>
|
||||
resourceCanvasGenerationTaskRowNode(
|
||||
row,
|
||||
nowMillis,
|
||||
onFocusTask,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<section
|
||||
className="game-resource-generation-tasks-section"
|
||||
aria-label="已完成"
|
||||
>
|
||||
<h3 className="game-resource-generation-tasks-section-title">
|
||||
<span>已完成</span>
|
||||
<span className="game-resource-generation-tasks-section-title-count">
|
||||
{rendered.done.length}
|
||||
</span>
|
||||
</h3>
|
||||
{rendered.done.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty">
|
||||
还没有完成的任务
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<ul className="game-resource-generation-tasks-list">
|
||||
{rendered.visibleDone.map((row) =>
|
||||
resourceCanvasGenerationTaskRowNode(
|
||||
row,
|
||||
nowMillis,
|
||||
onFocusTask,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
{rendered.done.length > rendered.visibleDone.length ? (
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
<div
|
||||
className="game-resource-generation-tasks-scroll"
|
||||
data-resource-generation-task-scroll=""
|
||||
>
|
||||
{rendered.ordered.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty" role="status">
|
||||
还没有生成任务
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<section
|
||||
className="game-resource-generation-tasks-section"
|
||||
aria-label="排队与生成中"
|
||||
>
|
||||
<h3 className="game-resource-generation-tasks-section-title">
|
||||
<span>排队/生成中</span>
|
||||
<span className="game-resource-generation-tasks-section-title-count">
|
||||
{rendered.active.length}
|
||||
</span>
|
||||
</h3>
|
||||
{rendered.active.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty">
|
||||
{`仅显示最近 ${RESOURCE_CANVAS_ASSET_GENERATION_DONE_SECTION_LIMIT} 条,另有 ${
|
||||
rendered.done.length - rendered.visibleDone.length
|
||||
} 条较早记录`}
|
||||
没有进行中的任务
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
) : (
|
||||
<ul className="game-resource-generation-tasks-list">
|
||||
{rendered.active.map((row) =>
|
||||
resourceCanvasGenerationTaskRowNode(
|
||||
row,
|
||||
nowMillis,
|
||||
onFocusTask,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<section
|
||||
className="game-resource-generation-tasks-section"
|
||||
aria-label="已完成"
|
||||
>
|
||||
<h3 className="game-resource-generation-tasks-section-title">
|
||||
<span>已完成</span>
|
||||
<span className="game-resource-generation-tasks-section-title-count">
|
||||
{rendered.done.length}
|
||||
</span>
|
||||
</h3>
|
||||
{rendered.done.length === 0 ? (
|
||||
<p className="game-resource-generation-tasks-empty">
|
||||
还没有完成的任务
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<ul className="game-resource-generation-tasks-list">
|
||||
{rendered.visibleDone.map((row) =>
|
||||
resourceCanvasGenerationTaskRowNode(
|
||||
row,
|
||||
nowMillis,
|
||||
onFocusTask,
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
{rendered.done.length > rendered.visibleDone.length ? (
|
||||
<p className="game-resource-generation-tasks-empty">
|
||||
{`仅显示最近 ${RESOURCE_CANVAS_ASSET_GENERATION_DONE_SECTION_LIMIT} 条,另有 ${
|
||||
rendered.done.length - rendered.visibleDone.length
|
||||
} 条较早记录`}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+76
-14
@@ -1,21 +1,80 @@
|
||||
/* 「生成任务」侧栏的样式。
|
||||
*
|
||||
* 分层与网页端美术画布的任务侧栏(`ImageCanvasTaskSidebarView.tsx` + `src/index.css:6084+`)一一对应:
|
||||
* 容器 → 头部(标题 + 在途计数)→ 分栏标题(各带条数)→ 条目卡片(状态徽标 / 阶段 / 耗时 / 定位)。
|
||||
* 差别只有一处:**颜色全部换成 AGC 的平台设计 token(`--platform-*`)**,不再照抄网页端的固定色值,
|
||||
* 右上角锚点(开关 + 浮动面板)→ 头部(标题 + 在途计数)→ 分栏标题(各带条数)→
|
||||
* 条目卡片(状态徽标 / 阶段 / 耗时 / 定位)。
|
||||
*
|
||||
* 位置与开合形态照抄美术画布:**开关常驻画布右上角,面板在开关左侧展开**,收起态只剩那一枚
|
||||
* 开关(美术画布同样如此:列表为空且侧栏关闭时只保留图标开关)。差别只有一处:
|
||||
* **颜色与外形全部走 AGC 的平台设计 token(`--platform-*`)**,不再照抄网页端的固定色值,
|
||||
* 这样亮/暗主题都跟着 `platform-theme` 走,也不引入第二套色板。
|
||||
*
|
||||
* 行为(分栏、条数、已完成封顶、折叠不清列表、提交后自动展开)都不在这里,样式只负责表现。
|
||||
*/
|
||||
|
||||
.game-resource-generation-tasks-sidebar {
|
||||
position: fixed;
|
||||
top: 4rem;
|
||||
bottom: 6rem;
|
||||
left: 0.75rem;
|
||||
/*
|
||||
* 右上角锚点:开关与面板同挂在这里,所以「收起只剩开关、展开面板贴在同一角」这两个状态
|
||||
* 共用同一处坐标,不会出现开关与面板各锚一处、开合时互相漂移。
|
||||
*
|
||||
* 锚点相对 `.game-workbench-stage` 定位(stage `position: relative`,见 styles.css):
|
||||
* 工具条占第一行(min-height 48px),下面的 auto 行在资源态是空行(高 0),所以
|
||||
* `top: 3.5rem` 正好落在画布区顶边再留 8px —— 资源画布右上角。运行表现层是另一个工作面:
|
||||
* 它的右上角(`top: 20px; right: 20px`)被版本入口占着,锚点因此下移到版本入口之下。
|
||||
*/
|
||||
.game-resource-generation-tasks-anchor {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.85rem;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
/* 锚点被拉到整宽时(窄屏),开关与面板仍然贴住右上角,而不是散在左侧。 */
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
gap: 0.45rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.game-resource-generation-tasks-anchor[data-generation-tasks-placement='run'] {
|
||||
top: 7rem;
|
||||
right: 1.25rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* 右上角开关:外观沿用原工具条那一枚「生成任务」按钮(次级胶囊:描边 + secondary 填充 +
|
||||
* 12px/700),搬到画布上多一档面板投影,保证压在画布素材上仍然看得清。
|
||||
*/
|
||||
.game-resource-generation-tasks-toggle {
|
||||
display: inline-flex;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 999px;
|
||||
background: var(--platform-button-secondary-fill);
|
||||
box-shadow: var(--platform-panel-shadow);
|
||||
color: var(--platform-button-secondary-text);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.game-resource-generation-tasks-toggle:hover {
|
||||
border-color: var(--platform-accent);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.game-resource-generation-tasks-toggle:focus-visible {
|
||||
outline: 2px solid var(--platform-accent);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
|
||||
}
|
||||
|
||||
.game-resource-generation-tasks-sidebar {
|
||||
display: flex;
|
||||
width: min(300px, 80vw);
|
||||
max-height: min(30rem, calc(100vh - 12rem));
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
@@ -27,11 +86,12 @@
|
||||
animation: game-resource-generation-tasks-enter 160ms ease-out;
|
||||
}
|
||||
|
||||
/* 提交后面板会重新挂载,动画只负责「进场」这一下;不做宽度 reflow,避免画布抖动。 */
|
||||
/* 提交后面板会重新挂载,动画只负责「进场」这一下;不做宽度 reflow,避免画布抖动。
|
||||
* 进场方向跟着锚点走:面板从右上角往左展开,所以位移取正 X。 */
|
||||
@keyframes game-resource-generation-tasks-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-0.5rem);
|
||||
transform: translateX(0.5rem);
|
||||
}
|
||||
|
||||
to {
|
||||
@@ -54,7 +114,7 @@
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-0.5rem);
|
||||
transform: translateX(0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,11 +416,13 @@
|
||||
|
||||
/* 窄屏(含 360px):侧栏占满可用宽度,不挡画布操作。 */
|
||||
@media (max-width: 480px) {
|
||||
.game-resource-generation-tasks-sidebar {
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
bottom: 5.5rem;
|
||||
.game-resource-generation-tasks-anchor {
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.game-resource-generation-tasks-sidebar {
|
||||
max-height: calc(100vh - 12rem);
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
Info,
|
||||
Layers,
|
||||
LayoutGrid,
|
||||
ListChecks,
|
||||
ListFilter,
|
||||
Maximize2,
|
||||
Minus,
|
||||
@@ -6849,7 +6848,8 @@ export default function ProjectDevelopmentView({
|
||||
*
|
||||
* 两处必须排除,否则会把用户刚做的动作吃掉:
|
||||
* - 侧栏内部:点任务卡、点「定位到素材」都不能收起侧栏;
|
||||
* - 工具条上那枚开合按钮:它自己负责 toggle,若在这里也被判成「点外部」就会先收起再被 toggle
|
||||
* - 右上角那枚开合开关(`data-resource-generation-task-toggle`):它自己负责 toggle,若在这里被
|
||||
* 判成「点外部」就会先收起再被 toggle
|
||||
* 打开,表现为按钮失灵。点它时这里的 pointerdown 直接放行,收尾交给它的 click。
|
||||
*
|
||||
* 用 pointerdown 而不是 click:画布空白处的左键 pointerdown 会 `preventDefault()`,掐掉指针的
|
||||
@@ -8396,17 +8396,6 @@ export default function ProjectDevelopmentView({
|
||||
}),
|
||||
[pendingResourceEdits, resourceEditSubmissions],
|
||||
);
|
||||
/** 入口按钮与折叠把手上显示的在途数量:只数当前项目的未终态任务(两套账本都算)。 */
|
||||
const resourceAssetGenerationInFlightCount =
|
||||
resourceAssetGenerationTasks.filter(
|
||||
(task) =>
|
||||
task.projectId === manifest.projectId &&
|
||||
!resourceCanvasAssetGenerationTaskIsTerminal(task),
|
||||
).length +
|
||||
resourceCanvasResourceEdits.filter(
|
||||
(task) => !resourceCanvasResourceEditTaskIsTerminal(task),
|
||||
).length;
|
||||
|
||||
const replaceResourceAssetGenerationTask = useCallback(
|
||||
(next: ResourceCanvasAssetGenerationTask) => {
|
||||
const current = resourceAssetGenerationTasksRef.current;
|
||||
@@ -9350,31 +9339,6 @@ export default function ProjectDevelopmentView({
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* 「生成任务」开合入口。**两个页签下都常驻**:侧栏本体是无条件渲染的非模态浮层,运行态一样
|
||||
* 可见,入口若只在资源页签,用户切到运行后关掉侧栏就再也打不开了。资源页签里它排在
|
||||
* 「依赖 / 类型」排列方式之前(动作在前、排列方式收在行尾),所以这里做成一个可复用的元素,
|
||||
* 由两处分支各自渲染一次——同一时刻只渲染一处,可访问名唯一。
|
||||
*/
|
||||
const generationTasksEntry = (
|
||||
<button
|
||||
type="button"
|
||||
className="game-workbench-resource-panel-button"
|
||||
aria-label="生成任务"
|
||||
aria-expanded={resourceAssetGenerationTasksPanelOpen}
|
||||
data-resource-generation-task-toggle=""
|
||||
data-resource-generation-task-count={resourceAssetGenerationInFlightCount}
|
||||
onClick={() =>
|
||||
setResourceAssetGenerationTasksPanelOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
<ListChecks size={15} aria-hidden="true" />
|
||||
{resourceAssetGenerationInFlightCount > 0
|
||||
? `生成任务 · ${resourceAssetGenerationInFlightCount}`
|
||||
: '生成任务'}
|
||||
</button>
|
||||
);
|
||||
|
||||
if (planningStartMode) {
|
||||
return (
|
||||
<section
|
||||
@@ -9572,12 +9536,6 @@ export default function ProjectDevelopmentView({
|
||||
})}
|
||||
</button>
|
||||
) : null}
|
||||
{/*
|
||||
「生成任务」入口排在「依赖 / 类型」排列方式之前:一个是开合任务侧栏的动作,
|
||||
一个是画布排列方式——动作在前、排列方式收在行尾。运行页签下由下面同一枚
|
||||
元素补渲染(见 `generationTasksEntry`)。
|
||||
*/}
|
||||
{generationTasksEntry}
|
||||
<div
|
||||
className="game-workbench-tabs game-resource-sort-tabs"
|
||||
role="group"
|
||||
@@ -9606,10 +9564,6 @@ export default function ProjectDevelopmentView({
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{/* 运行页签(或 UI 编辑器)下资源类按钮整组收起,但「生成任务」入口必须留着。 */}
|
||||
{mode === 'resources' && !uiEditorRoute
|
||||
? null
|
||||
: generationTasksEntry}
|
||||
</div>
|
||||
{/*
|
||||
布局状态提示**不进动作行**:它是一段随保存过程变长的文案(空 →「保存中」→
|
||||
@@ -10742,6 +10696,31 @@ export default function ProjectDevelopmentView({
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{/*
|
||||
「生成任务」侧栏:常驻在**工作面右上角**(资源画布 / 运行表现层 / UI 编辑器各自一档坐标),
|
||||
可折叠、非模态。它**不**参与 `isResourceCanvasFloatingPanelOpen` 的模态遮挡判据——
|
||||
生成在后台跑,侧栏展开时画布必须照样能看能用;折叠只影响这个视图,任务本身活在账本
|
||||
与本地队列里。位置与开合形态照抄美术画布(开关常驻右上角 + 面板贴着它展开),
|
||||
颜色与外形仍走 AGC 的平台 token。
|
||||
*/}
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={resourceAssetGenerationTasks.filter(
|
||||
(task) => task.projectId === manifest.projectId,
|
||||
)}
|
||||
resourceEditTasks={resourceCanvasResourceEdits.filter(
|
||||
(task) =>
|
||||
!task.restored ||
|
||||
!resourceCanvasResourceEditTaskIsTerminal(task),
|
||||
)}
|
||||
open={resourceAssetGenerationTasksPanelOpen}
|
||||
onToggleOpen={() =>
|
||||
setResourceAssetGenerationTasksPanelOpen((current) => !current)
|
||||
}
|
||||
onFocusTask={focusResourceAssetGenerationTask}
|
||||
placement={
|
||||
uiEditorRoute ? 'editor' : mode === 'run' ? 'run' : 'canvas'
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{!uiEditorRoute ? (
|
||||
@@ -10803,26 +10782,6 @@ export default function ProjectDevelopmentView({
|
||||
</footer>
|
||||
) : null}
|
||||
|
||||
{/*
|
||||
「生成任务」侧栏:常驻、可折叠、非模态。它**不**参与 `isResourceCanvasFloatingPanelOpen`
|
||||
的模态遮挡判据——生成在后台跑,侧栏展开时画布必须照样能看能用;折叠只影响这个视图,
|
||||
任务本身活在账本与本地队列里。
|
||||
*/}
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={resourceAssetGenerationTasks.filter(
|
||||
(task) => task.projectId === manifest.projectId,
|
||||
)}
|
||||
resourceEditTasks={resourceCanvasResourceEdits.filter(
|
||||
(task) =>
|
||||
!task.restored || !resourceCanvasResourceEditTaskIsTerminal(task),
|
||||
)}
|
||||
open={resourceAssetGenerationTasksPanelOpen}
|
||||
onToggleOpen={() =>
|
||||
setResourceAssetGenerationTasksPanelOpen((current) => !current)
|
||||
}
|
||||
onFocusTask={focusResourceAssetGenerationTask}
|
||||
/>
|
||||
|
||||
{mode === 'resources' &&
|
||||
!uiEditorRoute &&
|
||||
selectedResource &&
|
||||
|
||||
@@ -55,7 +55,10 @@ function task(
|
||||
|
||||
function renderSidebar(
|
||||
tasks: readonly ResourceCanvasAssetGenerationTask[],
|
||||
options: { open?: boolean } = {},
|
||||
options: {
|
||||
open?: boolean;
|
||||
placement?: 'canvas' | 'run' | 'editor';
|
||||
} = {},
|
||||
) {
|
||||
const onToggleOpen = vi.fn();
|
||||
const onFocusTask = vi.fn();
|
||||
@@ -65,6 +68,7 @@ function renderSidebar(
|
||||
open={options.open ?? true}
|
||||
onToggleOpen={onToggleOpen}
|
||||
onFocusTask={onFocusTask}
|
||||
placement={options.placement}
|
||||
/>,
|
||||
);
|
||||
return { onToggleOpen, onFocusTask, ...view };
|
||||
@@ -180,16 +184,98 @@ describe('「生成任务」侧栏', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test('折叠后不再渲染,画布上不留任何常驻入口或把手', async () => {
|
||||
renderSidebar([], { open: false });
|
||||
test('折叠后只剩画布右上角那一枚开关:面板不渲染,开关自己就是开合口', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { onToggleOpen, container } = renderSidebar([], { open: false });
|
||||
expect(screen.queryByRole('region', { name: '生成任务' })).toBeNull();
|
||||
expect(screen.queryByRole('button')).toBeNull();
|
||||
expect(
|
||||
document.querySelector('.game-resource-generation-tasks-handle'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
document.querySelector('.game-resource-generation-tasks-sidebar'),
|
||||
).toBeNull();
|
||||
|
||||
// 开关落在右上角锚点里(形态照抄美术画布),并且是「生成任务」这个可访问名的唯一持有者。
|
||||
const toggle = screen.getByRole('button', { name: '生成任务' });
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false');
|
||||
expect(toggle.dataset.resourceGenerationTaskCount).toBe('0');
|
||||
expect(
|
||||
toggle.closest('.game-resource-generation-tasks-anchor'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelectorAll('.game-resource-generation-tasks-toggle'),
|
||||
).toHaveLength(1);
|
||||
|
||||
await user.click(toggle);
|
||||
expect(onToggleOpen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('开关上的在途计数与面板头部同源,且跟着任务收口归零', () => {
|
||||
const running = task({
|
||||
taskId: 't1',
|
||||
dispatched: true,
|
||||
status: 'running',
|
||||
phaseDetail: '正在生成。',
|
||||
});
|
||||
const { rerender } = render(
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={[running]}
|
||||
open={false}
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const collapsedToggle = screen.getByRole('button', { name: '生成任务' });
|
||||
expect(collapsedToggle.dataset.resourceGenerationTaskCount).toBe('1');
|
||||
expect(collapsedToggle.textContent).toContain('生成任务 · 1');
|
||||
|
||||
rerender(
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={[{ ...running, status: 'completed', finishedAtMillis: 2 }]}
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: '生成任务' }).dataset
|
||||
.resourceGenerationTaskCount,
|
||||
).toBe('0');
|
||||
expect(screen.getByRole('button', { name: '生成任务' }).textContent).toBe(
|
||||
'生成任务',
|
||||
);
|
||||
expect(screen.getByLabelText('在途生成任务 0').textContent).toBe('0');
|
||||
});
|
||||
|
||||
test('锚点按工作面分档:资源画布 / 运行表现层 / UI 编辑器各挂一档', () => {
|
||||
const { container, rerender } = renderSidebar([], { placement: 'run' });
|
||||
const placementOf = () =>
|
||||
container
|
||||
.querySelector('.game-resource-generation-tasks-anchor')
|
||||
?.getAttribute('data-generation-tasks-placement');
|
||||
expect(placementOf()).toBe('run');
|
||||
|
||||
rerender(
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={[]}
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusTask={vi.fn()}
|
||||
placement="editor"
|
||||
/>,
|
||||
);
|
||||
expect(placementOf()).toBe('editor');
|
||||
|
||||
// 缺省 = 资源栏目画布,老调用方不传这个 prop 也落在画布右上角。
|
||||
rerender(
|
||||
<ResourceCanvasAssetGenerationTasksPanelView
|
||||
tasks={[]}
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(placementOf()).toBe('canvas');
|
||||
});
|
||||
|
||||
test('收起时先播完收起动画再卸载,动画期间列表内容不跳', () => {
|
||||
|
||||
+65
-8
@@ -188,18 +188,75 @@ describe('「生成任务」侧栏样式', () => {
|
||||
'var(--platform-line-soft)',
|
||||
);
|
||||
|
||||
// 侧栏高度有界:上下边界都给死,不靠内容撑高(内容再多也只滚列表)。
|
||||
// 面板高度有界:不再靠上下边界拉满,改成锚点里的浮动卡片 + 封顶高度(内容再多也只滚列表)。
|
||||
const sidebar = resolved(['.game-resource-generation-tasks-sidebar']);
|
||||
expect(declaration(sidebar, 'top')).toBe('4rem');
|
||||
expect(declaration(sidebar, 'bottom')).toBe('6rem');
|
||||
expect(declaration(sidebar, 'width')).toBe('min(300px, 80vw)');
|
||||
expect(declaration(sidebar, 'max-height')).toBe(
|
||||
'min(30rem, calc(100vh - 12rem))',
|
||||
);
|
||||
// 面板自己不再定位:坐标只由右上角锚点一处决定,避免两处各锚一处互相漂移。
|
||||
expect(sidebar.has('position')).toBe(false);
|
||||
expect(sidebar.has('left')).toBe(false);
|
||||
});
|
||||
|
||||
test('窄屏(360px)下侧栏不溢出不挡把手', () => {
|
||||
const narrow = resolved(['.game-resource-generation-tasks-sidebar'], 360);
|
||||
expect(declaration(narrow, 'width')).toBe('auto');
|
||||
expect(declaration(narrow, 'left')).toBe('0.5rem');
|
||||
expect(declaration(narrow, 'right')).toBe('0.5rem');
|
||||
test('锚点贴在工作面右上角,运行表现层让开版本入口,开关沿用次级胶囊样式', () => {
|
||||
const anchor = resolved(['.game-resource-generation-tasks-anchor']);
|
||||
expect(declaration(anchor, 'position')).toBe('absolute');
|
||||
expect(declaration(anchor, 'top')).toBe('3.5rem');
|
||||
expect(declaration(anchor, 'right')).toBe('0.85rem');
|
||||
expect(declaration(anchor, 'z-index')).toBe('40');
|
||||
expect(declaration(anchor, 'align-items')).toBe('flex-start');
|
||||
// 画布与开关之间只留一档小间距,开关不会盖住面板。
|
||||
expect(declaration(anchor, 'gap')).toBe('0.45rem');
|
||||
|
||||
// 运行表现层右上角被版本入口(`game-run-version-picker`:top 20px / right 20px)占着,
|
||||
// 这一档必须把开关压到那枚版本入口下面。
|
||||
const runAnchor = resolved([
|
||||
'.game-resource-generation-tasks-anchor',
|
||||
".game-resource-generation-tasks-anchor[data-generation-tasks-placement='run']",
|
||||
]);
|
||||
expect(declaration(runAnchor, 'top')).toBe('7rem');
|
||||
expect(
|
||||
Number.parseFloat(declaration(runAnchor, 'top') as string),
|
||||
).toBeGreaterThan(5);
|
||||
|
||||
// 开关保留原来工具条那一枚的外观(次级胶囊),只是搬到画布上并加一档面板投影;
|
||||
// 取值仍然全部走平台 token,不引入第二套色板。
|
||||
const toggle = resolved(['.game-resource-generation-tasks-toggle']);
|
||||
expect(declaration(toggle, 'border-radius')).toBe('999px');
|
||||
expect(declaration(toggle, 'border')).toBe(
|
||||
'1px solid var(--platform-surface-border)',
|
||||
);
|
||||
expect(declaration(toggle, 'background')).toBe(
|
||||
'var(--platform-button-secondary-fill)',
|
||||
);
|
||||
expect(declaration(toggle, 'color')).toBe(
|
||||
'var(--platform-button-secondary-text)',
|
||||
);
|
||||
expect(declaration(toggle, 'box-shadow')).toBe(
|
||||
'var(--platform-panel-shadow)',
|
||||
);
|
||||
for (const property of ['border', 'background', 'color', 'box-shadow']) {
|
||||
expect(declaration(toggle, property)).not.toMatch(
|
||||
/#[0-9a-f]{3,8}|rgba?\(|hsla?\(/iu,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('窄屏(360px)下锚点占满可用宽度,面板宽度自适应不溢出', () => {
|
||||
const narrowAnchor = resolved(
|
||||
['.game-resource-generation-tasks-anchor'],
|
||||
360,
|
||||
);
|
||||
expect(declaration(narrowAnchor, 'left')).toBe('0.5rem');
|
||||
expect(declaration(narrowAnchor, 'right')).toBe('0.5rem');
|
||||
expect(declaration(narrowAnchor, 'justify-content')).toBe('flex-end');
|
||||
|
||||
const narrowSidebar = resolved(
|
||||
['.game-resource-generation-tasks-sidebar'],
|
||||
360,
|
||||
);
|
||||
expect(declaration(narrowSidebar, 'width')).toBe('auto');
|
||||
});
|
||||
|
||||
test('prefers-reduced-motion 下进场动画与过渡都被关掉', () => {
|
||||
|
||||
+25
-10
@@ -16,12 +16,13 @@ import {
|
||||
import { repoPath } from './repoPath';
|
||||
|
||||
/**
|
||||
* 「生成任务」侧栏与工具条入口的三条口径:
|
||||
* 「生成任务」侧栏与右上角入口的三条口径:
|
||||
*
|
||||
* 1. **失去焦点即收起**:点画布、点别的工具栏按钮、点侧栏外部都算失去焦点;点侧栏内部
|
||||
* (含任务卡与「定位到素材」)与点那枚开合按钮不算——后者自己负责 toggle,否则会先被
|
||||
* (含任务卡与「定位到素材」)与点那枚开合开关不算——后者自己负责 toggle,否则会先被
|
||||
* 收起再被 toggle 打开,表现为按钮失灵。
|
||||
* 2. **入口顺序**:「生成任务」排在「依赖 / 类型」排列方式之前(动作在前、排列方式收行尾)。
|
||||
* 2. **入口位置**:开关常驻画布**右上角**的锚点里(形态照抄美术画布),不再占工具条那一行;
|
||||
* 工具条上因此只剩资源动作与排列方式。
|
||||
* 3. **行内间距只有一套**:「依赖 / 类型」与前一按钮之间不能只剩分段控件自带的那点间隙。
|
||||
*/
|
||||
|
||||
@@ -124,22 +125,36 @@ describe('「生成任务」侧栏的开合与入口位置', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('「生成任务」入口排在「依赖 / 类型」排列方式之前', async () => {
|
||||
it('「生成任务」开关落在画布右上角锚点里,不再占工具条那一行', async () => {
|
||||
await renderWorkbench('workbench-tasks-entry-order');
|
||||
const actionsRow = document.querySelector('.game-workbench-view-actions');
|
||||
expect(actionsRow).not.toBeNull();
|
||||
const buttons = Array.from(actionsRow!.querySelectorAll('button'));
|
||||
const tasksIndex = buttons.findIndex(
|
||||
(button) => button.getAttribute('aria-label') === '生成任务',
|
||||
);
|
||||
// 工具条不再有「生成任务」入口:入口只有一个,就是右上角那一枚开关。
|
||||
expect(
|
||||
buttons.some(
|
||||
(button) => button.getAttribute('aria-label') === '生成任务',
|
||||
),
|
||||
).toBe(false);
|
||||
const sortIndex = buttons.findIndex(
|
||||
(button) => button.getAttribute('aria-label') === '按依赖',
|
||||
);
|
||||
expect(tasksIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(sortIndex).toBeGreaterThanOrEqual(0);
|
||||
// 依赖 / 类型收在行尾,生成任务在它前面。
|
||||
expect(tasksIndex).toBeLessThan(sortIndex);
|
||||
// 依赖 / 类型仍然收在行尾。
|
||||
expect(sortIndex).toBe(buttons.length - 2);
|
||||
|
||||
const toggle = screen.getByRole('button', { name: '生成任务' });
|
||||
const anchor = toggle.closest('.game-resource-generation-tasks-anchor');
|
||||
expect(anchor).not.toBeNull();
|
||||
expect(anchor?.getAttribute('data-generation-tasks-placement')).toBe(
|
||||
'canvas',
|
||||
);
|
||||
// 锚点挂在主视窗(stage)里:右上角坐标是相对画布工作面算的,不是相对整个窗口。
|
||||
expect(
|
||||
document
|
||||
.querySelector('.game-workbench-stage')
|
||||
?.contains(anchor as Element),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('「依赖 / 类型」与前一按钮之间的间距跟行内其他按钮一致', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 游戏创作项目开发工作台 PRD
|
||||
|
||||
更新时间:`2026-09-21`(2026-09-20 音频生成并入图片类那份后台任务账本、派生/修改类任务并入同一「生成任务」侧栏,2026-09-21 卡片浮层改为「提交即关」,见 §3.10 / §7.9;2026-09-14 图片类生成后台化:入口 IPC 为 `start_local_project_asset_generation` + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板;2026-09-13 新增的功能画布底部工具栏入口矩阵 §3.10 / §7.9,以及右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变)
|
||||
更新时间:`2026-09-21`(2026-09-20 音频生成并入图片类那份后台任务账本、派生/修改类任务并入同一「生成任务」侧栏,2026-09-21 卡片浮层改为「提交即关」、**「生成任务」侧栏从画布左侧贴边改为画布右上角锚点(开关常驻)**,见 §3.10 / §7.9;2026-09-14 图片类生成后台化:入口 IPC 为 `start_local_project_asset_generation` + 项目内任务账本 + 本地排队 + 非模态「生成任务」面板;2026-09-13 新增的功能画布底部工具栏入口矩阵 §3.10 / §7.9,以及右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口,资源卡预览、分区布局、非破坏性资源编辑、资源替换与 Godot 双根合同保持不变)
|
||||
|
||||
## 1. 产品定位
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
- 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。
|
||||
- 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(形态对齐网页端美术画布的任务侧栏):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布且不留贴边把手,开合只走工具条上常驻的「生成任务 · N」按钮(两个页签下都在);每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布左侧标题栏之下、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
|
||||
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(位置与开合形态照抄网页端美术画布的任务侧栏的右上角锚点,颜色与外形仍走 AGC 平台 token;2026-09-21 由「左侧贴边 + 工具条入口」改为「右上角锚点 + 常驻开关」):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布、只留那一枚右上角开关(工具条上不再有重复入口),开合只走画布右上角常驻的「生成任务 · N」按钮(两个页签下都在);每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布右上角锚点里(照抄美术画布那一处)、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。锚点按工作面分档:资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
|
||||
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。
|
||||
- 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。
|
||||
- 规范类和固定档:生成规范下的图标规范 / 角色规范 / 自定义规范都是固定档——只读展示当前规格,不给点了不生效的比例控件。本地 IPC 只有一条规范通道(`spec` 在 Rust 侧收口到 `icon-spec`),**没有 `specType` 入参**,因此角色规范与自定义规范共用该通道,靠素材名与提示词区分。
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-09-21 AGC「生成任务」侧栏移到画布右上角(照抄美术画布,保留 AGC 样式)
|
||||
|
||||
- 背景:`docs/project-memory/todos/【待办】画布验收后续修复-2026-09-18.md` 的「后续需求」要求「生成任务列表移到画布右上角,进行中/已完成分组、失败可见、限高滚动及自动开合」。改前状态:侧栏本体是 `position: fixed; top: 4rem; bottom: 6rem; left: 0.75rem` 的左侧贴边面板,开合口只有工具条上那一枚「生成任务 · N」按钮(资源 / 运行两个页签各渲染一次),折叠态不留任何常驻入口;分组、失败可见、限高滚动、提交后自动展开这四条当时已经具备。
|
||||
- 决策:侧栏改挂**画布右上角的锚点**(`.game-resource-generation-tasks-anchor`),形态照抄网页端美术画布的任务侧栏(`ImageCanvasTaskSidebarView.tsx` + `src/index.css:6072+` 的 `.image-canvas-editor__task-sidebar*`):**开关常驻右上角、面板在开关左侧展开**,收起态只剩那一枚开关;工具条上那两处重复入口删掉——同一个功能两个入口本身就是两处随时会漂移的状态。锚点是覆盖式的,仍然不 reflow 画布视口。颜色、圆角、字重、阴影**全部继续走 `--platform-*` token**,不照搬网页端的固定色值(该 CSS 文件头既有的口径)。
|
||||
- 原因:右上角是画布上唯一「不被右侧『智能创作』对话面板占、也不与左下角栏目工具栏 / 右下角缩放 Dock 打架」的稳定空位;折叠态仍留一枚开关以后,用户不必先想起工具条在哪一行,也不再需要「关掉以后打不开」的兜底(原设计正是靠工具条入口常驻来解决这个问题)。
|
||||
- 决策(分档坐标):锚点由 view 的 `placement`(`canvas | run | editor`)落成 `data-generation-tasks-placement`,坐标在样式里分档:资源栏目画布与 UI 编辑器用画布顶边那一档(`top: 3.5rem; right: 0.85rem`),**运行表现层下移到 `top: 7rem`**——它右上角 `top: 20px; right: 20px` 被 C7 版本入口 `game-run-version-picker` 占着,不去抢那一块。
|
||||
- 实现要点:开关改由 view 自己渲染(`data-resource-generation-task-toggle` 与计数属性原样保留,可访问名 `生成任务` 唯一),在途计数只在 view 内算一次、开关与面板头部同源(宿主原先那份 `resourceAssetGenerationInFlightCount` 因此删除);面板改由 `max-height: min(30rem, calc(100vh - 12rem))` 封顶 + 内部滚动,自己不再定位(坐标只由锚点一处决定);进场 / 退场动画位移方向跟着锚点翻到正 X。宿主的「点外部收起」判据(排除侧栏本体与 `data-resource-generation-task-toggle`)与「提交受理后自动展开」均未改。
|
||||
- 验证:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 11 passed(新增「折叠只剩右上角开关」「开关计数与面板头部同源」「锚点按工作面分档」)、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed(锚点坐标 / 运行档下移 / 面板不再自定位 / 开关走 token 且无硬编码色 / 窄屏占满宽度)、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed(入口位置改为右上角锚点、工具条不再有生成任务按钮)、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;`appSurface.test.ts` 538 passed / 17 skipped / 0 failed(与既有基线逐条一致);`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/features/resource-canvas/{ResourceCanvasAssetGenerationTasksPanelView.tsx,resourceCanvasAssetGenerationTasksSidebar.css}`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/{resourceCanvasAssetGenerationTasksPanel.test.tsx,resourceCanvasAssetGenerationTasksSidebarStyle.test.ts,resourceCanvasGenerationTasksSidebarDismiss.test.tsx}`、PRD §3.10。未动 Rust、SpacetimeDB、`packages/`、共享弹窗组件。
|
||||
- 已知未覆盖:真实客户端观感(右上角坐标相对画布顶边的落点、与运行表现层版本入口的间距)未在 Tauri 里目视确认;窄屏(≤480px)只有声明级断言。
|
||||
## Unity 与 Godot 常用操作指导
|
||||
|
||||
两种编辑器的操作指导复用客户端审核 Skill pack:DirectProject 通过原生 Skill 或既有审核资源读取入口按需取得,Agent Runtime 的对应执行工具说明嵌入同源参考。指南不改变插件可用性、执行授权或 Runner 回执;只读说明不能证明编辑器已连接。常用示例与执行失败/部分修改、保存、撤销边界在同一参考中维护,避免提示词和文档各存一份代码。
|
||||
|
||||
@@ -55,10 +55,20 @@
|
||||
- 范围核对:48 个改动文件全部落在 AGC 前端、3 个共享画布组件与 3 份文档;`server-rs` / `spacetime` / `docs/openapi` / `src-tauri` **零改动**(后端与契约无红线涉及),无 `.env`、密钥、构建产物或日志进入改动。
|
||||
- 合并后仍**未复验**的只有两类,且都不阻塞合并:① 真实客户端观感(本批动了动画面板样式、生图浮层紧凑排布与卡片基类 token——声明级与结构级都有守卫,真机要客户端验收确认);② 远程 CI(runner/镜像问题仍在处理,见 Issue #431;按用户要求本批不追)。
|
||||
|
||||
### 2026-09-21 第四批(「生成任务」侧栏移到画布右上角,待客户端验收)
|
||||
|
||||
需求原文:「生成任务列表移到画布右上角(照抄美术画布,但是保留当前的样式)」,对应本文件「后续需求」里那条。
|
||||
|
||||
- 改前:侧栏本体是 `position: fixed; top: 4rem; bottom: 6rem; left: 0.75rem` 的左侧贴边面板,开合口只有工具条上那一枚「生成任务 · N」按钮(资源 / 运行两个页签各渲染一次);分组、失败可见、限高滚动、提交受理后自动展开这四条本批之前已经具备,本轮只动**位置与开合形态**。
|
||||
- 改后:新增右上角锚点 `.game-resource-generation-tasks-anchor`(`position: absolute; top: 3.5rem; right: 0.85rem; z-index: 40`,挂在 `.game-workbench-stage` 里),**开关常驻右上角、面板在开关左侧展开、收起只剩那一枚开关**(与美术画布同一形态);工具条上那两处重复入口删除;面板改由 `max-height: min(30rem, calc(100vh - 12rem))` 封顶 + 内部滚动,自己不再定位。
|
||||
- 颜色 / 圆角 / 字重 / 阴影仍全部走 `--platform-*` token(开关沿用原工具条那枚次级胶囊的外观,只多一档面板投影),没有照搬网页端的固定色值。
|
||||
- 分档坐标:view 的 `placement`(`canvas | run | editor`)落成 `data-generation-tasks-placement`;运行表现层下移到 `top: 7rem`,避开它右上角 `game-run-version-picker`(`top: 20px; right: 20px`)。
|
||||
- 本机执行过:`resourceCanvasAssetGenerationTasksPanel.test.tsx` 11 passed、`resourceCanvasAssetGenerationTasksSidebarStyle.test.ts` 11 passed、`resourceCanvasGenerationTasksSidebarDismiss.test.tsx` 5 passed、`resourceCanvasAssetGenerationBackgroundClose.test.tsx` 5 passed;仓库根 `tests/appSurface.test.ts` **538 passed / 17 skipped / 0 failed**(与既有基线逐条一致);`tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
|
||||
- 仍未做:真实客户端目视(右上角落点、与版本入口的间距)与远程 CI 都没跑;窄屏(≤480px)只有声明级断言。
|
||||
## 后续需求
|
||||
|
||||
- 资源卡顶部独立名称区与全类型统一样式,确认 assetName/用户素材名的正式来源。
|
||||
- 生成任务列表移到画布右上角,进行中/已完成分组、失败可见、限高滚动及自动开合。
|
||||
- ~~生成任务列表移到画布右上角,进行中/已完成分组、失败可见、限高滚动及自动开合。~~ **2026-09-21 已处理**:侧栏改挂画布右上角锚点(开关常驻、面板在开关左侧展开,形态照抄美术画布,样式仍走 AGC 平台 token),工具条上的重复入口删除;`placement` 分档让运行表现层让开右上角版本入口。见下「2026-09-21 第四批」。
|
||||
- 素材替换关系与画布点选替换;聊天拖拽批量引用、复制对话保留有效引用。
|
||||
|
||||
## 边界与验收
|
||||
|
||||
Reference in New Issue
Block a user