refactor: 背景图层接进主线
- 预设底纹统一走 sources/geometry 的 GEOMETRY_PRESETS(16 条), BackgroundMenu 不再自带一份 12 条的定义。 - 删掉 SourcePanel.vue(内容已搬进 BackgroundMenu),连带清掉 unsplash 里 只为它保留的 legacy 函数面和 index 的对应出口。 - PhotoProviderError 从 unsplash.ts 挪到独立的 errors.ts。原来放在叶子模块里 是为了绕开和 providers.ts 的循环依赖,独立文件同样绕得开而且不别扭。 - 背景改成和普通图层一样可点选可拖动:用户对背景的核心需求是「调整/缩放/裁剪 一下」,不可点选就没法在画布上取景;取消选中点画板外的灰底即可。 - useLayers 把背景钉在最底:它拖不上去,别的图层也压不到它下面。
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 图库调用失败(没配 key、网络错误、被限流)。界面据此显示一句话,不让路径崩掉。
|
||||
*
|
||||
* 单独一个文件而不是塞进 `providers.ts`:两家 provider 都要抛它,而
|
||||
* `providers.ts` 又要 import 两家来组注册表,定义在那里就成了循环依赖——
|
||||
* ESM 的环里谁先被加载谁踩 TDZ,症状是整页白屏。放在这个不依赖任何兄弟模块的
|
||||
* 叶子里,环从根上不成立。
|
||||
*/
|
||||
export class PhotoProviderError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "PhotoProviderError";
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@
|
||||
* - pexels.ts / unsplash.ts 图库搜索,都实现 `PhotoProvider` 接口,
|
||||
* Pexels 在前(`PHOTO_PROVIDERS` 见 providers.ts)
|
||||
*
|
||||
* T4 时代的 legacy 函数面(`searchUnsplash` / `toAsset` 等)保留到旧
|
||||
* SourcePanel 被 t13 清掉为止;新界面一律走 `PHOTO_PROVIDERS`。
|
||||
* 图库一律走 `PHOTO_PROVIDERS`,不要直接 import 某一家。
|
||||
*/
|
||||
|
||||
export { fromUpload, releaseObjectUrl, pickImageFiles } from "./upload";
|
||||
@@ -36,14 +35,9 @@ export type {
|
||||
} from "./geometry";
|
||||
|
||||
export {
|
||||
unsplashStatus,
|
||||
searchUnsplash,
|
||||
trackDownload,
|
||||
toAsset,
|
||||
parseUnsplashResponse,
|
||||
unsplashProvider,
|
||||
} from "./unsplash";
|
||||
export type { UnsplashStatus, UnsplashPhoto } from "./unsplash";
|
||||
|
||||
export { parsePexelsResponse, pexelsProvider } from "./pexels";
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import type {
|
||||
PhotoProviderStatus,
|
||||
StockPhoto,
|
||||
} from "../types";
|
||||
import { PhotoProviderError } from "./unsplash";
|
||||
import { PhotoProviderError } from "./errors";
|
||||
|
||||
/**
|
||||
* Pexels 搜索。与 unsplash.ts 一样归一到 `PhotoProvider` 接口。
|
||||
*
|
||||
* - API Key 从 `import.meta.env.VITE_PEXELS_KEY` 读(请求头 `Authorization: <key>`,
|
||||
* 没有 "Bearer " 前缀;Unsplash 才是 `Client-ID` 前缀)。
|
||||
* - 共享错误类 `PhotoProviderError` 定义在 unsplash.ts 那个不依赖兄弟模块的叶子里,
|
||||
* - 共享错误类 `PhotoProviderError` 在 `errors.ts`(不依赖任何兄弟模块的叶子),
|
||||
* 从那里 import,避免和 providers.ts 的注册表互相引成环。
|
||||
* - **没配 key 时不崩**:静默处于 "missing" 态;`search()` 抛 `PhotoProviderError`。
|
||||
* - 速率配额比 Unsplash demo key 高得多(200/时 vs 50/时),dev 期主用这家,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { unsplashProvider } from "./unsplash";
|
||||
|
||||
// 共享错误类定义在 unsplash.ts(依赖最少的叶子模块),这里 re-export 保持桩文件
|
||||
// 原本的出口路径;pexels.ts 也直接 import 它,避免 providers ↔ 各家模块引成环。
|
||||
export { PhotoProviderError } from "./unsplash";
|
||||
export { PhotoProviderError } from "./errors";
|
||||
|
||||
/**
|
||||
* 图库注册表。
|
||||
|
||||
@@ -21,9 +21,7 @@ import { pickImageFiles } from "./upload";
|
||||
import { parsePexelsResponse, pexelsProvider } from "./pexels";
|
||||
import {
|
||||
parseUnsplashResponse,
|
||||
searchUnsplash,
|
||||
unsplashProvider,
|
||||
unsplashStatus,
|
||||
} from "./unsplash";
|
||||
import { PHOTO_PROVIDERS, PhotoProviderError, providerById } from "./providers";
|
||||
import {
|
||||
@@ -264,13 +262,6 @@ for (const p of PHOTO_PROVIDERS) {
|
||||
`${p.id}.search() 没配 key 时抛 PhotoProviderError,而不是别的`,
|
||||
);
|
||||
}
|
||||
assert.equal(unsplashStatus(), "missing", "legacy unsplashStatus 同步读同一个 key");
|
||||
await assert.rejects(
|
||||
searchUnsplash("forest"),
|
||||
(err) => err instanceof PhotoProviderError,
|
||||
"legacy searchUnsplash 也走同一错误类(老面板显示 message 用)",
|
||||
);
|
||||
|
||||
console.log("✓ 注册表与缺失态:Pexels 在前,没 key 时两家 status=missing、search 抛 PhotoProviderError");
|
||||
|
||||
/* ---------------- 4. T14:几何预设的数量与白字对比度 ---------------- */
|
||||
|
||||
+4
-107
@@ -2,16 +2,13 @@ import type {
|
||||
Attribution,
|
||||
BackgroundAsset,
|
||||
PhotoProvider,
|
||||
PhotoProviderStatus,
|
||||
StockPhoto,
|
||||
} from "../types";
|
||||
import { PhotoProviderError } from "./errors";
|
||||
|
||||
/**
|
||||
* Unsplash 搜索。T14 起归一到 `PhotoProvider` 接口(界面只认接口,不写
|
||||
* `if (provider === "unsplash")`),同时**保留 T4 时代的 legacy 函数面**
|
||||
* (`searchUnsplash` / `unsplashStatus` / `toAsset` / `trackDownload` /
|
||||
* `UnsplashPhoto`):基座里的旧 SourcePanel 还直接 import 它们,删了会砸掉
|
||||
* 这一轮的 typecheck;等 t13 把旧面板清掉后这些只是没人用的历史出口。
|
||||
* Unsplash 搜索,归一到 `PhotoProvider` 接口——界面只认接口,不写
|
||||
* `if (provider === "unsplash")`。
|
||||
*
|
||||
* - Access Key 从 `import.meta.env.VITE_UNSPLASH_KEY` 读,用户放在 `.env.local`。
|
||||
* - **没配 key 时不崩**:模块静默处于 "missing" 态,面板据状态只显示提示,
|
||||
@@ -21,84 +18,6 @@ import type {
|
||||
* - 用户选定一张图时按 API 条款触发 download 统计(fire-and-forget)。
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ 共享错误 */
|
||||
|
||||
/**
|
||||
* 图库调用失败(没配 key、网络错误、被限流)。界面据此显示一句话。
|
||||
*
|
||||
* 故意定义在这个**不依赖任何兄弟模块**的叶子里:`pexels.ts` 从这 import、
|
||||
* `providers.ts` 从这里 re-export。若定义在 providers.ts 而让两家 provider
|
||||
* 反过去 import 它,就会和 providers 的注册表互相 import 成环——ESM 环里
|
||||
* 谁先被加载谁就踩 TDZ(旧 SourcePanel 只 import unsplash 时会先踩到)。
|
||||
*/
|
||||
export class PhotoProviderError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "PhotoProviderError";
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ legacy 类型 */
|
||||
|
||||
export type UnsplashStatus = PhotoProviderStatus;
|
||||
|
||||
/** 一张搜索结果,带网格展示与装进画布所需的全部信息(T4 时代的形状)。 */
|
||||
export interface UnsplashPhoto {
|
||||
id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
description: string | null;
|
||||
altDescription: string | null;
|
||||
authorName: string;
|
||||
thumbUrl: string;
|
||||
/** 装进画布用的尺寸(regular,约 1080px)。 */
|
||||
regularUrl: string;
|
||||
/** 原图页(带 utm_source,署名链接用)。 */
|
||||
pageUrl: string;
|
||||
/** API 要求的 download_location,选定该图时触发。 */
|
||||
downloadLocation: string;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ 状态 */
|
||||
|
||||
/** 当前是否配了 key。env 在 Vite 里必然有;Node/Bun/构建产物里可能没有,一律当缺失。 */
|
||||
export function unsplashStatus(): UnsplashStatus {
|
||||
return readKey() ? "ready" : "missing";
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ legacy 导出(T4 SourcePanel 还在用) */
|
||||
|
||||
/** legacy:一条 Unsplash 结果 → 一份可进画布的 BackgroundAsset。 */
|
||||
export function toAsset(photo: UnsplashPhoto): BackgroundAsset {
|
||||
const attribution: Attribution = {
|
||||
text: `Photo by ${photo.authorName} on Unsplash`,
|
||||
url: photo.pageUrl,
|
||||
};
|
||||
return {
|
||||
kind: "stock",
|
||||
src: photo.regularUrl,
|
||||
width: photo.width,
|
||||
height: photo.height,
|
||||
attribution,
|
||||
};
|
||||
}
|
||||
|
||||
/** legacy:单页搜索(不带分页参数),结果仍是 UnsplashPhoto。新界面走 provider。 */
|
||||
export async function searchUnsplash(query: string): Promise<UnsplashPhoto[]> {
|
||||
const raws = await fetchRawUnsplash(query, 1, 12);
|
||||
return raws.map(toLegacyPhoto);
|
||||
}
|
||||
|
||||
/** legacy:用户选定一张图时触发 download 统计,条款义务。失败静默。 */
|
||||
export function trackDownload(photo: UnsplashPhoto): void {
|
||||
const key = readKey();
|
||||
if (!key || !photo.downloadLocation) return;
|
||||
const sep = photo.downloadLocation.includes("?") ? "&" : "?";
|
||||
fetch(`${photo.downloadLocation}${sep}client_id=${encodeURIComponent(key)}`).catch(() => {
|
||||
/* download 统计失败不影响用户操作 */
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ PhotoProvider */
|
||||
|
||||
/** 纯函数解析:官方响应样本 → 归一化 StockPhoto[]。selfcheck 在 Node 里喂样本断言。 */
|
||||
@@ -114,7 +33,7 @@ export const unsplashProvider: PhotoProvider = {
|
||||
label: "Unsplash",
|
||||
// 申请 key 的页面,status 为 missing 时界面把它显示出来。
|
||||
keyUrl: "https://unsplash.com/oauth/applications",
|
||||
status: unsplashStatus,
|
||||
status: () => (readKey() ? "ready" : "missing"),
|
||||
search: async (query, page) => stocksFromRaws(await fetchRawUnsplash(query, page, 15)),
|
||||
toAsset: toStockAsset,
|
||||
/** 条款义务:用户选定一张图时打一次 download 统计。只有 Unsplash 有。 */
|
||||
@@ -180,10 +99,6 @@ function authorNameOf(raw: UnsplashRawPhoto): string {
|
||||
return raw.user?.name?.trim() || "Unsplash 用户";
|
||||
}
|
||||
|
||||
function pageUrlOf(raw: UnsplashRawPhoto): string {
|
||||
return raw.links?.html ? `${raw.links.html}?${UTM}` : `https://unsplash.com/?${UTM}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* raw → StockPhoto(provider / parseUnsplashResponse 共用)。
|
||||
* 空字段太多、连缩略图和原图页都没有的结果没法用,返回 null 让上层跳过。
|
||||
@@ -212,24 +127,6 @@ function toStockPhoto(raw: UnsplashRawPhoto): StockPhoto | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** raw → legacy UnsplashPhoto(保持 T4 语义原样,别和 StockPhoto 解析互相牵连)。 */
|
||||
function toLegacyPhoto(raw: UnsplashRawPhoto): UnsplashPhoto {
|
||||
const authorName = authorNameOf(raw);
|
||||
const pageUrl = pageUrlOf(raw);
|
||||
return {
|
||||
id: raw.id ?? "",
|
||||
width: raw.width ?? 0,
|
||||
height: raw.height ?? 0,
|
||||
description: raw.description ?? null,
|
||||
altDescription: raw.alt_description ?? null,
|
||||
authorName,
|
||||
thumbUrl: raw.urls?.small || raw.urls?.thumb || raw.urls?.regular || "",
|
||||
regularUrl: raw.urls?.regular || raw.urls?.raw || "",
|
||||
pageUrl,
|
||||
downloadLocation: raw.links?.download_location ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/** raw 数组 → StockPhoto[](provider.search 用;parse 系列只认整棵官方响应)。 */
|
||||
function stocksFromRaws(raws: readonly UnsplashRawPhoto[]): StockPhoto[] {
|
||||
const out: StockPhoto[] = [];
|
||||
|
||||
Reference in New Issue
Block a user