import type { UploadProps } from 'ant-design-vue' import { Upload } from 'ant-design-vue' /** حداکثر حجم فایل خروجی آپلود (۱ مگابایت) */ export const MAX_UPLOAD_IMAGE_BYTES = 1024 * 1024 /** حداکثر حجم فایل ورودی قبل از فشرده‌سازی (۲۵ مگابایت) */ export const MAX_SOURCE_IMAGE_BYTES = 25 * 1024 * 1024 const MAX_DIMENSION = 1920 const QUALITY_STEPS = [0.86, 0.78, 0.7, 0.62, 0.54, 0.46] const UPLOAD_TIMEOUT_MS = 60_000 function stripExtension(name: string): string { return name.replace(/\.(png|jpe?g|webp|gif|bmp|heic|heif)+$/iu, '') || 'image' } function loadImageElement(file: File): Promise { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file) const img = new Image() img.onload = () => { URL.revokeObjectURL(url) resolve(img) } img.onerror = () => { URL.revokeObjectURL(url) reject(new Error('خواندن تصویر ناموفق بود')) } img.src = url }) } function canvasToBlob( canvas: HTMLCanvasElement, type: string, quality: number ): Promise { return new Promise((resolve) => { canvas.toBlob((blob) => resolve(blob), type, quality) }) } function drawScaled(img: HTMLImageElement, maxDimension: number): HTMLCanvasElement { const scale = Math.min(1, maxDimension / Math.max(img.naturalWidth, img.naturalHeight)) const width = Math.max(1, Math.round(img.naturalWidth * scale)) const height = Math.max(1, Math.round(img.naturalHeight * scale)) const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') if (!ctx) { throw new Error('Canvas در دسترس نیست') } ctx.drawImage(img, 0, 0, width, height) return canvas } async function encodeWebpUnderLimit( img: HTMLImageElement, maxBytes: number ): Promise { let maxDimension = MAX_DIMENSION for (let resizePass = 0; resizePass < 4; resizePass += 1) { const canvas = drawScaled(img, maxDimension) for (const quality of QUALITY_STEPS) { const blob = await canvasToBlob(canvas, 'image/webp', quality) if (blob && blob.size > 0 && blob.size <= maxBytes) { return blob } } maxDimension = Math.round(maxDimension * 0.75) } throw new Error('پس از تبدیل به WebP حجم فایل بیش از حد مجاز ماند') } /** * تصویر را به WebP تبدیل می‌کند (با کاهش کیفیت/ابعاد در صورت نیاز) * و برای آپلود آماده می‌کند. خروجی همیشه ≤ maxBytes است. */ export async function prepareImageForUpload( file: File, maxBytes: number = MAX_UPLOAD_IMAGE_BYTES ): Promise { if (!file || file.size < 1) { throw new Error('فایل خالی است') } if (!file.type.startsWith('image/')) { throw new Error('فقط فایل تصویری مجاز است') } if (file.size > MAX_SOURCE_IMAGE_BYTES) { throw new Error('حجم فایل انتخابی بیش از حد مجاز است (حداکثر ۲۵ مگابایت)') } if (file.type === 'image/webp' && file.size <= maxBytes) { return file } const img = await loadImageElement(file) const blob = await encodeWebpUnderLimit(img, maxBytes) const baseName = stripExtension(file.name) return new File([blob], `${baseName}.webp`, { type: 'image/webp', lastModified: Date.now() }) } /** اعتبارسنجی سریع قبل از customRequest */ export function createImageBeforeUpload(messageError: (msg: string) => void) { return (file: File) => { if (!file?.type?.startsWith('image/')) { messageError('فقط فایل تصویری مجاز است') return Upload.LIST_IGNORE } if (file.size > MAX_SOURCE_IMAGE_BYTES) { messageError('حجم فایل انتخابی بیش از حد مجاز است (حداکثر ۲۵ مگابایت)') return Upload.LIST_IGNORE } return true } } type CustomRequestOption = Parameters>[0] function parseXhrBody(xhr: XMLHttpRequest): unknown { const text = xhr.responseText if (!text) return xhr.response ?? null try { return JSON.parse(text) } catch { return text } } /** * تبدیل به WebP + آپلود با XHR و تایم‌اوت — جایگزین action پیش‌فرض آنت‌دیزاین. * * `onUploaded` / `onFailed` مستقل از وضعیت داخلی ant-design-vue صدا زده می‌شوند؛ * با fileList کنترل‌شده، گاهی `onSuccess` آنت به‌خاطر getFileItem ساکت می‌ماند * و UI روی «در حال بارگذاری» گیر می‌کند. */ export function createImageCustomRequest(options: { action: string | (() => string) getHeaders: () => Record messageError: (msg: string) => void timeoutMs?: number onStarted?: () => void onUploaded?: (body: unknown, meta: { uid?: string }) => void onFailed?: (error: Error, meta: { uid?: string }) => void }): UploadProps['customRequest'] { return (opt: CustomRequestOption) => { const { file, onSuccess, onError, onProgress } = opt const action = typeof options.action === 'function' ? options.action() : options.action const timeoutMs = options.timeoutMs ?? UPLOAD_TIMEOUT_MS const uid = (file as File & { uid?: string })?.uid options.onStarted?.() void (async () => { try { const prepared = await prepareImageForUpload(file as File) const formData = new FormData() formData.append('file', prepared) await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open('POST', action) xhr.timeout = timeoutMs // text + JSON.parse دستی؛ responseType=json گاهی body را null می‌کند const headers = options.getHeaders() Object.entries(headers).forEach(([key, value]) => { if (value) { xhr.setRequestHeader(key, value) } }) xhr.upload.onprogress = (event) => { if (event.lengthComputable) { onProgress?.({ ...event, percent: Math.round((event.loaded / event.total) * 100) }) } } xhr.onload = () => { const body = parseXhrBody(xhr) if (xhr.status >= 200 && xhr.status < 300) { try { onSuccess?.(body, xhr) } catch { // ant ممکن است به‌خاطر fileList کنترل‌شده throw/no-op کند } options.onUploaded?.(body, { uid }) resolve() return } const msg = (body && typeof body === 'object' && 'message' in body && String((body as { message: unknown }).message)) || `خطای سرور (${xhr.status})` reject(new Error(msg)) } xhr.onerror = () => reject(new Error('خطای شبکه در آپلود')) xhr.ontimeout = () => reject(new Error('زمان آپلود به پایان رسید — لطفاً دوباره تلاش کنید')) xhr.onabort = () => reject(new Error('آپلود لغو شد')) xhr.send(formData) }) } catch (error) { const err = error instanceof Error ? error : new Error('خطا در آماده‌سازی تصویر') options.messageError(err.message) try { onError?.(err) } catch { // ignore ant internal errors } options.onFailed?.(err, { uid }) } })() } } /** @deprecated از createImageBeforeUpload + createImageCustomRequest استفاده کنید */ export function createWebpBeforeUpload(messageError: (msg: string) => void) { return createImageBeforeUpload(messageError) }