templates.md 28 KB


name: vben3-tree-wrapper-crud

description: 生成基于 TreeWrapper + EditTable + Drawer 的 Vben v3 CRUD 页面模板。适用于需要左侧菜单树 + 右侧数据表格的列表页面,含状态切换、文件上传预览下载、权限抽屉、批量删除等完整功能。当用户需要在成堪数智平台创建带左树的 CRUD 页面时使用此 skill。

Vben v3 TreeWrapper 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/adapterEditTable, Drawer, BussinessTree
  • @vben/wflowAddForm
  • @vben/common-uiPage
  • @vben/iconsgetIconFont
  • @vben/utilscloneDeep, session, commWay
  • element-plusElButton, ElMessage, ElPopconfirm, ElSwitch, ElRow, ElCol

三、与标准 CRUD 模板的关键差异

特性 此模板 标准 CRUD(vben3.md)
布局 TreeWrapper(ElRow 5:19) CommonPagePage
左树 自定义 tree.vue + BussinessTree 无 / CommonPage 内置
树↔表格数据流 activeMenuIdmenuId 参数 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 个文件的完整内容。

5.1 treeWrapper.vue(直接复用,无需修改)

<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>

5.2 tree.vue 要点

基于 BussinessTree,核心配置:

  • searchOptions.show: true — 搜索框
  • treeOptions.api.query — 调用你的树数据 API,返回含 id/name/children 的数组
  • treeOptions.api.isUseApi: true
  • 需要 defineExpose({ refreshTree }) 供父组件刷新
  • 树数据存入 session.setItem("menuNode", list) 供 drawer 的 ApiTreeSelect 使用

5.3 index.vue 核心事件

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> 插槽放置批量删除按钮。

5.4 drawer.vue 核心逻辑

  • openModal 回调中:编辑时调 idByFile 回填菜单权限;新增时用 data.menusArr 预设
  • onConfirm:收集表单值 → 处理文件(commWay.getFileRes) → 处理菜单关联 → 调 API

5.5 drawerAuth.vue 核心逻辑

  • init 时加载全部角色列表
  • openModal 时根据已授权角色拆分到左右两侧树
  • onConfirm 收集右侧树数据调授权 API

六、生成步骤

  1. 创建 API 文件 src/api/{module}.ts:分页查询 + 新增 + 修改 + 删除 + 详情查询
  2. 创建目录 src/views/{module}/comp/
  3. 复制 treeWrapper.vue:直接复用
  4. 创建 tree.vue:替换 menuNode API 为你的树数据接口
  5. 创建 config.ts:替换 {模块中文名}{module}、表格列、搜索字段、表单字段、操作按钮、API 导入
  6. 创建 drawer.vue:替换 API 导入
  7. 创建 drawerAuth.vue:按需,替换授权 API
  8. 创建 index.vue:替换 API 导入和权限标识
  9. 配置路由:添加新页面路由

七、TODO 替换清单

搜索 替换为
{module} 模块标识(如 user
{模块中文名} 中文名称(如 用户
#/api/fileMessage API 文件路径
filePage / addfile / updateFile / removeFile / idByFile 你的 CRUD 方法
menuNode 你的树数据 API
getRolePage / fileAuth 你的角色/授权 API
@ums:role: 替换 role 为模块权限前缀

所有模板使用 {module} 作为模块标识占位符,{模块中文名} 作为中文名占位符。 生成新页面时,按 TODO 注释替换即可。


1. tree.vue(BussinessTree 树组件)

<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>

2. treeWrapper.vue(布局容器,直接复用)

<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>

3. index.vue(主页面)

<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>

4. config.ts(配置中心)

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' },
    },
  },
};

5. drawer.vue(新增/编辑抽屉)

<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>

6. drawerAuth.vue(角色授权抽屉,可选)

<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>