9639a080c6
- 新增落点模型 resourceCardReferenceDropModel:对话栏矩形量取(0 x 0 判成没有落点,避免画布内拖动被误判)、命中判据、引用构造与落点提示文案。 - 引用构造与工具条「引用」按钮逐字一致(只认已登记 manifest 素材、source 为 resource-card、kind/分类/标签取自资源投影),一条都构造不出来时用提示条说明原因。 - 拖动中指针落到对话栏上即从排版切到引用:卡片不再跟指针走,改铺虚线落点浮层并提示「松手即可 @ 引用 N 项素材」;拖回画布内松手仍是原来的排版语义。 - 批量范围与拖动位移同一个集合(多选整批、单选只引用按住的那张);新增批量事件 RESOURCE_REFERENCE_INSERT_MANY_EVENT,App 侧一次插入整批并只聚焦一次,空批次不派发。 - 对话栏挂 ref(不 document.querySelector)、落点浮层按对话栏当时的屏幕矩形铺在根节点上,不改对话栏自身的定位与排版。 - 测试:新增落点模型用例 5 条与画布集成用例 4 条(单卡/多选/拖回画布/pointercancel),resourceReferenceInput 新增批量事件与一次插入整批 chip 两条,共 39 条定向全绿。 - 文档:功能说明新增「拖拽引用」一节与完成清单条目、PRD 更新时间、decision-log 新增 2026-09-21 条目。
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import {
|
|
resourceCardReferenceDropContains,
|
|
resourceCardReferenceDropHintLabel,
|
|
resourceCardReferenceDropRect,
|
|
resourceCardReferenceDropReferences,
|
|
} from '../src/view/project-development/resourceCardReferenceDropModel';
|
|
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
|
|
|
function resource(
|
|
patch: Partial<ProjectResource> & { id: string },
|
|
): ProjectResource {
|
|
return {
|
|
category: 'character',
|
|
subtype: 'character',
|
|
label: patch.id,
|
|
path: `assets/${patch.id}.png`,
|
|
mediaType: 'image/png',
|
|
sourceLabel: '本地上传',
|
|
taskTitle: null,
|
|
manifestAssetId: null,
|
|
producerTaskId: null,
|
|
externalResourceId: null,
|
|
referenceResourceIds: [],
|
|
dependencies: [],
|
|
dependencyDepth: 0,
|
|
...patch,
|
|
};
|
|
}
|
|
|
|
function elementWithRect(rect: {
|
|
left: number;
|
|
top: number;
|
|
width: number;
|
|
height: number;
|
|
}): Element {
|
|
return {
|
|
getBoundingClientRect: () =>
|
|
({
|
|
...rect,
|
|
right: rect.left + rect.width,
|
|
bottom: rect.top + rect.height,
|
|
x: rect.left,
|
|
y: rect.top,
|
|
toJSON: () => ({}),
|
|
}) as DOMRect,
|
|
} as unknown as Element;
|
|
}
|
|
|
|
describe('拖动素材到对话:落点与引用构造', () => {
|
|
test('没有元素或没量出尺寸都不算落点', () => {
|
|
expect(resourceCardReferenceDropRect(null)).toBeNull();
|
|
// 0 x 0 必须当「没有落点」:否则任何点都会被判成落在里面,画布内拖动松手也会被当成拖到对话。
|
|
expect(
|
|
resourceCardReferenceDropRect(
|
|
elementWithRect({ left: 0, top: 0, width: 0, height: 0 }),
|
|
),
|
|
).toBeNull();
|
|
expect(
|
|
resourceCardReferenceDropRect(
|
|
elementWithRect({ left: 10, top: 20, width: 400, height: 600 }),
|
|
),
|
|
).toEqual({ left: 10, top: 20, width: 400, height: 600 });
|
|
});
|
|
|
|
test('命中判据按矩形边界闭区间算', () => {
|
|
const rect = { left: 800, top: 0, width: 400, height: 600 };
|
|
expect(resourceCardReferenceDropContains(rect, 800, 0)).toBe(true);
|
|
expect(resourceCardReferenceDropContains(rect, 1200, 600)).toBe(true);
|
|
expect(resourceCardReferenceDropContains(rect, 1000, 300)).toBe(true);
|
|
expect(resourceCardReferenceDropContains(rect, 799, 300)).toBe(false);
|
|
expect(resourceCardReferenceDropContains(rect, 1000, 601)).toBe(false);
|
|
});
|
|
|
|
test('只把已登记的 manifest 素材折成引用,顺序与拖动集合一致', () => {
|
|
const references = resourceCardReferenceDropReferences(
|
|
['asset:registered-b', 'featured:not-registered', 'asset:registered-a'],
|
|
[
|
|
resource({
|
|
id: 'asset:registered-b',
|
|
manifestAssetId: 'registered-b',
|
|
subtype: 'character',
|
|
label: '主角立绘',
|
|
mediaType: 'image/png',
|
|
assetCategory: 'character',
|
|
assetTags: ['主角', '立绘'],
|
|
}),
|
|
resource({ id: 'featured:not-registered', label: '任务产物' }),
|
|
resource({
|
|
id: 'asset:registered-a',
|
|
manifestAssetId: 'registered-a',
|
|
subtype: 'background-music',
|
|
label: '主城 BGM',
|
|
mediaType: 'audio/mpeg',
|
|
assetCategory: 'audio',
|
|
}),
|
|
],
|
|
);
|
|
|
|
expect(references.map((reference) => reference.resourceId)).toEqual([
|
|
'registered-b',
|
|
'registered-a',
|
|
]);
|
|
expect(references[0]).toMatchObject({
|
|
type: 'resource',
|
|
// 引用认的是 manifest 资产 id,不是画布投影 id。
|
|
resourceId: 'registered-b',
|
|
kind: 'character',
|
|
label: '主角立绘',
|
|
mediaType: 'image/png',
|
|
category: 'character',
|
|
tags: ['主角', '立绘'],
|
|
// 与资源卡工具条那枚「引用」按钮同一个来源标记:同一素材两处进来是同一枚引用。
|
|
source: 'resource-card',
|
|
});
|
|
expect(references[1]).toMatchObject({
|
|
resourceId: 'registered-a',
|
|
kind: 'background-music',
|
|
category: 'audio',
|
|
});
|
|
});
|
|
|
|
test('拖动集合里一条登记素材都没有时返回空数组', () => {
|
|
expect(
|
|
resourceCardReferenceDropReferences(
|
|
['featured:task'],
|
|
[resource({ id: 'featured:task' })],
|
|
),
|
|
).toEqual([]);
|
|
expect(resourceCardReferenceDropReferences([], [])).toEqual([]);
|
|
});
|
|
|
|
test('提示文案把条数说清楚,0 条时说原因而不是「引用 0 项」', () => {
|
|
expect(resourceCardReferenceDropHintLabel(1)).toBe(
|
|
'松手即可 @ 引用 1 项素材',
|
|
);
|
|
expect(resourceCardReferenceDropHintLabel(3)).toBe(
|
|
'松手即可 @ 引用 3 项素材',
|
|
);
|
|
expect(resourceCardReferenceDropHintLabel(0)).toBe(
|
|
'这些素材还没登记为项目资源,不能 @ 引用',
|
|
);
|
|
});
|
|
});
|