fix bug 4
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@update:file-list="onFileListUpdate"
|
@update:file-list="onFileListUpdate"
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
:file-list="fileList"
|
:file-list="fileList"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@update:file-list="onFileListUpdate"
|
@update:file-list="onFileListUpdate"
|
||||||
@@ -91,7 +93,10 @@ import { uploadURL } from '@root/config'
|
|||||||
import { getToken } from '@/utils'
|
import { getToken } from '@/utils'
|
||||||
import { fixFileCapture } from '@/utils/fixFileCapture'
|
import { fixFileCapture } from '@/utils/fixFileCapture'
|
||||||
import { normalizeUploadFileResponse } from '@/utils/file'
|
import { normalizeUploadFileResponse } from '@/utils/file'
|
||||||
import { createWebpBeforeUpload } from '@/utils/prepareImageForUpload'
|
import {
|
||||||
|
createImageBeforeUpload,
|
||||||
|
createImageCustomRequest
|
||||||
|
} from '@/utils/prepareImageForUpload'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const { prefixCls } = usePrefix('index-image-uploader')
|
const { prefixCls } = usePrefix('index-image-uploader')
|
||||||
@@ -100,9 +105,9 @@ const previewImage = ref('')
|
|||||||
const previewTitle = ref('تصویر شاخص')
|
const previewTitle = ref('تصویر شاخص')
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const fileHeader = {
|
const fileHeader = computed(() => ({
|
||||||
Authorization: `Bearer ${getToken()}`
|
Authorization: `Bearer ${getToken()}`
|
||||||
}
|
}))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fileList?: Array<FileList>
|
fileList?: Array<FileList>
|
||||||
@@ -151,56 +156,58 @@ function removeImage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
const beforeUpload: UploadProps['beforeUpload'] = createImageBeforeUpload((msg) =>
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function applyUploadedIndexImage(body: unknown) {
|
||||||
|
loading.value = false
|
||||||
|
const fileResource = normalizeUploadFileResponse(body)
|
||||||
|
|
||||||
|
if (!fileResource?.id || !fileResource.full_src) {
|
||||||
|
fileList.value = []
|
||||||
|
message.error('آپلود انجام شد ولی آدرس تصویر ساخته نشد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.value = [
|
||||||
|
{
|
||||||
|
uid: String(fileResource.id),
|
||||||
|
name: fileResource.name || 'image',
|
||||||
|
status: 'done',
|
||||||
|
url: fileResource.full_src,
|
||||||
|
thumbUrl: fileResource.full_src,
|
||||||
|
response: body
|
||||||
|
}
|
||||||
|
]
|
||||||
|
message.success('تصویر شاخص با موفقیت بارگذاری شد')
|
||||||
|
emits('ok', fileResource.id, body as Record<string, any>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRequest: UploadProps['customRequest'] = createImageCustomRequest({
|
||||||
|
action: () => uploadURL,
|
||||||
|
getHeaders: () => fileHeader.value,
|
||||||
|
messageError: (msg) => message.error(msg),
|
||||||
|
onStarted: () => {
|
||||||
|
loading.value = true
|
||||||
|
},
|
||||||
|
onUploaded: applyUploadedIndexImage,
|
||||||
|
onFailed: () => {
|
||||||
|
loading.value = false
|
||||||
|
fileList.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function handleChange(info: UploadChangeParam) {
|
function handleChange(info: UploadChangeParam) {
|
||||||
fixFileCapture()
|
fixFileCapture()
|
||||||
|
|
||||||
const status = info.file.status
|
// success/error از onUploaded/onFailed اعمال میشود (fileList کنترلشده گاهی done را میبلعد)
|
||||||
|
if (info.file.status === 'uploading') {
|
||||||
if (status === 'uploading') {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// هر وضعیت غیر از uploading باید اسپینر را ببندد (error/done/removed/undefined)
|
if (info.file.status === 'removed') {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
if (status === 'error') {
|
|
||||||
fileList.value = []
|
|
||||||
const serverMsg =
|
|
||||||
(info.file.response as { message?: string } | undefined)?.message ||
|
|
||||||
(info.file.error as { message?: string } | undefined)?.message
|
|
||||||
message.error(serverMsg || 'خطا در بارگذاری تصویر')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'done') {
|
|
||||||
const fileResource = normalizeUploadFileResponse(info.file.response)
|
|
||||||
|
|
||||||
if (!fileResource?.id || !fileResource.full_src) {
|
|
||||||
fileList.value = []
|
|
||||||
message.error('آپلود انجام شد ولی آدرس تصویر ساخته نشد')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.value = [
|
|
||||||
{
|
|
||||||
...info.file,
|
|
||||||
uid: String(fileResource.id),
|
|
||||||
status: 'done',
|
|
||||||
url: fileResource.full_src,
|
|
||||||
thumbUrl: fileResource.full_src
|
|
||||||
}
|
|
||||||
]
|
|
||||||
message.success('تصویر شاخص با موفقیت بارگذاری شد')
|
|
||||||
emits('ok', fileResource.id, info.file.response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'removed') {
|
|
||||||
emits('delete', info.file.uid)
|
emits('delete', info.file.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
:showUploadList="true"
|
:showUploadList="true"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -32,16 +33,19 @@ import {uploadURL} from '@root/config'
|
|||||||
import {getToken} from '@/utils'
|
import {getToken} from '@/utils'
|
||||||
import {fixFileCapture} from '@/utils/fixFileCapture'
|
import {fixFileCapture} from '@/utils/fixFileCapture'
|
||||||
import {normalizeUploadFileResponse} from '@/utils/file'
|
import {normalizeUploadFileResponse} from '@/utils/file'
|
||||||
import {createWebpBeforeUpload} from '@/utils/prepareImageForUpload'
|
import {
|
||||||
|
createImageBeforeUpload,
|
||||||
|
createImageCustomRequest
|
||||||
|
} from '@/utils/prepareImageForUpload'
|
||||||
import {usePrefix} from '@core/composable/usePrefix'
|
import {usePrefix} from '@core/composable/usePrefix'
|
||||||
import {computed, onMounted, ref} from 'vue'
|
import {computed, onMounted, ref} from 'vue'
|
||||||
|
|
||||||
const {prefixCls} = usePrefix('product-image-uploader')
|
const {prefixCls} = usePrefix('product-image-uploader')
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
|
|
||||||
const fileHeader = {
|
const fileHeader = computed(() => ({
|
||||||
Authorization: `Bearer ${getToken()}`
|
Authorization: `Bearer ${getToken()}`
|
||||||
}
|
}))
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fileList?: Array<FileList>
|
fileList?: Array<FileList>
|
||||||
@@ -60,47 +64,66 @@ const fileList = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
const beforeUpload: UploadProps['beforeUpload'] = createImageBeforeUpload((msg) =>
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function applyUploadedProductImage(body: unknown, meta: { uid?: string }) {
|
||||||
|
loading.value = false
|
||||||
|
const fileResource = normalizeUploadFileResponse(body)
|
||||||
|
|
||||||
|
if (!fileResource?.id || !fileResource.full_src) {
|
||||||
|
if (meta.uid) {
|
||||||
|
fileList.value = fileList.value.filter((item) => item.uid !== meta.uid)
|
||||||
|
}
|
||||||
|
message.error('خطا در ساخت آدرس تصویر')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const doneItem = {
|
||||||
|
uid: String(fileResource.id),
|
||||||
|
name: fileResource.name || 'image',
|
||||||
|
status: 'done' as const,
|
||||||
|
url: fileResource.full_src,
|
||||||
|
thumbUrl: fileResource.full_src,
|
||||||
|
response: body
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = meta.uid && fileList.value.some((item) => item.uid === meta.uid)
|
||||||
|
fileList.value = matched
|
||||||
|
? fileList.value.map((item) => (item.uid === meta.uid ? doneItem : item))
|
||||||
|
: [
|
||||||
|
...fileList.value.filter((item) => item.status !== 'uploading'),
|
||||||
|
doneItem
|
||||||
|
]
|
||||||
|
|
||||||
|
message.success('تصویر با موفقیت بارگذاری شد')
|
||||||
|
emits('ok', fileResource.id, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRequest: UploadProps['customRequest'] = createImageCustomRequest({
|
||||||
|
action: () => uploadURL,
|
||||||
|
getHeaders: () => fileHeader.value,
|
||||||
|
messageError: (msg) => message.error(msg),
|
||||||
|
onStarted: () => {
|
||||||
|
loading.value = true
|
||||||
|
},
|
||||||
|
onUploaded: applyUploadedProductImage,
|
||||||
|
onFailed: (_err, meta) => {
|
||||||
|
loading.value = false
|
||||||
|
if (meta.uid) {
|
||||||
|
fileList.value = fileList.value.filter((item) => item.uid !== meta.uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function handleChange(info: UploadChangeParam) {
|
function handleChange(info: UploadChangeParam) {
|
||||||
fixFileCapture()
|
fixFileCapture()
|
||||||
|
|
||||||
if (info.file.status === 'uploading') {
|
if (info.file.status === 'uploading') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.file.status === 'error') {
|
|
||||||
loading.value = false
|
|
||||||
message.error('خطا در بارگذاری')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.file.status === 'done') {
|
|
||||||
loading.value = false
|
|
||||||
const fileResource = normalizeUploadFileResponse(info.file.response)
|
|
||||||
|
|
||||||
if (!fileResource?.id || !fileResource.full_src) {
|
|
||||||
fileList.value = fileList.value.filter((item) => item.uid !== info.file.uid)
|
|
||||||
message.error('خطا در ساخت آدرس تصویر')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.value = fileList.value.map((item) =>
|
|
||||||
item.uid === info.file.uid
|
|
||||||
? {
|
|
||||||
...item,
|
|
||||||
uid: String(fileResource.id),
|
|
||||||
status: 'done',
|
|
||||||
url: fileResource.full_src,
|
|
||||||
thumbUrl: fileResource.full_src,
|
|
||||||
}
|
|
||||||
: item,
|
|
||||||
)
|
|
||||||
message.success('تصویر با موفقیت بارگذاری شد')
|
|
||||||
emits('ok', fileResource.id, info.file.response)
|
|
||||||
}
|
|
||||||
if (info.file.status === 'removed') {
|
if (info.file.status === 'removed') {
|
||||||
emits('delete', info.file.uid)
|
emits('delete', info.file.uid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
@change="handelChange"
|
@change="handelChange"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -45,7 +46,10 @@ import { message } from 'ant-design-vue'
|
|||||||
import type { UploadProps } from 'ant-design-vue'
|
import type { UploadProps } from 'ant-design-vue'
|
||||||
import { getToken } from '@/utils'
|
import { getToken } from '@/utils'
|
||||||
import { fixFileCapture } from '@/utils/fixFileCapture'
|
import { fixFileCapture } from '@/utils/fixFileCapture'
|
||||||
import { createWebpBeforeUpload } from '@/utils/prepareImageForUpload'
|
import {
|
||||||
|
createImageBeforeUpload,
|
||||||
|
createImageCustomRequest
|
||||||
|
} from '@/utils/prepareImageForUpload'
|
||||||
import { normalizeUploadFileResponse, toAbsoluteFileUrl } from '@/utils/file'
|
import { normalizeUploadFileResponse, toAbsoluteFileUrl } from '@/utils/file'
|
||||||
|
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
@@ -72,37 +76,45 @@ const emit = defineEmits<{
|
|||||||
(e: 'errorUpload'): void
|
(e: 'errorUpload'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
const beforeUpload: UploadProps['beforeUpload'] = createImageBeforeUpload((msg) =>
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
function handelChange({ file, fileList }) {
|
const customRequest: UploadProps['customRequest'] = createImageCustomRequest({
|
||||||
if (Object.hasOwn(file, 'percent')) {
|
action: () => url.value || uploadURL,
|
||||||
|
getHeaders: () => fileHeader.value,
|
||||||
|
messageError: (msg) => message.error(msg),
|
||||||
|
onStarted: () => {
|
||||||
|
loading.value = true
|
||||||
|
percent.value = 0
|
||||||
|
emit('startUpload')
|
||||||
|
},
|
||||||
|
onUploaded: (body) => {
|
||||||
|
const fileResource = normalizeUploadFileResponse(body)
|
||||||
|
const record = body && typeof body === 'object' ? (body as Record<string, any>) : undefined
|
||||||
|
imageUrl.value =
|
||||||
|
fileResource?.full_src ||
|
||||||
|
toAbsoluteFileUrl(record?.full_src || record?.src) ||
|
||||||
|
''
|
||||||
|
loading.value = false
|
||||||
|
percent.value = 100
|
||||||
|
emit('upload', body)
|
||||||
|
message.success('فایل با موفقیت آپلود شد')
|
||||||
|
},
|
||||||
|
onFailed: () => {
|
||||||
|
loading.value = false
|
||||||
|
emit('errorUpload')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handelChange({ file }) {
|
||||||
|
if (Object.hasOwn(file, 'percent') && file.percent != null) {
|
||||||
percent.value = Math.floor(file.percent)
|
percent.value = Math.floor(file.percent)
|
||||||
}
|
}
|
||||||
fixFileCapture()
|
fixFileCapture()
|
||||||
|
|
||||||
if (file.status === 'uploading') {
|
if (file.status === 'uploading') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
emit('startUpload')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.status === 'error') {
|
|
||||||
loading.value = false
|
|
||||||
emit('errorUpload')
|
|
||||||
message.error('خطا در آپلود فایل')
|
|
||||||
}
|
|
||||||
if (file.status == 'done') {
|
|
||||||
const fileResource = normalizeUploadFileResponse(file.response)
|
|
||||||
imageUrl.value =
|
|
||||||
fileResource?.full_src ||
|
|
||||||
toAbsoluteFileUrl(file.response?.full_src || file.response?.src) ||
|
|
||||||
''
|
|
||||||
loading.value = false
|
|
||||||
emit('upload', file.response)
|
|
||||||
message.success('فایل با موفقیت آپلود شد')
|
|
||||||
} else if (fileList.status == 'removed') {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<div class="banner-image-upload__empty">
|
<div class="banner-image-upload__empty">
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<button type="button" class="banner-image-upload__btn banner-image-upload__btn--primary">
|
<button type="button" class="banner-image-upload__btn banner-image-upload__btn--primary">
|
||||||
@@ -58,7 +60,10 @@ import { message } from 'ant-design-vue'
|
|||||||
import { uploadURL } from '@root/config'
|
import { uploadURL } from '@root/config'
|
||||||
import { getToken } from '@/utils'
|
import { getToken } from '@/utils'
|
||||||
import { normalizeUploadFileResponse } from '@/utils/file'
|
import { normalizeUploadFileResponse } from '@/utils/file'
|
||||||
import { createWebpBeforeUpload } from '@/utils/prepareImageForUpload'
|
import {
|
||||||
|
createImageBeforeUpload,
|
||||||
|
createImageCustomRequest
|
||||||
|
} from '@/utils/prepareImageForUpload'
|
||||||
import type { UploadProps } from 'ant-design-vue'
|
import type { UploadProps } from 'ant-design-vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -76,31 +81,35 @@ const fileHeader = computed(() => ({
|
|||||||
Authorization: `Bearer ${getToken()}`,
|
Authorization: `Bearer ${getToken()}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
const beforeUpload: UploadProps['beforeUpload'] = createImageBeforeUpload((msg) =>
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
function onChange({ file }: { file: { status?: string; response?: unknown } }) {
|
const customRequest: UploadProps['customRequest'] = createImageCustomRequest({
|
||||||
if (file.status === 'uploading') {
|
action: () => uploadURL,
|
||||||
|
getHeaders: () => fileHeader.value,
|
||||||
|
messageError: (msg) => message.error(msg),
|
||||||
|
onStarted: () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
return
|
},
|
||||||
}
|
onUploaded: (body) => {
|
||||||
|
|
||||||
if (file.status === 'error') {
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
message.error('خطا در آپلود تصویر')
|
const fileResource = normalizeUploadFileResponse(body)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.status === 'done') {
|
|
||||||
loading.value = false
|
|
||||||
const fileResource = normalizeUploadFileResponse(file.response)
|
|
||||||
if (!fileResource?.full_src) {
|
if (!fileResource?.full_src) {
|
||||||
message.error('خطا در ساخت آدرس تصویر')
|
message.error('خطا در ساخت آدرس تصویر')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emit('upload', fileResource.full_src)
|
emit('upload', fileResource.full_src)
|
||||||
message.success('تصویر با موفقیت آپلود شد')
|
message.success('تصویر با موفقیت آپلود شد')
|
||||||
|
},
|
||||||
|
onFailed: () => {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onChange({ file }: { file: { status?: string } }) {
|
||||||
|
if (file.status === 'uploading') {
|
||||||
|
loading.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
import type { UploadProps } from 'ant-design-vue'
|
||||||
import { Upload } from 'ant-design-vue'
|
import { Upload } from 'ant-design-vue'
|
||||||
|
|
||||||
/** حداکثر حجم مجاز فایل ورودی و خروجی آپلود (۱ مگابایت) */
|
/** حداکثر حجم فایل خروجی آپلود (۱ مگابایت) */
|
||||||
export const MAX_UPLOAD_IMAGE_BYTES = 1024 * 1024
|
export const MAX_UPLOAD_IMAGE_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
/** حداکثر حجم فایل ورودی قبل از فشردهسازی (۲۵ مگابایت) */
|
||||||
|
export const MAX_SOURCE_IMAGE_BYTES = 25 * 1024 * 1024
|
||||||
|
|
||||||
const MAX_DIMENSION = 1920
|
const MAX_DIMENSION = 1920
|
||||||
const QUALITY_STEPS = [0.86, 0.78, 0.7, 0.62, 0.54, 0.46]
|
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 {
|
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> {
|
function loadImageElement(file: File): Promise<HTMLImageElement> {
|
||||||
@@ -36,10 +41,7 @@ function canvasToBlob(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawScaled(
|
function drawScaled(img: HTMLImageElement, maxDimension: number): HTMLCanvasElement {
|
||||||
img: HTMLImageElement,
|
|
||||||
maxDimension: number
|
|
||||||
): HTMLCanvasElement {
|
|
||||||
const scale = Math.min(1, maxDimension / Math.max(img.naturalWidth, img.naturalHeight))
|
const scale = Math.min(1, maxDimension / Math.max(img.naturalWidth, img.naturalHeight))
|
||||||
const width = Math.max(1, Math.round(img.naturalWidth * scale))
|
const width = Math.max(1, Math.round(img.naturalWidth * scale))
|
||||||
const height = Math.max(1, Math.round(img.naturalHeight * scale))
|
const height = Math.max(1, Math.round(img.naturalHeight * scale))
|
||||||
@@ -78,7 +80,7 @@ async function encodeWebpUnderLimit(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* تصویر را به WebP تبدیل میکند (با کاهش کیفیت/ابعاد در صورت نیاز)
|
* تصویر را به WebP تبدیل میکند (با کاهش کیفیت/ابعاد در صورت نیاز)
|
||||||
* و برای آپلود آماده میکند. اگر ورودی از قبل WebP و زیر سقف باشد، همان را برمیگرداند.
|
* و برای آپلود آماده میکند. خروجی همیشه ≤ maxBytes است.
|
||||||
*/
|
*/
|
||||||
export async function prepareImageForUpload(
|
export async function prepareImageForUpload(
|
||||||
file: File,
|
file: File,
|
||||||
@@ -92,8 +94,8 @@ export async function prepareImageForUpload(
|
|||||||
throw new Error('فقط فایل تصویری مجاز است')
|
throw new Error('فقط فایل تصویری مجاز است')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > maxBytes) {
|
if (file.size > MAX_SOURCE_IMAGE_BYTES) {
|
||||||
throw new Error('حداکثر حجم بارگذاری ۱ مگابایت میباشد')
|
throw new Error('حجم فایل انتخابی بیش از حد مجاز است (حداکثر ۲۵ مگابایت)')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.type === 'image/webp' && file.size <= maxBytes) {
|
if (file.type === 'image/webp' && file.size <= maxBytes) {
|
||||||
@@ -110,25 +112,127 @@ export async function prepareImageForUpload(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** برای beforeUpload آنتدیزاین: تبدیل به WebP یا رد آپلود */
|
/** اعتبارسنجی سریع قبل از customRequest */
|
||||||
export async function beforeUploadConvertToWebp(file: File) {
|
export function createImageBeforeUpload(messageError: (msg: string) => void) {
|
||||||
try {
|
return (file: File) => {
|
||||||
return await prepareImageForUpload(file)
|
if (!file?.type?.startsWith('image/')) {
|
||||||
} catch (error) {
|
messageError('فقط فایل تصویری مجاز است')
|
||||||
const msg = error instanceof Error ? error.message : 'خطا در آمادهسازی تصویر'
|
return Upload.LIST_IGNORE
|
||||||
return Promise.reject(msg)
|
}
|
||||||
|
if (file.size > MAX_SOURCE_IMAGE_BYTES) {
|
||||||
|
messageError('حجم فایل انتخابی بیش از حد مجاز است (حداکثر ۲۵ مگابایت)')
|
||||||
|
return Upload.LIST_IGNORE
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** نسخهٔ مناسب ant-design Upload با پیام خطا و LIST_IGNORE */
|
type CustomRequestOption = Parameters<NonNullable<UploadProps['customRequest']>>[0]
|
||||||
export function createWebpBeforeUpload(messageError: (msg: string) => void) {
|
|
||||||
return async (file: File) => {
|
function parseXhrBody(xhr: XMLHttpRequest): unknown {
|
||||||
try {
|
const text = xhr.responseText
|
||||||
return await prepareImageForUpload(file)
|
if (!text) return xhr.response ?? null
|
||||||
} catch (error) {
|
try {
|
||||||
const msg = error instanceof Error ? error.message : 'خطا در آمادهسازی تصویر'
|
return JSON.parse(text)
|
||||||
messageError(msg)
|
} catch {
|
||||||
return Upload.LIST_IGNORE
|
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