/** * 预览类型判定(纯函数,无副作用,便于单测与复用)。 * *
为什么集中在一个文件:前端多处 `FileList` 预览都按「后缀 → 渲染方式」分流, * 统一这里避免每个页面各写一套正则 / 后缀表。后端同源预览端点(`/web/workOrderDraft/preview/{ossId}`) * 按后缀纠正 Content-Type,这里与之后端口径保持一致的归类。
*/ export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio' | 'text' | 'unsupported' const IMAGE_SUFFIXES = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'] const PDF_SUFFIXES = ['pdf'] const VIDEO_SUFFIXES = ['mp4', 'webm', 'mov', 'avi', 'm4v'] const AUDIO_SUFFIXES = ['mp3', 'wav', 'ogg', 'm4a'] // 文本类:含 html/htm/svg —— 统一按纯文本展示源码,避免内联渲染触发存储型 XSS const TEXT_SUFFIXES = ['txt', 'sql', 'log', 'csv', 'json', 'xml', 'md', 'yml', 'yaml', 'html', 'htm', 'svg'] /** 取小写字尾(不含点);无点返回空串 */ export function getSuffix(name: string): string { const idx = name.lastIndexOf('.') return idx >= 0 ? name.slice(idx + 1).toLowerCase() : '' } /** 按文件名判定预览类型;office / 未知后缀一律 unsupported(前端不内联,给下载) */ export function resolvePreviewKind(name: string): PreviewKind { const ext = getSuffix(name) if (IMAGE_SUFFIXES.includes(ext)) return 'image' if (PDF_SUFFIXES.includes(ext)) return 'pdf' if (VIDEO_SUFFIXES.includes(ext)) return 'video' if (AUDIO_SUFFIXES.includes(ext)) return 'audio' if (TEXT_SUFFIXES.includes(ext)) return 'text' return 'unsupported' } /** 是否可被前端内联预览(true = image/pdf/video/audio/text) */ export function isPreviewable(name: string): boolean { return resolvePreviewKind(name) !== 'unsupported' } /** * 文本内联预览上限(1MB)。 * 整文件塞进 `` 会卡死浏览器(如 5MB+ 的 .sql/.log),故只读取前 1MB 渲染,超出截断并提示下载。
*/
export const PREVIEW_TEXT_MAX_BYTES = 1024 * 1024
/** 人类可读体积(B/KB/MB/GB) */
export function formatBytes(bytes: number): string {
if (!bytes || bytes < 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)))
const val = bytes / Math.pow(1024, i)
return `${i === 0 ? val : val.toFixed(1)} ${units[i]}`
}