name: vben3-tree-wrapper-crud
基于
apps/micro-apps/system/src/views/manager/提取的 TreeWrapper + EditTable + Drawer + DrawerAuth 四件套模板。 适用于左侧有系统/菜单树、右侧展示关联数据的 CRUD 页面。
| 场景 | 是否适用 |
|---|---|
| 左侧菜单树,点击过滤右侧表格数据 | ✅ 使用此模板 |
| 需要文件上传、预览、下载 | ✅ 使用此模板 |
| 需要角色/权限授权抽屉 | ✅ 使用此模板 |
| 需要开关启禁用状态 | ✅ 使用此模板 |
| 纯表格 CRUD,无左树 | ❌ 使用 vben3.md 基础版 |
| 机构树 + 普通 CRUD | ❌ 使用 vben3.md 带左树版(CommonPage) |
views/{module}/
├── comp/
│ ├── tree.vue # ① 先创建:BussinessTree 树组件
│ └── treeWrapper.vue # ② 复用:左树+右内容布局容器
├── config.ts # ③ 配置:表格+表单+权限树
├── drawer.vue # ④ 抽屉:新增/编辑
├── drawerAuth.vue # ⑤ 可选:角色授权抽屉
└── index.vue # ⑥ 最后:主页面组装
关键导入来源:
@vben/adapter → EditTable, Drawer, BussinessTree@vben/wflow → AddForm@vben/common-ui → Page@vben/icons → getIconFont@vben/utils → cloneDeep, session, commWayelement-plus → ElButton, ElMessage, ElPopconfirm, ElSwitch, ElRow, ElCol| 特性 | 此模板 | 标准 CRUD(vben3.md) |
|---|---|---|
| 布局 | TreeWrapper(ElRow 5:19) |
CommonPage 或 Page |
| 左树 | 自定义 tree.vue + BussinessTree |
无 / CommonPage 内置 |
| 树↔表格数据流 | activeMenuId → menuId 参数 |
organizationId → globParams |
| 新增预设 | 当前树节点 ID → drawer menusArr |
直接开空表单 |
| 权限抽屉 | drawerAuth.vue + TreeTransfer |
无 |
TreeWrapper
├── tree.vue @node-click → handleNodeClick(node)
│ └── activeMenuId = node.id
│ └── editTable.getData({ ...values, menuId: node.id })
│
└── EditTable
├── @operation-click → operationClick(data)
│ ├── 'add'/'edit' → drawer.openModal(data) // data.menusArr 预设菜单
│ ├── 'delete' → deleteClick(row)
│ └── 'menuAuth' → drawerAuth.openModal(data)
│
└── drawer/drawerAuth @on-confirm → getData() → refreshTree()
完整模板代码见 templates.md,包含所有 6 个文件的完整内容。
<script lang="ts" setup>
import { Page } from "@vben/common-ui";
import { ElCol, ElRow } from "element-plus";
import { useTemplateRef } from "vue";
import Tree from "./tree.vue";
const props = defineProps({
showDeptment: { type: Boolean, default: true },
showTree: { type: Boolean, default: true },
defaultData: { type: Array, default: () => [] },
});
const emits = defineEmits(["nodeClick", "getTreeData"]);
const treeRef = useTemplateRef("tree");
const events = {
handleNodeClick(node: any) { emits("nodeClick", node); },
getTreeData(data: any) { emits("getTreeData", data); },
};
defineExpose({ refreshTree() { treeRef.value?.refreshTree(); } });
</script>
<template>
<Page auto-content-height>
<ElRow class="h-full" :gutter="10">
<ElCol :span="5" class="h-full">
<Tree ref="tree" :show-deptment="props.showDeptment" :default-data="props.defaultData"
:show-tree="props.showTree" @node-click="events.handleNodeClick" @get-tree-data="events.getTreeData" />
</ElCol>
<ElCol :span="19" class="h-full">
<slot name="right-content"></slot>
</ElCol>
</ElRow>
</Page>
</template>
基于 BussinessTree,核心配置:
searchOptions.show: true — 搜索框treeOptions.api.query — 调用你的树数据 API,返回含 id/name/children 的数组treeOptions.api.isUseApi: truedefineExpose({ refreshTree }) 供父组件刷新session.setItem("menuNode", list) 供 drawer 的 ApiTreeSelect 使用const events = {
operationClick(data) {
// add/edit → drawer.openModal(data); data.menusArr = [activeMenuId]
// delete → deleteClick(row)
// menuAuth → drawerAuth.openModal(data)
},
changeStatus(row) { /* updateApi → getData */ },
deleteClick(row?) { /* row?.id 或 getCheckboxRecords → removeApi → getData */ },
async getData() { /* gridApi.formApi.getValues → editTable.getData → refreshTree */ },
async handleNodeClick(node) { /* activeMenuId = node.id → getData({ menuId }) */ },
};
模板中 <template #isEnable> 插槽实现 ElSwitch 启禁用切换,<template #toolbar-tools-after> 插槽放置批量删除按钮。
openModal 回调中:编辑时调 idByFile 回填菜单权限;新增时用 data.menusArr 预设onConfirm:收集表单值 → 处理文件(commWay.getFileRes) → 处理菜单关联 → 调 APIinit 时加载全部角色列表openModal 时根据已授权角色拆分到左右两侧树onConfirm 收集右侧树数据调授权 APIsrc/api/{module}.ts:分页查询 + 新增 + 修改 + 删除 + 详情查询src/views/{module}/comp/menuNode API 为你的树数据接口{模块中文名}、{module}、表格列、搜索字段、表单字段、操作按钮、API 导入| 搜索 | 替换为 |
|---|---|
{module} |
模块标识(如 user) |
{模块中文名} |
中文名称(如 用户) |
#/api/fileMessage |
API 文件路径 |
filePage / addfile / updateFile / removeFile / idByFile |
你的 CRUD 方法 |
menuNode |
你的树数据 API |
getRolePage / fileAuth |
你的角色/授权 API |
@ums:role: |
替换 role 为模块权限前缀 |
所有模板使用
{module}作为模块标识占位符,{模块中文名}作为中文名占位符。 生成新页面时,按 TODO 注释替换即可。
<script lang="ts" setup>
import { ref, useTemplateRef, computed } from "vue";
import { BussinessTree } from "@vben/adapter";
// TODO: 替换为你的树数据 API
import { menuNode } from "#/api/fileMessage";
import { session } from "@vben/utils";
const props = defineProps({
showDeptment: { type: Boolean, default: true },
showTree: { type: Boolean, default: true },
defaultData: { type: Array, default: () => [] },
outTreeOptions: { type: Object, default: () => ({}) },
isShowAll: { type: Boolean, default: false },
fieldNames: {
type: Object,
default: () => ({ key: "id", title: "name", children: "children" }),
},
});
const emits = defineEmits(["nodeClick", "getTreeData"]);
const treeRef = useTemplateRef("tree");
const elTreeProps = computed(() => ({
children: props.fieldNames.children || "children",
label: props.fieldNames.title || "name",
value: props.fieldNames.key || "id",
}));
const treeOptions = ref({
searchOptions: {
show: true,
placeholder: "请输入关键字",
...props.outTreeOptions.searchOptions,
},
treeOptions: {
...props.outTreeOptions?.treeOptions,
options: {
expandOnClickNode: false,
data: [],
props: elTreeProps.value,
...props.outTreeOptions?.treeOptions?.options,
},
api: {
query() {
return new Promise((resolve) => {
// TODO: 替换为你的树数据 API 调用
menuNode({}).then((res) => {
if (res.code == 1 && res.result) {
var list = [
{
id: "-1",
name: "全部",
value: "-1",
children: changeTree(res.result),
},
];
session.setItem("menuNode", list);
resolve(list);
emits("getTreeData", list);
}
});
});
},
isUseApi: true,
},
},
});
const treeOptionsComputed = computed(() => treeOptions.value);
function changeTree(nodes: any[]) {
if (!nodes || !Array.isArray(nodes)) return [];
return nodes.map((node) => {
if (!node) return null;
const newNode = { ...node };
if (newNode.children && Array.isArray(newNode.children)) {
newNode.children = changeTree(newNode.children);
}
return newNode;
});
}
const events = {
init() {},
handleNodeClick(node: any) {
emits("nodeClick", node);
},
};
events.init();
defineExpose({
getTreeRef() {
return treeRef.value?.getTreeRef();
},
refreshTree() {
// TODO: 替换为你的树数据 API
menuNode({}).then((res) => {
if (res.code == 1 && res.result) {
const list = [
{ id: "-1", name: "全部", value: "-1", children: changeTree(res.result) },
];
session.setItem("menuNode", list);
treeOptions.value = {
...treeOptions.value,
treeOptions: {
...treeOptions.value.treeOptions,
options: { ...treeOptions.value.treeOptions.options, data: list },
},
};
emits("getTreeData", list);
}
});
},
});
</script>
<template>
<BussinessTree ref="tree" v-bind="treeOptionsComputed" @node-click="events.handleNodeClick" />
</template>
<style scoped lang="scss"></style>
<script lang="ts" setup>
import { Page } from "@vben/common-ui";
import { ElCol, ElRow } from "element-plus";
import { useTemplateRef } from "vue";
import Tree from "./tree.vue";
const props = defineProps({
showDeptment: { type: Boolean, default: true },
showTree: { type: Boolean, default: true },
defaultData: { type: Array, default: () => [] },
});
const emits = defineEmits(["nodeClick", "getTreeData"]);
const treeRef = useTemplateRef("tree");
const events = {
handleNodeClick(node: any) {
emits("nodeClick", node);
},
getTreeData(data: any) {
emits("getTreeData", data);
},
};
defineExpose({
refreshTree() {
treeRef.value?.refreshTree();
},
});
</script>
<template>
<Page auto-content-height>
<ElRow class="h-full" :gutter="10">
<ElCol :span="5" class="h-full">
<Tree
ref="tree"
:show-deptment="props.showDeptment"
:default-data="props.defaultData"
:show-tree="props.showTree"
@node-click="events.handleNodeClick"
@get-tree-data="events.getTreeData"
/>
</ElCol>
<ElCol :span="19" class="h-full">
<slot name="right-content"></slot>
</ElCol>
</ElRow>
</Page>
</template>
<style scoped lang="scss"></style>
<template>
<TreeWrapper ref="treeWrapper" :show-deptment="true" @node-click="events.handleNodeClick">
<template #right-content>
<EditTable
ref="editTable"
v-bind="tableConfig"
@operation-click="events.operationClick"
>
<template #toolbar-tools-after>
<ElPopconfirm
v-auth="'@ums:{module}:delete'"
title="是否确认删除选中记录?"
confirm-button-text="确认"
cancel-button-text="取消"
@confirm="events.deleteClick()"
>
<template #reference>
<ElButton
v-auth="'@ums:{module}:delete'"
type="danger"
:icon="getIconFont('icon-shanchu')"
plain
>
删除
</ElButton>
</template>
</ElPopconfirm>
</template>
<template #isEnable="{ row }">
<ElSwitch
inline-prompt
active-text="启用"
inactive-text="禁用"
v-model="row.isEnable"
:active-value="true"
:inactive-value="false"
@change="events.changeStatus(row)"
/>
</template>
</EditTable>
<Drawer ref="drawer" @on-confirm="events.getData" />
<DrawerAuth ref="drawerAuth" :is-show-tab="false" @on-confirm="events.getData" />
</template>
</TreeWrapper>
</template>
<script lang="ts" setup>
import { useTemplateRef } from "vue";
import { EditTable } from "@vben/adapter";
import { getIconFont } from "@vben/icons";
import { ElButton, ElMessage, ElPopconfirm, ElSwitch } from "element-plus";
// TODO: 替换为你的 API
import { removeFile } from "#/api/fileMessage";
import { updateFile } from "#/api/fileMessage";
import TreeWrapper from "./comp/treeWrapper.vue";
import { tableConfig } from "./config";
import Drawer from "./drawer.vue";
import DrawerAuth from "./drawerAuth.vue";
const editTableRef = useTemplateRef("editTable");
const drawerRef = useTemplateRef("drawer");
const drawerAuthRef = useTemplateRef("drawerAuth");
const treeWrapperRef = useTemplateRef("treeWrapper");
let activeMenuId = "";
const events = {
operationClick(data: { gridApi: any; params: any }) {
const { params } = data;
if (params.code === "delete") {
events.deleteClick(params.row);
return;
}
if (["add", "edit"].includes(params.code)) {
data.menusArr = activeMenuId ? [activeMenuId] : [];
drawerRef.value?.openModal(data);
}
if (params.code === "menuAuth") {
drawerAuthRef.value?.openModal(data);
}
// TODO: 按需添加 preview、download 等操作
},
changeStatus(row: any) {
// TODO: 替换为你的修改状态 API
updateFile({ ...row }).then((r: any) => {
if (r.code === 1) {
events.getData();
ElMessage.success("操作成功");
}
});
},
deleteClick(row?: any) {
const instance = editTableRef.value?.getInstance();
if (!instance) return;
const gridApi: any = instance.gridApi;
const ids = row
? row.id
: gridApi.grid
.getCheckboxRecords()
.map((item: any) => item.id)
.join(",");
if (!ids) {
ElMessage.warning("请选择要删除的数据!");
return;
}
// TODO: 替换为你的删除 API
removeFile({ ids: ids }).then((r: any) => {
if (r.code === 1) {
ElMessage.success("删除成功");
events.getData();
} else {
ElMessage.error(r.message || "删除失败");
}
});
},
async getData() {
const instance = editTableRef.value?.getInstance();
if (!instance) return;
const gridApi: any = instance.gridApi;
const values = (await gridApi.formApi.getValues()) || {};
editTableRef.value?.getData({ ...values });
treeWrapperRef.value?.refreshTree();
},
async handleNodeClick(node: any) {
activeMenuId = node.id;
const instance = editTableRef.value?.getInstance();
if (!instance) return;
const gridApi: any = instance.gridApi;
const values = (await gridApi.formApi.getValues()) || {};
editTableRef.value?.getData({
...values,
menuId: node.id, // TODO: 根据实际接口调整参数名
});
},
};
</script>
<style scoped lang="scss"></style>
import { cloneDeep } from '@vben/utils';
import { session } from "@vben/utils";
// TODO: 替换为你的分页查询 API
import { filePage } from '#/api/fileMessage';
let globParams: Record<string, any> = {};
export const setGlobParams = (params: any) => {
globParams = { ...globParams, ...params };
};
export const tableConfig = {
options: {
tableTitle: '{模块中文名}列表',
gridOptions: {
checkboxConfig: { reserve: true },
columns: [
// TODO: 替换为你的表格列
{ field: 'name', minWidth: 200, title: '名称' },
{ field: 'ext', width: 80, title: '类型' },
{ field: 'createTime', width: 160, title: '创建时间' },
{ field: 'isEnable', title: '状态', width: 90, slots: { default: "isEnable" }, sortable: false },
],
proxyConfig: {
ajax: {
query: async (params: any, formValues: any) => {
// TODO: 替换为你的分页 API
const res: any = await filePage({
...params.page,
...formValues,
...globParams,
});
if (res.code !== 1) {
return { items: [], total: 0 };
}
return {
items: cloneDeep(res.result.records),
total: +res.result.total || 0,
};
},
},
},
},
formOptions: {
commonConfig: { labelWidth: 60 },
showCollapseButton: false,
collapsed: false,
schema: [
// TODO: 替换为你的搜索字段
{
component: 'Input',
fieldName: 'keyword',
label: '关键字',
componentProps: { clearable: true, placeholder: '请输入关键字' },
},
],
wrapperClass: 'grid-cols-1 md:grid-cols-4',
submitButtonOptions: { auth: '@ums:{module}:search' },
resetButtonOptions: { auth: '@ums:{module}:reset' },
},
},
formObj: {
operateOptions: [
// TODO: 替换为你的操作按钮
{ code: 'edit', text: '编辑', type: 'primary', directives: [{ name: 'auth', value: '@ums:{module}:edit' }] },
{ code: 'delete', text: '删除', type: 'danger', directives: [{ name: 'auth', value: '@ums:{module}:delete' }] },
{ code: 'menuAuth', text: '权限配置', type: 'primary', directives: [{ name: 'auth', value: '@ums:{module}:auth' }] },
],
},
isView: false,
showOperate: true,
showSeq: true,
buttonAuth: { addBtn: '@ums:{module}:add' },
};
export const drawerConfig = {
formConfig: {
wrapperClass: 'grid-cols-1',
layout: 'horizontal',
schemaConfig: [
{
schema: [
{
formItemClass: 'form-item-col3 hidden',
fieldName: 'id',
label: '',
dependencies: { show: false },
},
// TODO: 替换为你的表单字段
{
formItemClass: 'form-item-col3',
component: 'Input',
componentProps: { placeholder: '请输入名称', clearable: true },
fieldName: 'name',
label: '名称',
rules: 'required',
},
{
component: 'ApiTreeSelect',
componentProps: {
labelField: 'name',
valueField: 'id',
childrenField: 'children',
api: () => session.getItem("menuNode"),
multiple: true,
},
fieldName: 'menus',
label: '上级菜单',
rules: 'required',
},
{
formItemClass: 'form-item-col3',
component: 'Input',
componentProps: { placeholder: '请输入排序', clearable: true },
fieldName: 'seq',
label: '排序',
rules: 'required',
},
{
formItemClass: 'form-item-col3',
component: 'RadioGroup',
componentProps: {
options: [
{ label: '是', value: true },
{ label: '否', value: false },
],
},
defaultValue: true,
fieldName: 'isEnable',
label: '是否启用',
rules: 'required',
},
{
formItemClass: 'form-item-col3',
labelAlign: 'top',
component: 'Input',
componentProps: {
rows: 4, maxLength: 500,
placeholder: '请输入描述', type: 'textarea',
},
fieldName: 'description',
label: '描述',
},
],
},
],
},
tableConfig: { show: false },
mapConfig: { show: false },
layoutConfig: { column: 1 },
};
// 权限树公共配置
const treeConfig = {
searchOptions: { show: true, placeholder: '搜索' },
treeOptions: {
options: {
expandOnClickNode: false,
data: [] as any,
nodeKey: 'id',
checkStrictly: false,
},
},
};
export const authTreeOptions = {
title: '未授权角色',
...treeConfig,
treeOptions: {
...treeConfig.treeOptions,
options: {
...treeConfig.treeOptions.options,
showCheckbox: true,
props: { children: 'children', label: 'name', value: 'id' },
},
},
};
export const authSelectTreeOptions = {
title: '已授权角色',
...treeConfig,
treeOptions: {
...treeConfig.treeOptions,
options: {
...treeConfig.treeOptions.options,
showCheckbox: true,
props: { children: 'children', label: 'name', value: 'id' },
},
},
};
<template>
<Drawer ref="drawer" :title="title" class-name="w-[600px]" @on-confirm="events.onConfirm">
<template #default>
<AddForm ref="addForm" v-bind="drawerConfig" :is-auto-map-height="true" />
</template>
</Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from "vue";
import { Drawer } from "@vben/adapter";
import { AddForm } from "@vben/wflow";
import { ElMessage } from "element-plus";
// TODO: 替换为你的 API
import { addfile, updateFile, idByFile } from "#/api/fileMessage";
import { commWay } from "@vben/utils";
import { drawerConfig } from "./config";
const emits = defineEmits(["onConfirm"]);
const drawerRef = useTemplateRef("drawer");
const addFormRef = useTemplateRef("addForm");
const title = ref("");
const selectedRows: any = ref({});
const events = {
operationClick(data: { gridApi: any; params: any }) {
const { params } = data;
selectedRows.value = params.row || {};
title.value = params.code === "add" ? "新增" : "编辑";
drawerRef.value?.openModal(() => {
const { row } = params;
if (row) {
// 编辑:回填菜单权限
// TODO: 替换详情查询 API
idByFile(row.id).then((r: any) => {
if (r.code == 1 && r.result && r.result.menus) {
const instance = addFormRef.value?.getFormInstance();
instance?.formApi?.setValues({
menus: r.result.menus.map((i: any) => i.menuId),
});
}
});
const instance = addFormRef.value?.getFormInstance();
instance?.formApi?.setValues({ ...row });
}
if (params.code === "add") {
const instance = addFormRef.value?.getFormInstance();
instance?.formApi?.setValues({ menus: data.menusArr });
}
});
},
async onConfirm() {
const values = await addFormRef.value?.getValues();
if (!values) return;
values.file = commWay.getFileRes(values);
if (values.menus) {
values.menus = values.menus.map((i: any) => ({ menuId: i }));
}
// TODO: 替换为你的新增/修改 API
const Api = selectedRows.value.id ? updateFile : addfile;
const res: any = await Api({ ...selectedRows.value, ...values });
if (res.code === 1) {
ElMessage.success("保存成功");
drawerRef.value?.closeModal();
emits("onConfirm");
} else {
ElMessage.error(res.message || "保存失败");
}
},
};
defineExpose({
openModal(data: any) { events.operationClick(data); },
closeModal() { drawerRef.value?.closeModal(); },
});
</script>
<style scoped lang="scss">
.add-form { height: 100%; }
:deep(.add-form-item4) {
height: calc(100% - 362px);
& > div { height: 100% !important; }
}
</style>
<template>
<Drawer ref="drawer" :title="title" class-name="w-[800px]" @on-confirm="events.onConfirm" @on-closed="events.onCloseModal">
<template #default>
<div class="auth-container">
<ElRow class="h-full">
<ElCol class="h-full pl-2" :span="24">
<TreeTransfer
ref="treeTransfer"
:left-options="authTreeOptionsComputed"
:right-options="authSelectTreeOptionsComputed"
:all-menu-list="allRoleList"
:current-node="{ id: '0' }"
@update-data="events.onUpdateData"
/>
</ElCol>
</ElRow>
</div>
</template>
</Drawer>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, computed } from 'vue';
import { Drawer } from '@vben/adapter';
import { cloneDeep } from '@vben/utils';
import { ElCol, ElMessage, ElRow } from 'element-plus';
// TODO: 替换为你的 API
import { getRolePage } from '#/api/role';
import { idByFile, fileAuth } from "#/api/fileMessage";
import TreeTransfer from '#/components/treeTransfer/fileOuth.vue';
import {
authSelectTreeOptions as _authSelectTreeOptions,
authTreeOptions as _authTreeOptions,
} from './config';
const props = defineProps({
isShowTab: { type: Boolean, default: false },
});
const emits = defineEmits(['onConfirm']);
const drawerRef = useTemplateRef('drawer');
const title = ref('');
const selectedRows: any = ref({});
const allRoleList = ref<any[]>([]);
const authSelectTreeOptions = ref(_authSelectTreeOptions);
const authTreeOptions = ref(_authTreeOptions);
const authTreeOptionsComputed = computed(() => authTreeOptions.value);
const authSelectTreeOptionsComputed = computed(() => authSelectTreeOptions.value);
const events = {
init() { this.getRoleList(); },
getRoleList() {
// TODO: 替换为你的角色列表 API
getRolePage({ current: 1, size: 100000 }).then((res: any) => {
if (res.code === 1) {
allRoleList.value = cloneDeep(res.result.records || []).map((item: any) => ({
...item, parentId: '0',
}));
}
});
},
onUpdateData(data: { left: any[]; right: any[] }) {
authTreeOptions.value.treeOptions.options.data = data.left.length ? data.left[0].children : [];
authSelectTreeOptions.value.treeOptions.options.data = data.right.length ? data.right[0].children : [];
},
openModal(data: any) {
const { params } = data;
selectedRows.value = params.row || {};
title.value = `角色授权(${selectedRows.value.name})`;
drawerRef.value?.openModal(() => {
const { row } = params;
if (row) {
// TODO: 替换为你的详情查询 API
idByFile(row.id).then((r: any) => {
if (r.code === 1 && r.result) {
const roleIdList = r.result.roles?.map((item: any) => item.roleId) || [];
const selectedRoleList = allRoleList.value.filter((i: any) => roleIdList.includes(i.id));
const unselectedRoleList = allRoleList.value.filter((i: any) => !roleIdList.includes(i.id));
authSelectTreeOptions.value.treeOptions.options.data = selectedRoleList;
authTreeOptions.value.treeOptions.options.data = unselectedRoleList;
}
});
}
});
},
getAllTableData() {
return authSelectTreeOptions.value.treeOptions.options.data.map((item: any) => item.id);
},
async onConfirm() {
const tableData = events.getAllTableData();
if (tableData.length === 0) {
ElMessage.error('请选择要绑定的角色');
return;
}
// TODO: 替换为你的授权 API
fileAuth({
roleIds: tableData,
fileResourceIds: [selectedRows.value.id],
}).then((r: any) => {
if (r.code === 1) {
drawerRef.value?.closeModal();
ElMessage.success('角色绑定成功');
emits('onConfirm');
} else {
ElMessage.error(r.message || '操作失败');
}
});
},
closeModal() {
selectedRows.value = {};
authTreeOptions.value.treeOptions.options.data = [];
authSelectTreeOptions.value.treeOptions.options.data = [];
},
};
events.init();
defineExpose({
openModal(data: any) { events.openModal(data); },
closeModal() { drawerRef.value?.closeModal(); },
});
</script>
<style scoped lang="scss">
.auth-container { height: 100%; }
.tree-content { height: calc(100% - 24px); }
</style>