|
@@ -29,7 +29,7 @@ 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 { notification } from 'ant-design-vue';
|
|
|
|
|
|
|
+import { message, 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'),
|
|
@@ -75,6 +75,9 @@ const TimePicker = defineAsyncComponent(
|
|
|
const TreeSelect = defineAsyncComponent(
|
|
const TreeSelect = defineAsyncComponent(
|
|
|
() => import('ant-design-vue/es/tree-select'),
|
|
() => import('ant-design-vue/es/tree-select'),
|
|
|
);
|
|
);
|
|
|
|
|
+const Cascader = defineAsyncComponent(
|
|
|
|
|
+ () => import('ant-design-vue/es/cascader'),
|
|
|
|
|
+);
|
|
|
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
|
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
|
|
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
|
const Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
|
|
|
const PreviewGroup = defineAsyncComponent(() =>
|
|
const PreviewGroup = defineAsyncComponent(() =>
|
|
@@ -125,9 +128,149 @@ const withDefaultPlaceholder = <T extends Component>(
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const withPreviewUpload = () => {
|
|
const withPreviewUpload = () => {
|
|
|
|
|
+ // 创建默认的上传按钮插槽
|
|
|
|
|
+ const createDefaultSlotsWithUpload = (
|
|
|
|
|
+ listType: string,
|
|
|
|
|
+ placeholder: string,
|
|
|
|
|
+ ) => {
|
|
|
|
|
+ switch (listType) {
|
|
|
|
|
+ case 'picture-card': {
|
|
|
|
|
+ return {
|
|
|
|
|
+ default: () => placeholder,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ default: {
|
|
|
|
|
+ return {
|
|
|
|
|
+ default: () =>
|
|
|
|
|
+ h(
|
|
|
|
|
+ Button,
|
|
|
|
|
+ {
|
|
|
|
|
+ icon: h(IconifyIcon, {
|
|
|
|
|
+ icon: 'ant-design:upload-outlined',
|
|
|
|
|
+ class: 'mb-1 size-4',
|
|
|
|
|
+ }),
|
|
|
|
|
+ },
|
|
|
|
|
+ () => placeholder,
|
|
|
|
|
+ ),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ // 构建预览图片组
|
|
|
|
|
+ const previewImage = async (
|
|
|
|
|
+ file: UploadFile,
|
|
|
|
|
+ visible: Ref<boolean>,
|
|
|
|
|
+ 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 (file.url) {
|
|
|
|
|
+ window.open(file.url, '_blank');
|
|
|
|
|
+ } else if (file.preview) {
|
|
|
|
|
+ window.open(file.preview, '_blank');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.error($t('ui.formRules.previewWarning'));
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 对于图片文件,继续使用预览组
|
|
|
|
|
+ const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
|
|
|
|
+ Image,
|
|
|
|
|
+ PreviewGroup,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ const getBase64 = (file: File) => {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const reader = new FileReader();
|
|
|
|
|
+ reader.readAsDataURL(file);
|
|
|
|
|
+ reader.addEventListener('load', () => resolve(reader.result));
|
|
|
|
|
+ reader.addEventListener('error', (error) => reject(error));
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+ // 从fileList中过滤出所有图片文件
|
|
|
|
|
+ const imageFiles = (unref(fileList) || []).filter((element) =>
|
|
|
|
|
+ isImageFile(element),
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 为所有没有预览地址的图片生成预览
|
|
|
|
|
+ for (const imgFile of imageFiles) {
|
|
|
|
|
+ if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
|
|
|
|
+ imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const container: HTMLElement | null = document.createElement('div');
|
|
|
|
|
+ document.body.append(container);
|
|
|
|
|
+
|
|
|
|
|
+ // 用于追踪组件是否已卸载
|
|
|
|
|
+ let isUnmounted = false;
|
|
|
|
|
+
|
|
|
|
|
+ const PreviewWrapper = {
|
|
|
|
|
+ setup() {
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ if (isUnmounted) return null;
|
|
|
|
|
+ return h(
|
|
|
|
|
+ PreviewGroupComponent,
|
|
|
|
|
+ {
|
|
|
|
|
+ class: 'hidden',
|
|
|
|
|
+ preview: {
|
|
|
|
|
+ visible: visible.value,
|
|
|
|
|
+ // 设置初始显示的图片索引
|
|
|
|
|
+ current: imageFiles.findIndex((f) => f.uid === file.uid),
|
|
|
|
|
+ onVisibleChange: (value: boolean) => {
|
|
|
|
|
+ visible.value = value;
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ // 延迟清理,确保动画完成
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (!isUnmounted && container) {
|
|
|
|
|
+ isUnmounted = true;
|
|
|
|
|
+ render(null, container);
|
|
|
|
|
+ container.remove();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 300);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ () =>
|
|
|
|
|
+ // 渲染所有图片文件
|
|
|
|
|
+ imageFiles.map((imgFile) =>
|
|
|
|
|
+ h(ImageComponent, {
|
|
|
|
|
+ key: imgFile.uid,
|
|
|
|
|
+ src: imgFile.url || imgFile.preview,
|
|
|
|
|
+ }),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ render(h(PreviewWrapper), container);
|
|
|
|
|
+ };
|
|
|
return defineComponent({
|
|
return defineComponent({
|
|
|
name: Upload.name,
|
|
name: Upload.name,
|
|
|
- emits: ['change', 'update:modelValue'],
|
|
|
|
|
|
|
+ emits: ['update:modelValue'],
|
|
|
setup: (
|
|
setup: (
|
|
|
props: any,
|
|
props: any,
|
|
|
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
|
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
|
|
@@ -142,9 +285,19 @@ const withPreviewUpload = () => {
|
|
|
attrs?.fileList || attrs?.['file-list'] || [],
|
|
attrs?.fileList || attrs?.['file-list'] || [],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ const handleBeforeUpload = (file: UploadFile) => {
|
|
|
|
|
+ if (attrs.maxSize && (file.size || 0) / 1024 / 1024 > attrs.maxSize) {
|
|
|
|
|
+ message.error($t('ui.formRules.sizeLimit', [attrs.maxSize]));
|
|
|
|
|
+ file.status = 'removed';
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return attrs.beforeUpload?.(file) ?? true;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleChange = async (event: UploadChangeParam) => {
|
|
const handleChange = async (event: UploadChangeParam) => {
|
|
|
- fileList.value = event.fileList;
|
|
|
|
|
- emit('change', event);
|
|
|
|
|
|
|
+ fileList.value = event.fileList.filter(
|
|
|
|
|
+ (file) => file.status !== 'removed',
|
|
|
|
|
+ );
|
|
|
emit(
|
|
emit(
|
|
|
'update:modelValue',
|
|
'update:modelValue',
|
|
|
event.fileList?.length ? fileList.value : undefined,
|
|
event.fileList?.length ? fileList.value : undefined,
|
|
@@ -185,6 +338,7 @@ const withPreviewUpload = () => {
|
|
|
...props,
|
|
...props,
|
|
|
...attrs,
|
|
...attrs,
|
|
|
fileList: fileList.value,
|
|
fileList: fileList.value,
|
|
|
|
|
+ beforeUpload: handleBeforeUpload,
|
|
|
onChange: handleChange,
|
|
onChange: handleChange,
|
|
|
onPreview: handlePreview,
|
|
onPreview: handlePreview,
|
|
|
},
|
|
},
|
|
@@ -194,151 +348,13 @@ const withPreviewUpload = () => {
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const createDefaultSlotsWithUpload = (
|
|
|
|
|
- listType: string,
|
|
|
|
|
- placeholder: string,
|
|
|
|
|
-) => {
|
|
|
|
|
- switch (listType) {
|
|
|
|
|
- case 'picture-card': {
|
|
|
|
|
- return {
|
|
|
|
|
- default: () => placeholder,
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
- default: {
|
|
|
|
|
- return {
|
|
|
|
|
- default: () =>
|
|
|
|
|
- h(
|
|
|
|
|
- Button,
|
|
|
|
|
- {
|
|
|
|
|
- icon: h(IconifyIcon, {
|
|
|
|
|
- icon: 'ant-design:upload-outlined',
|
|
|
|
|
- class: 'mb-1 size-4',
|
|
|
|
|
- }),
|
|
|
|
|
- },
|
|
|
|
|
- () => placeholder,
|
|
|
|
|
- ),
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-const previewImage = async (
|
|
|
|
|
- file: UploadFile,
|
|
|
|
|
- visible: Ref<boolean>,
|
|
|
|
|
- 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 (file.url) {
|
|
|
|
|
- window.open(file.url, '_blank');
|
|
|
|
|
- } else if (file.preview) {
|
|
|
|
|
- window.open(file.preview, '_blank');
|
|
|
|
|
- } else {
|
|
|
|
|
- console.warn('无法打开文件,没有可用的URL或预览地址');
|
|
|
|
|
- }
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 对于图片文件,继续使用预览组
|
|
|
|
|
- const [ImageComponent, PreviewGroupComponent] = await Promise.all([
|
|
|
|
|
- Image,
|
|
|
|
|
- PreviewGroup,
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- const getBase64 = (file: File) => {
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- const reader = new FileReader();
|
|
|
|
|
- reader.readAsDataURL(file);
|
|
|
|
|
- reader.addEventListener('load', () => resolve(reader.result));
|
|
|
|
|
- reader.addEventListener('error', (error) => reject(error));
|
|
|
|
|
- });
|
|
|
|
|
- };
|
|
|
|
|
- // 从fileList中过滤出所有图片文件
|
|
|
|
|
- const imageFiles = (unref(fileList) || []).filter((element) =>
|
|
|
|
|
- isImageFile(element),
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- // 为所有没有预览地址的图片生成预览
|
|
|
|
|
- for (const imgFile of imageFiles) {
|
|
|
|
|
- if (!imgFile.url && !imgFile.preview && imgFile.originFileObj) {
|
|
|
|
|
- imgFile.preview = (await getBase64(imgFile.originFileObj)) as string;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- const container: HTMLElement | null = document.createElement('div');
|
|
|
|
|
- document.body.append(container);
|
|
|
|
|
-
|
|
|
|
|
- // 用于追踪组件是否已卸载
|
|
|
|
|
- let isUnmounted = false;
|
|
|
|
|
-
|
|
|
|
|
- const PreviewWrapper = {
|
|
|
|
|
- setup() {
|
|
|
|
|
- return () => {
|
|
|
|
|
- if (isUnmounted) return null;
|
|
|
|
|
- return h(
|
|
|
|
|
- PreviewGroupComponent,
|
|
|
|
|
- {
|
|
|
|
|
- class: 'hidden',
|
|
|
|
|
- preview: {
|
|
|
|
|
- visible: visible.value,
|
|
|
|
|
- // 设置初始显示的图片索引
|
|
|
|
|
- current: imageFiles.findIndex((f) => f.uid === file.uid),
|
|
|
|
|
- onVisibleChange: (value: boolean) => {
|
|
|
|
|
- visible.value = value;
|
|
|
|
|
- if (!value) {
|
|
|
|
|
- // 延迟清理,确保动画完成
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- if (!isUnmounted && container) {
|
|
|
|
|
- isUnmounted = true;
|
|
|
|
|
- render(null, container);
|
|
|
|
|
- container.remove();
|
|
|
|
|
- }
|
|
|
|
|
- }, 300);
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- () =>
|
|
|
|
|
- // 渲染所有图片文件
|
|
|
|
|
- imageFiles.map((imgFile) =>
|
|
|
|
|
- h(ImageComponent, {
|
|
|
|
|
- key: imgFile.uid,
|
|
|
|
|
- src: imgFile.url || imgFile.preview,
|
|
|
|
|
- }),
|
|
|
|
|
- ),
|
|
|
|
|
- );
|
|
|
|
|
- };
|
|
|
|
|
- },
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- render(h(PreviewWrapper), container);
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
|
|
export type ComponentType =
|
|
export type ComponentType =
|
|
|
|
|
+ | 'ApiCascader'
|
|
|
| 'ApiSelect'
|
|
| 'ApiSelect'
|
|
|
| 'ApiTreeSelect'
|
|
| 'ApiTreeSelect'
|
|
|
| 'AutoComplete'
|
|
| 'AutoComplete'
|
|
|
|
|
+ | 'Cascader'
|
|
|
| 'Checkbox'
|
|
| 'Checkbox'
|
|
|
| 'CheckboxGroup'
|
|
| 'CheckboxGroup'
|
|
|
| 'DatePicker'
|
|
| 'DatePicker'
|
|
@@ -369,6 +385,13 @@ async function initComponentAdapter() {
|
|
|
// Button: () =>
|
|
// Button: () =>
|
|
|
// import('xxx').then((res) => res.Button),
|
|
// import('xxx').then((res) => res.Button),
|
|
|
|
|
|
|
|
|
|
+ ApiCascader: withDefaultPlaceholder(ApiComponent, 'select', {
|
|
|
|
|
+ component: Cascader,
|
|
|
|
|
+ fieldNames: { label: 'label', value: 'value', children: 'children' },
|
|
|
|
|
+ loadingSlot: 'suffixIcon',
|
|
|
|
|
+ modelPropName: 'value',
|
|
|
|
|
+ visibleEvent: 'onVisibleChange',
|
|
|
|
|
+ }),
|
|
|
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
|
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
|
|
component: Select,
|
|
component: Select,
|
|
|
loadingSlot: 'suffixIcon',
|
|
loadingSlot: 'suffixIcon',
|
|
@@ -384,6 +407,7 @@ async function initComponentAdapter() {
|
|
|
visibleEvent: 'onVisibleChange',
|
|
visibleEvent: 'onVisibleChange',
|
|
|
}),
|
|
}),
|
|
|
AutoComplete,
|
|
AutoComplete,
|
|
|
|
|
+ Cascader,
|
|
|
Checkbox,
|
|
Checkbox,
|
|
|
CheckboxGroup,
|
|
CheckboxGroup,
|
|
|
DatePicker,
|
|
DatePicker,
|