读时丢弃的坐标不再静默:提示条带条数并在 DOM 上分开三类计数
- readReport 增加 droppedMissingResource(资源已不在 manifest)/ droppedSectionMismatch(分区与资源分类不匹配)/ dropped(两者之和),判据仍是 normalizeResourceCanvasPosition,未改 resourceCanvasLayoutModel 的丢弃语义。 - 文案补上跳过条数与两类原因:归并与丢弃同时发生时用「另有 N 条坐标无法对齐已跳过(…)」;只有丢弃时用「打开项目时有 N 条坐标无法对齐已跳过(…)」。 - 提示条暴露 data-resource-canvas-layout-normalized / -dropped / -dropped-missing-resource / -dropped-section-mismatch,供排障直接读 DOM;完全没丢时四个值都是 0。 - 读盘触发条件放宽为「有归并或有丢弃」,避免只有丢弃时什么都不提示。 - 断言:读盘统计用例覆盖三种情况分离;文案用例覆盖四形态(含真机 112 / 66 / 16 口径);appSurface 用例断言四个 data-* 与完整文案。 - 变异验证:把 dropped 计数改成恒 0 → 统计用例、读盘报告用例、提示条用例三处变红;去掉 data-resource-canvas-layout-dropped → 提示条用例变红。
This commit is contained in:
@@ -1396,8 +1396,8 @@ export default function ProjectDevelopmentView({
|
||||
const autoOpenedUiWorkflowResourceRef = useRef<string | null>(null);
|
||||
const [resourceWorkbenchNotice, setResourceWorkbenchNotice] = useState('');
|
||||
/**
|
||||
* 打开项目时读盘归并旧分区是当场写回 sidecar 的副作用,改前完全静默。
|
||||
* 这条提示只在项目刚打开时出现一次,可手动关掉,不是常驻说明。
|
||||
* 打开项目时读盘归并旧分区 / 跳过无法对齐的坐标都是当场写回 sidecar 的副作用,
|
||||
* 改前完全静默。这条提示只在项目刚打开时出现一次,可手动关掉,不是常驻说明。
|
||||
*/
|
||||
const [resourceLayoutReadNotice, setResourceLayoutReadNotice] = useState<{
|
||||
text: string;
|
||||
@@ -2099,7 +2099,7 @@ export default function ProjectDevelopmentView({
|
||||
const resourceLayoutNotice = activeResourceLayout.notice;
|
||||
const resourceLayoutSaving = activeResourceLayout.saving;
|
||||
/**
|
||||
* 读时归并发生在「打开项目」这一刻,且结果会被当场写回的 sidecar 固化,
|
||||
* 读时归并 / 读时丢弃都发生在「打开项目」这一刻,且结果会被当场写回的 sidecar 固化,
|
||||
* 所以必须让用户看见一次。两个排序模式各读一份 sidecar,这里只取先到的那份、
|
||||
* 每个项目只提示一次,避免两条提示互相覆盖;切项目后上一次的提示必须清掉。
|
||||
*/
|
||||
@@ -6066,6 +6066,15 @@ export default function ProjectDevelopmentView({
|
||||
data-resource-canvas-layout-normalized={
|
||||
resourceLayoutReadNotice.report.normalizedSections
|
||||
}
|
||||
data-resource-canvas-layout-dropped={
|
||||
resourceLayoutReadNotice.report.dropped
|
||||
}
|
||||
data-resource-canvas-layout-dropped-missing-resource={
|
||||
resourceLayoutReadNotice.report.droppedMissingResource
|
||||
}
|
||||
data-resource-canvas-layout-dropped-section-mismatch={
|
||||
resourceLayoutReadNotice.report.droppedSectionMismatch
|
||||
}
|
||||
>
|
||||
<span>{resourceLayoutReadNotice.text}</span>
|
||||
<button
|
||||
|
||||
+53
-15
@@ -232,11 +232,12 @@ function layoutCoordinatesAreSafe(layout: ProjectResourceCanvasLayout) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 读盘后按现行分区归并坐标的只读统计。
|
||||
* 读盘后按现行分区归并 / 丢弃坐标的只读统计。
|
||||
*
|
||||
* 读时归并(旧 `art` / `code` 分区 → 资源当前分区)会让协调路径判定 `changed`,
|
||||
* 于是第一次打开项目就会把整份布局写回一次 sidecar。改前这件事完全静默,
|
||||
* 所以这里把它数出来,交给调用方给用户一次可见反馈。
|
||||
* 于是第一次打开项目就会把整份布局写回一次 sidecar;无法归并的坐标则被丢弃,
|
||||
* 并且这次丢弃同样由那次写回固化。改前这两件事都完全静默,所以这里把它们数出来,
|
||||
* 交给调用方给用户一次可见反馈。
|
||||
*/
|
||||
export type ProjectResourceCanvasLayoutReadReport = {
|
||||
/** 项目作用域身份(不含排序模式):同一个项目的两份 sidecar 只提示一次。 */
|
||||
@@ -244,6 +245,12 @@ export type ProjectResourceCanvasLayoutReadReport = {
|
||||
mode: ProjectResourceCanvasLayoutMode;
|
||||
/** 旧栏目坐标被读时归并到资源当前分区的条数。 */
|
||||
normalizedSections: number;
|
||||
/** 因 `resourceId` 在当前资源集合里查不到而被丢弃的坐标条数。 */
|
||||
droppedMissingResource: number;
|
||||
/** 因持久化分区与资源当前分类不匹配而被丢弃的坐标条数(资源仍在,会按现行分区重新落位)。 */
|
||||
droppedSectionMismatch: number;
|
||||
/** 上面两项之和:读盘时被丢弃的坐标总数。 */
|
||||
dropped: number;
|
||||
};
|
||||
|
||||
export type ProjectResourceCanvasLayoutReadCounts = Omit<
|
||||
@@ -253,8 +260,8 @@ export type ProjectResourceCanvasLayoutReadCounts = Omit<
|
||||
|
||||
/**
|
||||
* 读盘统计。判据直接复用 `normalizeResourceCanvasPosition`——也就是
|
||||
* `reconcileResourceCanvasLayout` 读盘用的同一个函数,所以计数与真正落盘的结果逐条一致;
|
||||
* 这里只统计,判定与写回语义都不动。
|
||||
* `reconcileResourceCanvasLayout` 丢失坐标用的同一个函数,所以三种计数与真正落盘的
|
||||
* 结果逐条一致;这里只统计,判定与写回语义都不动。
|
||||
*/
|
||||
export function inspectProjectResourceCanvasLayoutRead(
|
||||
layout: ProjectResourceCanvasLayout,
|
||||
@@ -264,28 +271,59 @@ export function inspectProjectResourceCanvasLayoutRead(
|
||||
resources.map((resource) => [resource.id, resource.category]),
|
||||
);
|
||||
let normalizedSections = 0;
|
||||
let droppedMissingResource = 0;
|
||||
let droppedSectionMismatch = 0;
|
||||
for (const position of layout.positions) {
|
||||
const normalized = normalizeResourceCanvasPosition(
|
||||
position,
|
||||
categoryByResourceId,
|
||||
);
|
||||
if (normalized?.changed) {
|
||||
normalizedSections += 1;
|
||||
if (normalized) {
|
||||
if (normalized.changed) {
|
||||
normalizedSections += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (categoryByResourceId.has(position.resourceId)) {
|
||||
droppedSectionMismatch += 1;
|
||||
} else {
|
||||
droppedMissingResource += 1;
|
||||
}
|
||||
}
|
||||
return { normalizedSections };
|
||||
return {
|
||||
normalizedSections,
|
||||
droppedMissingResource,
|
||||
droppedSectionMismatch,
|
||||
dropped: droppedMissingResource + droppedSectionMismatch,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读盘统计的用户可见文案。只描述已经发生的事:归并会当场写回一次 sidecar、坐标位置不变。
|
||||
* 这里不写长期规则,也不承诺后续行为。
|
||||
* 读盘统计的用户可见文案。只描述已经发生的事:归并会当场写回一次 sidecar、坐标位置不变;
|
||||
* 无法归并的坐标会被跳过。这里不写长期规则,也不承诺后续行为。
|
||||
*/
|
||||
export function describeProjectResourceCanvasLayoutRead(
|
||||
report: ProjectResourceCanvasLayoutReadCounts,
|
||||
): string {
|
||||
return report.normalizedSections > 0
|
||||
? `已把 ${report.normalizedSections} 条旧分区坐标对齐到新分区并写回,坐标位置未变`
|
||||
: '';
|
||||
const sentences: string[] = [];
|
||||
if (report.normalizedSections > 0) {
|
||||
sentences.push(
|
||||
`已把 ${report.normalizedSections} 条旧分区坐标对齐到新分区并写回,坐标位置未变`,
|
||||
);
|
||||
}
|
||||
if (report.dropped > 0) {
|
||||
const reasons: string[] = [];
|
||||
if (report.droppedMissingResource > 0) {
|
||||
reasons.push(`${report.droppedMissingResource} 条资源已不在项目中`);
|
||||
}
|
||||
if (report.droppedSectionMismatch > 0) {
|
||||
reasons.push(`${report.droppedSectionMismatch} 条分区与资源分类不匹配`);
|
||||
}
|
||||
sentences.push(
|
||||
`${report.normalizedSections > 0 ? '另有 ' : '打开项目时有 '}${report.dropped} 条坐标无法对齐已跳过(${reasons.join(',')})`,
|
||||
);
|
||||
}
|
||||
return sentences.join(';');
|
||||
}
|
||||
|
||||
function reconcileLayout(
|
||||
@@ -789,9 +827,9 @@ export function useProjectResourceCanvasLayout({
|
||||
).changed
|
||||
) {
|
||||
enqueueResourceSyncRef.current(epoch);
|
||||
// 有归并才会有这次写回;单纯补新资源落位的写回不报读时统计,
|
||||
// 有归并或丢弃才会有这次写回;单纯补新资源落位的写回不报读时统计,
|
||||
// 否则提示会声称「已对齐旧分区」而实际上一条都没对齐。
|
||||
if (readCounts.normalizedSections > 0) {
|
||||
if (readCounts.normalizedSections > 0 || readCounts.dropped > 0) {
|
||||
setReadReport({
|
||||
...readCounts,
|
||||
projectScopeKey: createProjectScopeKey(projectPath, projectId),
|
||||
|
||||
@@ -1790,22 +1790,26 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
|
||||
const notice = await waitFor(() => {
|
||||
const element = document.querySelector<HTMLElement>(
|
||||
'.game-resource-book-notices [data-resource-canvas-layout-normalized]',
|
||||
'.game-resource-book-notices [data-resource-canvas-layout-dropped]',
|
||||
);
|
||||
expect(element).not.toBeNull();
|
||||
return element as HTMLElement;
|
||||
});
|
||||
expect(notice.querySelector('span')?.textContent).toBe(
|
||||
'已把 1 条旧分区坐标对齐到新分区并写回,坐标位置未变',
|
||||
'已把 1 条旧分区坐标对齐到新分区并写回,坐标位置未变;另有 2 条坐标无法对齐已跳过(1 条资源已不在项目中,1 条分区与资源分类不匹配)',
|
||||
);
|
||||
// 排障口径:完全没丢是 0 / 0,丢的是哪一类由两个子计数分开。
|
||||
expect(notice.dataset.resourceCanvasLayoutNormalized).toBe('1');
|
||||
expect(notice.dataset.resourceCanvasLayoutDropped).toBe('2');
|
||||
expect(notice.dataset.resourceCanvasLayoutDroppedMissingResource).toBe('1');
|
||||
expect(notice.dataset.resourceCanvasLayoutDroppedSectionMismatch).toBe('1');
|
||||
// 读时归并会当场写回一次 sidecar,提示说的就是这次写回。
|
||||
await waitFor(() => expect(layoutRevision).toBeGreaterThan(0));
|
||||
|
||||
// 一次性提示,不是常驻说明:关掉即从 DOM 消失。
|
||||
fireEvent.click(within(notice).getByRole('button', { name: '知道了' }));
|
||||
expect(
|
||||
document.querySelector('[data-resource-canvas-layout-normalized]'),
|
||||
document.querySelector('[data-resource-canvas-layout-dropped]'),
|
||||
).toBeNull();
|
||||
}, 20_000);
|
||||
|
||||
@@ -1877,7 +1881,7 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
// 读盘已经落地:此刻既没有读时提示,也没有任何写回。
|
||||
await act(async () => {});
|
||||
expect(
|
||||
document.querySelector('[data-resource-canvas-layout-normalized]'),
|
||||
document.querySelector('[data-resource-canvas-layout-dropped]'),
|
||||
).toBeNull();
|
||||
expect(document.querySelector('.game-resource-live-notice')).toBeNull();
|
||||
}, 20_000);
|
||||
|
||||
@@ -1244,9 +1244,12 @@ describe('useProjectResourceCanvasLayout', () => {
|
||||
projectScopeKey: JSON.stringify([projectPath, projectId]),
|
||||
mode: 'type',
|
||||
normalizedSections: 1,
|
||||
droppedMissingResource: 1,
|
||||
droppedSectionMismatch: 1,
|
||||
dropped: 2,
|
||||
});
|
||||
expect(describeProjectResourceCanvasLayoutRead(report!)).toBe(
|
||||
'已把 1 条旧分区坐标对齐到新分区并写回,坐标位置未变',
|
||||
'已把 1 条旧分区坐标对齐到新分区并写回,坐标位置未变;另有 2 条坐标无法对齐已跳过(1 条资源已不在项目中,1 条分区与资源分类不匹配)',
|
||||
);
|
||||
// 归并与丢弃的结果确实被那一次写回固化:写盘内容里旧 `art` 与不匹配分区都不在了。
|
||||
await waitFor(() => expect(updates).toHaveLength(1));
|
||||
@@ -1332,6 +1335,9 @@ describe('useProjectResourceCanvasLayout', () => {
|
||||
describe('resource canvas layout read report', () => {
|
||||
const emptyCounts = {
|
||||
normalizedSections: 0,
|
||||
droppedMissingResource: 0,
|
||||
droppedSectionMismatch: 0,
|
||||
dropped: 0,
|
||||
};
|
||||
|
||||
function layoutWith(
|
||||
@@ -1340,13 +1346,20 @@ describe('resource canvas layout read report', () => {
|
||||
return persistedLayout('type', 1, positions);
|
||||
}
|
||||
|
||||
it('counts the coordinates realigned onto the current sections', () => {
|
||||
it('separates realigned sections from both drop reasons', () => {
|
||||
const counts = inspectProjectResourceCanvasLayoutRead(
|
||||
layoutWith([
|
||||
{ resourceId: 'a', section: 'art', x: 0, y: 0, manuallyPlaced: true },
|
||||
{
|
||||
resourceId: 'b',
|
||||
section: 'audio',
|
||||
section: 'document',
|
||||
x: 0,
|
||||
y: 0,
|
||||
manuallyPlaced: true,
|
||||
},
|
||||
{
|
||||
resourceId: 'ghost',
|
||||
section: 'art',
|
||||
x: 0,
|
||||
y: 0,
|
||||
manuallyPlaced: true,
|
||||
@@ -1358,7 +1371,12 @@ describe('resource canvas layout read report', () => {
|
||||
],
|
||||
);
|
||||
|
||||
expect(counts).toEqual({ normalizedSections: 1 });
|
||||
expect(counts).toEqual({
|
||||
normalizedSections: 1,
|
||||
droppedMissingResource: 1,
|
||||
droppedSectionMismatch: 1,
|
||||
dropped: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('describes each read-report shape', () => {
|
||||
@@ -1369,5 +1387,30 @@ describe('resource canvas layout read report', () => {
|
||||
normalizedSections: 112,
|
||||
}),
|
||||
).toBe('已把 112 条旧分区坐标对齐到新分区并写回,坐标位置未变');
|
||||
expect(
|
||||
describeProjectResourceCanvasLayoutRead({
|
||||
...emptyCounts,
|
||||
normalizedSections: 112,
|
||||
droppedMissingResource: 66,
|
||||
droppedSectionMismatch: 16,
|
||||
dropped: 82,
|
||||
}),
|
||||
).toBe(
|
||||
'已把 112 条旧分区坐标对齐到新分区并写回,坐标位置未变;另有 82 条坐标无法对齐已跳过(66 条资源已不在项目中,16 条分区与资源分类不匹配)',
|
||||
);
|
||||
expect(
|
||||
describeProjectResourceCanvasLayoutRead({
|
||||
...emptyCounts,
|
||||
droppedMissingResource: 3,
|
||||
dropped: 3,
|
||||
}),
|
||||
).toBe('打开项目时有 3 条坐标无法对齐已跳过(3 条资源已不在项目中)');
|
||||
expect(
|
||||
describeProjectResourceCanvasLayoutRead({
|
||||
...emptyCounts,
|
||||
droppedSectionMismatch: 2,
|
||||
dropped: 2,
|
||||
}),
|
||||
).toBe('打开项目时有 2 条坐标无法对齐已跳过(2 条分区与资源分类不匹配)');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user