mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-08 07:31:09 +08:00
merge
This commit is contained in:
@@ -127,6 +127,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
|
|
||||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||||
export type ComponentType =
|
export type ComponentType =
|
||||||
|
| 'ApiCascader'
|
||||||
| 'ApiSelect'
|
| 'ApiSelect'
|
||||||
| 'ApiTreeSelect'
|
| 'ApiTreeSelect'
|
||||||
| 'AutoComplete'
|
| 'AutoComplete'
|
||||||
@@ -166,6 +167,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(
|
ApiSelect: withDefaultPlaceholder(
|
||||||
{
|
{
|
||||||
...ApiComponent,
|
...ApiComponent,
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ if (!import.meta.env.SSR) {
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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` 文件中,你可以删除不需要的路由。其中 `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">
|
<template v-for="menu in menusView" :key="menu.key">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
|
v-if="!menu.hidden"
|
||||||
:class="itemClass"
|
:class="itemClass"
|
||||||
:disabled="menu.disabled"
|
:disabled="menu.disabled"
|
||||||
:inset="menu.inset || !menu.icon"
|
:inset="menu.inset || !menu.icon"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ interface IContextMenuItem {
|
|||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
handler?: (data: any) => void;
|
handler?: (data: any) => void;
|
||||||
|
/**
|
||||||
|
* @zh_CN 是否隐藏
|
||||||
|
*/
|
||||||
|
hidden?: boolean;
|
||||||
/**
|
/**
|
||||||
* @zh_CN 图标
|
* @zh_CN 图标
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function handleItemClick(value: string) {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<template v-for="menu in menus" :key="menu.key">
|
<template v-for="menu in menus" :key="menu.value">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
:class="
|
:class="
|
||||||
menu.value === modelValue
|
menu.value === modelValue
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface Props {
|
|||||||
childrenField?: string;
|
childrenField?: string;
|
||||||
/** value字段名 */
|
/** value字段名 */
|
||||||
valueField?: string;
|
valueField?: string;
|
||||||
|
/** disabled字段名 */
|
||||||
|
disabledField?: string;
|
||||||
/** 组件接收options数据的属性名 */
|
/** 组件接收options数据的属性名 */
|
||||||
optionsPropName?: string;
|
optionsPropName?: string;
|
||||||
/** 是否立即调用api */
|
/** 是否立即调用api */
|
||||||
@@ -75,6 +77,7 @@ defineOptions({ name: 'ApiComponent', inheritAttrs: false });
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
labelField: 'label',
|
labelField: 'label',
|
||||||
valueField: 'value',
|
valueField: 'value',
|
||||||
|
disabledField: 'disabled',
|
||||||
childrenField: '',
|
childrenField: '',
|
||||||
optionsPropName: 'options',
|
optionsPropName: 'options',
|
||||||
resultField: '',
|
resultField: '',
|
||||||
@@ -108,17 +111,25 @@ const isFirstLoaded = ref(false);
|
|||||||
const hasPendingRequest = ref(false);
|
const hasPendingRequest = ref(false);
|
||||||
|
|
||||||
const getOptions = computed(() => {
|
const getOptions = computed(() => {
|
||||||
const { labelField, valueField, childrenField, numberToString } = props;
|
const {
|
||||||
|
labelField,
|
||||||
|
valueField,
|
||||||
|
disabledField,
|
||||||
|
childrenField,
|
||||||
|
numberToString,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const refOptionsData = unref(refOptions);
|
const refOptionsData = unref(refOptions);
|
||||||
|
|
||||||
function transformData(data: OptionsItem[]): OptionsItem[] {
|
function transformData(data: OptionsItem[]): OptionsItem[] {
|
||||||
return data.map((item) => {
|
return data.map((item) => {
|
||||||
const value = get(item, valueField);
|
const value = get(item, valueField);
|
||||||
|
const disabled = get(item, disabledField);
|
||||||
return {
|
return {
|
||||||
...objectOmit(item, [labelField, valueField, childrenField]),
|
...objectOmit(item, [labelField, valueField, disabled, childrenField]),
|
||||||
label: get(item, labelField),
|
label: get(item, labelField),
|
||||||
value: numberToString ? `${value}` : value,
|
value: numberToString ? `${value}` : value,
|
||||||
|
disabled: get(item, disabledField),
|
||||||
...(childrenField && item[childrenField]
|
...(childrenField && item[childrenField]
|
||||||
? { children: transformData(item[childrenField]) }
|
? { children: transformData(item[childrenField]) }
|
||||||
: {}),
|
: {}),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export {
|
|||||||
VbenButtonGroup,
|
VbenButtonGroup,
|
||||||
VbenCheckbox,
|
VbenCheckbox,
|
||||||
VbenCheckButtonGroup,
|
VbenCheckButtonGroup,
|
||||||
|
VbenContextMenu,
|
||||||
VbenCountToAnimator,
|
VbenCountToAnimator,
|
||||||
VbenFullScreen,
|
VbenFullScreen,
|
||||||
VbenInputPassword,
|
VbenInputPassword,
|
||||||
|
|||||||
@@ -488,6 +488,6 @@ async function handleReset() {
|
|||||||
:deep(.sticky-tabs-header [role='tablist']) {
|
:deep(.sticky-tabs-header [role='tablist']) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -12px;
|
top: -12px;
|
||||||
z-index: 10;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,10 +13,28 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
|||||||
const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');
|
const xmlDoc = parser.parseFromString(svgData, 'image/svg+xml');
|
||||||
const svgElement = xmlDoc.documentElement;
|
const svgElement = xmlDoc.documentElement;
|
||||||
|
|
||||||
|
// 提取 SVG 根元素的关键样式属性
|
||||||
|
const getAttrs = (el: Element, attrs: string[]) =>
|
||||||
|
attrs
|
||||||
|
.map((attr) =>
|
||||||
|
el.hasAttribute(attr) ? `${attr}="${el.getAttribute(attr)}"` : '',
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
const rootAttrs = getAttrs(svgElement, [
|
||||||
|
'fill',
|
||||||
|
'stroke',
|
||||||
|
'fill-rule',
|
||||||
|
'stroke-width',
|
||||||
|
]);
|
||||||
|
|
||||||
const svgContent = [...svgElement.childNodes]
|
const svgContent = [...svgElement.childNodes]
|
||||||
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
|
||||||
.map((node) => new XMLSerializer().serializeToString(node))
|
.map((node) => new XMLSerializer().serializeToString(node))
|
||||||
.join('');
|
.join('');
|
||||||
|
// 若根有属性,用一个 g 标签包裹内容并继承属性
|
||||||
|
const body = rootAttrs ? `<g ${rootAttrs}>${svgContent}</g>` : svgContent;
|
||||||
|
|
||||||
const viewBoxValue = svgElement.getAttribute('viewBox') || '';
|
const viewBoxValue = svgElement.getAttribute('viewBox') || '';
|
||||||
const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {
|
const [left, top, width, height] = viewBoxValue.split(' ').map((val) => {
|
||||||
@@ -25,7 +43,7 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body: svgContent,
|
body,
|
||||||
height,
|
height,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"length": "{0} must be {1} characters long",
|
"length": "{0} must be {1} characters long",
|
||||||
"alreadyExists": "{0} `{1}` already exists",
|
"alreadyExists": "{0} `{1}` already exists",
|
||||||
"startWith": "{0} must start with `{1}`",
|
"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": {
|
"actionTitle": {
|
||||||
"edit": "Modify {0}",
|
"edit": "Modify {0}",
|
||||||
@@ -24,7 +26,8 @@
|
|||||||
},
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"input": "Please enter",
|
"input": "Please enter",
|
||||||
"select": "Please select"
|
"select": "Please select",
|
||||||
|
"upload": "Click to upload"
|
||||||
},
|
},
|
||||||
"captcha": {
|
"captcha": {
|
||||||
"title": "Please complete the security verification",
|
"title": "Please complete the security verification",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"length": "{0}长度必须为{1}个字符",
|
"length": "{0}长度必须为{1}个字符",
|
||||||
"alreadyExists": "{0} `{1}` 已存在",
|
"alreadyExists": "{0} `{1}` 已存在",
|
||||||
"startWith": "{0}必须以 {1} 开头",
|
"startWith": "{0}必须以 {1} 开头",
|
||||||
"invalidURL": "请输入有效的链接"
|
"invalidURL": "请输入有效的链接",
|
||||||
|
"sizeLimit": "文件大小不能超过 {0}MB",
|
||||||
|
"previewWarning": "无法打开文件,没有可用的URL或预览地址"
|
||||||
},
|
},
|
||||||
"actionTitle": {
|
"actionTitle": {
|
||||||
"edit": "修改{0}",
|
"edit": "修改{0}",
|
||||||
@@ -24,7 +26,8 @@
|
|||||||
},
|
},
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"input": "请输入",
|
"input": "请输入",
|
||||||
"select": "请选择"
|
"select": "请选择",
|
||||||
|
"upload": "点击上传"
|
||||||
},
|
},
|
||||||
"captcha": {
|
"captcha": {
|
||||||
"title": "请完成安全验证",
|
"title": "请完成安全验证",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
RouteMeta,
|
RouteMeta,
|
||||||
} from '@vben-core/typings';
|
} from '@vben-core/typings';
|
||||||
|
|
||||||
import { filterTree, mapTree } from '@vben-core/shared/utils';
|
import { filterTree, mapTree, sortTree } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 routes 生成菜单列表
|
* 根据 routes 生成菜单列表
|
||||||
@@ -81,7 +81,7 @@ function generateMenus(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 对菜单进行排序,避免order=0时被替换成999的问题
|
// 对菜单进行排序,避免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);
|
return filterTree(menus, (menu) => !!menu.show);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/vue-query": "catalog:",
|
"@tanstack/vue-query": "catalog:",
|
||||||
"@vben-core/menu-ui": "workspace:*",
|
"@vben-core/menu-ui": "workspace:*",
|
||||||
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben/access": "workspace:*",
|
"@vben/access": "workspace:*",
|
||||||
"@vben/common-ui": "workspace:*",
|
"@vben/common-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
|
|||||||
@@ -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,75 +128,7 @@ const withDefaultPlaceholder = <T extends Component>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const withPreviewUpload = () => {
|
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,
|
listType: string,
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
@@ -221,7 +156,7 @@ const createDefaultSlotsWithUpload = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 构建预览图片组
|
||||||
const previewImage = async (
|
const previewImage = async (
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
visible: Ref<boolean>,
|
visible: Ref<boolean>,
|
||||||
@@ -255,7 +190,7 @@ const previewImage = async (
|
|||||||
} else if (file.preview) {
|
} else if (file.preview) {
|
||||||
window.open(file.preview, '_blank');
|
window.open(file.preview, '_blank');
|
||||||
} else {
|
} else {
|
||||||
console.warn('无法打开文件,没有可用的URL或预览地址');
|
message.error($t('ui.formRules.previewWarning'));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -333,12 +268,93 @@ const previewImage = async (
|
|||||||
|
|
||||||
render(h(PreviewWrapper), container);
|
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 =
|
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,
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ setupVbenVxeTable({
|
|||||||
|
|
||||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||||
vxeUI.renderer.add('CellImage', {
|
vxeUI.renderer.add('CellImage', {
|
||||||
renderTableDefault(_renderOpts, params) {
|
renderTableDefault(renderOpts, params) {
|
||||||
|
const { props } = renderOpts;
|
||||||
const { column, row } = params;
|
const { column, row } = params;
|
||||||
return h(Image, { src: row[column.field] });
|
return h(Image, { src: row[column.field], ...props });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -72,5 +72,8 @@
|
|||||||
},
|
},
|
||||||
"button-group": {
|
"button-group": {
|
||||||
"title": "Button Group"
|
"title": "Button Group"
|
||||||
|
},
|
||||||
|
"function": {
|
||||||
|
"contentMenu": "Content Menu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,5 +72,8 @@
|
|||||||
},
|
},
|
||||||
"button-group": {
|
"button-group": {
|
||||||
"title": "按钮组"
|
"title": "按钮组"
|
||||||
|
},
|
||||||
|
"function": {
|
||||||
|
"contentMenu": "上下文菜单"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,6 +328,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: $t('examples.button-group.title'),
|
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,
|
customRequest: upload_file,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
maxCount: 1,
|
maxCount: 1,
|
||||||
|
// 单位:MB
|
||||||
|
maxSize: 2,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
showUploadList: true,
|
showUploadList: true,
|
||||||
// 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
|
// 上传列表的内建样式,支持四种基本样式 text, picture, picture-card 和 picture-circle
|
||||||
@@ -354,7 +356,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
|
|||||||
default: () => $t('examples.form.upload-image'),
|
default: () => $t('examples.form.upload-image'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'selectRequired',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||||
|
|||||||
Reference in New Issue
Block a user