news 2026/6/10 4:00:19

利用Cursor快速用vue3-treeselect实现权限分配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
利用Cursor快速用vue3-treeselect实现权限分配

目录

1、核心单文件⭐️

src/components/permission/OrgPermissionDialog.vue

2、测试页

src/views/dev/PermissionTestView.vue

3、Excel 薄封装

src/components/excel/ExcelPermissionDialog.vue

4、类型定义

src/types/excel.ts


基于之前的需求:

记录利用Cursor快速实现Excel共享编辑-CSDN博客

    将项目里的树形结构逻辑及右侧人员和部门都提取到一个".vue"文件里面,并配置一个单独路由页面,页面里只有两个按钮,一个是“新增权限分配”按钮,一个是“回显权限分配”按钮,点击分别弹出对应的弹框,左边按钮为了测试分配权限功能,右边按钮主要是测试后端返回数据时前端进行回显,代码逻辑最好基于"vue3-treeselect"基本功能来实现,然后我是一个Vue3小白使用者,别写的太复杂了。


中间再给我加一个“回显本地权限分配”按钮,点击可以回显出来我第一个按钮新增的权限数据。

在测试页增加第三个按钮,并在“新增权限分配”确认后将数据保存到本地供回显。

正在查找项目中树形结构与人员/部门权限相关逻辑。

正在创建自包含的权限分配组件与测试页,并简化 Excel 中的引用;

结论: 树形选人、右侧人员/部门展示已收拢到单个组件 OrgPermissionDialog.vue;测试页 /permission-test 提供「新增」与「回显」两个按钮;Excel 权限仍可用,逻辑复用同一组件。

文件结构

文件作用

src/components/permission/OrgPermissionDialog.vue

核心单文件:左侧 vue3-treeselect + 右侧人员表/部门列表 + 勾选联动逻辑

src/views/dev/PermissionTestView.vue

测试页:两个按钮 + 引用上面的弹框

src/components/excel/ExcelPermissionDialog.vue

Excel 薄封装(约 50 行),内部仍调用 OrgPermissionDialog

已删除 useExcelPermissionDialog.ts,避免逻辑分散。数据大屏小标题标头已改为 40px。

怎么用(小白向)

  1. npm run dev
  2. 首页 → 权限分配测试,或打开 /permission-test
  3. 新增权限分配:空数据打开,自己勾选测试
  4. 回显权限分配:模拟后端返回:
    • 人员:person-int-1person-ext-2
    • 部门:dept-2-internal
      打开后看左侧树和右侧列表是否一致

对接真实后端时

回显只需在 open() 里传入接口返回的两个数组:

dialogRef.value?.open({ resourceText: '你的文档名', personIds: 接口返回的人员id数组, departmentIds: 接口返回的部门id数组, })

点「确定」会触发 @confirm,参数同样是 { personIds, departmentIds },再发给保存接口即可。

核心逻辑都在 OrgPermissionDialog.vue 里,用中文注释标了 open()treeModel(树勾选)和右侧列表的关系,便于对照阅读。


结论: 已在中间加入「回显本地权限分配」按钮;只有通过「新增权限分配」点「确定」后的数据才会写入本地,第三个按钮用来回显这份数据。

使用顺序

  1. 点 新增权限分配 → 勾选人员/部门 → 点 确定(会提示「已保存到本地」)
  2. 点 回显本地权限分配 → 弹框会按上一步保存的内容回显

若还没做过第 1 步就点第 2 个按钮,会提示:请先用「新增权限分配」勾选并确定。

和另外两个按钮的区别

按钮数据来源

新增权限分配

空数据,确定后写入本地

回显本地权限分配

本页内存里上次「新增」确定的数据

回显权限分配

写死的模拟后端数据(与新增无关)

页面下方在本地有数据时会显示一行橙色提示,方便确认是否已保存。刷新页面后本地数据会清空(仅当前会话有效);若需要刷新后仍保留,可以说一声再加 localStorage


1、核心单文件⭐️

src/components/permission/OrgPermissionDialog.vue

子组件:左侧 vue3-treeselect + 右侧人员表/部门列表 + 勾选联动逻辑

取消勾选不生效,很可能与 @deselect 里同步调用 refreshTree() 有关——它在 v-model 更新前就重挂载了组件。正在查看当前代码并修复。


结论: 取消勾选不生效,是上轮修复里 @deselect 调用时机不对导致的——它在 v-model 更新之前就 refreshTree() 重挂载了组件,用旧选中值把取消操作覆盖掉了。已修复。


原因

vue3-treeselect 的事件顺序是:

1. 用户点击取消勾选

2. 触发 @deselect ← 之前的 handleTreeDeselect 在这里 refreshTree()

3. 触发 update:modelValue ← applyTreeChange 在这里才更新 treeBindIds

refreshTree() 会立刻用 :key 重挂载树组件,此时 treeBindIds 还是旧值(仍包含「张磊」),新组件按旧数据渲染,取消勾选等于被撤销。


修复

  1. 移除 @deselect 监听,避免在 v-model 更新前 remount。
  2. 把「搜索态下取消勾选后恢复层级」的逻辑挪到 applyTreeChange 里,在 v-model 更新完成后再 nextTick 刷新:
