feat(upload prop:crop,aspectRatio): from Upload component accept prop… (#7095)
* feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio * feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio * feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio * feat(upload prop:crop,aspectRatio): from Upload component accept prop crop,aspectRatio
This commit is contained in:
@@ -26,12 +26,17 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import {
|
||||||
|
ApiComponent,
|
||||||
|
globalShareState,
|
||||||
|
IconPicker,
|
||||||
|
VCropper,
|
||||||
|
} from '@vben/common-ui';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { message, notification } from 'ant-design-vue';
|
import { message, Modal, notification } from 'ant-design-vue';
|
||||||
|
|
||||||
const AutoComplete = defineAsyncComponent(
|
const AutoComplete = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/auto-complete'),
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
@@ -121,6 +126,33 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const withPreviewUpload = () => {
|
const withPreviewUpload = () => {
|
||||||
|
// 检查是否为图片文件的辅助函数
|
||||||
|
const isImageFile = (file: UploadFile): boolean => {
|
||||||
|
const imageExtensions = new Set([
|
||||||
|
'bmp',
|
||||||
|
'gif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'svg',
|
||||||
|
'webp',
|
||||||
|
]);
|
||||||
|
if (file.url) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||||
|
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
} catch {
|
||||||
|
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file.type) {
|
||||||
|
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
return file.type.startsWith('image/');
|
||||||
|
};
|
||||||
// 创建默认的上传按钮插槽
|
// 创建默认的上传按钮插槽
|
||||||
const createDefaultSlotsWithUpload = (
|
const createDefaultSlotsWithUpload = (
|
||||||
listType: string,
|
listType: string,
|
||||||
@@ -155,27 +187,6 @@ const withPreviewUpload = () => {
|
|||||||
visible: Ref<boolean>,
|
visible: Ref<boolean>,
|
||||||
fileList: Ref<UploadProps['fileList']>,
|
fileList: Ref<UploadProps['fileList']>,
|
||||||
) => {
|
) => {
|
||||||
// 检查是否为图片文件的辅助函数
|
|
||||||
const isImageFile = (file: UploadFile): boolean => {
|
|
||||||
const imageExtensions = new Set([
|
|
||||||
'bmp',
|
|
||||||
'gif',
|
|
||||||
'jpeg',
|
|
||||||
'jpg',
|
|
||||||
'png',
|
|
||||||
'webp',
|
|
||||||
]);
|
|
||||||
if (file.url) {
|
|
||||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
|
||||||
return ext ? imageExtensions.has(ext) : false;
|
|
||||||
}
|
|
||||||
if (!file.type) {
|
|
||||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
|
||||||
return ext ? imageExtensions.has(ext) : false;
|
|
||||||
}
|
|
||||||
return file.type.startsWith('image/');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果当前文件不是图片,直接打开
|
// 如果当前文件不是图片,直接打开
|
||||||
if (!isImageFile(file)) {
|
if (!isImageFile(file)) {
|
||||||
if (file.url) {
|
if (file.url) {
|
||||||
@@ -261,6 +272,107 @@ const withPreviewUpload = () => {
|
|||||||
|
|
||||||
render(h(PreviewWrapper), container);
|
render(h(PreviewWrapper), container);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 图片裁剪操作
|
||||||
|
const cropImage = (file: File, aspectRatio: string | undefined) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const container: HTMLElement | null = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
// 用于追踪组件是否已卸载
|
||||||
|
let isUnmounted = false;
|
||||||
|
let objectUrl: null | string = null;
|
||||||
|
|
||||||
|
const open = ref<boolean>(true);
|
||||||
|
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
// 延迟清理,确保动画完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CropperWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
if (!objectUrl) {
|
||||||
|
objectUrl = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
Modal,
|
||||||
|
{
|
||||||
|
open: open.value,
|
||||||
|
title: $t('ui.crop.title'),
|
||||||
|
centered: true,
|
||||||
|
width: 548,
|
||||||
|
keyboard: false,
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
cancelText: $t('common.cancel'),
|
||||||
|
okText: $t('ui.crop.confirm'),
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOk: async () => {
|
||||||
|
const cropper = cropperRef.value;
|
||||||
|
if (!cropper) {
|
||||||
|
reject(new Error('Cropper not found'));
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dataUrl = await cropper.getCropImage();
|
||||||
|
resolve(dataUrl);
|
||||||
|
} catch {
|
||||||
|
reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
} finally {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve('');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
h(VCropper, {
|
||||||
|
ref: (ref: any) => (cropperRef.value = ref),
|
||||||
|
img: objectUrl as string,
|
||||||
|
aspectRatio,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(CropperWrapper), container);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const base64ToBlob = (base64: Base64URLString) => {
|
||||||
|
try {
|
||||||
|
const [typeStr, encodeStr] = base64.split(',');
|
||||||
|
if (!typeStr || !encodeStr) return;
|
||||||
|
const mime = typeStr.match(/:(.*?);/)?.[1];
|
||||||
|
const raw = window.atob(encodeStr);
|
||||||
|
const rawLength = raw.length;
|
||||||
|
const uInt8Array = new Uint8Array(rawLength);
|
||||||
|
for (let i = 0; i < rawLength; ++i) {
|
||||||
|
uInt8Array[i] = raw.codePointAt(i) as number;
|
||||||
|
}
|
||||||
|
return new Blob([uInt8Array], { type: mime });
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
name: Upload.name,
|
name: Upload.name,
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
@@ -278,12 +390,37 @@ const withPreviewUpload = () => {
|
|||||||
attrs?.fileList || attrs?.['file-list'] || [],
|
attrs?.fileList || attrs?.['file-list'] || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBeforeUpload = (file: UploadFile) => {
|
const handleBeforeUpload = async (
|
||||||
|
file: UploadFile,
|
||||||
|
originFileList: Array<File>,
|
||||||
|
) => {
|
||||||
if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
|
if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
|
||||||
message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
|
message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
|
||||||
file.status = 'removed';
|
file.status = 'removed';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 多选或者非图片不唤起裁剪框
|
||||||
|
if (
|
||||||
|
attrs.crop &&
|
||||||
|
!attrs.multiple &&
|
||||||
|
originFileList[0] &&
|
||||||
|
isImageFile(file)
|
||||||
|
) {
|
||||||
|
file.status = 'removed';
|
||||||
|
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
|
||||||
|
const base64 = await cropImage(originFileList[0], attrs.aspectRatio);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!base64) {
|
||||||
|
return reject(new Error($t('ui.crop.cancel')));
|
||||||
|
}
|
||||||
|
const blob = base64ToBlob(base64 as string);
|
||||||
|
if (!blob) {
|
||||||
|
return reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
}
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return attrs.beforeUpload?.(file) ?? true;
|
return attrs.beforeUpload?.(file) ?? true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -377,6 +514,7 @@ async function initComponentAdapter() {
|
|||||||
// 如果你的组件体积比较大,可以使用异步加载
|
// 如果你的组件体积比较大,可以使用异步加载
|
||||||
// Button: () =>
|
// Button: () =>
|
||||||
// import('xxx').then((res) => res.Button),
|
// import('xxx').then((res) => res.Button),
|
||||||
|
|
||||||
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
component: Cascader,
|
component: Cascader,
|
||||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
@@ -384,34 +522,20 @@ async function initComponentAdapter() {
|
|||||||
modelPropName: 'value',
|
modelPropName: 'value',
|
||||||
visibleEvent: 'onVisibleChange',
|
visibleEvent: 'onVisibleChange',
|
||||||
}),
|
}),
|
||||||
ApiSelect: withDefaultPlaceholder(
|
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
{
|
component: Select,
|
||||||
...ApiComponent,
|
loadingSlot: 'suffixIcon',
|
||||||
name: 'ApiSelect',
|
modelPropName: 'value',
|
||||||
},
|
visibleEvent: 'onVisibleChange',
|
||||||
'select',
|
}),
|
||||||
{
|
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||||
component: Select,
|
component: TreeSelect,
|
||||||
loadingSlot: 'suffixIcon',
|
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||||
visibleEvent: 'onDropdownVisibleChange',
|
loadingSlot: 'suffixIcon',
|
||||||
modelPropName: 'value',
|
modelPropName: 'value',
|
||||||
},
|
optionsPropName: 'treeData',
|
||||||
),
|
visibleEvent: 'onVisibleChange',
|
||||||
ApiTreeSelect: withDefaultPlaceholder(
|
}),
|
||||||
{
|
|
||||||
...ApiComponent,
|
|
||||||
name: 'ApiTreeSelect',
|
|
||||||
},
|
|
||||||
'select',
|
|
||||||
{
|
|
||||||
component: TreeSelect,
|
|
||||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
|
||||||
loadingSlot: 'suffixIcon',
|
|
||||||
modelPropName: 'value',
|
|
||||||
optionsPropName: 'treeData',
|
|
||||||
visibleEvent: 'onVisibleChange',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
Cascader,
|
Cascader,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copied": "Copied"
|
"copied": "Copied"
|
||||||
},
|
},
|
||||||
|
"crop": {
|
||||||
|
"title": "Image Cropping",
|
||||||
|
"confirm": "Crop",
|
||||||
|
"cancel": "Cancel cropping",
|
||||||
|
"errorTip": "Cropping error"
|
||||||
|
},
|
||||||
"fallback": {
|
"fallback": {
|
||||||
"pageNotFound": "Oops! Page Not Found",
|
"pageNotFound": "Oops! Page Not Found",
|
||||||
"pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.",
|
"pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.",
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copied": "已复制"
|
"copied": "已复制"
|
||||||
},
|
},
|
||||||
|
"crop": {
|
||||||
|
"title": "图片裁剪",
|
||||||
|
"confirm": "裁剪",
|
||||||
|
"cancel": "取消裁剪",
|
||||||
|
"errorTip": "裁剪错误"
|
||||||
|
},
|
||||||
"fallback": {
|
"fallback": {
|
||||||
"pageNotFound": "哎呀!未找到页面",
|
"pageNotFound": "哎呀!未找到页面",
|
||||||
"pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。",
|
"pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。",
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
import {
|
||||||
|
ApiComponent,
|
||||||
|
globalShareState,
|
||||||
|
IconPicker,
|
||||||
|
VCropper,
|
||||||
|
} from '@vben/common-ui';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { message, notification } from 'ant-design-vue';
|
import { message, Modal, notification } from 'ant-design-vue';
|
||||||
|
|
||||||
const AutoComplete = defineAsyncComponent(
|
const AutoComplete = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/auto-complete'),
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
@@ -101,7 +106,6 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
$t(`ui.placeholder.${type}`);
|
$t(`ui.placeholder.${type}`);
|
||||||
// 透传组件暴露的方法
|
// 透传组件暴露的方法
|
||||||
const innerRef = ref();
|
const innerRef = ref();
|
||||||
// const publicApi: Recordable<any> = {};
|
|
||||||
expose(
|
expose(
|
||||||
new Proxy(
|
new Proxy(
|
||||||
{},
|
{},
|
||||||
@@ -111,14 +115,6 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// const instance = getCurrentInstance();
|
|
||||||
// instance?.proxy?.$nextTick(() => {
|
|
||||||
// for (const key in innerRef.value) {
|
|
||||||
// if (typeof innerRef.value[key] === 'function') {
|
|
||||||
// publicApi[key] = innerRef.value[key];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
return () =>
|
return () =>
|
||||||
h(
|
h(
|
||||||
component,
|
component,
|
||||||
@@ -130,6 +126,33 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const withPreviewUpload = () => {
|
const withPreviewUpload = () => {
|
||||||
|
// 检查是否为图片文件的辅助函数
|
||||||
|
const isImageFile = (file: UploadFile): boolean => {
|
||||||
|
const imageExtensions = new Set([
|
||||||
|
'bmp',
|
||||||
|
'gif',
|
||||||
|
'jpeg',
|
||||||
|
'jpg',
|
||||||
|
'png',
|
||||||
|
'svg',
|
||||||
|
'webp',
|
||||||
|
]);
|
||||||
|
if (file.url) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(file.url, 'http://localhost').pathname;
|
||||||
|
const ext = pathname.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
} catch {
|
||||||
|
const ext = file.url?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file.type) {
|
||||||
|
const ext = file.name?.split('.').pop()?.toLowerCase();
|
||||||
|
return ext ? imageExtensions.has(ext) : false;
|
||||||
|
}
|
||||||
|
return file.type.startsWith('image/');
|
||||||
|
};
|
||||||
// 创建默认的上传按钮插槽
|
// 创建默认的上传按钮插槽
|
||||||
const createDefaultSlotsWithUpload = (
|
const createDefaultSlotsWithUpload = (
|
||||||
listType: string,
|
listType: string,
|
||||||
@@ -164,27 +187,6 @@ const withPreviewUpload = () => {
|
|||||||
visible: Ref<boolean>,
|
visible: Ref<boolean>,
|
||||||
fileList: Ref<UploadProps['fileList']>,
|
fileList: Ref<UploadProps['fileList']>,
|
||||||
) => {
|
) => {
|
||||||
// 检查是否为图片文件的辅助函数
|
|
||||||
const isImageFile = (file: UploadFile): boolean => {
|
|
||||||
const imageExtensions = new Set([
|
|
||||||
'bmp',
|
|
||||||
'gif',
|
|
||||||
'jpeg',
|
|
||||||
'jpg',
|
|
||||||
'png',
|
|
||||||
'webp',
|
|
||||||
]);
|
|
||||||
if (file.url) {
|
|
||||||
const ext = file.url?.split('.').pop()?.toLowerCase();
|
|
||||||
return ext ? imageExtensions.has(ext) : false;
|
|
||||||
}
|
|
||||||
if (!file.type) {
|
|
||||||
const ext = file.name?.split('.').pop()?.toLowerCase();
|
|
||||||
return ext ? imageExtensions.has(ext) : false;
|
|
||||||
}
|
|
||||||
return file.type.startsWith('image/');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果当前文件不是图片,直接打开
|
// 如果当前文件不是图片,直接打开
|
||||||
if (!isImageFile(file)) {
|
if (!isImageFile(file)) {
|
||||||
if (file.url) {
|
if (file.url) {
|
||||||
@@ -270,6 +272,107 @@ const withPreviewUpload = () => {
|
|||||||
|
|
||||||
render(h(PreviewWrapper), container);
|
render(h(PreviewWrapper), container);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 图片裁剪操作
|
||||||
|
const cropImage = (file: File, aspectRatio: string | undefined) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const container: HTMLElement | null = document.createElement('div');
|
||||||
|
document.body.append(container);
|
||||||
|
|
||||||
|
// 用于追踪组件是否已卸载
|
||||||
|
let isUnmounted = false;
|
||||||
|
let objectUrl: null | string = null;
|
||||||
|
|
||||||
|
const open = ref<boolean>(true);
|
||||||
|
const cropperRef = ref<InstanceType<typeof VCropper> | null>(null);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
open.value = false;
|
||||||
|
// 延迟清理,确保动画完成
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isUnmounted && container) {
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
isUnmounted = true;
|
||||||
|
render(null, container);
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CropperWrapper = {
|
||||||
|
setup() {
|
||||||
|
return () => {
|
||||||
|
if (isUnmounted) return null;
|
||||||
|
if (!objectUrl) {
|
||||||
|
objectUrl = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
Modal,
|
||||||
|
{
|
||||||
|
open: open.value,
|
||||||
|
title: $t('ui.crop.title'),
|
||||||
|
centered: true,
|
||||||
|
width: 548,
|
||||||
|
keyboard: false,
|
||||||
|
maskClosable: false,
|
||||||
|
closable: false,
|
||||||
|
cancelText: $t('common.cancel'),
|
||||||
|
okText: $t('ui.crop.confirm'),
|
||||||
|
destroyOnClose: true,
|
||||||
|
onOk: async () => {
|
||||||
|
const cropper = cropperRef.value;
|
||||||
|
if (!cropper) {
|
||||||
|
reject(new Error('Cropper not found'));
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dataUrl = await cropper.getCropImage();
|
||||||
|
resolve(dataUrl);
|
||||||
|
} catch {
|
||||||
|
reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
} finally {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
resolve('');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
h(VCropper, {
|
||||||
|
ref: (ref: any) => (cropperRef.value = ref),
|
||||||
|
img: objectUrl as string,
|
||||||
|
aspectRatio,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(h(CropperWrapper), container);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const base64ToBlob = (base64: Base64URLString) => {
|
||||||
|
try {
|
||||||
|
const [typeStr, encodeStr] = base64.split(',');
|
||||||
|
if (!typeStr || !encodeStr) return;
|
||||||
|
const mime = typeStr.match(/:(.*?);/)?.[1];
|
||||||
|
const raw = window.atob(encodeStr);
|
||||||
|
const rawLength = raw.length;
|
||||||
|
const uInt8Array = new Uint8Array(rawLength);
|
||||||
|
for (let i = 0; i < rawLength; ++i) {
|
||||||
|
uInt8Array[i] = raw.codePointAt(i) as number;
|
||||||
|
}
|
||||||
|
return new Blob([uInt8Array], { type: mime });
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
name: Upload.name,
|
name: Upload.name,
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
@@ -287,12 +390,37 @@ const withPreviewUpload = () => {
|
|||||||
attrs?.fileList || attrs?.['file-list'] || [],
|
attrs?.fileList || attrs?.['file-list'] || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBeforeUpload = (file: UploadFile) => {
|
const handleBeforeUpload = async (
|
||||||
|
file: UploadFile,
|
||||||
|
originFileList: Array<File>,
|
||||||
|
) => {
|
||||||
if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
|
if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
|
||||||
message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
|
message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
|
||||||
file.status = 'removed';
|
file.status = 'removed';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 多选或者非图片不唤起裁剪框
|
||||||
|
if (
|
||||||
|
attrs.crop &&
|
||||||
|
!attrs.multiple &&
|
||||||
|
originFileList[0] &&
|
||||||
|
isImageFile(file)
|
||||||
|
) {
|
||||||
|
file.status = 'removed';
|
||||||
|
// antd Upload组件问题 file参数获取的是UploadFile类型对象无法取到File类型 所以通过originFileList[0]获取
|
||||||
|
const base64 = await cropImage(originFileList[0], attrs.aspectRatio);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!base64) {
|
||||||
|
return reject(new Error($t('ui.crop.cancel')));
|
||||||
|
}
|
||||||
|
const blob = base64ToBlob(base64 as string);
|
||||||
|
if (!blob) {
|
||||||
|
return reject(new Error($t('ui.crop.errorTip')));
|
||||||
|
}
|
||||||
|
resolve(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return attrs.beforeUpload?.(file) ?? true;
|
return attrs.beforeUpload?.(file) ?? true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"upload-error": "Partial file upload failed",
|
"upload-error": "Partial file upload failed",
|
||||||
"upload-urls": "Urls after file upload",
|
"upload-urls": "Urls after file upload",
|
||||||
"file": "file",
|
"file": "file",
|
||||||
|
"crop-image": "Crop image",
|
||||||
"upload-image": "Click to upload image"
|
"upload-image": "Click to upload image"
|
||||||
},
|
},
|
||||||
"vxeTable": {
|
"vxeTable": {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"upload-error": "部分文件上传失败",
|
"upload-error": "部分文件上传失败",
|
||||||
"upload-urls": "文件上传后的网址",
|
"upload-urls": "文件上传后的网址",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
|
"crop-image": "裁剪图片",
|
||||||
"upload-image": "点击上传图片"
|
"upload-image": "点击上传图片"
|
||||||
},
|
},
|
||||||
"vxeTable": {
|
"vxeTable": {
|
||||||
|
|||||||
@@ -358,6 +358,28 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
|||||||
},
|
},
|
||||||
rules: 'selectRequired',
|
rules: 'selectRequired',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'Upload',
|
||||||
|
componentProps: {
|
||||||
|
accept: '.png,.jpg,.jpeg',
|
||||||
|
customRequest: upload_file,
|
||||||
|
maxCount: 1,
|
||||||
|
maxSize: 2,
|
||||||
|
listType: 'picture-card',
|
||||||
|
// 是否启用图片裁剪(多选或者非图片不唤起裁剪框)
|
||||||
|
crop: true,
|
||||||
|
// 裁剪比例
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
},
|
||||||
|
fieldName: 'cropImage',
|
||||||
|
label: $t('examples.form.crop-image'),
|
||||||
|
renderComponentContent: () => {
|
||||||
|
return {
|
||||||
|
default: () => $t('examples.form.upload-image'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
rules: 'selectRequired',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||||
@@ -365,13 +387,20 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
|||||||
|
|
||||||
function onSubmit(values: Record<string, any>) {
|
function onSubmit(values: Record<string, any>) {
|
||||||
const files = toRaw(values.files) as UploadFile[];
|
const files = toRaw(values.files) as UploadFile[];
|
||||||
|
const cropImage = (toRaw(values.cropImage) ?? []) as UploadFile[];
|
||||||
const doneFiles = files.filter((file) => file.status === 'done');
|
const doneFiles = files.filter((file) => file.status === 'done');
|
||||||
const failedFiles = files.filter((file) => file.status !== 'done');
|
const failedFiles = files.filter((file) => file.status !== 'done');
|
||||||
|
const doneCrop = cropImage.filter((file) => file.status === 'done');
|
||||||
|
const failedCrop = cropImage.filter((file) => file.status !== 'done');
|
||||||
|
|
||||||
const msg = [
|
const msg = [
|
||||||
...doneFiles.map((file) => file.response?.url || file.url),
|
...doneFiles.map((file) => file.response?.url || file.url),
|
||||||
...failedFiles.map((file) => file.name),
|
...failedFiles.map((file) => file.name),
|
||||||
].join(', ');
|
].join(', ');
|
||||||
|
const msgCrop = [
|
||||||
|
...doneCrop.map((file) => file.response?.url || file.url),
|
||||||
|
...failedCrop.map((file) => file.name),
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
if (failedFiles.length === 0) {
|
if (failedFiles.length === 0) {
|
||||||
message.success({
|
message.success({
|
||||||
@@ -383,8 +412,19 @@ function onSubmit(values: Record<string, any>) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (doneCrop.length > 0 && failedCrop.length === 0) {
|
||||||
|
message.success({
|
||||||
|
content: `${$t('examples.form.upload-urls')}: ${msgCrop}`,
|
||||||
|
});
|
||||||
|
} else if (failedCrop.length > 0) {
|
||||||
|
message.error({
|
||||||
|
content: `${$t('examples.form.upload-error')}: ${msgCrop}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 如果需要可提交前替换为需要的urls
|
// 如果需要可提交前替换为需要的urls
|
||||||
values.files = doneFiles.map((file) => file.response?.url || file.url);
|
values.files = doneFiles.map((file) => file.response?.url || file.url);
|
||||||
|
values.cropImage = doneCrop.map((file) => file.response?.url || file.url);
|
||||||
message.success({
|
message.success({
|
||||||
content: `form values: ${JSON.stringify(values)}`,
|
content: `form values: ${JSON.stringify(values)}`,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user