feat: role management page with component `tree` (#5675)
* feat: add shadcn tree * fix: update vbenTree component * feat: role management demo page * feat: add cellSwitch renderer for vxeTable * chore: remove tree examplespull/62/head
							parent
							
								
									4b9cfcb867
								
							
						
					
					
						commit
						b37ed48b9d
					
				|  | @ -0,0 +1,83 @@ | ||||||
|  | import { faker } from '@faker-js/faker'; | ||||||
|  | import { verifyAccessToken } from '~/utils/jwt-utils'; | ||||||
|  | import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data'; | ||||||
|  | import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; | ||||||
|  | 
 | ||||||
|  | const formatterCN = new Intl.DateTimeFormat('zh-CN', { | ||||||
|  |   timeZone: 'Asia/Shanghai', | ||||||
|  |   year: 'numeric', | ||||||
|  |   month: '2-digit', | ||||||
|  |   day: '2-digit', | ||||||
|  |   hour: '2-digit', | ||||||
|  |   minute: '2-digit', | ||||||
|  |   second: '2-digit', | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const menuIds = getMenuIds(MOCK_MENU_LIST); | ||||||
|  | 
 | ||||||
|  | function generateMockDataList(count: number) { | ||||||
|  |   const dataList = []; | ||||||
|  | 
 | ||||||
|  |   for (let i = 0; i < count; i++) { | ||||||
|  |     const dataItem: Record<string, any> = { | ||||||
|  |       id: faker.string.uuid(), | ||||||
|  |       name: faker.commerce.product(), | ||||||
|  |       status: faker.helpers.arrayElement([0, 1]), | ||||||
|  |       createTime: formatterCN.format( | ||||||
|  |         faker.date.between({ from: '2022-01-01', to: '2025-01-01' }), | ||||||
|  |       ), | ||||||
|  |       permissions: faker.helpers.arrayElements(menuIds), | ||||||
|  |       remark: faker.lorem.sentence(), | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     dataList.push(dataItem); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return dataList; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const mockData = generateMockDataList(100); | ||||||
|  | 
 | ||||||
|  | export default eventHandler(async (event) => { | ||||||
|  |   const userinfo = verifyAccessToken(event); | ||||||
|  |   if (!userinfo) { | ||||||
|  |     return unAuthorizedResponse(event); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     page = 1, | ||||||
|  |     pageSize = 20, | ||||||
|  |     name, | ||||||
|  |     id, | ||||||
|  |     remark, | ||||||
|  |     startTime, | ||||||
|  |     endTime, | ||||||
|  |     status, | ||||||
|  |   } = getQuery(event); | ||||||
|  |   let listData = structuredClone(mockData); | ||||||
|  |   if (name) { | ||||||
|  |     listData = listData.filter((item) => | ||||||
|  |       item.name.toLowerCase().includes(String(name).toLowerCase()), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   if (id) { | ||||||
|  |     listData = listData.filter((item) => | ||||||
|  |       item.id.toLowerCase().includes(String(id).toLowerCase()), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   if (remark) { | ||||||
|  |     listData = listData.filter((item) => | ||||||
|  |       item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |   if (startTime) { | ||||||
|  |     listData = listData.filter((item) => item.createTime >= startTime); | ||||||
|  |   } | ||||||
|  |   if (endTime) { | ||||||
|  |     listData = listData.filter((item) => item.createTime <= endTime); | ||||||
|  |   } | ||||||
|  |   if (['0', '1'].includes(status as string)) { | ||||||
|  |     listData = listData.filter((item) => item.status === Number(status)); | ||||||
|  |   } | ||||||
|  |   return usePageResponseSuccess(page as string, pageSize as string, listData); | ||||||
|  | }); | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { faker } from '@faker-js/faker'; | import { faker } from '@faker-js/faker'; | ||||||
| import { verifyAccessToken } from '~/utils/jwt-utils'; | import { verifyAccessToken } from '~/utils/jwt-utils'; | ||||||
| import { unAuthorizedResponse } from '~/utils/response'; | import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response'; | ||||||
| 
 | 
 | ||||||
| function generateMockDataList(count: number) { | function generateMockDataList(count: number) { | ||||||
|   const dataList = []; |   const dataList = []; | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ export default defineEventHandler(async (event) => { | ||||||
|     ['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) && |     ['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) && | ||||||
|     event.path.startsWith('/api/system/') |     event.path.startsWith('/api/system/') | ||||||
|   ) { |   ) { | ||||||
|     await sleep(Math.floor(Math.random() * 1000)); |     await sleep(Math.floor(Math.random() * 2000)); | ||||||
|     return forbiddenResponse(event, '演示环境,禁止修改'); |     return forbiddenResponse(event, '演示环境,禁止修改'); | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -377,3 +377,14 @@ export const MOCK_MENU_LIST = [ | ||||||
|     path: '/about', |     path: '/about', | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  | 
 | ||||||
|  | export function getMenuIds(menus: any[]) { | ||||||
|  |   const ids: number[] = []; | ||||||
|  |   menus.forEach((item) => { | ||||||
|  |     ids.push(item.id); | ||||||
|  |     if (item.children && item.children.length > 0) { | ||||||
|  |       ids.push(...getMenuIds(item.children)); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   return ids; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -55,6 +55,9 @@ export { | ||||||
|   SearchX, |   SearchX, | ||||||
|   Settings, |   Settings, | ||||||
|   Shrink, |   Shrink, | ||||||
|  |   Square, | ||||||
|  |   SquareCheckBig, | ||||||
|  |   SquareMinus, | ||||||
|   Sun, |   Sun, | ||||||
|   SunMoon, |   SunMoon, | ||||||
|   SwatchBook, |   SwatchBook, | ||||||
|  |  | ||||||
|  | @ -27,3 +27,4 @@ export * from './textarea'; | ||||||
| export * from './toggle'; | export * from './toggle'; | ||||||
| export * from './toggle-group'; | export * from './toggle-group'; | ||||||
| export * from './tooltip'; | export * from './tooltip'; | ||||||
|  | export * from './tree'; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | export { default as VbenTree } from './tree.vue'; | ||||||
|  | export type { FlattenedItem } from 'radix-vue'; | ||||||
|  | @ -0,0 +1,301 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { Arrayable } from '@vueuse/core'; | ||||||
|  | import type { FlattenedItem } from 'radix-vue'; | ||||||
|  | 
 | ||||||
|  | import type { ClassType, Recordable } from '@vben-core/typings'; | ||||||
|  | 
 | ||||||
|  | import { onMounted, ref, watch, watchEffect } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { ChevronRight, IconifyIcon } from '@vben-core/icons'; | ||||||
|  | import { cn, get } from '@vben-core/shared/utils'; | ||||||
|  | 
 | ||||||
|  | import { useVModel } from '@vueuse/core'; | ||||||
|  | import { TreeItem, TreeRoot } from 'radix-vue'; | ||||||
|  | 
 | ||||||
|  | import { Checkbox } from '../checkbox'; | ||||||
|  | 
 | ||||||
|  | interface TreeProps { | ||||||
|  |   /** 单选时允许取消已有选项 */ | ||||||
|  |   allowClear?: boolean; | ||||||
|  |   /** 显示边框 */ | ||||||
|  |   bordered?: boolean; | ||||||
|  |   /** 取消父子关联选择 */ | ||||||
|  |   checkStrictly?: boolean; | ||||||
|  |   /** 子级字段名 */ | ||||||
|  |   childrenField?: string; | ||||||
|  |   /** 默认展开的键 */ | ||||||
|  |   defaultExpandedKeys?: Array<number | string>; | ||||||
|  |   /** 默认展开的级别(优先级高于defaultExpandedKeys) */ | ||||||
|  |   defaultExpandedLevel?: number; | ||||||
|  |   /** 默认值 */ | ||||||
|  |   defaultValue?: Arrayable<number | string>; | ||||||
|  |   /** 禁用 */ | ||||||
|  |   disabled?: boolean; | ||||||
|  |   /** 自定义节点类名 */ | ||||||
|  |   getNodeClass?: (item: FlattenedItem<Recordable<any>>) => string; | ||||||
|  |   iconField?: string; | ||||||
|  |   /** label字段 */ | ||||||
|  |   labelField?: string; | ||||||
|  |   /** 当前值 */ | ||||||
|  |   modelValue?: Arrayable<number | string>; | ||||||
|  |   /** 是否多选 */ | ||||||
|  |   multiple?: boolean; | ||||||
|  |   /** 显示由iconField指定的图标 */ | ||||||
|  |   showIcon?: boolean; | ||||||
|  |   /** 启用展开收缩动画 */ | ||||||
|  |   transition?: boolean; | ||||||
|  |   /** 树数据 */ | ||||||
|  |   treeData: Recordable<any>[]; | ||||||
|  |   /** 值字段 */ | ||||||
|  |   valueField?: string; | ||||||
|  | } | ||||||
|  | const props = withDefaults(defineProps<TreeProps>(), { | ||||||
|  |   allowClear: false, | ||||||
|  |   bordered: false, | ||||||
|  |   checkStrictly: false, | ||||||
|  |   defaultExpandedKeys: () => [], | ||||||
|  |   disabled: false, | ||||||
|  |   expanded: () => [], | ||||||
|  |   iconField: 'icon', | ||||||
|  |   labelField: 'label', | ||||||
|  |   modelValue: () => [], | ||||||
|  |   multiple: false, | ||||||
|  |   showIcon: true, | ||||||
|  |   transition: false, | ||||||
|  |   valueField: 'value', | ||||||
|  |   childrenField: 'children', | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const emits = defineEmits<{ | ||||||
|  |   expand: [value: FlattenedItem<Recordable<any>>]; | ||||||
|  |   select: [value: FlattenedItem<Recordable<any>>]; | ||||||
|  |   'update:modelValue': [value: Arrayable<Recordable<any>>]; | ||||||
|  | }>(); | ||||||
|  | 
 | ||||||
|  | interface InnerFlattenItem<T = Recordable<any>> { | ||||||
|  |   hasChildren: boolean; | ||||||
|  |   level: number; | ||||||
|  |   value: T; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function flatten<T = Recordable<any>>( | ||||||
|  |   items: T[], | ||||||
|  |   childrenField: string = 'children', | ||||||
|  |   level = 0, | ||||||
|  | ): InnerFlattenItem<T>[] { | ||||||
|  |   const result: InnerFlattenItem<T>[] = []; | ||||||
|  |   items.forEach((item) => { | ||||||
|  |     const children = get(item, childrenField) as Array<T>; | ||||||
|  |     const val = { | ||||||
|  |       hasChildren: Array.isArray(children) && children.length > 0, | ||||||
|  |       level, | ||||||
|  |       value: item, | ||||||
|  |     }; | ||||||
|  |     result.push(val); | ||||||
|  |     if (val.hasChildren) | ||||||
|  |       result.push(...flatten(children, childrenField, level + 1)); | ||||||
|  |   }); | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const flattenData = ref<Array<InnerFlattenItem>>([]); | ||||||
|  | const modelValue = useVModel(props, 'modelValue', emits, { | ||||||
|  |   deep: true, | ||||||
|  |   defaultValue: props.defaultValue ?? [], | ||||||
|  |   passive: (props.modelValue === undefined) as false, | ||||||
|  | }); | ||||||
|  | const expanded = ref<Array<number | string>>(props.defaultExpandedKeys ?? []); | ||||||
|  | 
 | ||||||
|  | const treeValue = ref(); | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  |   watchEffect(() => { | ||||||
|  |     flattenData.value = flatten(props.treeData, props.childrenField); | ||||||
|  |     updateTreeValue(); | ||||||
|  |     if ( | ||||||
|  |       props.defaultExpandedLevel !== undefined && | ||||||
|  |       props.defaultExpandedLevel > 0 | ||||||
|  |     ) | ||||||
|  |       expandToLevel(props.defaultExpandedLevel); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function getItemByValue(value: number | string) { | ||||||
|  |   return flattenData.value.find( | ||||||
|  |     (item) => get(item.value, props.valueField) === value, | ||||||
|  |   )?.value; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function updateTreeValue() { | ||||||
|  |   const val = modelValue.value; | ||||||
|  |   treeValue.value = Array.isArray(val) | ||||||
|  |     ? val.map((v) => getItemByValue(v)) | ||||||
|  |     : getItemByValue(val); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch( | ||||||
|  |   modelValue, | ||||||
|  |   () => { | ||||||
|  |     updateTreeValue(); | ||||||
|  |   }, | ||||||
|  |   { deep: true, immediate: true }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | function updateModelValue(val: Arrayable<Recordable<any>>) { | ||||||
|  |   modelValue.value = Array.isArray(val) | ||||||
|  |     ? val.map((v) => get(v, props.valueField)) | ||||||
|  |     : get(val, props.valueField); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function expandToLevel(level: number) { | ||||||
|  |   const keys: string[] = []; | ||||||
|  |   flattenData.value.forEach((item) => { | ||||||
|  |     if (item.level <= level - 1) { | ||||||
|  |       keys.push(get(item.value, props.valueField)); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   expanded.value = keys; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function collapseNodes(value: Arrayable<number | string>) { | ||||||
|  |   const keys = new Set(Array.isArray(value) ? value : [value]); | ||||||
|  |   expanded.value = expanded.value.filter((key) => !keys.has(key)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function expandNodes(value: Arrayable<number | string>) { | ||||||
|  |   const keys = [...(Array.isArray(value) ? value : [value])]; | ||||||
|  |   keys.forEach((key) => { | ||||||
|  |     if (expanded.value.includes(key)) return; | ||||||
|  |     const item = getItemByValue(key); | ||||||
|  |     if (item) { | ||||||
|  |       expanded.value.push(key); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function expandAll() { | ||||||
|  |   expanded.value = flattenData.value | ||||||
|  |     .filter((item) => item.hasChildren) | ||||||
|  |     .map((item) => get(item.value, props.valueField)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function collapseAll() { | ||||||
|  |   expanded.value = []; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onToggle(item: FlattenedItem<Recordable<any>>) { | ||||||
|  |   emits('expand', item); | ||||||
|  | } | ||||||
|  | function onSelect(item: FlattenedItem<Recordable<any>>) { | ||||||
|  |   emits('select', item); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | defineExpose({ | ||||||
|  |   collapseAll, | ||||||
|  |   collapseNodes, | ||||||
|  |   expandAll, | ||||||
|  |   expandNodes, | ||||||
|  |   expandToLevel, | ||||||
|  |   getItemByValue, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <TreeRoot | ||||||
|  |     :get-key="(item) => get(item, valueField)" | ||||||
|  |     :get-children="(item) => get(item, childrenField)" | ||||||
|  |     :items="treeData" | ||||||
|  |     :model-value="treeValue" | ||||||
|  |     v-model:expanded="expanded as string[]" | ||||||
|  |     :default-expanded="defaultExpandedKeys as string[]" | ||||||
|  |     :propagate-select="!checkStrictly" | ||||||
|  |     :multiple="multiple" | ||||||
|  |     :disabled="disabled" | ||||||
|  |     :selection-behavior="allowClear || multiple ? 'toggle' : 'replace'" | ||||||
|  |     @update:model-value="updateModelValue" | ||||||
|  |     v-slot="{ flattenItems }" | ||||||
|  |     :class=" | ||||||
|  |       cn( | ||||||
|  |         'text-blackA11 select-none list-none rounded-lg p-2 text-sm font-medium', | ||||||
|  |         $attrs.class as unknown as ClassType, | ||||||
|  |         bordered ? 'border' : '', | ||||||
|  |       ) | ||||||
|  |     " | ||||||
|  |   > | ||||||
|  |     <div class="w-full" v-if="$slots.header"> | ||||||
|  |       <slot name="header"> </slot> | ||||||
|  |     </div> | ||||||
|  |     <TreeItem | ||||||
|  |       v-for="item in flattenItems" | ||||||
|  |       v-slot="{ | ||||||
|  |         isExpanded, | ||||||
|  |         isSelected, | ||||||
|  |         isIndeterminate, | ||||||
|  |         handleSelect, | ||||||
|  |         handleToggle, | ||||||
|  |       }" | ||||||
|  |       :key="item._id" | ||||||
|  |       :style="{ 'padding-left': `${item.level - 0.5}rem` }" | ||||||
|  |       :class=" | ||||||
|  |         cn('cursor-pointer', getNodeClass?.(item), { | ||||||
|  |           'data-[selected]:bg-accent': !multiple, | ||||||
|  |         }) | ||||||
|  |       " | ||||||
|  |       v-bind="item.bind" | ||||||
|  |       @select=" | ||||||
|  |         (event) => { | ||||||
|  |           if (event.detail.originalEvent.type === 'click') { | ||||||
|  |             // event.preventDefault(); | ||||||
|  |           } | ||||||
|  |           onSelect(item); | ||||||
|  |         } | ||||||
|  |       " | ||||||
|  |       @toggle=" | ||||||
|  |         (event) => { | ||||||
|  |           if (event.detail.originalEvent.type === 'click') { | ||||||
|  |             event.preventDefault(); | ||||||
|  |           } | ||||||
|  |           onToggle(item); | ||||||
|  |         } | ||||||
|  |       " | ||||||
|  |       class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2" | ||||||
|  |     > | ||||||
|  |       <ChevronRight | ||||||
|  |         v-if="item.hasChildren" | ||||||
|  |         class="size-4 cursor-pointer transition" | ||||||
|  |         :class="{ 'rotate-90': isExpanded }" | ||||||
|  |         @click.stop="handleToggle" | ||||||
|  |       /> | ||||||
|  |       <div v-else class="h-4 w-4"> | ||||||
|  |         <!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> --> | ||||||
|  |       </div> | ||||||
|  |       <Checkbox | ||||||
|  |         v-if="multiple" | ||||||
|  |         :checked="isSelected" | ||||||
|  |         :indeterminate="isIndeterminate" | ||||||
|  |         @click.stop="handleSelect" | ||||||
|  |       /> | ||||||
|  |       <div | ||||||
|  |         class="flex items-center gap-1 pl-2" | ||||||
|  |         @click=" | ||||||
|  |           ($event) => { | ||||||
|  |             $event.stopPropagation(); | ||||||
|  |             $event.preventDefault(); | ||||||
|  |             handleSelect(); | ||||||
|  |           } | ||||||
|  |         " | ||||||
|  |       > | ||||||
|  |         <slot name="node" v-bind="item"> | ||||||
|  |           <IconifyIcon | ||||||
|  |             class="size-4" | ||||||
|  |             v-if="showIcon && get(item.value, iconField)" | ||||||
|  |             :icon="get(item.value, iconField)" | ||||||
|  |           /> | ||||||
|  |           {{ get(item.value, labelField) }} | ||||||
|  |         </slot> | ||||||
|  |       </div> | ||||||
|  |     </TreeItem> | ||||||
|  |     <div class="w-full" v-if="$slots.footer"> | ||||||
|  |       <slot name="footer"> </slot> | ||||||
|  |     </div> | ||||||
|  |   </TreeRoot> | ||||||
|  | </template> | ||||||
|  | @ -22,6 +22,8 @@ export { | ||||||
|   VbenLoading, |   VbenLoading, | ||||||
|   VbenPinInput, |   VbenPinInput, | ||||||
|   VbenSpinner, |   VbenSpinner, | ||||||
|  |   VbenTree, | ||||||
| } from '@vben-core/shadcn-ui'; | } from '@vben-core/shadcn-ui'; | ||||||
| 
 | 
 | ||||||
|  | export type { FlattenedItem } from '@vben-core/shadcn-ui'; | ||||||
| export { globalShareState } from '@vben-core/shared/global-state'; | export { globalShareState } from '@vben-core/shared/global-state'; | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; | ||||||
| import { get, isFunction, isString } from '@vben/utils'; | import { get, isFunction, isString } from '@vben/utils'; | ||||||
| 
 | 
 | ||||||
| import { objectOmit } from '@vueuse/core'; | import { objectOmit } from '@vueuse/core'; | ||||||
| import { Button, Image, Popconfirm, Tag } from 'ant-design-vue'; | import { Button, Image, Popconfirm, Switch, Tag } from 'ant-design-vue'; | ||||||
| 
 | 
 | ||||||
| import { $t } from '#/locales'; | import { $t } from '#/locales'; | ||||||
| 
 | 
 | ||||||
|  | @ -94,6 +94,34 @@ setupVbenVxeTable({ | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     vxeUI.renderer.add('CellSwitch', { | ||||||
|  |       renderTableDefault({ attrs, props }, { column, row }) { | ||||||
|  |         const loadingKey = `__loading_${column.field}`; | ||||||
|  |         const finallyProps = { | ||||||
|  |           checkedChildren: $t('common.enabled'), | ||||||
|  |           checkedValue: 1, | ||||||
|  |           unCheckedChildren: $t('common.disabled'), | ||||||
|  |           unCheckedValue: 0, | ||||||
|  |           ...props, | ||||||
|  |           checked: row[column.field], | ||||||
|  |           loading: row[loadingKey] ?? false, | ||||||
|  |           'onUpdate:checked': onChange, | ||||||
|  |         }; | ||||||
|  |         async function onChange(newVal: any) { | ||||||
|  |           row[loadingKey] = true; | ||||||
|  |           try { | ||||||
|  |             const result = await attrs?.beforeChange?.(newVal, row); | ||||||
|  |             if (result !== false) { | ||||||
|  |               row[column.field] = newVal; | ||||||
|  |             } | ||||||
|  |           } finally { | ||||||
|  |             row[loadingKey] = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         return h(Switch, finallyProps); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * 注册表格的操作按钮渲染器 |      * 注册表格的操作按钮渲染器 | ||||||
|      */ |      */ | ||||||
|  | @ -183,6 +211,9 @@ setupVbenVxeTable({ | ||||||
|           return h( |           return h( | ||||||
|             Popconfirm, |             Popconfirm, | ||||||
|             { |             { | ||||||
|  |               getPopupContainer(el) { | ||||||
|  |                 return el.closest('tbody') || document.body; | ||||||
|  |               }, | ||||||
|               placement: 'topLeft', |               placement: 'topLeft', | ||||||
|               title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), |               title: $t('ui.actionTitle.delete', [attrs?.nameTitle || '']), | ||||||
|               ...props, |               ...props, | ||||||
|  |  | ||||||
|  | @ -1,2 +1,3 @@ | ||||||
| export * from './core'; | export * from './core'; | ||||||
| export * from './examples'; | export * from './examples'; | ||||||
|  | export * from './system'; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | export * from './dept'; | ||||||
|  | export * from './menu'; | ||||||
|  | export * from './role'; | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | import type { Recordable } from '@vben/types'; | ||||||
|  | 
 | ||||||
|  | import { requestClient } from '#/api/request'; | ||||||
|  | 
 | ||||||
|  | export namespace SystemRoleApi { | ||||||
|  |   export interface SystemRole { | ||||||
|  |     [key: string]: any; | ||||||
|  |     id: string; | ||||||
|  |     name: string; | ||||||
|  |     permissions: string[]; | ||||||
|  |     remark?: string; | ||||||
|  |     status: 0 | 1; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 获取角色列表数据 | ||||||
|  |  */ | ||||||
|  | async function getRoleList(params: Recordable<any>) { | ||||||
|  |   return requestClient.get<Array<SystemRoleApi.SystemRole>>( | ||||||
|  |     '/system/role/list', | ||||||
|  |     { params }, | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 创建角色 | ||||||
|  |  * @param data 角色数据 | ||||||
|  |  */ | ||||||
|  | async function createRole(data: Omit<SystemRoleApi.SystemRole, 'id'>) { | ||||||
|  |   return requestClient.post('/system/role', data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 更新角色 | ||||||
|  |  * | ||||||
|  |  * @param id 角色 ID | ||||||
|  |  * @param data 角色数据 | ||||||
|  |  */ | ||||||
|  | async function updateRole( | ||||||
|  |   id: string, | ||||||
|  |   data: Omit<SystemRoleApi.SystemRole, 'id'>, | ||||||
|  | ) { | ||||||
|  |   return requestClient.put(`/system/role/${id}`, data); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 删除角色 | ||||||
|  |  * @param id 角色 ID | ||||||
|  |  */ | ||||||
|  | async function deleteRole(id: string) { | ||||||
|  |   return requestClient.delete(`/system/role/${id}`); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { createRole, deleteRole, getRoleList, updateRole }; | ||||||
|  | @ -48,5 +48,18 @@ | ||||||
|       "none": "None" |       "none": "None" | ||||||
|     }, |     }, | ||||||
|     "badgeVariants": "Badge Style" |     "badgeVariants": "Badge Style" | ||||||
|  |   }, | ||||||
|  |   "role": { | ||||||
|  |     "title": "Role Management", | ||||||
|  |     "list": "Role List", | ||||||
|  |     "name": "Role", | ||||||
|  |     "roleName": "Role Name", | ||||||
|  |     "id": "Role ID", | ||||||
|  |     "status": "Status", | ||||||
|  |     "remark": "Remark", | ||||||
|  |     "createTime": "Creation Time", | ||||||
|  |     "operation": "Operation", | ||||||
|  |     "permissions": "Permissions", | ||||||
|  |     "setPermissions": "Permissions" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| { | { | ||||||
|   "dept": { |   "dept": { | ||||||
|  |     "list": "部门列表", | ||||||
|     "createTime": "创建时间", |     "createTime": "创建时间", | ||||||
|     "deptName": "部门名称", |     "deptName": "部门名称", | ||||||
|     "name": "部门", |     "name": "部门", | ||||||
|  | @ -10,6 +11,7 @@ | ||||||
|     "title": "部门管理" |     "title": "部门管理" | ||||||
|   }, |   }, | ||||||
|   "menu": { |   "menu": { | ||||||
|  |     "list": "菜单列表", | ||||||
|     "activeIcon": "激活图标", |     "activeIcon": "激活图标", | ||||||
|     "activePath": "激活路径", |     "activePath": "激活路径", | ||||||
|     "activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径", |     "activePathHelp": "跳转到当前路由时,需要激活的菜单路径。\n当不在导航菜单中显示时,需要指定激活路径", | ||||||
|  | @ -48,5 +50,18 @@ | ||||||
|     "typeLink": "外链", |     "typeLink": "外链", | ||||||
|     "typeMenu": "菜单" |     "typeMenu": "菜单" | ||||||
|   }, |   }, | ||||||
|  |   "role": { | ||||||
|  |     "title": "角色管理", | ||||||
|  |     "list": "角色列表", | ||||||
|  |     "name": "角色", | ||||||
|  |     "roleName": "角色名称", | ||||||
|  |     "id": "角色ID", | ||||||
|  |     "status": "状态", | ||||||
|  |     "remark": "备注", | ||||||
|  |     "createTime": "创建时间", | ||||||
|  |     "operation": "操作", | ||||||
|  |     "permissions": "权限", | ||||||
|  |     "setPermissions": "授权" | ||||||
|  |   }, | ||||||
|   "title": "系统管理" |   "title": "系统管理" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,15 @@ const routes: RouteRecordRaw[] = [ | ||||||
|     name: 'System', |     name: 'System', | ||||||
|     path: '/system', |     path: '/system', | ||||||
|     children: [ |     children: [ | ||||||
|  |       { | ||||||
|  |         path: '/system/role', | ||||||
|  |         name: 'SystemRole', | ||||||
|  |         meta: { | ||||||
|  |           icon: 'mdi:account-group', | ||||||
|  |           title: $t('system.role.title'), | ||||||
|  |         }, | ||||||
|  |         component: () => import('#/views/system/role/list.vue'), | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         path: '/system/menu', |         path: '/system/menu', | ||||||
|         name: 'SystemMenu', |         name: 'SystemMenu', | ||||||
|  |  | ||||||
|  | @ -0,0 +1,127 @@ | ||||||
|  | import type { VbenFormSchema } from '#/adapter/form'; | ||||||
|  | import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||||
|  | import type { SystemRoleApi } from '#/api'; | ||||||
|  | 
 | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | 
 | ||||||
|  | export function useFormSchema(): VbenFormSchema[] { | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       component: 'Input', | ||||||
|  |       fieldName: 'name', | ||||||
|  |       label: $t('system.role.roleName'), | ||||||
|  |       rules: 'required', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: 'RadioGroup', | ||||||
|  |       componentProps: { | ||||||
|  |         buttonStyle: 'solid', | ||||||
|  |         options: [ | ||||||
|  |           { label: $t('common.enabled'), value: 1 }, | ||||||
|  |           { label: $t('common.disabled'), value: 0 }, | ||||||
|  |         ], | ||||||
|  |         optionType: 'button', | ||||||
|  |       }, | ||||||
|  |       defaultValue: 1, | ||||||
|  |       fieldName: 'status', | ||||||
|  |       label: $t('system.role.status'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: 'Textarea', | ||||||
|  |       fieldName: 'remark', | ||||||
|  |       label: $t('system.role.remark'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: 'Input', | ||||||
|  |       fieldName: 'permissions', | ||||||
|  |       formItemClass: 'items-start', | ||||||
|  |       label: $t('system.role.setPermissions'), | ||||||
|  |       modelPropName: 'modelValue', | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useGridFormSchema(): VbenFormSchema[] { | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       component: 'Input', | ||||||
|  |       fieldName: 'name', | ||||||
|  |       label: $t('system.role.roleName'), | ||||||
|  |     }, | ||||||
|  |     { component: 'Input', fieldName: 'id', label: $t('system.role.id') }, | ||||||
|  |     { | ||||||
|  |       component: 'Select', | ||||||
|  |       componentProps: { | ||||||
|  |         allowClear: true, | ||||||
|  |         options: [ | ||||||
|  |           { label: $t('common.enabled'), value: 1 }, | ||||||
|  |           { label: $t('common.disabled'), value: 0 }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       fieldName: 'status', | ||||||
|  |       label: $t('system.role.status'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: 'Input', | ||||||
|  |       fieldName: 'remark', | ||||||
|  |       label: $t('system.role.remark'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       component: 'RangePicker', | ||||||
|  |       fieldName: 'createTime', | ||||||
|  |       label: $t('system.role.createTime'), | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function useColumns<T = SystemRoleApi.SystemRole>( | ||||||
|  |   onActionClick: OnActionClickFn<T>, | ||||||
|  |   onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>, | ||||||
|  | ): VxeTableGridOptions['columns'] { | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       field: 'name', | ||||||
|  |       title: $t('system.role.roleName'), | ||||||
|  |       width: 200, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       field: 'id', | ||||||
|  |       title: $t('system.role.id'), | ||||||
|  |       width: 200, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       cellRender: { | ||||||
|  |         attrs: { beforeChange: onStatusChange }, | ||||||
|  |         name: onStatusChange ? 'CellSwitch' : 'CellTag', | ||||||
|  |       }, | ||||||
|  |       field: 'status', | ||||||
|  |       title: $t('system.role.status'), | ||||||
|  |       width: 100, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       field: 'remark', | ||||||
|  |       minWidth: 100, | ||||||
|  |       title: $t('system.role.remark'), | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       field: 'createTime', | ||||||
|  |       title: $t('system.role.createTime'), | ||||||
|  |       width: 200, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       align: 'center', | ||||||
|  |       cellRender: { | ||||||
|  |         attrs: { | ||||||
|  |           nameField: 'name', | ||||||
|  |           nameTitle: $t('system.role.name'), | ||||||
|  |           onClick: onActionClick, | ||||||
|  |         }, | ||||||
|  |         name: 'CellOperation', | ||||||
|  |       }, | ||||||
|  |       field: 'operation', | ||||||
|  |       fixed: 'right', | ||||||
|  |       title: $t('system.role.operation'), | ||||||
|  |       width: 130, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,164 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { Recordable } from '@vben/types'; | ||||||
|  | 
 | ||||||
|  | import type { | ||||||
|  |   OnActionClickParams, | ||||||
|  |   VxeTableGridOptions, | ||||||
|  | } from '#/adapter/vxe-table'; | ||||||
|  | import type { SystemRoleApi } from '#/api'; | ||||||
|  | 
 | ||||||
|  | import { Page, useVbenDrawer } from '@vben/common-ui'; | ||||||
|  | import { Plus } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { Button, message, Modal } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
|  | import { useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||||
|  | import { deleteRole, getRoleList, updateRole } from '#/api'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | 
 | ||||||
|  | import { useColumns, useGridFormSchema } from './data'; | ||||||
|  | import Form from './modules/form.vue'; | ||||||
|  | 
 | ||||||
|  | const [FormDrawer, formDrawerApi] = useVbenDrawer({ | ||||||
|  |   connectedComponent: Form, | ||||||
|  |   destroyOnClose: true, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const [Grid, gridApi] = useVbenVxeGrid({ | ||||||
|  |   formOptions: { | ||||||
|  |     fieldMappingTime: [['createTime', ['startTime', 'endTime']]], | ||||||
|  |     schema: useGridFormSchema(), | ||||||
|  |     submitOnChange: true, | ||||||
|  |   }, | ||||||
|  |   gridOptions: { | ||||||
|  |     columns: useColumns(onActionClick, onStatusChange), | ||||||
|  |     height: 'auto', | ||||||
|  |     keepSource: true, | ||||||
|  |     proxyConfig: { | ||||||
|  |       ajax: { | ||||||
|  |         query: async ({ page }, formValues) => { | ||||||
|  |           return await getRoleList({ | ||||||
|  |             page: page.currentPage, | ||||||
|  |             pageSize: page.pageSize, | ||||||
|  |             ...formValues, | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     rowConfig: { | ||||||
|  |       keyField: 'id', | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     toolbarConfig: { | ||||||
|  |       custom: true, | ||||||
|  |       export: false, | ||||||
|  |       refresh: { code: 'query' }, | ||||||
|  |       search: true, | ||||||
|  |       zoom: true, | ||||||
|  |     }, | ||||||
|  |   } as VxeTableGridOptions<SystemRoleApi.SystemRole>, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRole>) { | ||||||
|  |   switch (e.code) { | ||||||
|  |     case 'delete': { | ||||||
|  |       onDelete(e.row); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case 'edit': { | ||||||
|  |       onEdit(e.row); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 将Antd的Modal.confirm封装为promise,方便在异步函数中调用。 | ||||||
|  |  * @param content 提示内容 | ||||||
|  |  * @param title 提示标题 | ||||||
|  |  */ | ||||||
|  | function confirm(content: string, title: string) { | ||||||
|  |   return new Promise((reslove, reject) => { | ||||||
|  |     Modal.confirm({ | ||||||
|  |       content, | ||||||
|  |       onCancel() { | ||||||
|  |         reject(new Error('已取消')); | ||||||
|  |       }, | ||||||
|  |       onOk() { | ||||||
|  |         reslove(true); | ||||||
|  |       }, | ||||||
|  |       title, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 状态开关即将改变 | ||||||
|  |  * @param newStatus 期望改变的状态值 | ||||||
|  |  * @param row 行数据 | ||||||
|  |  * @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变 | ||||||
|  |  */ | ||||||
|  | async function onStatusChange( | ||||||
|  |   newStatus: number, | ||||||
|  |   row: SystemRoleApi.SystemRole, | ||||||
|  | ) { | ||||||
|  |   const status: Recordable<string> = { | ||||||
|  |     0: '禁用', | ||||||
|  |     1: '启用', | ||||||
|  |   }; | ||||||
|  |   try { | ||||||
|  |     await confirm( | ||||||
|  |       `你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`, | ||||||
|  |       `切换状态`, | ||||||
|  |     ); | ||||||
|  |     await updateRole(row.id, { status: newStatus }); | ||||||
|  |     return true; | ||||||
|  |   } catch { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onEdit(row: SystemRoleApi.SystemRole) { | ||||||
|  |   formDrawerApi.setData(row).open(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onDelete(row: SystemRoleApi.SystemRole) { | ||||||
|  |   const hideLoading = message.loading({ | ||||||
|  |     content: $t('ui.actionMessage.deleting', [row.name]), | ||||||
|  |     duration: 0, | ||||||
|  |     key: 'action_process_msg', | ||||||
|  |   }); | ||||||
|  |   deleteRole(row.id) | ||||||
|  |     .then(() => { | ||||||
|  |       message.success({ | ||||||
|  |         content: $t('ui.actionMessage.deleteSuccess', [row.name]), | ||||||
|  |         key: 'action_process_msg', | ||||||
|  |       }); | ||||||
|  |       onRefresh(); | ||||||
|  |     }) | ||||||
|  |     .catch(() => { | ||||||
|  |       hideLoading(); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onRefresh() { | ||||||
|  |   gridApi.query(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onCreate() { | ||||||
|  |   formDrawerApi.setData({}).open(); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <Page auto-content-height> | ||||||
|  |     <FormDrawer /> | ||||||
|  |     <Grid :table-title="$t('system.role.list')"> | ||||||
|  |       <template #toolbar-tools> | ||||||
|  |         <Button type="primary" @click="onCreate"> | ||||||
|  |           <Plus class="size-5" /> | ||||||
|  |           {{ $t('ui.actionTitle.create', [$t('system.role.name')]) }} | ||||||
|  |         </Button> | ||||||
|  |       </template> | ||||||
|  |     </Grid> | ||||||
|  |   </Page> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,139 @@ | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import type { DataNode } from 'ant-design-vue/es/tree'; | ||||||
|  | 
 | ||||||
|  | import type { Recordable } from '@vben/types'; | ||||||
|  | 
 | ||||||
|  | import type { SystemRoleApi } from '#/api/system/role'; | ||||||
|  | 
 | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { useVbenDrawer, VbenTree } from '@vben/common-ui'; | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { Spin } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
|  | import { useVbenForm } from '#/adapter/form'; | ||||||
|  | import { getMenuList } from '#/api/system/menu'; | ||||||
|  | import { createRole, updateRole } from '#/api/system/role'; | ||||||
|  | import { $t } from '#/locales'; | ||||||
|  | 
 | ||||||
|  | import { useFormSchema } from '../data'; | ||||||
|  | 
 | ||||||
|  | const emits = defineEmits(['success']); | ||||||
|  | 
 | ||||||
|  | const formData = ref<SystemRoleApi.SystemRole>(); | ||||||
|  | 
 | ||||||
|  | const [Form, formApi] = useVbenForm({ | ||||||
|  |   schema: useFormSchema(), | ||||||
|  |   showDefaultActions: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const permissions = ref<DataNode[]>([]); | ||||||
|  | const loadingPermissions = ref(false); | ||||||
|  | 
 | ||||||
|  | const id = ref(); | ||||||
|  | const [Drawer, drawerApi] = useVbenDrawer({ | ||||||
|  |   async onConfirm() { | ||||||
|  |     const { valid } = await formApi.validate(); | ||||||
|  |     if (!valid) return; | ||||||
|  |     const values = await formApi.getValues(); | ||||||
|  |     drawerApi.lock(); | ||||||
|  |     (id.value ? updateRole(id.value, values) : createRole(values)) | ||||||
|  |       .then(() => { | ||||||
|  |         emits('success'); | ||||||
|  |         drawerApi.close(); | ||||||
|  |       }) | ||||||
|  |       .catch(() => { | ||||||
|  |         drawerApi.unlock(); | ||||||
|  |       }); | ||||||
|  |   }, | ||||||
|  |   onOpenChange(isOpen) { | ||||||
|  |     if (isOpen) { | ||||||
|  |       const data = drawerApi.getData<SystemRoleApi.SystemRole>(); | ||||||
|  |       formApi.resetForm(); | ||||||
|  |       if (data) { | ||||||
|  |         formData.value = data; | ||||||
|  |         id.value = data.id; | ||||||
|  |         formApi.setValues(data); | ||||||
|  |       } else { | ||||||
|  |         id.value = undefined; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (permissions.value.length === 0) { | ||||||
|  |         loadPermissions(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function loadPermissions() { | ||||||
|  |   loadingPermissions.value = true; | ||||||
|  |   try { | ||||||
|  |     const res = await getMenuList(); | ||||||
|  |     permissions.value = res as unknown as DataNode[]; | ||||||
|  |   } finally { | ||||||
|  |     loadingPermissions.value = false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const getDrawerTitle = computed(() => { | ||||||
|  |   return formData.value?.id | ||||||
|  |     ? $t('common.edit', $t('system.role.name')) | ||||||
|  |     : $t('common.create', $t('system.role.name')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function getNodeClass(node: Recordable<any>) { | ||||||
|  |   const classes: string[] = []; | ||||||
|  |   if (node.value?.type === 'button') { | ||||||
|  |     classes.push('inline-flex'); | ||||||
|  |     if (node.index % 3 >= 1) { | ||||||
|  |       classes.push('!pl-0'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return classes.join(' '); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <Drawer :title="getDrawerTitle"> | ||||||
|  |     <Form> | ||||||
|  |       <template #permissions="slotProps"> | ||||||
|  |         <Spin :spinning="loadingPermissions"> | ||||||
|  |           <VbenTree | ||||||
|  |             :tree-data="permissions" | ||||||
|  |             multiple | ||||||
|  |             bordered | ||||||
|  |             :default-expanded-level="2" | ||||||
|  |             :get-node-class="getNodeClass" | ||||||
|  |             v-bind="slotProps" | ||||||
|  |             value-field="id" | ||||||
|  |             label-field="meta.title" | ||||||
|  |             icon-field="meta.icon" | ||||||
|  |           > | ||||||
|  |             <template #node="{ value }"> | ||||||
|  |               <IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" /> | ||||||
|  |               {{ $t(value.meta.title) }} | ||||||
|  |             </template> | ||||||
|  |           </VbenTree> | ||||||
|  |         </Spin> | ||||||
|  |       </template> | ||||||
|  |     </Form> | ||||||
|  |   </Drawer> | ||||||
|  | </template> | ||||||
|  | <style lang="css" scoped> | ||||||
|  | :deep(.ant-tree-title) { | ||||||
|  |   .tree-actions { | ||||||
|  |     display: none; | ||||||
|  |     margin-left: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | :deep(.ant-tree-title:hover) { | ||||||
|  |   .tree-actions { | ||||||
|  |     display: flex; | ||||||
|  |     flex: auto; | ||||||
|  |     justify-content: flex-end; | ||||||
|  |     margin-left: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
		Loading…
	
		Reference in New Issue
	
	 Netfan
						Netfan