73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
|
|
import { newId } from "./id";
|
|||
|
|
|
|||
|
|
/** 編輯器內附圖(本地選檔或 mock 產圖) */
|
|||
|
|
export type AttachedImage = {
|
|||
|
|
id: string;
|
|||
|
|
url: string;
|
|||
|
|
name?: string;
|
|||
|
|
/** 產圖 prompt;選檔可無 */
|
|||
|
|
prompt?: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const MAX_ATTACHED_IMAGES = 10;
|
|||
|
|
|
|||
|
|
/** 單檔上限(讀成 data URL 前) */
|
|||
|
|
const MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|||
|
|
|
|||
|
|
export function withAttachedImageNote(text: string, count: number): string {
|
|||
|
|
const body = text.trim();
|
|||
|
|
if (!count || count <= 0) return body;
|
|||
|
|
if (/(附圖\s*\d+\s*張)/.test(body)) return body;
|
|||
|
|
return `${body}\n\n(附圖 ${count} 張)`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readFileAsDataUrl(file: File): Promise<string> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = () => resolve(String(reader.result || ""));
|
|||
|
|
reader.onerror = () => reject(new Error(`讀取失敗:${file.name}`));
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 把本機圖檔讀成可預覽的 data URL。
|
|||
|
|
* mock 階段不真上傳;送出時 repo 只記張數/可選縮圖。
|
|||
|
|
*/
|
|||
|
|
export async function filesToAttachedImages(
|
|||
|
|
files: FileList | File[],
|
|||
|
|
opts?: { max?: number; existingCount?: number },
|
|||
|
|
): Promise<AttachedImage[]> {
|
|||
|
|
const max = opts?.max ?? MAX_ATTACHED_IMAGES;
|
|||
|
|
const existing = opts?.existingCount ?? 0;
|
|||
|
|
const room = Math.max(0, max - existing);
|
|||
|
|
const list = Array.from(files).filter((f) => f.type.startsWith("image/"));
|
|||
|
|
if (list.length === 0) throw new Error("請選擇圖片檔");
|
|||
|
|
const take = list.slice(0, room);
|
|||
|
|
if (take.length === 0) throw new Error(`最多附 ${max} 張圖`);
|
|||
|
|
|
|||
|
|
const out: AttachedImage[] = [];
|
|||
|
|
for (const file of take) {
|
|||
|
|
if (file.size > MAX_FILE_BYTES) {
|
|||
|
|
throw new Error(`${file.name} 超過 8MB`);
|
|||
|
|
}
|
|||
|
|
const url = await readFileAsDataUrl(file);
|
|||
|
|
if (!url.startsWith("data:image")) throw new Error(`無法讀取:${file.name}`);
|
|||
|
|
out.push({
|
|||
|
|
id: newId("img"),
|
|||
|
|
url,
|
|||
|
|
name: file.name,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 大 data URL 不寫進 localStorage;改成可顯示的 mock 縮圖 seed */
|
|||
|
|
export function toPersistableImageUrl(img: AttachedImage): string {
|
|||
|
|
if (img.url.startsWith("data:") && img.url.length > 1800) {
|
|||
|
|
const seed = encodeURIComponent((img.name || img.id || "attach").slice(0, 40));
|
|||
|
|
return `https://api.dicebear.com/9.x/shapes/svg?seed=${seed}&backgroundColor=c0aede,ffd5dc,b6e3f4`;
|
|||
|
|
}
|
|||
|
|
return img.url;
|
|||
|
|
}
|