fix bug 4
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="index-image-uploader__icon" />
|
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="index-image-uploader__icon" />
|
||||||
<span class="index-image-uploader__cta">افزودن تصویر شاخص</span>
|
<span class="index-image-uploader__cta">افزودن تصویر شاخص</span>
|
||||||
<span class="index-image-uploader__hint">JPG، PNG یا WEBP — حداکثر ۲۰۰ کیلوبایت</span>
|
<span class="index-image-uploader__hint">JPG، PNG یا WEBP — حداکثر ۱ مگابایت (تبدیل خودکار به WebP)</span>
|
||||||
<span class="index-image-uploader__hint">نسبت مربعی برای نمایش بهتر توصیه میشود</span>
|
<span class="index-image-uploader__hint">نسبت مربعی برای نمایش بهتر توصیه میشود</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,13 +84,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UploadChangeParam, UploadProps } from 'ant-design-vue'
|
import type { UploadChangeParam, UploadProps } from 'ant-design-vue'
|
||||||
import { Upload, message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { LoadingOutlined } from '@ant-design/icons-vue'
|
import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||||
import { usePrefix } from '@core/composable/usePrefix'
|
import { usePrefix } from '@core/composable/usePrefix'
|
||||||
import { uploadURL } from '@root/config'
|
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 { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const { prefixCls } = usePrefix('index-image-uploader')
|
const { prefixCls } = usePrefix('index-image-uploader')
|
||||||
@@ -150,42 +151,38 @@ function removeImage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
||||||
if (!file.size || file.size < 1) {
|
message.error(msg)
|
||||||
message.error('فایل خالی است')
|
)
|
||||||
return Upload.LIST_IGNORE
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLt200k = file.size / 1024 < 200
|
|
||||||
if (!isLt200k) {
|
|
||||||
message.error('حداکثر حجم بارگذاری ۲۰۰ کیلوبایت میباشد')
|
|
||||||
return Upload.LIST_IGNORE
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(info: UploadChangeParam) {
|
function handleChange(info: UploadChangeParam) {
|
||||||
fixFileCapture()
|
fixFileCapture()
|
||||||
|
|
||||||
if (info.file.status === 'uploading') {
|
const status = info.file.status
|
||||||
|
|
||||||
|
if (status === 'uploading') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.file.status === 'error') {
|
// هر وضعیت غیر از uploading باید اسپینر را ببندد (error/done/removed/undefined)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
message.error('خطا در بارگذاری')
|
const serverMsg =
|
||||||
|
(info.file.response as { message?: string } | undefined)?.message ||
|
||||||
|
(info.file.error as { message?: string } | undefined)?.message
|
||||||
|
message.error(serverMsg || 'خطا در بارگذاری تصویر')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.file.status === 'done') {
|
if (status === 'done') {
|
||||||
loading.value = false
|
|
||||||
const fileResource = normalizeUploadFileResponse(info.file.response)
|
const fileResource = normalizeUploadFileResponse(info.file.response)
|
||||||
|
|
||||||
if (!fileResource?.id || !fileResource.full_src) {
|
if (!fileResource?.id || !fileResource.full_src) {
|
||||||
fileList.value = []
|
fileList.value = []
|
||||||
message.error('خطا در ساخت آدرس تصویر')
|
message.error('آپلود انجام شد ولی آدرس تصویر ساخته نشد')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +197,10 @@ function handleChange(info: UploadChangeParam) {
|
|||||||
]
|
]
|
||||||
message.success('تصویر شاخص با موفقیت بارگذاری شد')
|
message.success('تصویر شاخص با موفقیت بارگذاری شد')
|
||||||
emits('ok', fileResource.id, info.file.response)
|
emits('ok', fileResource.id, info.file.response)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (info.file.status === 'removed') {
|
|
||||||
|
if (status === 'removed') {
|
||||||
emits('delete', info.file.uid)
|
emits('delete', info.file.uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {UploadChangeParam, UploadProps} from 'ant-design-vue'
|
import type {UploadChangeParam, UploadProps} from 'ant-design-vue'
|
||||||
import {Upload, message} from 'ant-design-vue'
|
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 {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 {usePrefix} from '@core/composable/usePrefix'
|
import {usePrefix} from '@core/composable/usePrefix'
|
||||||
import {computed, onMounted, ref} from 'vue'
|
import {computed, onMounted, ref} from 'vue'
|
||||||
|
|
||||||
@@ -59,20 +60,9 @@ const fileList = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
||||||
if (!file.size || file.size < 1) {
|
message.error(msg)
|
||||||
message.error('فایل خالی است')
|
)
|
||||||
return Upload.LIST_IGNORE
|
|
||||||
}
|
|
||||||
|
|
||||||
const isLt240k = file.size / 1024 < 240
|
|
||||||
if (!isLt240k) {
|
|
||||||
message.error('حداکثر حجم بارگذاری ۲۴۰ کیلوبایت میباشد')
|
|
||||||
return Upload.LIST_IGNORE
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(info: UploadChangeParam) {
|
function handleChange(info: UploadChangeParam) {
|
||||||
fixFileCapture()
|
fixFileCapture()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
:action="url"
|
:action="url"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
@change="handelChange"
|
@change="handelChange"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -41,8 +42,11 @@ import { uploadURL } from '@root/config'
|
|||||||
import { onMounted, ref, watch, computed } from 'vue'
|
import { onMounted, ref, watch, computed } from 'vue'
|
||||||
import { LoadingOutlined } from '@ant-design/icons-vue'
|
import { LoadingOutlined } from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } 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 { normalizeUploadFileResponse, toAbsoluteFileUrl } from '@/utils/file'
|
||||||
|
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const percent = ref<number>(0)
|
const percent = ref<number>(0)
|
||||||
@@ -68,6 +72,10 @@ const emit = defineEmits<{
|
|||||||
(e: 'errorUpload'): void
|
(e: 'errorUpload'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
||||||
|
message.error(msg)
|
||||||
|
)
|
||||||
|
|
||||||
function handelChange({ file, fileList }) {
|
function handelChange({ file, fileList }) {
|
||||||
if (Object.hasOwn(file, 'percent')) {
|
if (Object.hasOwn(file, 'percent')) {
|
||||||
percent.value = Math.floor(file.percent)
|
percent.value = Math.floor(file.percent)
|
||||||
@@ -86,7 +94,11 @@ function handelChange({ file, fileList }) {
|
|||||||
message.error('خطا در آپلود فایل')
|
message.error('خطا در آپلود فایل')
|
||||||
}
|
}
|
||||||
if (file.status == 'done') {
|
if (file.status == 'done') {
|
||||||
imageUrl.value = file.response.full_src
|
const fileResource = normalizeUploadFileResponse(file.response)
|
||||||
|
imageUrl.value =
|
||||||
|
fileResource?.full_src ||
|
||||||
|
toAbsoluteFileUrl(file.response?.full_src || file.response?.src) ||
|
||||||
|
''
|
||||||
loading.value = false
|
loading.value = false
|
||||||
emit('upload', file.response)
|
emit('upload', file.response)
|
||||||
message.success('فایل با موفقیت آپلود شد')
|
message.success('فایل با موفقیت آپلود شد')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
:show-upload-list="false"
|
:show-upload-list="false"
|
||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
>
|
>
|
||||||
<div class="banner-image-upload__empty">
|
<div class="banner-image-upload__empty">
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="banner-image-upload__icon" />
|
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="banner-image-upload__icon" />
|
||||||
<span class="banner-image-upload__cta">افزودن تصویر بنر</span>
|
<span class="banner-image-upload__cta">افزودن تصویر بنر</span>
|
||||||
<span class="banner-image-upload__hint">کلیک کنید — تصویر افقی با نسبت ۱۶:۹</span>
|
<span class="banner-image-upload__hint">حداکثر ۱ مگابایت — تبدیل خودکار به WebP — نسبت ۱۶:۹</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
:show-upload-list="false"
|
:show-upload-list="false"
|
||||||
:action="uploadURL"
|
:action="uploadURL"
|
||||||
:headers="fileHeader"
|
:headers="fileHeader"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
@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">
|
||||||
@@ -56,6 +58,8 @@ 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 type { UploadProps } from 'ant-design-vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
@@ -72,6 +76,10 @@ const fileHeader = computed(() => ({
|
|||||||
Authorization: `Bearer ${getToken()}`,
|
Authorization: `Bearer ${getToken()}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = createWebpBeforeUpload((msg) =>
|
||||||
|
message.error(msg)
|
||||||
|
)
|
||||||
|
|
||||||
function onChange({ file }: { file: { status?: string; response?: unknown } }) {
|
function onChange({ file }: { file: { status?: string; response?: unknown } }) {
|
||||||
if (file.status === 'uploading') {
|
if (file.status === 'uploading') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|||||||
@@ -733,8 +733,12 @@ const weightFieldLabel = computed(() => {
|
|||||||
return weightFieldLabelForUnit(tradeWeightUnit.value)
|
return weightFieldLabelForUnit(tradeWeightUnit.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// واحد حداقل تحویل فیزیکی: محصول وزنی → گرم/کیلوگرم، محصول تعدادی → عدد
|
// واحد حداقل تحویل فیزیکی: تعدادی → عدد، مقداری/وزنی → گرم/کیلوگرم
|
||||||
const physicalDeliveryUnit = computed(() => {
|
const physicalDeliveryUnit = computed(() => {
|
||||||
|
if (!isQuantityTrade.value && !isMeltedGroup.value) {
|
||||||
|
return 'عدد'
|
||||||
|
}
|
||||||
|
|
||||||
const productUnit = props.form.unit?.trim()
|
const productUnit = props.form.unit?.trim()
|
||||||
if (productUnit === 'کیلوگرم' || productUnit === 'گرم') {
|
if (productUnit === 'کیلوگرم' || productUnit === 'گرم') {
|
||||||
return productUnit
|
return productUnit
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { Upload } from 'ant-design-vue'
|
||||||
|
|
||||||
|
/** حداکثر حجم مجاز فایل ورودی و خروجی آپلود (۱ مگابایت) */
|
||||||
|
export const MAX_UPLOAD_IMAGE_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
const MAX_DIMENSION = 1920
|
||||||
|
const QUALITY_STEPS = [0.86, 0.78, 0.7, 0.62, 0.54, 0.46]
|
||||||
|
|
||||||
|
function stripExtension(name: string): string {
|
||||||
|
return name.replace(/\.[^.]+$/u, '') || 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImageElement(file: File): Promise<HTMLImageElement> {
|
||||||
|
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<Blob | null> {
|
||||||
|
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<Blob> {
|
||||||
|
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 تبدیل میکند (با کاهش کیفیت/ابعاد در صورت نیاز)
|
||||||
|
* و برای آپلود آماده میکند. اگر ورودی از قبل WebP و زیر سقف باشد، همان را برمیگرداند.
|
||||||
|
*/
|
||||||
|
export async function prepareImageForUpload(
|
||||||
|
file: File,
|
||||||
|
maxBytes: number = MAX_UPLOAD_IMAGE_BYTES
|
||||||
|
): Promise<File> {
|
||||||
|
if (!file || file.size < 1) {
|
||||||
|
throw new Error('فایل خالی است')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
throw new Error('فقط فایل تصویری مجاز است')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxBytes) {
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** برای 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** نسخهٔ مناسب 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user