T10: 工具面板收进右栏折叠分组,拖放图片改为新增图层
- ToolPanels:背景/滤镜/文字/导出与模板四组可折叠分组,选中图片/文字自动展开相关组 - SourcePanel:拖放与文件选择默认作为新图层加入(addImage toBack:false 并选中), 「作为背景铺满」改为面板里的显式勾选项,不再是一切的默认行为;多张可一次全加 - 上传 object URL 改为会话内保留、卸载时统一释放(撤销与模板重载要按 src 重新取图) - 预设几何与 Unsplash 保持原「铺背景」语义,不受勾选项影响 - 新增 pickImageFiles 纯函数与 src/sources/selfcheck.ts Node 侧自检
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 【T10】Node 侧自验:多文件拖放的取值逻辑。运行:`bun src/sources/selfcheck.ts`
|
||||
*
|
||||
* 拖放一次可能给一批混合文件(图片、文档、压缩包…),这条线把「从里面
|
||||
* 挑出图片、保持相对顺序」抽成纯函数 `pickImageFiles`(src/sources/upload.ts),
|
||||
* 这里断言它:只挑图片、顺序不变、空 type / 非图片全部排除。取值逻辑不
|
||||
* 依赖浏览器,Node 里就能验;真正把图拖进窗口的行为由主控用 Chrome 验。
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { pickImageFiles } from "./upload";
|
||||
|
||||
/** 模拟 File 的最小形状:拖放取值只用到 mime type。 */
|
||||
interface FakeFile {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/* ---------------- 1. 混合文件:只挑图片,顺序不变 ---------------- */
|
||||
|
||||
const mixed: FakeFile[] = [
|
||||
{ name: "封面主图.png", type: "image/png" },
|
||||
{ name: "说明.txt", type: "text/plain" },
|
||||
{ name: "背景.jpg", type: "image/jpeg" },
|
||||
{ name: "archive.zip", type: "application/zip" },
|
||||
{ name: "动画.gif", type: "image/gif" },
|
||||
{ name: "数据.json", type: "application/json" },
|
||||
{ name: "矢量.svg", type: "image/svg+xml" },
|
||||
];
|
||||
|
||||
const picked = pickImageFiles(mixed);
|
||||
assert.deepEqual(
|
||||
picked.map((f) => f.name),
|
||||
["封面主图.png", "背景.jpg", "动画.gif", "矢量.svg"],
|
||||
"只挑出图片,且相对顺序与拖入时一致",
|
||||
);
|
||||
|
||||
console.log("✓ 混合文件:4 张图片被挑出,txt/zip/json 被剔除,顺序不变");
|
||||
|
||||
/* ---------------- 2. 边界:没有图片 / 空 type / 空数组 ---------------- */
|
||||
|
||||
assert.equal(pickImageFiles([]).length, 0, "空拖放 → 空结果");
|
||||
|
||||
const noImage: FakeFile[] = [
|
||||
{ name: "笔记.txt", type: "text/plain" },
|
||||
{ name: "无扩展名", type: "" }, // 系统认不出 mime 的也当非图片
|
||||
{ name: "包.zip", type: "application/zip" },
|
||||
];
|
||||
assert.equal(pickImageFiles(noImage).length, 0, "不含图片时返回空数组,不报错");
|
||||
|
||||
const typeLess: FakeFile[] = [{ name: "a.webp", type: "image/webp" }, { name: "b", type: "" }];
|
||||
assert.deepEqual(
|
||||
pickImageFiles(typeLess).map((f) => f.name),
|
||||
["a.webp"],
|
||||
"type 为空的文件不进(不猜后缀)",
|
||||
);
|
||||
|
||||
/* ---------------- 3. 全是图片:原样全收,顺序不变 ---------------- */
|
||||
|
||||
const allImage: FakeFile[] = [
|
||||
{ name: "1.webp", type: "image/webp" },
|
||||
{ name: "2.png", type: "image/png" },
|
||||
{ name: "3.bmp", type: "image/bmp" },
|
||||
];
|
||||
assert.deepEqual(pickImageFiles(allImage), allImage, "全是图片时顺序原样返回");
|
||||
|
||||
console.log("✓ 边界:空 / 全非图片 / 空 type / 全图片,四种情况取值都正确");
|
||||
|
||||
console.log("\n全部 Node 侧自验通过 ✅");
|
||||
+16
-2
@@ -3,10 +3,24 @@ import type { BackgroundAsset } from "../types";
|
||||
/**
|
||||
* 本地上传:本地文件 → object URL。
|
||||
*
|
||||
* object URL 的生命周期由调用方(SourcePanel)管理:换新背景时对旧的
|
||||
* `releaseObjectUrl`,组件卸载时兜底释放一次,避免每次换图都漏一个 blob。
|
||||
* object URL 的生命周期由调用方(SourcePanel)管理。自 T10 起拖入的图片
|
||||
* 是**加图层**而不是替换背景:一个会话里可能有多张存活的 blob URL
|
||||
* (撤销 / 模板重载时要重新取图,不能提前 revoke),调用方负责在组件
|
||||
* 卸载时统一释放,避免每拖一张都漏一个 blob。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 拖放/多选的取值逻辑:**只挑图片,保持原顺序**。
|
||||
*
|
||||
* 参数用最小结构 `{ type }` 而不是 DOM 的 `File`,是为了能在 Node 侧直接
|
||||
* 断言(见 `src/sources/selfcheck.ts`)——取值只依赖 mime,不碰文件内容。
|
||||
* 拖放进来的一批文件可能是图片、文档、压缩包的混合,非图片直接忽略,
|
||||
* 别让一张 txt 把整批拖放打断。
|
||||
*/
|
||||
export function pickImageFiles<T extends { type: string }>(files: readonly T[]): T[] {
|
||||
return files.filter((f) => f.type.startsWith("image/"));
|
||||
}
|
||||
|
||||
/** 把本地图片文件变成 BackgroundAsset。非图片/解不出尺寸会抛错并释放 URL。 */
|
||||
export async function fromUpload(file: File): Promise<BackgroundAsset> {
|
||||
const src = URL.createObjectURL(file);
|
||||
|
||||
Reference in New Issue
Block a user