mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-07 23:31:08 +08:00
Merge branch 'main' of https://github.com/xingyu4j/vue-vben-admin into fix
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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` 文件夹中删除对应的页面文件。
|
||||
|
||||
### 删除不需要的组件
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,6 +10,10 @@ interface IContextMenuItem {
|
||||
* @param data
|
||||
*/
|
||||
handler?: (data: any) => void;
|
||||
/**
|
||||
* @zh_CN 是否隐藏
|
||||
*/
|
||||
hidden?: boolean;
|
||||
/**
|
||||
* @zh_CN 图标
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,7 @@ export {
|
||||
VbenButtonGroup,
|
||||
VbenCheckbox,
|
||||
VbenCheckButtonGroup,
|
||||
VbenContextMenu,
|
||||
VbenCountToAnimator,
|
||||
VbenFullScreen,
|
||||
VbenInputPassword,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "请完成安全验证",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
},
|
||||
"button-group": {
|
||||
"title": "Button Group"
|
||||
},
|
||||
"function": {
|
||||
"contentMenu": "Content Menu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
},
|
||||
"button-group": {
|
||||
"title": "按钮组"
|
||||
},
|
||||
"function": {
|
||||
"contentMenu": "上下文菜单"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
playground/src/views/examples/context-menu/index.vue
Normal file
60
playground/src/views/examples/context-menu/index.vue
Normal file
@@ -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>
|
||||
@@ -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个
|
||||
|
||||
1104
pnpm-lock.yaml
generated
1104
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user