Browse Source

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

pull/6997/head
xingyu4j 1 month ago
parent
commit
ce7b7b910a
  1. 178
      apps/web-antd/src/adapter/component/index.ts
  2. 4
      docs/src/guide/introduction/thin.md
  3. 30
      packages/@core/base/shared/src/utils/tree.ts
  4. 1
      packages/@core/ui-kit/shadcn-ui/src/components/context-menu/context-menu.vue
  5. 4
      packages/@core/ui-kit/shadcn-ui/src/components/context-menu/interface.ts
  6. 2
      packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-radio-menu.vue
  7. 1
      packages/effects/common-ui/src/components/index.ts
  8. 6
      packages/locales/src/langs/en-US/ui.json
  9. 6
      packages/locales/src/langs/zh-CN/ui.json
  10. 4
      packages/utils/src/helpers/generate-menus.ts
  11. 1
      playground/package.json
  12. 178
      playground/src/adapter/component/index.ts
  13. 3
      playground/src/locales/langs/en-US/examples.json
  14. 3
      playground/src/locales/langs/zh-CN/examples.json
  15. 9
      playground/src/router/routes/modules/examples.ts
  16. 60
      playground/src/views/examples/context-menu/index.vue
  17. 4
      playground/src/views/examples/form/basic.vue
  18. 1106
      pnpm-lock.yaml

178
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,79 +119,11 @@ const withDefaultPlaceholder = <T extends Component>(
};
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['change', 'update:modelValue'],
setup: (
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const handleChange = async (event: UploadChangeParam) => {
fileList.value = event.fileList;
emit('change', event);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
);
},
});
};
const createDefaultSlotsWithUpload = (
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
) => {
switch (listType) {
case 'picture-card': {
return {
@ -211,13 +146,13 @@ const createDefaultSlotsWithUpload = (
};
}
}
};
const previewImage = async (
};
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
) => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
@ -246,7 +181,7 @@ const previewImage = async (
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
console.warn('无法打开文件,没有可用的URL或预览地址');
message.error($t('ui.formRules.previewWarning'));
}
return;
}
@ -323,13 +258,94 @@ const previewImage = async (
};
render(h(PreviewWrapper), container);
};
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup: (
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
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.filter(
(file) => file.status !== 'removed',
);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
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,

4
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` 文件夹中删除对应的页面文件
### 删除不需要的组件

30
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
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
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
*/

2
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
packages/effects/common-ui/src/components/index.ts

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

6
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",

6
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": "请完成安全验证",

4
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
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:*",

178
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,79 +128,11 @@ const withDefaultPlaceholder = <T extends Component>(
};
const withPreviewUpload = () => {
return defineComponent({
name: Upload.name,
emits: ['change', 'update:modelValue'],
setup: (
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
attrs?.fileList || attrs?.['file-list'] || [],
);
const handleChange = async (event: UploadChangeParam) => {
fileList.value = event.fileList;
emit('change', event);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
);
},
});
};
const createDefaultSlotsWithUpload = (
// 创建默认的上传按钮插槽
const createDefaultSlotsWithUpload = (
listType: string,
placeholder: string,
) => {
) => {
switch (listType) {
case 'picture-card': {
return {
@ -220,13 +155,13 @@ const createDefaultSlotsWithUpload = (
};
}
}
};
const previewImage = async (
};
// 构建预览图片组
const previewImage = async (
file: UploadFile,
visible: Ref<boolean>,
fileList: Ref<UploadProps['fileList']>,
) => {
) => {
// 检查是否为图片文件的辅助函数
const isImageFile = (file: UploadFile): boolean => {
const imageExtensions = new Set([
@ -255,7 +190,7 @@ const previewImage = async (
} else if (file.preview) {
window.open(file.preview, '_blank');
} else {
console.warn('无法打开文件,没有可用的URL或预览地址');
message.error($t('ui.formRules.previewWarning'));
}
return;
}
@ -332,13 +267,94 @@ const previewImage = async (
};
render(h(PreviewWrapper), container);
};
return defineComponent({
name: Upload.name,
emits: ['update:modelValue'],
setup: (
props: any,
{ attrs, slots, emit }: { attrs: any; emit: any; slots: any },
) => {
const previewVisible = ref<boolean>(false);
const placeholder = attrs?.placeholder || $t(`ui.placeholder.upload`);
const listType = attrs?.listType || attrs?.['list-type'] || 'text';
const fileList = ref<UploadProps['fileList']>(
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.filter(
(file) => file.status !== 'removed',
);
emit(
'update:modelValue',
event.fileList?.length ? fileList.value : undefined,
);
};
const handlePreview = async (file: UploadFile) => {
previewVisible.value = true;
await previewImage(file, previewVisible, fileList);
};
const renderUploadButton = (): any => {
const isDisabled = attrs.disabled;
// 如果禁用,不渲染上传按钮
if (isDisabled) {
return null;
}
// 否则渲染默认上传按钮
return isEmpty(slots)
? createDefaultSlotsWithUpload(listType, placeholder)
: slots;
};
// 可以监听到表单API设置的值
watch(
() => attrs.modelValue,
(res) => {
fileList.value = res;
},
);
return () =>
h(
Upload,
{
...props,
...attrs,
fileList: fileList.value,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onPreview: handlePreview,
},
renderUploadButton(),
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
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
playground/src/locales/langs/en-US/examples.json

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

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

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

9
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
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>

4
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',
},
],
// 321

1106
pnpm-lock.yaml

File diff suppressed because it is too large
Loading…
Cancel
Save