function applyTreeChange(rawIds: string[]): void { // ...先更新 personIds / departmentIds / treeBindIds if (isDeselect) { nextTick(() => { refreshTree() // 此时 treeBindIds 已是取消后的新值 treeSearchQuery.value = '' }) } }
<template> <!-- 组织权限分配弹窗组件:左侧组织树多选,右侧展示已选人员/部门 --> <el-dialog v-model="visible" :title="dialogTitle" :close-on-click-modal="false" destroy-on-close @close="handleClose" > <div> <!-- 左侧:组织树选择面板 --> <div> <div> <!-- 组织多选树:支持部门/人员选择、搜索、自动去重合并 --> <Treeselect :key="treeKey" v-model="treeModel" :options="orgTree" :multiple="true" :always-open="true" :max-height="360" :searchable="true" :match-keys="['label', 'name', 'company']" :flatten-search-results="false" :clear-on-select="true" :clearable="false" :disable-branch-nodes="false" :before-clear-all="() => false" placeholder="请输入姓名或部门名称" no-results-text="未找到匹配项" no-options-text="暂无可选项" @search-change="handleSearchChange" /> </div> </div> <!-- 右侧:已选人员/部门展示面板 --> <div> <!-- 资源名称展示 --> <div> <span>{ { resourceText }},权限分配至:</span> </div> <!-- 已选人员列表 --> <div> <div> <span>人员 ({ { selectedPersons.length }})</span> <el-button v-if="selectedPersons.length" link type="primary" @click="clearPersons"> 清空已选 </el-button> </div> <el-table :data="selectedPersons" border size="small" max-height="200" empty-text="暂无已选人员" > <el-table-column prop="name" label="姓名" min-width="70" /> <el-table-column prop="nature" label="人员性质" min-width="70" /> <el-table-column prop="department" label="部门" min-width="100" show-overflow-tooltip /> <el-table-column prop="company" label="所属公司" min-width="80" show-overflow-tooltip /> <el-table-column label="操作" align="center"> <template #default="{ row }"> <el-button link type="primary" @click="removePerson(row.id)">删除</el-button> </template> </el-table-column> </el-table> </div> <!-- 已选部门列表 --> <div> <div> <span>部门 ({ { selectedDepartments.length }})</span> <el-button v-if="selectedDepartments.length" link type="primary" @click="clearDepartments" > 清空已选 </el-button> </div> <div v-if="selectedDepartments.length"> <div v-for="dept in selectedDepartments" :key="dept.id"> <span>{ { dept.name }} - { { dept.nature }}</span> <el-icon @click="removeDepartment(dept.id)"><Close /></el-icon> </div> </div> <div v-else>暂无已选部门</div> </div> </div> </div> <!-- 弹窗底部操作按钮 --> <div> <el-button @click="handleClose">取消</el-button> <el-button type="primary" :loading="saving" @click="handleConfirm">确定</el-button> </div> </el-dialog> </template> <script setup lang="ts"> /** * @description 组织权限分配弹框组件 * 组织权限分配弹框(单文件组件) * 功能:通过vue3-treeselect组织树选择人员/部门,自动合并为【部门全选 + 单独人员】 * 支持多选,且自动处理级联逻辑(全选部门则视为选择部门而非散落的人员)。 * 右侧分类实时展示已选的“人员”和“部门”。 * 支持双向同步,删除右侧项会同步更新左侧树勾选状态。 * 父组件调用:ref.value.open() 打开;回显传入 personIds、departmentIds 即可 * 输出:最终选中的人员ID数组、部门ID数组 */ import { ref, computed, nextTick } from 'vue' import { Close } from '@element-plus/icons-vue' import Treeselect from 'vue3-treeselect' import 'vue3-treeselect/dist/vue3-treeselect.css' import { mockOrgTree } from '@/composables/useExcelApi' import type { OrgTreeNode, SelectedPerson, SelectedDepartment, PermissionUserItem } from '@/types/excel' /** 打开弹窗时的配置项类型 */ export interface PermissionOpenOptions { title?: string // 弹窗标题 resourceText?: string // 资源名称描述(如:文档A,权限分配至:) personIds?: string[] // 回显人员ID departmentIds?: string[] // 回显部门ID } // 事件定义:确认选择时向外抛出最终选中的用户列表与部门 ID const emit = defineEmits<{ confirm: [payload: { userList: PermissionUserItem[]; departmentIds: string[] }] }>() // ==================== 基础响应式状态 ==================== const visible = ref(false) // 弹窗显隐 const dialogTitle = ref('权限分配') // 弹窗标题 const resourceText = ref('测试资源') // 资源名称文本 const saving = ref(false) // 确定按钮加载状态 const orgTree = ref<OrgTreeNode[]>([...mockOrgTree]) // 组织树数据源 // 内部维护的扁平化数据 const personIds = ref<string[]>([]) // 最终选中的人员ID(去重后) const departmentIds = ref<string[]>([]) // 最终选中的部门ID(去重后) const treeBindIds = ref<string[]>([]) // 绑定到树组件的ID(包含分类/部门/人员) const treeKey = ref(0) // 树组件刷新key(强制重新渲染) const treeSearchQuery = ref('') // 树组件当前搜索词(用于搜索态下取消勾选时恢复层级) // ==================== 核心逻辑:树节点工具方法 搜索与递归 ==================== /** * 递归查找指定ID的树节点 * @param id 节点ID * @returns 节点对象 / null */ function findNode(id: string): OrgTreeNode | null { function walk(nodes: OrgTreeNode[]): OrgTreeNode | null { for (const n of nodes) { if (n.id === id) return n if (n.children) { const found = walk(n.children) if (found) return found } } return null } return walk(mockOrgTree) } /** * 获取一个部门下所有人员ID * @param deptId 部门ID * @returns 人员ID数组 */ function getDeptPersonIds(deptId: string): string[] { const dept = findNode(deptId) if (!dept?.children) return [] return dept.children.filter((n) => n.type === 'person').map((n) => n.id) } /** * 获取一个分类下所有部门ID * @param categoryId 分类ID * @returns 部门ID数组 */ function getCategoryDeptIds(categoryId: string): string[] { const cat = findNode(categoryId) if (!cat?.children) return [] return cat.children.filter((n) => n.type === 'department').map((n) => n.id) } /** * 查找人员所属的父部门ID * @param personId 人员ID * @returns 部门ID / null */ function findPersonParentDeptId(personId: string): string | null { for (const cat of mockOrgTree) { for (const dept of cat.children ?? []) { if ((dept.children ?? []).some((p) => p.id === personId)) return dept.id } } return null } /** * 查找人员所属的第一层分类 ID(即 flag:自有 '01' / 外协 '02') */ function findPersonCategoryId(personId: string): string | null { for (const cat of mockOrgTree) { for (const dept of cat.children ?? []) { if ((dept.children ?? []).some((p) => p.id === personId)) return cat.id } } return null } /** * 将已选人员 ID 组装为后端需要的 userList 格式 */ function buildUserList(ids: strin
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 3:59:05

Linux---进程控制(1)(创建,终止,退出,等待)

进程创建写时拷贝(补充)关于写时拷贝在上一篇博客里已经提到过了&#xff0c;只不过现在想补充一个点&#xff0c;fork之后&#xff0c;父子进程的代码和数据是共享的&#xff0c;一般来说&#xff0c;代码是只读的&#xff0c;数据是读写的&#xff0c;但是fork之后会特殊一点…

作者头像 李华
网站建设 2026/6/10 3:58:12

制造业电子数据交换EDI软件落地|五大落地实施全流程

在智能制造全面普及、供应链全球化协同的时代&#xff0c;EDI电子数据交换已经成为制造业企业对接头部品牌、稳定承接订单、实现数字化合规的硬性准入标准。无论是消费电子、汽车零部件、精密制造还是通用机械行业&#xff0c;上下游供需协同早已告别人工传单、表格核对、线下对…

作者头像 李华
网站建设 2026/6/10 3:57:05

12-Hooks 上篇:五种事件 + 实用模板 —— 让 AI 自动执行你的脚本

Hooks 上篇&#xff1a;五种事件 实用模板 —— 让 AI 自动执行你的脚本 Skills 让 AI 学会了你的工作流&#xff0c;MCP 给 AI 装上了手脚。但还有一个问题没解决&#xff1a;你希望某些事在特定时机自动发生——AI 编辑文件后自动格式化、提交前自动跑测试、启动时自动加载环…

作者头像 李华
网站建设 2026/6/10 3:56:58

工商业储能系列: 主动均衡之集中式主动均衡<双向隔离DCDC+开关矩阵>

前言 在锂电池储能系统中&#xff0c;电芯之间存在难以避免的容量和内阻差异&#xff0c;形成“木桶效应”——最差的那节电芯限制了整个模组的可用容量&#xff0c;并加速整体衰减。主动均衡技术正是为了解决这一问题而生。 集中式主动均衡属于主动均衡的一种主流技术路线&a…

作者头像 李华
网站建设 2026/6/10 3:55:35

还在死磕 写空格?这4招让HTML空格代码原地起飞

怎样于网页里呈现空格吗? 这看上去好像是个简易的问题, 然而事实上, 它关联到诸多不一样的方法以及技巧。在这篇文章当中, 我们会介绍几种常见的技巧用以呈现空格, 涵盖实体字符、CSS样式、HTML标签以及代码。使用实体字符首种办法乃是运用实体字符, 其为一种特殊的HTML字符, …

作者头像 李华
网站建设 2026/6/10 3:54:30

【Spring Boot + MyBatis|第3篇】统一返回结果 Result 类设计

前言上一篇我们学习了 Controller 中常见的三种参数接收方式&#xff1a;RequestParam、PathVariable、RequestBody。这一篇继续学习一个项目中非常常见的设计&#xff1a;统一返回结果类 Result。在 Spring Boot 项目中&#xff0c;如果每个接口返回的数据格式都不一样&#x…

作者头像 李华