Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into warmflow
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
export default {
|
||||
'*.md': ['prettier --cache --ignore-unknown --write'],
|
||||
'*.vue': [
|
||||
'prettier --write',
|
||||
'eslint --cache --fix',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'*.{js,jsx,ts,tsx}': [
|
||||
'prettier --cache --ignore-unknown --write',
|
||||
'eslint --cache --fix',
|
||||
@@ -7,14 +13,8 @@ export default {
|
||||
'prettier --cache --ignore-unknown --write',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'*.md': ['prettier --cache --ignore-unknown --write'],
|
||||
'*.vue': [
|
||||
'prettier --write',
|
||||
'eslint --cache --fix',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'package.json': ['prettier --cache --write'],
|
||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
|
||||
'prettier --cache --write--parser json',
|
||||
],
|
||||
'package.json': ['prettier --cache --write'],
|
||||
};
|
||||
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -9,7 +9,7 @@
|
||||
"url": "http://localhost:5555",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/playground"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "http://localhost:5666",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-antd"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -27,7 +27,7 @@
|
||||
"url": "http://localhost:5777",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-ele"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -36,7 +36,7 @@
|
||||
"url": "http://localhost:5888",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-naive"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,8 +1,22 @@
|
||||
# 1.1.4
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 菜单选择组件重构为Table形式
|
||||
|
||||
**Features**
|
||||
|
||||
- 通用的vxe-table排序事件(排序逻辑改为在排序事件中处理而非在api处理)
|
||||
- getDict/getDictOptions 提取公共逻辑 减少冗余代码
|
||||
|
||||
**BUG FIXES**
|
||||
|
||||
- 字典项为空时getDict方法无限调用接口((无奈兼容 不给字典item本来就是错误用法))
|
||||
- 表格排序翻页会丢失排序参数
|
||||
|
||||
**OTHERS**
|
||||
|
||||
- 用户管理 新增只获取一次(mounted)默认密码而非每次打开modal都获取
|
||||
|
||||
# 1.1.3
|
||||
|
||||
|
@@ -43,6 +43,31 @@ export default eventHandler(async (event) => {
|
||||
|
||||
await sleep(600);
|
||||
|
||||
const { page, pageSize } = getQuery(event);
|
||||
return usePageResponseSuccess(page as string, pageSize as string, mockData);
|
||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
||||
const listData = structuredClone(mockData);
|
||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
||||
listData.sort((a, b) => {
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(a[sortBy as string]) -
|
||||
Number.parseFloat(b[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(b[sortBy as string]) -
|
||||
Number.parseFloat(a[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
|
@@ -4,6 +4,7 @@ export interface UserInfo {
|
||||
realName: string;
|
||||
roles: string[];
|
||||
username: string;
|
||||
homePath?: string;
|
||||
}
|
||||
|
||||
export const MOCK_USERS: UserInfo[] = [
|
||||
@@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||
realName: 'Admin',
|
||||
roles: ['admin'],
|
||||
username: 'admin',
|
||||
homePath: '/workspace',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||
realName: 'Jack',
|
||||
roles: ['user'],
|
||||
username: 'jack',
|
||||
homePath: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -4,13 +4,12 @@
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
@@ -35,9 +34,7 @@ import {
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
import { h } from 'vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import { h, type Ref } from 'vue';
|
||||
|
||||
import {
|
||||
setupVbenVxeTable,
|
||||
useVbenVxeGrid,
|
||||
type VxeGridDefines,
|
||||
} from '@vben/plugins/vxe-table';
|
||||
import type { VxeGridDefines, VxeGridPropTypes } from '@vben/plugins/vxe-table';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
@@ -133,6 +130,7 @@ export function vxeCheckboxChecked(
|
||||
|
||||
/**
|
||||
* 通用的vxe-table排序事件 支持单/多字段排序
|
||||
* @deprecated 翻页后排序会丢失,使用addSortParams代替
|
||||
* @param tableApi api
|
||||
* @param sortParams 排序参数
|
||||
*/
|
||||
@@ -151,3 +149,23 @@ export function vxeSortEvent(
|
||||
const isAsc = sortList.map((item) => item.order).join(',');
|
||||
tableApi.query({ orderByColumn, isAsc });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的 排序参数添加到请求参数中
|
||||
* @param params 请求参数
|
||||
* @param sortList vxe-table的排序参数
|
||||
*/
|
||||
export function addSortParams(
|
||||
params: Record<string, any>,
|
||||
sortList: VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[],
|
||||
) {
|
||||
// 这里是排序取消 length为0 就不添加参数了
|
||||
if (sortList.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 支持单/多字段排序
|
||||
const orderByColumn = sortList.map((item) => item.field).join(',');
|
||||
const isAsc = sortList.map((item) => item.order).join(',');
|
||||
params.orderByColumn = orderByColumn;
|
||||
params.isAsc = isAsc;
|
||||
}
|
||||
|
@@ -36,5 +36,5 @@ export function forceLogout(tokenId: string) {
|
||||
* @returns void
|
||||
*/
|
||||
export function forceLogout2(tokenId: string) {
|
||||
return requestClient.postWithMsg<void>(`${Api.root}/${tokenId}`);
|
||||
return requestClient.deleteWithMsg<void>(`${Api.root}/myself/${tokenId}`);
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { Menu, MenuOption, MenuResp } from './model';
|
||||
|
||||
import type { ID, IDS } from '#/api/common';
|
||||
|
||||
import type { Menu, MenuOption, MenuResp } from './model';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
enum Api {
|
||||
|
2
apps/web-antd/src/api/system/menu/model.d.ts
vendored
2
apps/web-antd/src/api/system/menu/model.d.ts
vendored
@@ -33,6 +33,8 @@ export interface MenuOption {
|
||||
weight: number;
|
||||
children: MenuOption[];
|
||||
key: string; // 实际上不存在 ide报错
|
||||
menuType: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { OssFile } from './model';
|
||||
|
||||
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
|
||||
|
||||
import type { OssFile } from './model';
|
||||
|
||||
import { ContentTypeEnum } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
|
@@ -1 +1,2 @@
|
||||
export { default as MenuSelectTable } from './src/menu-select-table.vue';
|
||||
export { default as TreeSelectPanel } from './src/tree-select-panel.vue';
|
||||
|
85
apps/web-antd/src/components/tree/src/data.tsx
Normal file
85
apps/web-antd/src/components/tree/src/data.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { ID } from '#/api/common';
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { FolderIcon, MenuIcon, OkButtonIcon, VbenIcon } from '@vben/icons';
|
||||
|
||||
export interface Permission {
|
||||
checked: boolean;
|
||||
id: ID;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface MenuPermissionOption extends MenuOption {
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
const menuTypes = {
|
||||
C: { icon: markRaw(MenuIcon), value: '菜单' },
|
||||
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
|
||||
M: { icon: markRaw(FolderIcon), value: '目录' },
|
||||
};
|
||||
|
||||
export const nodeOptions = [
|
||||
{ label: '节点关联', value: true },
|
||||
{ label: '节点独立', value: false },
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{
|
||||
type: 'checkbox',
|
||||
title: '菜单名称',
|
||||
field: 'label',
|
||||
treeNode: true,
|
||||
headerAlign: 'left',
|
||||
align: 'left',
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
field: 'icon',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
if (row?.icon === '#') {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
<span class={'flex justify-center'}>
|
||||
<VbenIcon icon={row.icon} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
field: 'menuType',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const current = menuTypes[row.menuType as 'C' | 'F' | 'M'];
|
||||
if (!current) {
|
||||
return '未知';
|
||||
}
|
||||
return (
|
||||
<span class="flex items-center justify-center gap-1">
|
||||
{h(current.icon, { class: 'size-[18px]' })}
|
||||
<span>{current.value}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '权限标识',
|
||||
field: 'permissions',
|
||||
headerAlign: 'left',
|
||||
align: 'left',
|
||||
slots: {
|
||||
default: 'permissions',
|
||||
},
|
||||
},
|
||||
];
|
133
apps/web-antd/src/components/tree/src/helper.tsx
Normal file
133
apps/web-antd/src/components/tree/src/helper.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { MenuPermissionOption } from './data';
|
||||
|
||||
import type { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { eachTree, treeToList } from '@vben/utils';
|
||||
|
||||
import { difference, isEmpty, isUndefined } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 权限列设置是否全选
|
||||
* @param record 行记录
|
||||
* @param checked 是否选中
|
||||
*/
|
||||
export function setPermissionsChecked(
|
||||
record: MenuPermissionOption,
|
||||
checked: boolean,
|
||||
) {
|
||||
if (record?.permissions?.length > 0) {
|
||||
// 全部设置为选中
|
||||
record.permissions.forEach((permission) => {
|
||||
permission.checked = checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前行 & 所有子节点选中状态
|
||||
* @param record 行
|
||||
* @param checked 是否选中
|
||||
*/
|
||||
export function rowAndChildrenChecked(
|
||||
record: MenuPermissionOption,
|
||||
checked: boolean,
|
||||
) {
|
||||
// 当前行选中
|
||||
setPermissionsChecked(record, checked);
|
||||
// 所有子节点选中
|
||||
record?.children?.forEach?.((permission) => {
|
||||
rowAndChildrenChecked(permission as MenuPermissionOption, checked);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* void方法 会直接修改原始数据
|
||||
* 将树结构转为 tree+permissions结构
|
||||
* @param menus 后台返回的menu
|
||||
*/
|
||||
export function menusWithPermissions(menus: MenuOption[]) {
|
||||
eachTree(menus, (item: MenuPermissionOption) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
/**
|
||||
* 所有为按钮的节点提取出来
|
||||
* 需要注意 这里需要过滤目录下直接是按钮的情况item.menuType !== 'M'
|
||||
* 将按钮往children添加而非加到permissions
|
||||
*/
|
||||
const permissions = item.children.filter(
|
||||
(child: MenuOption) => child.menuType === 'F' && item.menuType !== 'M',
|
||||
);
|
||||
// 取差集
|
||||
const diffCollection = difference(item.children, permissions);
|
||||
// 更新后的children 即去除按钮
|
||||
item.children = diffCollection;
|
||||
|
||||
// permissions作为字段添加到item
|
||||
const permissionsArr = permissions.map((permission) => {
|
||||
return {
|
||||
id: permission.id,
|
||||
label: permission.label,
|
||||
checked: false,
|
||||
};
|
||||
});
|
||||
item.permissions = permissionsArr;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表格选中
|
||||
* @param checkedKeys 选中的keys
|
||||
* @param menus 菜单 转换后的菜单
|
||||
* @param tableApi api
|
||||
* @param association 是否节点关联
|
||||
*/
|
||||
export function setTableChecked(
|
||||
checkedKeys: (number | string)[],
|
||||
menus: MenuPermissionOption[],
|
||||
tableApi: ReturnType<typeof useVbenVxeGrid>['1'],
|
||||
association: boolean,
|
||||
) {
|
||||
// tree转list
|
||||
const menuList: MenuPermissionOption[] = treeToList(menus);
|
||||
// 拿到勾选的行数据
|
||||
let checkedRows = menuList.filter((item) => checkedKeys.includes(item.id));
|
||||
|
||||
/**
|
||||
* 节点独立切换到节点关联 只需要最末尾的数据 即children为空
|
||||
*/
|
||||
if (!association) {
|
||||
checkedRows = checkedRows.filter(
|
||||
(item) => isUndefined(item.children) || isEmpty(item.children),
|
||||
);
|
||||
}
|
||||
|
||||
// 设置行选中 & permissions选中
|
||||
checkedRows.forEach((item) => {
|
||||
tableApi.grid.setCheckboxRow(item, true);
|
||||
if (item?.permissions?.length > 0) {
|
||||
item.permissions.forEach((permission) => {
|
||||
if (checkedKeys.includes(permission.id)) {
|
||||
permission.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 节点独立切换到节点关联
|
||||
* 勾选后还需要过滤权限没有任何勾选的情况 这时候取消行的勾选
|
||||
*/
|
||||
if (!association) {
|
||||
const emptyRows = checkedRows.filter((item) => {
|
||||
if (isUndefined(item.permissions) || isEmpty(item.permissions)) {
|
||||
return false;
|
||||
}
|
||||
return item.permissions.every(
|
||||
(permission) => permission.checked === false,
|
||||
);
|
||||
});
|
||||
// 设置为不选中
|
||||
tableApi.grid.setCheckboxRow(emptyRows, false);
|
||||
}
|
||||
}
|
57
apps/web-antd/src/components/tree/src/hook.tsx
Normal file
57
apps/web-antd/src/components/tree/src/hook.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { TourProps } from 'ant-design-vue';
|
||||
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { Tour } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* 全屏引导
|
||||
* @returns value
|
||||
*/
|
||||
export function useFullScreenGuide() {
|
||||
const open = ref(false);
|
||||
/**
|
||||
* 是否已读 只显示一次
|
||||
*/
|
||||
const read = useLocalStorage('menu_select_fullscreen_read', false);
|
||||
|
||||
function openGuide() {
|
||||
if (!read.value) {
|
||||
open.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeGuide() {
|
||||
open.value = false;
|
||||
read.value = true;
|
||||
}
|
||||
|
||||
const steps: TourProps['steps'] = [
|
||||
{
|
||||
title: '提示',
|
||||
description: '点击这里可以全屏',
|
||||
target: () =>
|
||||
document.querySelector(
|
||||
'div#menu-select-table .vxe-tools--operate > button[title="全屏"]',
|
||||
)!,
|
||||
},
|
||||
];
|
||||
|
||||
const FullScreenGuide = defineComponent({
|
||||
name: 'FullScreenGuide',
|
||||
inheritAttrs: false,
|
||||
setup() {
|
||||
return () => (
|
||||
<Tour onClose={closeGuide} open={open.value} steps={steps} />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
FullScreenGuide,
|
||||
openGuide,
|
||||
closeGuide,
|
||||
};
|
||||
}
|
412
apps/web-antd/src/components/tree/src/menu-select-table.vue
Normal file
412
apps/web-antd/src/components/tree/src/menu-select-table.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<!--
|
||||
不兼容也不会兼容一些错误用法
|
||||
比如: 菜单下放目录 菜单下放菜单
|
||||
比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
|
||||
-->
|
||||
<script setup lang="tsx">
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import type { MenuPermissionOption } from './data';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import { cloneDeep, findGroupParentIds } from '@vben/utils';
|
||||
|
||||
import { Alert, Checkbox, RadioGroup, Space } from 'ant-design-vue';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { columns, nodeOptions } from './data';
|
||||
import {
|
||||
menusWithPermissions,
|
||||
rowAndChildrenChecked,
|
||||
setPermissionsChecked,
|
||||
setTableChecked,
|
||||
} from './helper';
|
||||
import { useFullScreenGuide } from './hook';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuSelectTable',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
checkedKeys: (number | string)[];
|
||||
defaultExpandAll?: boolean;
|
||||
menus: MenuOption[];
|
||||
}>(),
|
||||
{
|
||||
/**
|
||||
* 是否默认展开全部
|
||||
*/
|
||||
defaultExpandAll: true,
|
||||
/**
|
||||
* 注意这里不是双向绑定 需要调用getCheckedKeys实例方法来获取真正选中的节点
|
||||
*/
|
||||
checkedKeys: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 是否节点关联
|
||||
*/
|
||||
const association = defineModel('association', {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// checkbox显示的字段
|
||||
labelField: 'label',
|
||||
// 是否严格模式 即节点不关联
|
||||
checkStrictly: !association.value,
|
||||
},
|
||||
size: 'small',
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 自定义列
|
||||
custom: false,
|
||||
// 最大化
|
||||
zoom: true,
|
||||
// 刷新
|
||||
refresh: false,
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: false,
|
||||
isCurrent: false,
|
||||
keyField: 'id',
|
||||
},
|
||||
/**
|
||||
* 开启虚拟滚动
|
||||
* 数据量小可以选择关闭
|
||||
* 如果遇到样式问题(空白、错位 滚动等)可以选择关闭虚拟滚动
|
||||
*/
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
// 溢出换行显示
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于界面显示选中的数量
|
||||
*/
|
||||
const checkedNum = ref(0);
|
||||
/**
|
||||
* 更新选中的数量
|
||||
*/
|
||||
function updateCheckedNumber() {
|
||||
checkedNum.value = getCheckedKeys().length;
|
||||
}
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
// 勾选事件
|
||||
checkboxChange: (params) => {
|
||||
// 选中还是取消选中
|
||||
const checked = params.checked;
|
||||
// 行
|
||||
const record = params.row;
|
||||
if (association.value) {
|
||||
// 节点关联
|
||||
// 设置所有子节点选中状态
|
||||
rowAndChildrenChecked(record, checked);
|
||||
} else {
|
||||
// 节点独立
|
||||
// 点行会勾选/取消全部权限 点权限不会勾选行
|
||||
setPermissionsChecked(record, checked);
|
||||
}
|
||||
updateCheckedNumber();
|
||||
},
|
||||
// 全选事件
|
||||
checkboxAll: (params) => {
|
||||
const records = params.$grid.getData();
|
||||
records.forEach((item) => {
|
||||
rowAndChildrenChecked(item, params.checked);
|
||||
});
|
||||
updateCheckedNumber();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 设置表格选中
|
||||
* @param menus menu
|
||||
* @param keys 选中的key
|
||||
* @param triggerOnchange 节点独立情况 不需要触发onChange(false)
|
||||
*/
|
||||
function setCheckedByKeys(
|
||||
menus: MenuPermissionOption[],
|
||||
keys: (number | string)[],
|
||||
triggerOnchange: boolean,
|
||||
) {
|
||||
menus.forEach((item) => {
|
||||
// 设置行选中
|
||||
if (keys.includes(item.id)) {
|
||||
tableApi.grid.setCheckboxRow(item, true);
|
||||
}
|
||||
// 设置权限columns选中
|
||||
if (item.permissions && item.permissions.length > 0) {
|
||||
// 遍历 设置勾选
|
||||
item.permissions.forEach((permission) => {
|
||||
if (keys.includes(permission.id)) {
|
||||
permission.checked = true;
|
||||
// 手动触发onChange来选中 节点独立情况不需要处理
|
||||
triggerOnchange && handlePermissionChange(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 设置children选中
|
||||
if (item.children && item.children.length > 0) {
|
||||
setCheckedByKeys(item.children as any, keys, triggerOnchange);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { FullScreenGuide, openGuide } = useFullScreenGuide();
|
||||
onMounted(() => {
|
||||
/**
|
||||
* 加载表格数据 转为指定结构
|
||||
*/
|
||||
watch(
|
||||
() => props.menus,
|
||||
async (menus) => {
|
||||
const clonedMenus = cloneDeep(menus);
|
||||
menusWithPermissions(clonedMenus);
|
||||
// console.log(clonedMenus);
|
||||
await tableApi.grid.loadData(clonedMenus);
|
||||
// 展开全部 默认true
|
||||
if (props.defaultExpandAll) {
|
||||
await nextTick();
|
||||
setExpandOrCollapse(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 节点关联变动 更新表格勾选效果
|
||||
*/
|
||||
watch(association, (value) => {
|
||||
tableApi.setGridOptions({
|
||||
checkboxConfig: {
|
||||
checkStrictly: !value,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* checkedKeys依赖menus
|
||||
* 要注意加载顺序
|
||||
* !!!要在外部确保menus先加载!!!
|
||||
*/
|
||||
watch(
|
||||
() => props.checkedKeys,
|
||||
(value) => {
|
||||
const allCheckedKeys = uniq([...value]);
|
||||
// 获取表格data 如果checkedKeys在menus的watch之前触发 这里会拿到空 导致勾选异常
|
||||
const records = tableApi.grid.getData();
|
||||
setCheckedByKeys(records, allCheckedKeys, association.value);
|
||||
updateCheckedNumber();
|
||||
|
||||
// 全屏引导
|
||||
setTimeout(openGuide, 1000);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// 缓存上次(切换节点关系前)选中的keys
|
||||
const lastCheckedKeys = shallowRef<(number | string)[]>([]);
|
||||
/**
|
||||
* 节点关联变动 事件
|
||||
*/
|
||||
async function handleAssociationChange(e: RadioChangeEvent) {
|
||||
lastCheckedKeys.value = getCheckedKeys();
|
||||
// 清空全部permissions选中
|
||||
const records = tableApi.grid.getData();
|
||||
records.forEach((item) => {
|
||||
rowAndChildrenChecked(item, false);
|
||||
});
|
||||
// 需要清空全部勾选
|
||||
await tableApi.grid.clearCheckboxRow();
|
||||
// 滚动到顶部
|
||||
await tableApi.grid.scrollTo(0, 0);
|
||||
|
||||
// 节点切换 不同的选中
|
||||
setTableChecked(lastCheckedKeys.value, records, tableApi, !e.target.value);
|
||||
|
||||
updateCheckedNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部展开/折叠
|
||||
* @param expand 是否展开
|
||||
*/
|
||||
function setExpandOrCollapse(expand: boolean) {
|
||||
tableApi.grid?.setAllTreeExpand(expand);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限列表 checkbox勾选的事件
|
||||
* @param row 行
|
||||
*/
|
||||
function handlePermissionChange(row: any) {
|
||||
// 节点关联
|
||||
if (association.value) {
|
||||
const checkedPermissions = row.permissions.filter(
|
||||
(item: any) => item.checked === true,
|
||||
);
|
||||
// 有一条选中 则整个行选中
|
||||
if (checkedPermissions.length > 0) {
|
||||
tableApi.grid.setCheckboxRow(row, true);
|
||||
}
|
||||
// 无任何选中 则整个行不选中
|
||||
if (checkedPermissions.length === 0) {
|
||||
tableApi.grid.setCheckboxRow(row, false);
|
||||
}
|
||||
}
|
||||
// 节点独立 不处理
|
||||
updateCheckedNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取勾选的key
|
||||
* @param records 行记录列表
|
||||
* @param addCurrent 是否添加当前行的id
|
||||
*/
|
||||
function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
|
||||
const allKeys: (number | string)[] = [];
|
||||
records.forEach((item) => {
|
||||
// 处理children
|
||||
if (item.children && item.children.length > 0) {
|
||||
const keys = getKeys(item.children as MenuPermissionOption[], addCurrent);
|
||||
allKeys.push(...keys);
|
||||
} else {
|
||||
// 当前行的id
|
||||
addCurrent && allKeys.push(item.id);
|
||||
// 当前行权限id 获取已经选中的
|
||||
if (item.permissions && item.permissions.length > 0) {
|
||||
const ids = item.permissions
|
||||
.filter((m) => m.checked === true)
|
||||
.map((m) => m.id);
|
||||
allKeys.push(...ids);
|
||||
}
|
||||
}
|
||||
});
|
||||
return uniq(allKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中的key
|
||||
*/
|
||||
function getCheckedKeys() {
|
||||
// 节点关联
|
||||
if (association.value) {
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
|
||||
// 子节点
|
||||
const nodeKeys = getKeys(records, true);
|
||||
// 所有父节点
|
||||
const parentIds = findGroupParentIds(props.menus, nodeKeys as number[]);
|
||||
// 拼接 去重
|
||||
const realKeys = uniq([...parentIds, ...nodeKeys]);
|
||||
return realKeys;
|
||||
}
|
||||
// 节点独立
|
||||
|
||||
// 勾选的行
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
|
||||
// 全部数据 用于获取permissions
|
||||
const allRecords = tableApi?.grid?.getData?.() ?? [];
|
||||
// 表格已经选中的行ids
|
||||
const checkedIds = records.map((item) => item.id);
|
||||
// 所有已经勾选权限的ids
|
||||
const permissionIds = getKeys(allRecords, false);
|
||||
// 合并 去重
|
||||
const allIds = uniq([...checkedIds, ...permissionIds]);
|
||||
return allIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露给外部使用 获取已选中的key
|
||||
*/
|
||||
defineExpose({
|
||||
getCheckedKeys,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col" id="menu-select-table">
|
||||
<BasicTable>
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="association"
|
||||
:options="nodeOptions"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@change="handleAssociationChange"
|
||||
/>
|
||||
<Alert class="mx-2" type="info">
|
||||
<template #message>
|
||||
<div>
|
||||
已选中
|
||||
<span class="text-primary mx-1 font-semibold">
|
||||
{{ checkedNum }}
|
||||
</span>
|
||||
个节点
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button @click="setExpandOrCollapse(false)">
|
||||
{{ $t('pages.common.collapse') }}
|
||||
</a-button>
|
||||
<a-button @click="setExpandOrCollapse(true)">
|
||||
{{ $t('pages.common.expand') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #permissions="{ row }">
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1">
|
||||
<Checkbox
|
||||
v-for="permission in row.permissions"
|
||||
:key="permission.id"
|
||||
v-model:checked="permission.checked"
|
||||
@change="() => handlePermissionChange(row)"
|
||||
>
|
||||
{{ permission.label }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<!-- 全屏引导 -->
|
||||
<FullScreenGuide />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-alert) {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
</style>
|
@@ -3,7 +3,9 @@ import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
|
||||
|
||||
import { computed, nextTick, onMounted, type PropType, ref, watch } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { findGroupParentIds, treeToList } from '@vben/utils';
|
||||
|
||||
@@ -108,8 +110,8 @@ const stop = watch([checkedKeys, () => props.treeData], () => {
|
||||
* @param info info.halfCheckedKeys为父节点的ID
|
||||
*/
|
||||
type CheckedState<T = number | string> =
|
||||
| { checked: T[]; halfChecked: T[] }
|
||||
| T[];
|
||||
| T[]
|
||||
| { checked: T[]; halfChecked: T[] };
|
||||
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
|
||||
// 数组的话为节点关联
|
||||
if (Array.isArray(checkedStateKeys)) {
|
||||
|
@@ -2,15 +2,13 @@
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { $t } from '@vben/locales';
|
||||
import { message, Modal, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { checkImageFileType, defaultImageAccept } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
@@ -37,7 +35,7 @@ const props = withDefaults(
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
// 返回的字段 默认url
|
||||
resultField?: 'fileName' | 'ossId' | 'url' | string;
|
||||
resultField?: 'fileName' | 'ossId' | 'url';
|
||||
value?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
@@ -50,7 +48,7 @@ const props = withDefaults(
|
||||
accept: () => defaultImageAccept,
|
||||
multiple: false,
|
||||
api: uploadApi,
|
||||
resultField: '',
|
||||
resultField: 'url',
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
@@ -74,7 +72,7 @@ const isFirstRender = ref<boolean>(true);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => {
|
||||
async (v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
@@ -90,19 +88,40 @@ watch(
|
||||
}
|
||||
// 直接赋值 可能为string | string[]
|
||||
value = v;
|
||||
fileList.value = _fileList.map((item, i) => {
|
||||
const withUrlList: UploadProps['fileList'] = [];
|
||||
for (const item of _fileList) {
|
||||
// ossId情况
|
||||
if (props.resultField === 'ossId') {
|
||||
const resp = await ossInfo([item]);
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
withUrlList.push({
|
||||
uid: item, // ossId作为uid 方便getValue获取
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
withUrlList.push({
|
||||
...(item as any),
|
||||
uid: item,
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 非ossId情况
|
||||
if (item && isString(item)) {
|
||||
withUrlList.push({
|
||||
uid: uniqueId(),
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
};
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
withUrlList.push(item);
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
}
|
||||
}
|
||||
fileList.value = withUrlList;
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
@@ -200,12 +219,17 @@ async function customRequest(info: UploadRequestOption<any>) {
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
console.log(fileList.value);
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response?.[props.resultField];
|
||||
}
|
||||
// ossId兼容 uid为ossId直接返回
|
||||
if (props.resultField === 'ossId' && item.uid) {
|
||||
return item.uid;
|
||||
}
|
||||
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||
if (item?.url) {
|
||||
return item.url;
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
import type { Locale } from 'ant-design-vue/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
$t,
|
||||
@@ -10,10 +8,10 @@ import {
|
||||
loadLocalesMapFromDir,
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
|
||||
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const antdLocale = ref<Locale>(antdDefaultLocale);
|
||||
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -1,15 +1,22 @@
|
||||
import type { DictData } from '#/api/system/dict/dict-data-model';
|
||||
|
||||
import { dictDataInfo } from '#/api/system/dict/dict-data';
|
||||
import { type Option, useDictStore } from '#/store/dict';
|
||||
import { useDictStore } from '#/store/dict';
|
||||
|
||||
/**
|
||||
* 抽取公共逻辑的基础方法
|
||||
* @param dictName 字典名称
|
||||
* @param dataGetter 获取字典数据的函数
|
||||
* @returns 数据
|
||||
*/
|
||||
function fetchAndCacheDictData<T>(
|
||||
dictName: string,
|
||||
dataGetter: () => T[],
|
||||
): T[] {
|
||||
const { dictRequestCache, setDictInfo } = useDictStore();
|
||||
// 有调用方决定如何获取数据
|
||||
const dataList = dataGetter();
|
||||
|
||||
// todo 重复代码的封装
|
||||
export function getDict(dictName: string): DictData[] {
|
||||
const { dictRequestCache, getDict, setDictInfo } = useDictStore();
|
||||
// 这里拿到
|
||||
const dictList = getDict(dictName);
|
||||
// 检查请求状态缓存
|
||||
if (dictList.length === 0 && !dictRequestCache.has(dictName)) {
|
||||
if (dataList.length === 0 && !dictRequestCache.has(dictName)) {
|
||||
dictRequestCache.set(
|
||||
dictName,
|
||||
dictDataInfo(dictName)
|
||||
@@ -20,31 +27,36 @@ export function getDict(dictName: string): DictData[] {
|
||||
})
|
||||
.finally(() => {
|
||||
// 移除请求状态缓存
|
||||
/**
|
||||
* 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)
|
||||
* 会导致if一直进入逻辑导致接口无限刷新
|
||||
* 在这里dictList为空时 不删除缓存
|
||||
*/
|
||||
if (dataList.length > 0) {
|
||||
dictRequestCache.delete(dictName);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return dictList;
|
||||
return dataList;
|
||||
}
|
||||
|
||||
export function getDictOptions(dictName: string): Option[] {
|
||||
const { dictRequestCache, getDictOptions, setDictInfo } = useDictStore();
|
||||
const dictOptionList = getDictOptions(dictName);
|
||||
// 检查请求状态缓存
|
||||
if (dictOptionList.length === 0 && !dictRequestCache.has(dictName)) {
|
||||
dictRequestCache.set(
|
||||
dictName,
|
||||
dictDataInfo(dictName)
|
||||
.then((resp) => {
|
||||
// 缓存到store 这样就不用重复获取了
|
||||
// 内部处理了push的逻辑 这里不用push
|
||||
setDictInfo(dictName, resp);
|
||||
})
|
||||
.finally(() => {
|
||||
// 移除请求状态缓存
|
||||
dictRequestCache.delete(dictName);
|
||||
}),
|
||||
);
|
||||
/**
|
||||
* 这里是提供给渲染标签使用的方法
|
||||
* @param dictName 字典名称
|
||||
* @returns 字典信息
|
||||
*/
|
||||
export function getDict(dictName: string) {
|
||||
const { getDict } = useDictStore();
|
||||
return fetchAndCacheDictData(dictName, () => getDict(dictName));
|
||||
}
|
||||
return dictOptionList;
|
||||
|
||||
/**
|
||||
* 一般是Select, Radio, Checkbox等组件使用
|
||||
* @param dictName 字典名称
|
||||
* @returns Options数组
|
||||
*/
|
||||
export function getDictOptions(dictName: string) {
|
||||
const { getDictOptions } = useDictStore();
|
||||
return fetchAndCacheDictData(dictName, () => getDictOptions(dictName));
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { useAuthStore } from '#/store';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const tenantInfo = ref<TenantResp>({
|
||||
tenantEnabled: false,
|
||||
@@ -98,7 +99,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -10,7 +10,9 @@ import { columns } from '#/views/monitor/online/data';
|
||||
const gridOptions: VxeGridProps = {
|
||||
columns,
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async () => {
|
||||
|
@@ -1,18 +1,34 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import { Result } from 'ant-design-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CommonSkeleton',
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="flex h-[600px] w-full items-center justify-center">
|
||||
<Fallback
|
||||
description="等待后端重构工作流后开发"
|
||||
status="coming-soon"
|
||||
title="等待开发"
|
||||
/>
|
||||
<Page autoContentHeight={true}>
|
||||
<Result
|
||||
status="success"
|
||||
sub-title="等待后端发布"
|
||||
title="已经开发完毕(warmflow分支)"
|
||||
>
|
||||
{{
|
||||
extra: (
|
||||
<div>
|
||||
<a-button
|
||||
onClick={() => openWindow('http://106.55.255.76')}
|
||||
type="primary"
|
||||
>
|
||||
前往工作流版本演示站
|
||||
</a-button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
</Result>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -26,18 +26,22 @@ const gridOptions: VxeGridProps = {
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
query: async (_, formValues) => {
|
||||
return await onlineList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'tokenId',
|
||||
@@ -51,11 +55,24 @@ async function handleForceOffline(row: Recordable<any>) {
|
||||
await forceLogout(row.tokenId);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function onlineCount() {
|
||||
return tableApi?.grid?.getData?.()?.length ?? 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="在线用户列表">
|
||||
<BasicTable>
|
||||
<template #toolbar-actions>
|
||||
<div class="mr-1 pl-1 text-[1rem]">
|
||||
<div>
|
||||
在线用户列表 (共
|
||||
<span class="text-primary font-bold">{{ onlineCount() }}</span>
|
||||
人在线)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
|
@@ -9,10 +9,10 @@ import { $t } from '@vben/locales';
|
||||
import { Modal, Space } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
addSortParams,
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps,
|
||||
vxeSortEvent,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
operLogClean,
|
||||
@@ -60,12 +60,14 @@ const gridOptions: VxeGridProps<OperationLog> = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
query: async ({ page, sorts }, formValues = {}) => {
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
// 添加排序参数
|
||||
addSortParams(params, sorts);
|
||||
return await operLogList(params);
|
||||
},
|
||||
},
|
||||
@@ -87,7 +89,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
|
||||
// 排序 重新请求接口
|
||||
sortChange: () => tableApi.query(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -48,7 +48,12 @@ async function setupMenuSelect() {
|
||||
item.menuName = $t(item.menuName);
|
||||
});
|
||||
// const folderArray = menuArray.filter((item) => item.menuType === 'M');
|
||||
const menuTree = listToTree(menuArray, { id: 'menuId', pid: 'parentId' });
|
||||
/**
|
||||
* 这里需要过滤掉按钮类型
|
||||
* 不允许在按钮下添加数据
|
||||
*/
|
||||
const filteredList = menuArray.filter((item) => item.menuType !== 'F');
|
||||
const menuTree = listToTree(filteredList, { id: 'menuId', pid: 'parentId' });
|
||||
const fullMenuTree = [
|
||||
{
|
||||
menuId: 0,
|
||||
|
@@ -128,6 +128,12 @@ export const drawerSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'domain',
|
||||
label: '自定义域名',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'tip',
|
||||
label: '占位作为提示使用',
|
||||
hideLabel: true,
|
||||
},
|
||||
{
|
||||
component: 'Divider',
|
||||
componentProps: {
|
||||
|
@@ -5,6 +5,8 @@ import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { Alert } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
ossConfigAdd,
|
||||
@@ -79,7 +81,17 @@ async function handleCancel() {
|
||||
|
||||
<template>
|
||||
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[650px]">
|
||||
<BasicForm />
|
||||
<BasicForm>
|
||||
<template #tip>
|
||||
<div class="ml-7 w-full">
|
||||
<Alert
|
||||
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
|
||||
show-icon
|
||||
type="warning"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
|
@@ -19,10 +19,10 @@ import {
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
addSortParams,
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps,
|
||||
vxeSortEvent,
|
||||
} from '#/adapter/vxe-table';
|
||||
import { configInfoByKey } from '#/api/system/config';
|
||||
import { ossDownload, ossList, ossRemove } from '#/api/system/oss';
|
||||
@@ -66,12 +66,14 @@ const gridOptions: VxeGridProps = {
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
query: async ({ page, sorts }, formValues = {}) => {
|
||||
const params: any = {
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
};
|
||||
// 添加排序参数
|
||||
addSortParams(params, sorts);
|
||||
return await ossList(params);
|
||||
},
|
||||
},
|
||||
@@ -94,7 +96,8 @@ const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
sortChange: (sortParams) => vxeSortEvent(tableApi, sortParams),
|
||||
// 排序 重新请求接口
|
||||
sortChange: () => tableApi.query(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -134,8 +137,8 @@ function handleToSettings() {
|
||||
|
||||
const preview = ref(false);
|
||||
onMounted(async () => {
|
||||
const resp = await configInfoByKey('sys.oss.previewListResource');
|
||||
preview.value = Boolean(resp);
|
||||
const previewStr = await configInfoByKey('sys.oss.previewListResource');
|
||||
preview.value = previewStr === 'true';
|
||||
});
|
||||
|
||||
function isImageFile(ext: string) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
@@ -5,7 +6,6 @@ import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { type FormSchemaGetter } from '#/adapter/form';
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,7 @@ export const authScopeOptions = [
|
||||
{ color: 'orange', label: '本部门数据权限', value: '3' },
|
||||
{ color: 'cyan', label: '本部门及以下数据权限', value: '4' },
|
||||
{ color: 'error', label: '仅本人数据权限', value: '5' },
|
||||
{ color: 'default', label: '部门及以下或本人数据权限', value: '6' },
|
||||
];
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
|
@@ -1,16 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import {
|
||||
Page,
|
||||
useVbenDrawer,
|
||||
useVbenModal,
|
||||
type VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
@@ -22,11 +20,7 @@ import {
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
useVbenVxeGrid,
|
||||
vxeCheckboxChecked,
|
||||
type VxeGridProps,
|
||||
} from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
roleChangeStatus,
|
||||
roleExport,
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -8,7 +10,7 @@ import { cloneDeep, eachTree } from '@vben/utils';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { menuTreeSelect, roleMenuTreeSelect } from '#/api/system/menu';
|
||||
import { roleAdd, roleInfo, roleUpdate } from '#/api/system/role';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { MenuSelectTable } from '#/components/tree';
|
||||
|
||||
import { drawerSchema } from './data';
|
||||
|
||||
@@ -32,11 +34,10 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-2 gap-x-4',
|
||||
});
|
||||
|
||||
const menuTree = ref<any[]>([]);
|
||||
const menuTree = ref<MenuOption[]>([]);
|
||||
async function setupMenuTree(id?: number | string) {
|
||||
if (id) {
|
||||
const resp = await roleMenuTreeSelect(id);
|
||||
formApi.setFieldValue('menuIds', resp.checkedKeys);
|
||||
const menus = resp.menus;
|
||||
// i18n处理
|
||||
eachTree(menus, (node) => {
|
||||
@@ -44,15 +45,20 @@ async function setupMenuTree(id?: number | string) {
|
||||
});
|
||||
// 设置菜单信息
|
||||
menuTree.value = resp.menus;
|
||||
// keys依赖于menu 需要先加载menu
|
||||
await nextTick();
|
||||
await formApi.setFieldValue('menuIds', resp.checkedKeys);
|
||||
} else {
|
||||
const resp = await menuTreeSelect();
|
||||
formApi.setFieldValue('menuIds', []);
|
||||
// i18n处理
|
||||
eachTree(resp, (node) => {
|
||||
node.label = $t(node.label);
|
||||
});
|
||||
// 设置菜单信息
|
||||
menuTree.value = resp;
|
||||
// keys依赖于menu 需要先加载menu
|
||||
await nextTick();
|
||||
await formApi.setFieldValue('menuIds', []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,11 +84,7 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里拿到的是一个数组ref
|
||||
*/
|
||||
const menuSelectRef = ref();
|
||||
|
||||
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
drawerApi.drawerLoading(true);
|
||||
@@ -91,7 +93,7 @@ async function handleConfirm() {
|
||||
return;
|
||||
}
|
||||
// 这个用于提交
|
||||
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
|
||||
// formApi.getValues拿到的是一个readonly对象,不能直接修改,需要cloneDeep
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
data.menuIds = menuIds;
|
||||
@@ -120,17 +122,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
|
||||
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
|
||||
<BasicForm>
|
||||
<template #menuIds="slotProps">
|
||||
<!-- check-strictly为readonly 不能通过v-model绑定 -->
|
||||
<TreeSelectPanel
|
||||
<div class="h-[600px] w-full">
|
||||
<!-- association为readonly 不能通过v-model绑定 -->
|
||||
<MenuSelectTable
|
||||
ref="menuSelectRef"
|
||||
v-bind="slotProps"
|
||||
:check-strictly="formApi.form.values.menuCheckStrictly"
|
||||
:tree-data="menuTree"
|
||||
@check-strictly-change="handleMenuCheckStrictlyChange"
|
||||
:checked-keys="slotProps.value"
|
||||
:association="formApi.form.values.menuCheckStrictly"
|
||||
:menus="menuTree"
|
||||
@update:association="handleMenuCheckStrictlyChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicDrawer>
|
||||
|
@@ -1,23 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep, eachTree, listToTree } from '@vben/utils';
|
||||
import { cloneDeep, eachTree } from '@vben/utils';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { menuList, tenantPackageMenuTreeSelect } from '#/api/system/menu';
|
||||
import { menuTreeSelect, tenantPackageMenuTreeSelect } from '#/api/system/menu';
|
||||
import {
|
||||
packageAdd,
|
||||
packageInfo,
|
||||
packageUpdate,
|
||||
} from '#/api/system/tenant-package';
|
||||
import { TreeSelectPanel } from '#/components/tree';
|
||||
import { MenuSelectTable } from '#/components/tree';
|
||||
|
||||
import { drawerSchema } from './data';
|
||||
import TreeItem from './tree-item';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
@@ -36,23 +37,32 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function setupMenuTreeSelect(id?: number | string) {
|
||||
const menuTree = ref<MenuOption[]>([]);
|
||||
async function setupMenuTree(id?: number | string) {
|
||||
if (id) {
|
||||
const resp = await tenantPackageMenuTreeSelect(id);
|
||||
await formApi.setFieldValue('menuIds', resp.checkedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
const menuTree = ref<any[]>([]);
|
||||
async function setupMenuTree() {
|
||||
const resp = await menuList();
|
||||
const treeData = listToTree(resp, { id: 'menuId' });
|
||||
const menus = resp.menus;
|
||||
// i18n处理
|
||||
eachTree(treeData, (node) => {
|
||||
node.menuName = $t(node.menuName);
|
||||
eachTree(menus, (node) => {
|
||||
node.label = $t(node.label);
|
||||
});
|
||||
// 设置菜单信息
|
||||
menuTree.value = treeData;
|
||||
menuTree.value = resp.menus;
|
||||
// keys依赖于menu 需要先加载menu
|
||||
await nextTick();
|
||||
await formApi.setFieldValue('menuIds', resp.checkedKeys);
|
||||
} else {
|
||||
const resp = await menuTreeSelect();
|
||||
// i18n处理
|
||||
eachTree(resp, (node) => {
|
||||
node.label = $t(node.label);
|
||||
});
|
||||
// 设置菜单信息
|
||||
menuTree.value = resp;
|
||||
// keys依赖于menu 需要先加载menu
|
||||
await nextTick();
|
||||
await formApi.setFieldValue('menuIds', []);
|
||||
}
|
||||
}
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
@@ -72,20 +82,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
// 通过setupMenuTreeSelect设置
|
||||
await formApi.setValues(omit(record, ['menuIds']));
|
||||
}
|
||||
/**
|
||||
* 加载菜单树和已勾选菜单
|
||||
*/
|
||||
await Promise.all([setupMenuTree(), setupMenuTreeSelect(id)]);
|
||||
// init菜单 注意顺序要放在赋值record之后 内部watch会依赖record
|
||||
await setupMenuTree(id);
|
||||
|
||||
drawerApi.drawerLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里拿到的是一个数组ref
|
||||
*/
|
||||
const menuSelectRef = ref();
|
||||
|
||||
const menuSelectRef = ref<InstanceType<typeof MenuSelectTable>>();
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
drawerApi.drawerLoading(true);
|
||||
@@ -94,7 +98,7 @@ async function handleConfirm() {
|
||||
return;
|
||||
}
|
||||
// 这个用于提交
|
||||
const menuIds = menuSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
||||
const menuIds = menuSelectRef.value?.getCheckedKeys?.() ?? [];
|
||||
// formApi.getValues拿到的是一个readonly对象,不能直接修改,需要cloneDeep
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
data.menuIds = menuIds;
|
||||
@@ -123,22 +127,19 @@ function handleMenuCheckStrictlyChange(value: boolean) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
|
||||
<BasicDrawer :close-on-click-modal="false" :title="title" class="w-[800px]">
|
||||
<BasicForm>
|
||||
<template #menuIds="slotProps">
|
||||
<TreeSelectPanel
|
||||
<div class="h-[600px] w-full">
|
||||
<!-- association为readonly 不能通过v-model绑定 -->
|
||||
<MenuSelectTable
|
||||
ref="menuSelectRef"
|
||||
v-bind="slotProps"
|
||||
:check-strictly="formApi.form.values.menuCheckStrictly"
|
||||
:expand-all-on-init="false"
|
||||
:field-names="{ title: 'menuName', key: 'menuId' }"
|
||||
:tree-data="menuTree"
|
||||
@check-strictly-change="handleMenuCheckStrictlyChange"
|
||||
>
|
||||
<template #title="data">
|
||||
<TreeItem :data="data" />
|
||||
</template>
|
||||
</TreeSelectPanel>
|
||||
:checked-keys="slotProps.value"
|
||||
:association="formApi.form.values.menuCheckStrictly"
|
||||
:menus="menuTree"
|
||||
@update:association="handleMenuCheckStrictlyChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicDrawer>
|
||||
|
@@ -1,14 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Role } from '#/api/system/user/model';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { configInfoByKey } from '#/api/system/config';
|
||||
import { postOptionSelect } from '#/api/system/post';
|
||||
@@ -19,6 +11,11 @@ import {
|
||||
userUpdate,
|
||||
} from '#/api/system/user';
|
||||
import { authScopeOptions } from '#/views/system/role/data';
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
import { drawerSchema } from './data';
|
||||
|
||||
@@ -117,13 +114,20 @@ async function setupDeptSelect() {
|
||||
]);
|
||||
}
|
||||
|
||||
const defaultPassword = ref('');
|
||||
onMounted(async () => {
|
||||
const password = await configInfoByKey('sys.user.initPassword');
|
||||
if (password) {
|
||||
defaultPassword.value = password;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 新增时候 从参数设置获取默认密码
|
||||
*/
|
||||
async function loadDefaultPassword(update: boolean) {
|
||||
if (!update) {
|
||||
const defaultPassword = await configInfoByKey('sys.user.initPassword');
|
||||
defaultPassword && formApi.setFieldValue('password', defaultPassword);
|
||||
if (!update && defaultPassword.value) {
|
||||
formApi.setFieldValue('password', defaultPassword.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
27
apps/web-antd/src/views/演示使用自行删除/menu/index.vue
Normal file
27
apps/web-antd/src/views/演示使用自行删除/menu/index.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { roleMenuTreeSelect } from '#/api/system/menu';
|
||||
import { MenuSelectTable } from '#/components/tree';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { onMounted, ref, shallowRef } from 'vue';
|
||||
|
||||
const checkedKeys = ref<number[]>([]);
|
||||
const menus = shallowRef<MenuOption[]>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await roleMenuTreeSelect(3);
|
||||
menus.value = resp.menus;
|
||||
checkedKeys.value = resp.checkedKeys;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<MenuSelectTable
|
||||
:menus="menus"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
:association="true"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
@@ -1,11 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, Page } from '@vben/common-ui';
|
||||
|
||||
import { Alert, RadioGroup } from 'ant-design-vue';
|
||||
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
import { JsonPreview, Page } from '@vben/common-ui';
|
||||
import { Alert, RadioGroup } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const resultField = ref<'ossId' | 'url'>('ossId');
|
||||
|
||||
@@ -17,7 +14,7 @@ const fieldOptions = [
|
||||
];
|
||||
const fileAccept = ['xlsx', 'word', 'pdf'];
|
||||
|
||||
const signleImage = ref<string>('');
|
||||
const signleImage = ref<string>('1745443704356782081');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,7 +24,11 @@ const signleImage = ref<string>('');
|
||||
:show-icon="true"
|
||||
message="新特性: 设置max-number为1时, 会被绑定为string而非string[]类型 省去手动转换"
|
||||
/>
|
||||
<ImageUpload v-model:value="signleImage" :max-number="1" />
|
||||
<ImageUpload
|
||||
v-model:value="signleImage"
|
||||
:max-number="1"
|
||||
result-field="ossId"
|
||||
/>
|
||||
<JsonPreview :data="signleImage" />
|
||||
</div>
|
||||
<div class="bg-background flex flex-col gap-[12px] rounded-lg p-6">
|
||||
|
234
apps/web-antd/src/views/演示使用自行删除/vxe/edit-table.vue
Normal file
234
apps/web-antd/src/views/演示使用自行删除/vxe/edit-table.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<script setup lang="tsx">
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
import { JsonPreview } from '@vben/common-ui';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
editConfig: {
|
||||
// 触发编辑的方式
|
||||
trigger: 'click',
|
||||
// 触发编辑的模式
|
||||
mode: 'row',
|
||||
showStatus: true,
|
||||
},
|
||||
border: true,
|
||||
rowConfig: {
|
||||
drag: true,
|
||||
},
|
||||
checkboxConfig: {},
|
||||
editRules: {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
age: [
|
||||
{ required: true, message: '请输入年龄', trigger: 'blur' },
|
||||
{ min: 0, max: 200, message: '年龄必须为1-200' },
|
||||
],
|
||||
job: [{ required: true, message: '请选择工作', trigger: 'blur' }],
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'checkbox',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
dragSort: true,
|
||||
title: '排序',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '姓名',
|
||||
align: 'left',
|
||||
editRender: {},
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
if (!row.name) {
|
||||
return <span class="text-red-500">未填写</span>;
|
||||
}
|
||||
return <span>{row.name}</span>;
|
||||
},
|
||||
edit: ({ row }) => {
|
||||
return <Input placeholder={'请输入'} v-model:value={row.name} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
title: '年龄',
|
||||
align: 'left',
|
||||
editRender: {},
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
if (!row.age) {
|
||||
return <span class="text-red-500">未填写</span>;
|
||||
}
|
||||
return <span>{row.age}</span>;
|
||||
},
|
||||
edit: ({ row }) => {
|
||||
return (
|
||||
<InputNumber
|
||||
class="w-full"
|
||||
placeholder={'请输入'}
|
||||
v-model:value={row.age}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: '工作',
|
||||
title: 'job',
|
||||
align: 'left',
|
||||
editRender: {},
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
if (!row.job) {
|
||||
return <span class="text-red-500">未选择</span>;
|
||||
}
|
||||
return <span>{row.job}</span>;
|
||||
},
|
||||
edit: ({ row }) => {
|
||||
const options = ['前端佬', '后端佬', '组长'].map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
class="w-full"
|
||||
getPopupContainer={getPopupContainer}
|
||||
options={options}
|
||||
placeholder={'请选择'}
|
||||
v-model:value={row.job}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
slots: {
|
||||
default: ({ $table, row }) => {
|
||||
function handleDelete() {
|
||||
$table.remove(row);
|
||||
}
|
||||
return (
|
||||
<Button danger={true} onClick={handleDelete} size={'small'}>
|
||||
删除
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 自定义列
|
||||
custom: false,
|
||||
// 最大化
|
||||
zoom: false,
|
||||
// 刷新
|
||||
refresh: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const data = [
|
||||
{
|
||||
name: '张三',
|
||||
age: 18,
|
||||
job: '前端佬',
|
||||
},
|
||||
{
|
||||
name: '李四',
|
||||
age: 19,
|
||||
job: '后端佬',
|
||||
},
|
||||
{
|
||||
name: '王五',
|
||||
age: 20,
|
||||
job: '组长',
|
||||
},
|
||||
];
|
||||
await nextTick();
|
||||
await tableApi.grid.loadData(data);
|
||||
});
|
||||
async function handleAdd() {
|
||||
const record = { name: '', age: undefined, job: undefined };
|
||||
const { row: newRow } = await tableApi.grid.insert(record);
|
||||
await tableApi.grid.setEditCell(newRow, 'name');
|
||||
}
|
||||
|
||||
async function handleRemove() {
|
||||
await tableApi.grid.removeCheckboxRow();
|
||||
}
|
||||
|
||||
async function handleValidate() {
|
||||
const result = await tableApi.grid.validate(true);
|
||||
if (result) {
|
||||
message.error('校验失败');
|
||||
} else {
|
||||
message.success('校验成功');
|
||||
}
|
||||
}
|
||||
|
||||
function getData() {
|
||||
const data = tableApi.grid.getTableData();
|
||||
const { fullData } = data;
|
||||
console.log(fullData);
|
||||
Modal.info({
|
||||
title: '提示',
|
||||
content: (
|
||||
<div class="max-h-[350px] overflow-y-auto">
|
||||
<JsonPreview data={fullData} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicTable>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button @click="getData">获取表格数据</a-button>
|
||||
<a-button @click="handleValidate">校验</a-button>
|
||||
<a-button danger @click="handleRemove"> 删除勾选 </a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['system:config:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</template>
|
17
apps/web-antd/src/views/演示使用自行删除/vxe/index.vue
Normal file
17
apps/web-antd/src/views/演示使用自行删除/vxe/index.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
|
||||
import EditTable from './edit-table.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Card title="可编辑表格" size="small">
|
||||
<EditTable class="h-[500px]" />
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -5,13 +5,10 @@
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
@@ -32,6 +29,7 @@ import {
|
||||
ElTreeSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
import { h } from 'vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
import type { Language } from 'element-plus/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
$t,
|
||||
@@ -10,10 +8,10 @@ import {
|
||||
loadLocalesMapFromDir,
|
||||
} from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import enLocale from 'element-plus/es/locale/lang/en';
|
||||
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const elementLocale = ref<Language>(defaultLocale);
|
||||
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -139,6 +139,7 @@ const [Form, formApi] = useVbenForm({
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -4,13 +4,11 @@
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
NButton,
|
||||
NCheckbox,
|
||||
@@ -29,8 +27,7 @@ import {
|
||||
NTreeSelect,
|
||||
NUpload,
|
||||
} from 'naive-ui';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { h } from 'vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
|
@@ -10,8 +10,6 @@ import { $t } from '@vben/locales';
|
||||
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// naive-ui组件不接受onChang事件,所以需要禁用
|
||||
disabledOnChangeListener: true,
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
baseModelPropName: 'value',
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import {
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -101,7 +106,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { NotificationType } from 'naive-ui';
|
||||
|
||||
import { type NotificationType } from 'naive-ui';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui';
|
||||
|
||||
const notification = useNotification();
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { type DefaultTheme, defineConfig } from 'vitepress';
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { type DefaultTheme, defineConfig } from 'vitepress';
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
@@ -186,6 +188,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
link: 'common-ui/vben-count-to-animator',
|
||||
text: 'CountToAnimator 数字动画',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText 省略文本',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -10,7 +10,6 @@ import {
|
||||
|
||||
// import { useAntdDesignTokens } from '@vben/hooks';
|
||||
// import { initPreferences } from '@vben/preferences';
|
||||
|
||||
import { ConfigProvider, theme } from 'ant-design-vue';
|
||||
import mediumZoom from 'medium-zoom';
|
||||
import { useRoute } from 'vitepress';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
@@ -4,13 +4,10 @@
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
@@ -35,6 +32,7 @@ import {
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
|
@@ -14,8 +14,6 @@ initComponentAdapter();
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
// naive-ui组件不接受onChang事件,所以需要禁用
|
||||
disabledOnChangeListener: true,
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
modelPropNameMap: {
|
||||
|
@@ -123,6 +123,8 @@ function fetchApi(): Promise<Record<string, any>> {
|
||||
|
||||
:::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
|
@@ -54,6 +54,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
|
||||
|
||||
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
|
||||
:::
|
||||
|
||||
@@ -75,12 +76,15 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
| isOpen | 弹窗打开状态 | `boolean` | `false` |
|
||||
| loading | 弹窗加载状态 | `boolean` | `false` |
|
||||
| closable | 显示关闭按钮 | `boolean` | `true` |
|
||||
| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
|
||||
| modal | 显示遮罩 | `boolean` | `true` |
|
||||
| header | 显示header | `boolean` | `true` |
|
||||
| footer | 显示footer | `boolean\|slot` | `true` |
|
||||
@@ -108,12 +112,14 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
|
||||
| 事件名 | 描述 | 类型 | 版本限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` | --- |
|
||||
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
|
||||
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
|
||||
### Slots
|
||||
|
||||
@@ -124,6 +130,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| default | 默认插槽 - 弹窗内容 |
|
||||
| prepend-footer | 取消按钮左侧 |
|
||||
| append-footer | 取消按钮右侧 |
|
||||
| close-icon | 关闭按钮图标 |
|
||||
| extra | 额外内容(标题右侧) |
|
||||
|
||||
### modalApi
|
||||
|
||||
|
56
docs/src/components/common-ui/vben-ellipsis-text.md
Normal file
56
docs/src/components/common-ui/vben-ellipsis-text.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben EllipsisText 省略文本
|
||||
|
||||
框架提供的文本展示组件,可配置超长省略、tooltip提示、展开收起等功能。
|
||||
|
||||
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过默认插槽设置文本内容,`maxWidth`属性设置最大宽度。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/line" />
|
||||
|
||||
## 可折叠的文本块
|
||||
|
||||
通过`line`设置折叠后的行数,`expand`属性设置是否支持展开收起。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/expand" />
|
||||
|
||||
## 自定义提示浮层
|
||||
|
||||
通过名为`tooltip`的插槽定制提示信息。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| expand | 支持点击展开或收起 | `boolean` | `false` |
|
||||
| line | 文本最大行数 | `number` | `1` |
|
||||
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
||||
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
|
||||
| tooltip | 启用文本提示 | `boolean` | `true` |
|
||||
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
|
||||
| tooltipColor | 提示文本的颜色 | `string` | - |
|
||||
| tooltipFontSize | 提示文本的大小 | `string` | - |
|
||||
| tooltipMaxWidth | 提示浮层的最大宽度。如不设置则保持与文本宽度一致 | `number` | - |
|
||||
| tooltipOverlayStyle | 提示框内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| ------------ | ------------ | -------------------------- |
|
||||
| expandChange | 展开状态改变 | `(isExpand:boolean)=>void` |
|
||||
|
||||
### Slots
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------- | -------------------------------- |
|
||||
| tooltip | 启用文本提示时,用来定制提示内容 |
|
@@ -287,6 +287,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
|
||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
|
||||
| validate | 表单校验 | `()=>Promise<void>` |
|
||||
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
|
||||
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
|
||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
|
||||
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
|
||||
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
|
||||
@@ -311,14 +313,14 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
|
||||
| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
|
||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||
| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||
| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
|
||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||
|
||||
### TS 类型说明
|
||||
|
||||
@@ -355,10 +357,21 @@ export interface FormCommonConfig {
|
||||
* 所有表单项的props
|
||||
*/
|
||||
componentProps?: ComponentProps;
|
||||
/**
|
||||
* 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
|
||||
* 在有设置校验规则的场景下,建议不要将其设置为true
|
||||
* 默认为false。但用作表格的搜索表单时,默认为true
|
||||
* @default false
|
||||
*/
|
||||
compact?: boolean;
|
||||
/**
|
||||
* 所有表单项的控件样式
|
||||
*/
|
||||
controlClass?: string;
|
||||
/**
|
||||
* 在表单项的Label后显示一个冒号
|
||||
*/
|
||||
colon?: boolean;
|
||||
/**
|
||||
* 所有表单项的禁用状态
|
||||
* @default false
|
||||
@@ -418,7 +431,7 @@ export interface FormSchema<
|
||||
dependencies?: FormItemDependencies;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 字段名 */
|
||||
/** 字段名,也作为自定义插槽的名称 */
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: string;
|
||||
@@ -441,7 +454,7 @@ export interface FormSchema<
|
||||
|
||||
```ts
|
||||
dependencies: {
|
||||
// 只有当 name 字段的值变化时,才会触发联动
|
||||
// 触发字段。只有这些字段值变动时,联动才会触发
|
||||
triggerFields: ['name'],
|
||||
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
||||
if(values,formApi){},
|
||||
@@ -462,11 +475,11 @@ dependencies: {
|
||||
|
||||
### 表单校验
|
||||
|
||||
表单联动需要通过 schema 内的 `rules` 属性进行配置。
|
||||
表单校验需要通过 schema 内的 `rules` 属性进行配置。
|
||||
|
||||
rules的值可以是一个字符串,也可以是一个zod的schema。
|
||||
rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
|
||||
|
||||
#### 字符串
|
||||
#### 预定义的校验规则
|
||||
|
||||
```ts
|
||||
// 表示字段必填,默认会根据适配器的required进行国际化
|
||||
@@ -492,11 +505,16 @@ import { z } from '#/adapter/form';
|
||||
rules: z.string().min(1, { message: '请输入字符串' });
|
||||
}
|
||||
|
||||
// 可选,并且携带默认值
|
||||
// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
|
||||
{
|
||||
rules: z.string().default('默认值').optional(),
|
||||
}
|
||||
|
||||
// 可以是空字符串、undefined或者一个邮箱地址
|
||||
{
|
||||
rules: z.union(z.string().email().optional(), z.literal(""))
|
||||
}
|
||||
|
||||
// 复杂校验
|
||||
{
|
||||
z.string().min(1, { message: "请输入" })
|
||||
|
@@ -60,6 +60,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda
|
||||
|
||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
|
||||
:::
|
||||
|
||||
@@ -81,6 +82,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
|
@@ -165,6 +165,8 @@ vxeUI.renderer.add('CellLink', {
|
||||
|
||||
**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
|
||||
|
||||
当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/form" />
|
||||
|
||||
## 单元格编辑
|
||||
@@ -215,14 +217,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表单的方法。
|
||||
|
||||
| 方法名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| setLoading | 设置loading状态 | `(loading)=>void` |
|
||||
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` |
|
||||
| reload | 重载表格,会进行初始化 | `(params:any)=>void` |
|
||||
| query | 重载表格,会保留当前分页 | `(params:any)=>void` |
|
||||
| grid | vxe-table grid实例 | `VxeGridInstance` |
|
||||
| formApi | vbenForm api实例 | `FormApi` |
|
||||
| 方法名 | 描述 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| setLoading | 设置loading状态 | `(loading)=>void` | - |
|
||||
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` | - |
|
||||
| reload | 重载表格,会进行初始化 | `(params:any)=>void` | - |
|
||||
| query | 重载表格,会保留当前分页 | `(params:any)=>void` | - |
|
||||
| grid | vxe-table grid实例 | `VxeGridInstance` | - |
|
||||
| formApi | vbenForm api实例 | `FormApi` | - |
|
||||
| toggleSearchForm | 设置搜索表单显示状态 | `(show?: boolean)=>boolean` | 当省略参数时,则将表单在显示和隐藏两种状态之间切换 |
|
||||
|
||||
## Props
|
||||
|
||||
@@ -236,3 +239,4 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
|
||||
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
|
||||
| gridEvents | grid组件的触发的⌚️ | `VxeGridListeners` |
|
||||
| formOptions | 表单参数 | `VbenFormProps` |
|
||||
| showSearchForm | 是否显示搜索表单 | `boolean` |
|
||||
|
10
docs/src/demos/vben-ellipsis-text/expand/index.vue
Normal file
10
docs/src/demos/vben-ellipsis-text/expand/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
|
||||
</template>
|
10
docs/src/demos/vben-ellipsis-text/line/index.vue
Normal file
10
docs/src/demos/vben-ellipsis-text/line/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||
</template>
|
14
docs/src/demos/vben-ellipsis-text/tooltip/index.vue
Normal file
14
docs/src/demos/vben-ellipsis-text/tooltip/index.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="240">
|
||||
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
|
||||
<template #tooltip>
|
||||
<div style="text-align: center">
|
||||
《秦皇岛》<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
|
||||
深海的光 停滞的海浪
|
||||
</div>
|
||||
</template>
|
||||
</EllipsisText>
|
||||
</template>
|
@@ -110,6 +110,11 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
|
@@ -217,6 +217,7 @@ const defaultPreferences: Preferences = {
|
||||
globalSearch: true,
|
||||
},
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: false,
|
||||
enable: true,
|
||||
|
@@ -240,6 +240,7 @@ const defaultPreferences: Preferences = {
|
||||
globalSearch: true,
|
||||
},
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: false,
|
||||
enable: true,
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
## 新增组件库应用
|
||||
|
||||
如果你想用其他别的组件库,你只需要按一下步骤进行操作:
|
||||
如果你想用其他别的组件库,你只需要按以下步骤进行操作:
|
||||
|
||||
1. 在`apps`内创建一个新的文件夹,例如`apps/web-xxx`。
|
||||
2. 更改`apps/web-xxx/package.json`的`name`字段为`web-xxx`。
|
||||
|
@@ -67,7 +67,7 @@ pnpm install
|
||||
::: tip 注意
|
||||
|
||||
- 项目只支持使用 `pnpm` 进行依赖安装,默认会使用 `corepack` 来安装指定版本的 `pnpm`。:
|
||||
- 如果你的网络环境无法访问npm源,你可以设置系统的环境变量`COREPACK_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
|
||||
- 如果你的网络环境无法访问npm源,你可以设置系统的环境变量`COREPACK_NPM_REGISTRY=https://registry.npmmirror.com`,然后再执行`pnpm install`。
|
||||
- 如果你不想使用`corepack`,你需要禁用`corepack`,然后使用你自己的`pnpm`进行安装。
|
||||
|
||||
:::
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/commitlint-config",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -10,6 +10,7 @@ export async function importPluginConfig(): Promise<Linter.Config[]> {
|
||||
import: pluginImport,
|
||||
},
|
||||
rules: {
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
'import/first': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
// @ts-expect-error - no types
|
||||
import js from '@eslint/js';
|
||||
import pluginUnusedImports from 'eslint-plugin-unused-imports';
|
||||
import globals from 'globals';
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
import perfectionistPlugin from 'eslint-plugin-perfectionist';
|
||||
import { interopDefault } from '../util';
|
||||
|
||||
export async function perfectionist(): Promise<Linter.Config[]> {
|
||||
const perfectionistPlugin = await interopDefault(
|
||||
// @ts-expect-error - no types
|
||||
import('eslint-plugin-perfectionist'),
|
||||
);
|
||||
|
||||
return [
|
||||
perfectionistPlugin.configs['recommended-natural'],
|
||||
{
|
||||
@@ -19,21 +24,28 @@ export async function perfectionist(): Promise<Linter.Config[]> {
|
||||
{
|
||||
customGroups: {
|
||||
type: {
|
||||
vben: 'vben',
|
||||
vue: 'vue',
|
||||
'vben-core-type': ['^@vben-core/.+'],
|
||||
'vben-type': ['^@vben/.+'],
|
||||
'vue-type': ['^vue$', '^vue-.+', '^@vue/.+'],
|
||||
},
|
||||
value: {
|
||||
vben: ['@vben*', '@vben/**/**', '@vben-core/**/**'],
|
||||
vue: ['vue', 'vue-*', '@vue*'],
|
||||
vben: ['^@vben/.+'],
|
||||
'vben-core': ['^@vben-core/.+'],
|
||||
vue: ['^vue$', '^vue-.+', '^@vue/.+'],
|
||||
},
|
||||
},
|
||||
environment: 'node',
|
||||
groups: [
|
||||
['external-type', 'builtin-type', 'type'],
|
||||
'vue-type',
|
||||
'vben-type',
|
||||
'vben-core-type',
|
||||
['parent-type', 'sibling-type', 'index-type'],
|
||||
['internal-type'],
|
||||
'builtin',
|
||||
'vue',
|
||||
'vben',
|
||||
'vben-core',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling', 'index'],
|
||||
@@ -43,12 +55,13 @@ export async function perfectionist(): Promise<Linter.Config[]> {
|
||||
'object',
|
||||
'unknown',
|
||||
],
|
||||
internalPattern: ['#*', '#*/**'],
|
||||
internalPattern: ['^#/.+'],
|
||||
newlinesBetween: 'always',
|
||||
order: 'asc',
|
||||
type: 'natural',
|
||||
},
|
||||
],
|
||||
'perfectionist/sort-modules': 'off',
|
||||
'perfectionist/sort-named-exports': [
|
||||
'error',
|
||||
{
|
||||
@@ -67,42 +80,6 @@ export async function perfectionist(): Promise<Linter.Config[]> {
|
||||
groups: ['unknown', 'items', 'list', 'children'],
|
||||
ignorePattern: ['children'],
|
||||
order: 'asc',
|
||||
partitionByComment: 'Part:**',
|
||||
type: 'natural',
|
||||
},
|
||||
],
|
||||
'perfectionist/sort-vue-attributes': [
|
||||
'error',
|
||||
{
|
||||
// Based on: https://vuejs.org/style-guide/rules-recommended.html#element-attribute-order
|
||||
customGroups: {
|
||||
/* eslint-disable perfectionist/sort-objects */
|
||||
DEFINITION: '*(is|:is|v-is)',
|
||||
LIST_RENDERING: 'v-for',
|
||||
CONDITIONALS: 'v-*(else-if|if|else|show|cloak)',
|
||||
RENDER_MODIFIERS: 'v-*(pre|once)',
|
||||
GLOBAL: '*(:id|id)',
|
||||
UNIQUE: '*(ref|key|:ref|:key)',
|
||||
SLOT: '*(v-slot|slot)',
|
||||
TWO_WAY_BINDING: '*(v-model|v-model:*)',
|
||||
// OTHER_DIRECTIVES e.g. 'v-custom-directive'
|
||||
EVENTS: '*(v-on|@*)',
|
||||
CONTENT: 'v-*(html|text)',
|
||||
/* eslint-enable perfectionist/sort-objects */
|
||||
},
|
||||
groups: [
|
||||
'DEFINITION',
|
||||
'LIST_RENDERING',
|
||||
'CONDITIONALS',
|
||||
'RENDER_MODIFIERS',
|
||||
'GLOBAL',
|
||||
'UNIQUE',
|
||||
'SLOT',
|
||||
'TWO_WAY_BINDING',
|
||||
'unknown',
|
||||
'EVENTS',
|
||||
'CONTENT',
|
||||
],
|
||||
type: 'natural',
|
||||
},
|
||||
],
|
||||
|
@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
|
||||
|
||||
export async function vue(): Promise<Linter.Config[]> {
|
||||
const [pluginVue, parserVue, parserTs] = await Promise.all([
|
||||
// @ts-expect-error missing types
|
||||
interopDefault(import('eslint-plugin-vue')),
|
||||
interopDefault(import('vue-eslint-parser')),
|
||||
// @ts-expect-error missing types
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/stylelint-config",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/node-utils",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import ora, { type Ora } from 'ora';
|
||||
import type { Ora } from 'ora';
|
||||
|
||||
import ora from 'ora';
|
||||
|
||||
interface SpinnerOptions {
|
||||
failedText?: string;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tailwind-config",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tsconfig",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/vite-config",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { UserConfig } from 'vite';
|
||||
import type { CSSOptions, UserConfig } from 'vite';
|
||||
|
||||
import type { DefineApplicationOptions } from '../typing';
|
||||
|
||||
@@ -100,7 +100,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function createCssOptions(injectGlobalScss = true) {
|
||||
function createCssOptions(injectGlobalScss = true): CSSOptions {
|
||||
const root = findMonorepoRoot();
|
||||
return {
|
||||
preprocessorOptions: injectGlobalScss
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
@@ -5,8 +7,6 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { readPackageJSON } from '@vben/node-utils';
|
||||
|
||||
import { type PluginOption } from 'vite';
|
||||
|
||||
/**
|
||||
* 用于生成将loading样式注入到项目中
|
||||
* 为多app提供loading样式,无需在每个 app -> index.html单独引入
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user