浏览代码

Merge branch 'main' of https://github.com/xingyu4j/vue-vben-admin into fix

xingyu4j 5 月之前
父节点
当前提交
ce7b7b910a

+ 168 - 144
apps/web-antd/src/adapter/component/index.ts

@@ -29,7 +29,7 @@ import { IconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
 import { isEmpty } from '@vben/utils';
 
-import { notification } from 'ant-design-vue';
+import { message, notification } from 'ant-design-vue';
 
 const AutoComplete = defineAsyncComponent(
   () => import('ant-design-vue/es/auto-complete'),
@@ -75,6 +75,9 @@ const TimePicker = defineAsyncComponent(
 const TreeSelect = defineAsyncComponent(
   () => 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 Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
 const PreviewGroup = defineAsyncComponent(() =>
@@ -116,9 +119,149 @@ const withDefaultPlaceholder = <T extends Component>(
 };
 
 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({
     name: Upload.name,
-    emits: ['change', 'update:modelValue'],
+    emits: ['update:modelValue'],
     setup: (
       props: any,
       { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
@@ -133,9 +276,19 @@ const withPreviewUpload = () => {
         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) => {
-        fileList.value = event.fileList;
-        emit('change', event);
+        fileList.value = event.fileList.filter(
+          (file) => file.status !== 'removed',
+        );
         emit(
           'update:modelValue',
           event.fileList?.length ? fileList.value : undefined,
@@ -176,6 +329,7 @@ const withPreviewUpload = () => {
             ...props,
             ...attrs,
             fileList: fileList.value,
+            beforeUpload: handleBeforeUpload,
             onChange: handleChange,
             onPreview: handlePreview,
           },
@@ -185,151 +339,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 =
+  | 'ApiCascader'
   | 'ApiSelect'
   | 'ApiTreeSelect'
   | 'AutoComplete'
+  | 'Cascader'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
@@ -359,6 +375,13 @@ async function initComponentAdapter() {
     // 如果你的组件体积比较大,可以使用异步加载
     // 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,
@@ -388,6 +411,7 @@ async function initComponentAdapter() {
       },
     ),
     AutoComplete,
+    Cascader,
     Checkbox,
     CheckboxGroup,
     DatePicker,

+ 2 - 2
docs/src/guide/introduction/thin.md

@@ -24,7 +24,7 @@ apps/web-naive
 
 ## 演示代码精简
 
-如果你不需要演示代码,你可以直接删除的`playground`文件夹。
+如果你不需要演示代码,你可以直接删除 `playground` 文件夹。
 
 ## 文档精简
 
@@ -88,7 +88,7 @@ pnpm install
 
 - 在应用的 `src/router/routes` 文件中,你可以删除不需要的路由。其中 `core` 文件夹内,如果只需要登录和忘记密码,你可以删除其他路由,如忘记密码、注册等。路由删除后,你可以删除对应的页面文件,在 `src/views/_core` 文件夹中。
 
-- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以删除对应的页面文件,在 `src/views` 文件夹中。
+- 在应用的 `src/router/routes` 文件中,你可以按需求删除不需要的路由,如`demos`、`vben` 目录等。路由删除后,你可以在 `src/views` 文件夹中删除对应的页面文件
 
 ### 删除不需要的组件
 

+ 29 - 1
packages/@core/base/shared/src/utils/tree.ts

@@ -94,4 +94,32 @@ function mapTree<T, V extends Record<string, any>>(
   });
 }
 
-export { filterTree, mapTree, traverseTreeValues };
+/**
+ * 对树形结构数据进行递归排序
+ * @param treeData - 树形数据数组
+ * @param sortFunction - 排序函数,用于定义排序规则
+ * @param options - 配置选项,包括子节点属性名
+ * @returns 排序后的树形数据
+ */
+function sortTree<T extends Record<string, any>>(
+  treeData: T[],
+  sortFunction: (a: T, b: T) => number,
+  options?: TreeConfigOptions,
+): T[] {
+  const { childProps } = options || {
+    childProps: 'children',
+  };
+
+  return treeData.toSorted(sortFunction).map((item) => {
+    const children = item[childProps];
+    if (children && Array.isArray(children) && children.length > 0) {
+      return {
+        ...item,
+        [childProps]: sortTree(children, sortFunction, options),
+      };
+    }
+    return item;
+  });
+}
+
+export { filterTree, mapTree, sortTree, traverseTreeValues };

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/src/components/context-menu/context-menu.vue

@@ -73,6 +73,7 @@ function handleClick(menu: IContextMenuItem) {
     >
       <template v-for="menu in menusView" :key="menu.key">
         <ContextMenuItem
+          v-if="!menu.hidden"
           :class="itemClass"
           :disabled="menu.disabled"
           :inset="menu.inset || !menu.icon"

+ 4 - 0
packages/@core/ui-kit/shadcn-ui/src/components/context-menu/interface.ts

@@ -10,6 +10,10 @@ interface IContextMenuItem {
    * @param data
    */
   handler?: (data: any) => void;
+  /**
+   * @zh_CN 是否隐藏
+   */
+  hidden?: boolean;
   /**
    * @zh_CN 图标
    */

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-radio-menu.vue

@@ -27,7 +27,7 @@ function handleItemClick(value: string) {
     </DropdownMenuTrigger>
     <DropdownMenuContent align="start">
       <DropdownMenuGroup>
-        <template v-for="(menu, index) in menus" :key="index">
+        <template v-for="menu in menus" :key="menu.value">
           <DropdownMenuItem
             :class="
               menu.value === modelValue

+ 1 - 0
packages/effects/common-ui/src/components/index.ts

@@ -20,6 +20,7 @@ export {
   VbenButtonGroup,
   VbenCheckbox,
   VbenCheckButtonGroup,
+  VbenContextMenu,
   VbenCountToAnimator,
   VbenFullScreen,
   VbenInputPassword,

+ 4 - 2
packages/locales/src/langs/en-US/ui.json

@@ -7,7 +7,9 @@
     "length": "{0} must be {1} characters long",
     "alreadyExists": "{0} `{1}` already exists",
     "startWith": "{0} must start with `{1}`",
-    "invalidURL": "Please input a valid URL"
+    "invalidURL": "Please input a valid URL",
+    "sizeLimit": "The file size cannot exceed {0}MB",
+    "previewWarning": "Unable to open the file, there is no available URL or preview address"
   },
   "actionTitle": {
     "edit": "Modify {0}",
@@ -25,7 +27,7 @@
   "placeholder": {
     "input": "Please enter",
     "select": "Please select",
-    "upload": "Please upload"
+    "upload": "Click to upload"
   },
   "captcha": {
     "title": "Please complete the security verification",

+ 4 - 2
packages/locales/src/langs/zh-CN/ui.json

@@ -7,7 +7,9 @@
     "length": "{0}长度必须为{1}个字符",
     "alreadyExists": "{0} `{1}` 已存在",
     "startWith": "{0}必须以 {1} 开头",
-    "invalidURL": "请输入有效的链接"
+    "invalidURL": "请输入有效的链接",
+    "sizeLimit": "文件大小不能超过 {0}MB",
+    "previewWarning": "无法打开文件,没有可用的URL或预览地址"
   },
   "actionTitle": {
     "edit": "修改{0}",
@@ -25,7 +27,7 @@
   "placeholder": {
     "input": "请输入",
     "select": "请选择",
-    "upload": "上传"
+    "upload": "点击上传"
   },
   "captcha": {
     "title": "请完成安全验证",

+ 2 - 2
packages/utils/src/helpers/generate-menus.ts

@@ -6,7 +6,7 @@ import type {
   RouteMeta,
 } from '@vben-core/typings';
 
-import { filterTree, mapTree } from '@vben-core/shared/utils';
+import { filterTree, mapTree, sortTree } from '@vben-core/shared/utils';
 
 /**
  * 根据 routes 生成菜单列表
@@ -81,7 +81,7 @@ function generateMenus(
   });
 
   // 对菜单进行排序,避免order=0时被替换成999的问题
-  menus = menus.toSorted((a, b) => (a?.order ?? 999) - (b?.order ?? 999));
+  menus = sortTree(menus, (a, b) => (a?.order ?? 999) - (b?.order ?? 999));
 
   // 过滤掉隐藏的菜单项
   return filterTree(menus, (menu) => !!menu.show);

+ 1 - 0
playground/package.json

@@ -31,6 +31,7 @@
   "dependencies": {
     "@tanstack/vue-query": "catalog:",
     "@vben-core/menu-ui": "workspace:*",
+    "@vben-core/shadcn-ui": "workspace:*",
     "@vben/access": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",

+ 168 - 144
playground/src/adapter/component/index.ts

@@ -29,7 +29,7 @@ import { IconifyIcon } from '@vben/icons';
 import { $t } from '@vben/locales';
 import { isEmpty } from '@vben/utils';
 
-import { notification } from 'ant-design-vue';
+import { message, notification } from 'ant-design-vue';
 
 const AutoComplete = defineAsyncComponent(
   () => import('ant-design-vue/es/auto-complete'),
@@ -75,6 +75,9 @@ const TimePicker = defineAsyncComponent(
 const TreeSelect = defineAsyncComponent(
   () => 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 Image = defineAsyncComponent(() => import('ant-design-vue/es/image'));
 const PreviewGroup = defineAsyncComponent(() =>
@@ -125,9 +128,149 @@ const withDefaultPlaceholder = <T extends Component>(
 };
 
 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({
     name: Upload.name,
-    emits: ['change', 'update:modelValue'],
+    emits: ['update:modelValue'],
     setup: (
       props: any,
       { attrs, slots, emit }: { attrs: any; emit: any; slots: any },
@@ -142,9 +285,19 @@ const withPreviewUpload = () => {
         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) => {
-        fileList.value = event.fileList;
-        emit('change', event);
+        fileList.value = event.fileList.filter(
+          (file) => file.status !== 'removed',
+        );
         emit(
           'update:modelValue',
           event.fileList?.length ? fileList.value : undefined,
@@ -185,6 +338,7 @@ const withPreviewUpload = () => {
             ...props,
             ...attrs,
             fileList: fileList.value,
+            beforeUpload: handleBeforeUpload,
             onChange: handleChange,
             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 =
+  | 'ApiCascader'
   | 'ApiSelect'
   | 'ApiTreeSelect'
   | 'AutoComplete'
+  | 'Cascader'
   | 'Checkbox'
   | 'CheckboxGroup'
   | 'DatePicker'
@@ -369,6 +385,13 @@ async function initComponentAdapter() {
     // 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', {
       component: Select,
       loadingSlot: 'suffixIcon',
@@ -384,6 +407,7 @@ async function initComponentAdapter() {
       visibleEvent: 'onVisibleChange',
     }),
     AutoComplete,
+    Cascader,
     Checkbox,
     CheckboxGroup,
     DatePicker,

+ 3 - 0
playground/src/locales/langs/en-US/examples.json

@@ -72,5 +72,8 @@
   },
   "button-group": {
     "title": "Button Group"
+  },
+  "function": {
+    "contentMenu": "Content Menu"
   }
 }

+ 3 - 0
playground/src/locales/langs/zh-CN/examples.json

@@ -72,5 +72,8 @@
   },
   "button-group": {
     "title": "按钮组"
+  },
+  "function": {
+    "contentMenu": "上下文菜单"
   }
 }

+ 9 - 0
playground/src/router/routes/modules/examples.ts

@@ -328,6 +328,15 @@ const routes: RouteRecordRaw[] = [
           title: $t('examples.button-group.title'),
         },
       },
+      {
+        name: 'ContextMenu',
+        path: '/examples/context-menu',
+        component: () => import('#/views/examples/context-menu/index.vue'),
+        meta: {
+          icon: 'mdi:menu',
+          title: $t('examples.function.contentMenu'),
+        },
+      },
     ],
   },
 ];

+ 60 - 0
playground/src/views/examples/context-menu/index.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+
+import { VbenContextMenu } from '@vben-core/shadcn-ui';
+
+import { Button, Card, message } from 'ant-design-vue';
+
+const needHidden = (role: string) => {
+  return role === 'user';
+};
+
+const contextMenus = () => {
+  return [
+    {
+      text: '刷新',
+      key: 'refresh',
+      handler: (data: any) => {
+        message.success('刷新成功', data);
+      },
+      hidden: needHidden('admin'),
+    },
+    {
+      text: '关闭当前',
+      key: 'close-current',
+      handler: (data: any) => {
+        message.success('关闭当前', data);
+      },
+      hidden: needHidden('user'),
+    },
+    {
+      text: '关闭其他',
+      key: 'close-other',
+      handler: (data: any) => {
+        message.success('关闭其他', data);
+      },
+    },
+    {
+      text: '关闭所有',
+      key: 'close-all',
+      handler: (data: any) => {
+        message.success('关闭所有', data);
+      },
+    },
+  ];
+};
+
+</script>
+
+<template>
+  <Page title="Context Menu 上下文菜单">
+    <Card title="基本使用">
+      <div>一共四个菜单(刷新、关闭当前、关闭其他、关闭所有)</div>
+      <br/>
+      <br/>
+      <VbenContextMenu :menus="contextMenus" :modal="true" item-class="pr-6">
+        <Button> 右键点击我打开上下文菜单(有隐藏项) </Button>
+      </VbenContextMenu>
+    </Card>
+  </Page>
+</template>

+ 3 - 1
playground/src/views/examples/form/basic.vue

@@ -342,6 +342,8 @@ const [BaseForm, baseFormApi] = useVbenForm({
         customRequest: upload_file,
         disabled: false,
         maxCount: 1,
+        // 单位:MB
+        maxSize: 2,
         multiple: false,
         showUploadList: true,
         // 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
@@ -354,7 +356,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
           default: () => $t('examples.form.upload-image'),
         };
       },
-      rules: 'required',
+      rules: 'selectRequired',
     },
   ],
   // 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个

文件差异内容过多而无法显示
+ 201 - 186
pnpm-lock.yaml


部分文件因为文件数量过多而无法显示