/** * 剪贴板图片提取的公用件,供「新建工单」与 profile 值编辑器共享。 * * 刻意拆成「同步探测」+「异步读取」两步:粘贴的默认行为(把二进制当乱码文本插入) * 必须在事件处理里同步 `preventDefault` 才拦得住,而读文件是异步的。所以调用方先用 * `getPastedImageFiles` 同步拿到图片文件、立刻 preventDefault,再 await `readClipboardImages`。 */ export interface PastedImage { /** e.g. "image/png" */ mediaType: string /** 裸 base64(不含 `data:...;base64,` 前缀) */ base64: string /** `data:...;base64,...` 形式,供 `` 直接预览 */ previewDataUrl: string } /** 同步从粘贴事件里挑出所有 image/* 文件;无图返回空数组(调用方据此决定是否 preventDefault)。 */ export function getPastedImageFiles(e: React.ClipboardEvent): File[] { const items = e.clipboardData?.items if (!items) return [] const files: File[] = [] for (const item of items) { if (item.kind === 'file' && item.type.startsWith('image/')) { const f = item.getAsFile() if (f) files.push(f) } } return files } /** 把单个图片文件读成 base64 + data URL;读失败回 null。 */ export function readClipboardImage(file: File): Promise { return new Promise((resolve) => { const reader = new FileReader() reader.onload = () => { const result = reader.result if (typeof result !== 'string') { resolve(null) return } // result 形如 "data:image/png;base64,iVBORw0K..." const commaIdx = result.indexOf(',') const header = commaIdx > 0 ? result.slice(0, commaIdx) : '' const base64 = commaIdx > 0 ? result.slice(commaIdx + 1) : '' const mediaMatch = header.match(/^data:([^;]+);base64$/) const mediaType = mediaMatch ? mediaMatch[1] : file.type || 'image/png' resolve({ mediaType, base64, previewDataUrl: result }) } reader.onerror = () => resolve(null) reader.readAsDataURL(file) }) } /** 批量读取,丢弃读失败的项。 */ export function readClipboardImages(files: File[]): Promise { return Promise.all(files.map(readClipboardImage)).then( arr => arr.filter((p): p is PastedImage => p !== null), ) }