抽出素材上传生命周期模型

扩展 ImageCanvasUploadModel 承载素材上传占位、进度、失败和持久化回写状态迁移

精简 useImageCanvasUploadWorkflow 中的资产与图层状态补丁逻辑

补充上传模型单测覆盖生命周期状态和图层绑定

更新 TRACKING.md 记录第四十六执行批次与验证结果
This commit is contained in:
2026-06-17 20:36:11 +08:00
parent 6afc1cf920
commit d8bd371c69
4 changed files with 409 additions and 86 deletions

View File

@@ -7,9 +7,17 @@ import type {
} from './ImageCanvasEditorTypes';
import {
applyGenerationReferenceUpload,
applyMeasuredUploadAssetSize,
applyPersistedUploadAsset,
applyUploadAssetCreateFailure,
applyUploadAssetCreatePending,
applyUploadAssetReadFailure,
applyUploadAssetReadSuccess,
bindUploadLayerToPersistedAsset,
createUploadCanvasLayer,
createUploadedGenerationReference,
createUploadingAssetPlaceholder,
expandUploadFolder,
resizeUploadCanvasLayerToImage,
resolveUploadFolderId,
setFailedGenerationIdle,
@@ -47,6 +55,19 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
};
}
function createUploadingAsset(
overrides: Partial<ReturnType<typeof createUploadingAssetPlaceholder>> = {},
) {
return {
...createUploadingAssetPlaceholder({
uploadIndex: 1,
fileName: '上传图片.png',
folderId: 'project',
}),
...overrides,
};
}
function createDialog(
overrides: Partial<GenerateDialogState> = {},
): GenerateDialogState {
@@ -109,6 +130,167 @@ describe('ImageCanvasUploadModel', () => {
});
});
it('opens the target upload folder without changing other folders', () => {
expect(
expandUploadFolder({
folders: [
createFolder({ id: 'project', collapsed: true }),
createFolder({ id: 'characters', label: '角色素材', collapsed: true }),
],
folderId: 'characters',
}),
).toEqual([
createFolder({ id: 'project', collapsed: true }),
createFolder({ id: 'characters', label: '角色素材', collapsed: false }),
]);
});
it('applies upload asset lifecycle patches', () => {
const asset = createUploadingAsset();
const otherAsset = createUploadingAsset({
id: 'upload-other',
label: '其他图片.png',
});
expect(
applyUploadAssetReadSuccess({
assets: [asset, otherAsset],
uploadAssetId: asset.id,
imageSrc: 'data:image/png;base64,read',
}),
).toMatchObject([
{
id: 'upload-1',
src: 'data:image/png;base64,read',
uploadProgress: 42,
uploadMessage: '读取图片',
},
{ id: 'upload-other', label: '其他图片.png' },
]);
expect(
applyUploadAssetCreatePending({
assets: [asset],
uploadAssetId: asset.id,
}),
).toMatchObject([
{
id: 'upload-1',
uploadStatus: 'uploading',
uploadProgress: 68,
uploadMessage: '上传中',
},
]);
expect(
applyUploadAssetReadFailure({
assets: [asset],
uploadAssetId: asset.id,
}),
).toMatchObject([
{
id: 'upload-1',
uploadStatus: 'failed',
uploadProgress: 100,
uploadMessage: '读取失败',
},
]);
expect(
applyUploadAssetCreateFailure({
assets: [asset],
uploadAssetId: asset.id,
message: '请先登录',
}),
).toMatchObject([
{
id: 'upload-1',
uploadStatus: 'failed',
uploadProgress: 100,
uploadMessage: '请先登录',
},
]);
});
it('replaces upload placeholders with persisted assets and clears upload state', () => {
expect(
applyPersistedUploadAsset({
assets: [createUploadingAsset()],
uploadAssetId: 'upload-1',
persistedAsset: {
assetId: 'asset-1',
folderId: 'characters',
label: '角色.png',
imageSrc: 'data:image/png;base64,persisted',
width: 1024,
height: 768,
objectKey: 'object-key',
assetObjectId: 'asset-object',
},
}),
).toEqual([
{
id: 'asset-1',
label: '角色.png',
src: 'data:image/png;base64,persisted',
width: 1024,
height: 768,
folderId: 'characters',
sourceKind: 'uploaded',
sourceType: 'uploaded',
persisted: true,
objectKey: 'object-key',
assetObjectId: 'asset-object',
uploadStatus: undefined,
uploadProgress: undefined,
uploadMessage: undefined,
},
]);
});
it('updates measured asset size after the browser loads the uploaded image', () => {
expect(
applyMeasuredUploadAssetSize({
assets: [createUploadingAsset()],
uploadAssetId: 'upload-1',
originalWidth: 1600,
originalHeight: 900,
}),
).toMatchObject([
{
id: 'upload-1',
width: 1600,
height: 900,
},
]);
});
it('binds uploaded canvas layers to persisted asset metadata', () => {
expect(
bindUploadLayerToPersistedAsset({
layers: [createLayer({ objectKey: 'local-object' })],
layerId: 'layer-upload-1',
persistedAsset: {
assetId: 'asset-1',
folderId: 'project',
label: '上传图片.png',
imageSrc: 'data:image/png;base64,persisted',
width: 420,
height: 315,
objectKey: 'object-key',
assetObjectId: 'asset-object',
},
}),
).toMatchObject([
{
id: 'layer-upload-1',
sourceAssetId: 'asset-1',
objectKey: 'object-key',
assetObjectId: 'asset-object',
},
]);
});
it('creates uploaded generation references with fallback labels', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-17T12:00:00.000Z'));