fix bug 4
This commit is contained in:
@@ -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<HTMLImageElement> {
|
||||
@@ -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<NonNullable<UploadProps['customRequest']>>[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<string, string>
|
||||
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<void>((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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user