From 59ad1106deb728ba1f0c7e8fac8029c3568ff08d Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 16 Jul 2026 17:30:06 +0330 Subject: [PATCH 1/2] fix bug 4 --- .../ProductUploader/IndexImageUploader.vue | 47 +++--- .../ProductUploader/ProductImageUploader.vue | 20 +-- src/components/UploadPic.vue | 14 +- .../banners/components/BannerImageUpload.vue | 10 +- .../base/components/ProductEditorFormBody.vue | 6 +- src/utils/prepareImageForUpload.ts | 134 ++++++++++++++++++ 6 files changed, 189 insertions(+), 42 deletions(-) create mode 100644 src/utils/prepareImageForUpload.ts diff --git a/src/components/ProductUploader/IndexImageUploader.vue b/src/components/ProductUploader/IndexImageUploader.vue index d4ff0f6..3f3e32b 100644 --- a/src/components/ProductUploader/IndexImageUploader.vue +++ b/src/components/ProductUploader/IndexImageUploader.vue @@ -31,7 +31,7 @@ @@ -84,13 +84,14 @@ diff --git a/src/utils/prepareImageForUpload.ts b/src/utils/prepareImageForUpload.ts index 440be42..dd5190e 100644 --- a/src/utils/prepareImageForUpload.ts +++ b/src/utils/prepareImageForUpload.ts @@ -1,13 +1,18 @@ +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(/\.[^.]+$/u, '') || 'image' + return name.replace(/\.(png|jpe?g|webp|gif|bmp|heic|heif)+$/iu, '') || 'image' } function loadImageElement(file: File): Promise { @@ -36,10 +41,7 @@ function canvasToBlob( }) } -function drawScaled( - img: HTMLImageElement, - maxDimension: number -): HTMLCanvasElement { +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)) @@ -78,7 +80,7 @@ async function encodeWebpUnderLimit( /** * تصویر را به WebP تبدیل می‌کند (با کاهش کیفیت/ابعاد در صورت نیاز) - * و برای آپلود آماده می‌کند. اگر ورودی از قبل WebP و زیر سقف باشد، همان را برمی‌گرداند. + * و برای آپلود آماده می‌کند. خروجی همیشه ≤ maxBytes است. */ export async function prepareImageForUpload( file: File, @@ -92,8 +94,8 @@ export async function prepareImageForUpload( throw new Error('فقط فایل تصویری مجاز است') } - if (file.size > maxBytes) { - throw new Error('حداکثر حجم بارگذاری ۱ مگابایت می‌باشد') + if (file.size > MAX_SOURCE_IMAGE_BYTES) { + throw new Error('حجم فایل انتخابی بیش از حد مجاز است (حداکثر ۲۵ مگابایت)') } if (file.type === 'image/webp' && file.size <= maxBytes) { @@ -110,25 +112,127 @@ export async function prepareImageForUpload( }) } -/** برای beforeUpload آنت‌دیزاین: تبدیل به WebP یا رد آپلود */ -export async function beforeUploadConvertToWebp(file: File) { - try { - return await prepareImageForUpload(file) - } catch (error) { - const msg = error instanceof Error ? error.message : 'خطا در آماده‌سازی تصویر' - return Promise.reject(msg) +/** اعتبارسنجی سریع قبل از 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 } } -/** نسخهٔ مناسب ant-design Upload با پیام خطا و LIST_IGNORE */ -export function createWebpBeforeUpload(messageError: (msg: string) => void) { - return async (file: File) => { - try { - return await prepareImageForUpload(file) - } catch (error) { - const msg = error instanceof Error ? error.message : 'خطا در آماده‌سازی تصویر' - messageError(msg) - return Upload.LIST_IGNORE - } +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) +}