Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into warmflow
This commit is contained in:
@@ -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) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
const withUrlList: UploadProps['fileList'] = [];
|
||||
for (const item of _fileList) {
|
||||
// ossId情况
|
||||
if (props.resultField === 'ossId') {
|
||||
const resp = await ossInfo([item]);
|
||||
if (item && isString(item)) {
|
||||
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)) {
|
||||
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(() => {
|
||||
// 移除请求状态缓存
|
||||
dictRequestCache.delete(dictName);
|
||||
/**
|
||||
* 这里主要判断字典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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return dictOptionList;
|
||||
/**
|
||||
* 这里是提供给渲染标签使用的方法
|
||||
* @param dictName 字典名称
|
||||
* @returns 字典信息
|
||||
*/
|
||||
export function getDict(dictName: string) {
|
||||
const { getDict } = useDictStore();
|
||||
return fetchAndCacheDictData(dictName, () => getDict(dictName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般是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="等待开发"
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
ref="menuSelectRef"
|
||||
v-bind="slotProps"
|
||||
:check-strictly="formApi.form.values.menuCheckStrictly"
|
||||
:tree-data="menuTree"
|
||||
@check-strictly-change="handleMenuCheckStrictlyChange"
|
||||
/>
|
||||
<div class="h-[600px] w-full">
|
||||
<!-- association为readonly 不能通过v-model绑定 -->
|
||||
<MenuSelectTable
|
||||
ref="menuSelectRef"
|
||||
: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,25 +37,34 @@ 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);
|
||||
const menus = resp.menus;
|
||||
// i18n处理
|
||||
eachTree(menus, (node) => {
|
||||
node.label = $t(node.label);
|
||||
});
|
||||
// 设置菜单信息
|
||||
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 menuTree = ref<any[]>([]);
|
||||
async function setupMenuTree() {
|
||||
const resp = await menuList();
|
||||
const treeData = listToTree(resp, { id: 'menuId' });
|
||||
// i18n处理
|
||||
eachTree(treeData, (node) => {
|
||||
node.menuName = $t(node.menuName);
|
||||
});
|
||||
// 设置菜单信息
|
||||
menuTree.value = treeData;
|
||||
}
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
onCancel: handleCancel,
|
||||
onConfirm: handleConfirm,
|
||||
@@ -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
|
||||
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>
|
||||
<div class="h-[600px] w-full">
|
||||
<!-- association为readonly 不能通过v-model绑定 -->
|
||||
<MenuSelectTable
|
||||
ref="menuSelectRef"
|
||||
: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>
|
Reference in New Issue
Block a user