vue2server7 7.0.45 → 7.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,7 +153,7 @@ const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
153
153
  labelKey: 'name',
154
154
  childrenKey: 'children',
155
155
  defaultExpandedKeys: () => [],
156
- defaultExpandAll: false
156
+ defaultExpandAll: true
157
157
  })
158
158
 
159
159
  const emit = defineEmits<OrgTreeSelectEmits>()
@@ -265,20 +265,65 @@ function openDialog(): void {
265
265
  selectedValue.value = props.modelValue
266
266
  }
267
267
 
268
+ /**
269
+ * 获取所有节点 ID(递归)
270
+ * @param data - 树数据
271
+ * @returns 所有节点 ID 数组
272
+ */
273
+ function getAllNodeIds(data: OrgTreeNode[]): (string | number)[] {
274
+ const ids: (string | number)[] = []
275
+ for (const item of data) {
276
+ ids.push(item.id)
277
+ if (item.children && item.children.length > 0) {
278
+ ids.push(...getAllNodeIds(item.children))
279
+ }
280
+ }
281
+ return ids
282
+ }
283
+
284
+ /**
285
+ * 分批展开节点(避免卡顿)
286
+ * @param ids - 节点 ID 数组
287
+ * @param batchSize - 每批处理的节点数
288
+ */
289
+ async function expandNodesBatch(ids: (string | number)[], batchSize: number = 50): Promise<void> {
290
+ if (!treeRef.value) return
291
+
292
+ for (let i = 0; i < ids.length; i += batchSize) {
293
+ const batch = ids.slice(i, i + batchSize)
294
+ batch.forEach(id => {
295
+ treeRef.value?.expandNode(id as string)
296
+ })
297
+ // 每批处理后让出主线程,避免阻塞UI
298
+ if (i + batchSize < ids.length) {
299
+ await new Promise(resolve => setTimeout(resolve, 0))
300
+ }
301
+ }
302
+ }
303
+
268
304
  /**
269
305
  * 弹窗打开后的回调
270
306
  */
271
307
  function onDialogOpened(): void {
272
- nextTick(() => {
308
+ nextTick(async () => {
273
309
  updateSelectedNodes()
274
310
  if (!treeRef.value) return
275
311
 
312
+ // 先恢复选中状态
276
313
  if (props.multiple) {
277
314
  const keys = Array.isArray(selectedValue.value) ? selectedValue.value : []
278
315
  treeRef.value.setCheckedKeys(keys as (string | number)[])
279
316
  } else if (selectedValue.value !== null) {
280
317
  treeRef.value.setCurrentKey(selectedValue.value as string | number)
281
318
  }
319
+
320
+ // 异步展开节点,避免弹窗卡顿
321
+ if (props.defaultExpandAll) {
322
+ const allIds = getAllNodeIds(props.data)
323
+ await expandNodesBatch(allIds, 50)
324
+ } else if (props.defaultExpandedKeys.length > 0) {
325
+ await expandNodesBatch(props.defaultExpandedKeys, 50)
326
+ }
282
327
  })
283
328
  }
284
329
 
@@ -103,165 +103,72 @@ import { ref } from 'vue'
103
103
  import OrgTreeSelect, { type OrgTreeNode } from '../components/OrgTreeSelect.vue'
104
104
  import OrgTreeSelectCascade from '../components/OrgTreeSelectCascade.vue'
105
105
 
106
- // 机构树数据(模拟)
107
- const orgData = ref<OrgTreeNode[]>([
108
- {
109
- id: '25001',
110
- code: '25001',
111
- name: '北京银行总行核算中心',
112
- icon: 'OfficeBuilding',
113
- children: [
114
- {
115
- id: '00019',
116
- code: '00019',
117
- name: '北京分行',
118
- icon: 'OfficeBuilding',
119
- children: []
120
- },
121
- {
122
- id: '00023',
123
- code: '00023',
124
- name: '中关村分行',
125
- icon: 'OfficeBuilding',
126
- children: []
127
- },
128
- {
129
- id: '00025',
130
- code: '00025',
131
- name: '城市副中心分行',
132
- icon: 'OfficeBuilding',
133
- children: []
134
- },
135
- {
136
- id: '25101',
137
- code: '25101',
138
- name: '北京核算中心',
106
+ // 机构树数据(模拟)- 生成600条数据
107
+ function generateOrgData(): OrgTreeNode[] {
108
+ const orgNames = [
109
+ '北京银行', '工商银行', '建设银行', '农业银行', '中国银行',
110
+ '交通银行', '招商银行', '浦发银行', '兴业银行', '民生银行',
111
+ '中信银行', '光大银行', '华夏银行', '广发银行', '平安银行'
112
+ ]
113
+ const branchTypes = ['分行', '支行', '营业部', '分理处', '储蓄所']
114
+ const districts = [
115
+ '东城', '西城', '朝阳', '丰台', '石景山', '海淀', '门头沟', '房山', '通州', '顺义',
116
+ '昌平', '大兴', '怀柔', '平谷', '密云', '延庆', '天津', '河北', '山西', '内蒙古',
117
+ '辽宁', '吉林', '黑龙江', '上海', '江苏', '浙江', '安徽', '福建', '江西', '山东'
118
+ ]
119
+
120
+ const data: OrgTreeNode[] = []
121
+ let idCounter = 10000
122
+
123
+ // 生成10个总行
124
+ for (let i = 0; i < 10; i++) {
125
+ const bankName = orgNames[i % orgNames.length]
126
+ const bankId = String(idCounter++)
127
+ const bank: OrgTreeNode = {
128
+ id: bankId,
129
+ code: bankId,
130
+ name: `${bankName}总行`,
131
+ icon: 'OfficeBuilding',
132
+ children: []
133
+ }
134
+
135
+ // 每个总行下生成5-8个分行
136
+ const branchCount = 5 + Math.floor(Math.random() * 4)
137
+ for (let j = 0; j < branchCount; j++) {
138
+ const district = districts[j % districts.length]
139
+ const branchId = String(idCounter++)
140
+ const branch: OrgTreeNode = {
141
+ id: branchId,
142
+ code: branchId,
143
+ name: `${bankName}${district}${branchTypes[0]}`,
139
144
  icon: 'OfficeBuilding',
140
145
  children: []
141
146
  }
142
- ]
143
- },
144
- {
145
- id: '25111',
146
- code: '25111',
147
- name: '天津核算中心',
148
- icon: 'OfficeBuilding',
149
- children: [
150
- {
151
- id: '25311',
152
- code: '25311',
153
- name: '天津分行会计部',
154
- icon: 'Document',
155
- children: []
156
- },
157
- {
158
- id: '30001',
159
- code: '30001',
160
- name: '天津营业部',
161
- icon: 'Document',
162
- children: []
163
- },
164
- {
165
- id: '30011',
166
- code: '30011',
167
- name: '天津河西支行营业部',
168
- icon: 'Document',
169
- children: []
170
- },
171
- {
172
- id: '30021',
173
- code: '30021',
174
- name: '天津开发区支行营业部',
175
- icon: 'Document',
176
- children: []
177
- },
178
- {
179
- id: '30031',
180
- code: '30031',
181
- name: '天津南开支行营业部',
182
- icon: 'Document',
183
- children: []
184
- },
185
- {
186
- id: '30041',
187
- code: '30041',
188
- name: '北京银行股份有限公司天津八纬路支行',
189
- icon: 'Document',
190
- children: []
191
- },
192
- {
193
- id: '30051',
194
- code: '30051',
195
- name: '天津梅江支行营业部',
196
- icon: 'Document',
197
- children: []
198
- },
199
- {
200
- id: '30061',
201
- code: '30061',
202
- name: '天津津南支行营业部',
203
- icon: 'Document',
204
- children: []
205
- },
206
- {
207
- id: '30071',
208
- code: '30071',
209
- name: '天津河北支行营业部',
210
- icon: 'Document',
211
- children: []
212
- },
213
- {
214
- id: '30081',
215
- code: '30081',
216
- name: '北京银行股份有限公司天津河东支行',
217
- icon: 'Document',
218
- children: []
219
- },
220
- {
221
- id: '30091',
222
- code: '30091',
223
- name: '北京银行股份有限公司天津和平支行',
224
- icon: 'Document',
225
- children: []
226
- },
227
- {
228
- id: '30101',
229
- code: '30101',
230
- name: '天津滨海支行营业部',
231
- icon: 'Document',
232
- children: []
233
- },
234
- {
235
- id: '30111',
236
- code: '30111',
237
- name: '天津西青支行营业部',
238
- icon: 'Document',
239
- children: []
240
- },
241
- {
242
- id: '30121',
243
- code: '30121',
244
- name: '北京银行股份有限公司天津科创支行',
245
- icon: 'Document',
246
- children: []
247
- },
248
- {
249
- id: '30131',
250
- code: '30131',
251
- name: '北京银行股份有限公司天津北辰支行',
252
- icon: 'Document',
253
- children: []
254
- },
255
- {
256
- id: '30141',
257
- code: '30141',
258
- name: '北京银行天津中新生态城绿色支行',
259
- icon: 'Document',
260
- children: []
147
+
148
+ // 每个分行下生成8-12个支行
149
+ const subBranchCount = 8 + Math.floor(Math.random() * 5)
150
+ for (let k = 0; k < subBranchCount; k++) {
151
+ const subDistrict = districts[(j + k) % districts.length]
152
+ const subBranchId = String(idCounter++)
153
+ branch.children!.push({
154
+ id: subBranchId,
155
+ code: subBranchId,
156
+ name: `${bankName}${subDistrict}${branchTypes[1]}${k + 1}`,
157
+ icon: 'Document',
158
+ children: []
159
+ })
261
160
  }
262
- ]
161
+
162
+ bank.children!.push(branch)
163
+ }
164
+
165
+ data.push(bank)
263
166
  }
264
- ])
167
+
168
+ return data
169
+ }
170
+
171
+ const orgData = ref<OrgTreeNode[]>(generateOrgData())
265
172
 
266
173
  const singleValue = ref<string | null>(null)
267
174
  const multipleValue = ref<string[]>([])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.45",
3
+ "version": "7.0.47",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "dev": "nodemon --watch src --ext ts --exec \"ts-node src/app.ts\"",
package/test/111 ADDED
@@ -0,0 +1,52 @@
1
+ // src/directives/permission.ts
2
+ import type { Directive } from 'vue'
3
+
4
+ function getPermissions(): string[] {
5
+ return JSON.parse(localStorage.getItem('permissions') || '[]')
6
+ }
7
+
8
+ export const permissionDirective: Directive = {
9
+ mounted(el, binding) {
10
+ const { value } = binding
11
+ const permissions = getPermissions()
12
+
13
+ if (!value) return
14
+
15
+ const hasPermission = Array.isArray(value)
16
+ ? value.some(p => permissions.includes(p))
17
+ : permissions.includes(value)
18
+
19
+ if (!hasPermission) {
20
+ el.parentNode?.removeChild(el)
21
+ }
22
+ }
23
+ }
24
+
25
+
26
+ // main.ts
27
+ import { createApp } from 'vue'
28
+ import App from './App.vue'
29
+ import { permissionDirective } from '@/directives/permission'
30
+
31
+ const app = createApp(App)
32
+
33
+ app.directive('permission', permissionDirective)
34
+
35
+ app.mount('#app')
36
+
37
+
38
+ <el-button v-permission="'user:add'">新增</el-button>
39
+
40
+ <el-button v-permission="['user:edit', 'user:update']">
41
+ 编辑
42
+ </el-button>
43
+
44
+
45
+
46
+
47
+ <el-button v-permission="'user:add'">新增</el-button>
48
+
49
+ <el-button v-permission="['user:edit', 'user:update']">
50
+ 编辑
51
+ </el-button>
52
+
@@ -0,0 +1,248 @@
1
+ # Vue3 按钮权限管理(进阶版:自定义指令)
2
+
3
+ ## 📌 目标
4
+ 实现一套通用的按钮权限控制方案,支持:
5
+
6
+ - ✅ 单权限 / 多权限
7
+ - ✅ AND / OR 权限判断
8
+ - ✅ 隐藏 或 禁用 控制
9
+ - ✅ 动态更新权限
10
+ - ✅ 可扩展(Pinia / 后端权限)
11
+
12
+ ---
13
+
14
+ ## 🧩 一、指令实现
15
+
16
+ ```ts
17
+ // src/directives/permission.ts
18
+ import type { Directive } from 'vue'
19
+
20
+ function getPermissions(): string[] {
21
+ return JSON.parse(localStorage.getItem('permissions') || '[]')
22
+ }
23
+
24
+ function checkPermission(value: any, permissions: string[]) {
25
+ if (typeof value === 'string') {
26
+ return permissions.includes(value)
27
+ }
28
+
29
+ if (Array.isArray(value)) {
30
+ return value.some(p => permissions.includes(p))
31
+ }
32
+
33
+ if (typeof value === 'object') {
34
+ const { code, mode = 'or' } = value
35
+
36
+ if (Array.isArray(code)) {
37
+ return mode === 'and'
38
+ ? code.every(p => permissions.includes(p))
39
+ : code.some(p => permissions.includes(p))
40
+ }
41
+
42
+ return permissions.includes(code)
43
+ }
44
+
45
+ return false
46
+ }
47
+
48
+ function handleEl(el: HTMLElement, hasPermission: boolean, effect = 'remove') {
49
+ if (hasPermission) return
50
+
51
+ if (effect === 'disable') {
52
+ el.setAttribute('disabled', 'true')
53
+ el.classList.add('is-disabled')
54
+ el.style.pointerEvents = 'none'
55
+ } else {
56
+ el.parentNode?.removeChild(el)
57
+ }
58
+ }
59
+
60
+ export const permissionDirective: Directive = {
61
+ mounted(el, binding) {
62
+ const permissions = getPermissions()
63
+ const { value, arg } = binding
64
+
65
+ const hasPermission = checkPermission(value, permissions)
66
+
67
+ handleEl(el, hasPermission, arg)
68
+ },
69
+
70
+ updated(el, binding) {
71
+ const permissions = getPermissions()
72
+ const { value, arg } = binding
73
+
74
+ const hasPermission = checkPermission(value, permissions)
75
+
76
+ handleEl(el, hasPermission, arg)
77
+ }
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 🚀 二、全局注册
84
+
85
+ ```ts
86
+ // main.ts
87
+ import { createApp } from 'vue'
88
+ import App from './App.vue'
89
+ import { permissionDirective } from '@/directives/permission'
90
+
91
+ const app = createApp(App)
92
+
93
+ app.directive('permission', permissionDirective)
94
+
95
+ app.mount('#app')
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 🎯 三、使用方式
101
+
102
+ ### 1️⃣ 单权限
103
+ ```vue
104
+ <el-button v-permission="'user:add'">新增</el-button>
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 2️⃣ 多权限(满足一个)
110
+ ```vue
111
+ <el-button v-permission="['user:add','user:create']">
112
+ 新增
113
+ </el-button>
114
+ ```
115
+
116
+ ---
117
+
118
+ ### 3️⃣ 多权限(必须全部满足)
119
+ ```vue
120
+ <el-button v-permission="{ code: ['user:add','user:vip'], mode: 'and' }">
121
+ 高级新增
122
+ </el-button>
123
+ ```
124
+
125
+ ---
126
+
127
+ ### 4️⃣ 无权限 → 禁用按钮
128
+ ```vue
129
+ <el-button v-permission:disable="'user:add'">
130
+ 新增
131
+ </el-button>
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 🧠 四、推荐优化(生产环境必做)
137
+
138
+ ### ✔ 使用 Pinia 管理权限
139
+
140
+ ```ts
141
+ // stores/user.ts
142
+ import { defineStore } from 'pinia'
143
+
144
+ export const useUserStore = defineStore('user', {
145
+ state: () => ({
146
+ permissions: [] as string[]
147
+ })
148
+ })
149
+ ```
150
+
151
+ ### ✔ 修改指令权限来源
152
+
153
+ ```ts
154
+ import { useUserStore } from '@/stores/user'
155
+
156
+ const permissions = useUserStore().permissions
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 📦 五、权限格式规范
162
+
163
+ 建议统一格式:
164
+
165
+ ```
166
+ user:add
167
+ user:delete
168
+ order:list
169
+ order:create
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 🔐 六、后端权限校验(必须)
175
+
176
+ 前端权限只是 UI 控制,后端必须校验:
177
+
178
+ ```ts
179
+ if (!user.permissions.includes('user:add')) {
180
+ throw new Error('Forbidden')
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## ⚠️ 七、常见问题
187
+
188
+ ### ❌ 权限变化 UI 不更新
189
+ ✔ 已通过 `updated` 生命周期解决
190
+
191
+ ---
192
+
193
+ ### ❌ Element Plus 禁用无效
194
+ ✔ 必须加:
195
+
196
+ ```ts
197
+ el.style.pointerEvents = 'none'
198
+ ```
199
+
200
+ ---
201
+
202
+ ### ❌ 使用 v-if 控制权限
203
+ 不推荐,会导致:
204
+ - 模板混乱
205
+ - 逻辑分散
206
+
207
+ ---
208
+
209
+ ## 🏁 总结
210
+
211
+ 推荐最终方案:
212
+
213
+ 👉 自定义指令(控制 UI)
214
+ 👉 Pinia(存权限)
215
+ 👉 后端校验(保证安全)
216
+
217
+ 这样你的权限系统:
218
+ - 清晰
219
+ - 可维护
220
+ - 可扩展
221
+
222
+
223
+ type Button = {
224
+ appName: string
225
+ }
226
+
227
+ type Menu = {
228
+ children?: Menu[]
229
+ buttonChildren?: Button[]
230
+ }
231
+
232
+ export function getButtonPermissions(menuTree: Menu[]): string[] {
233
+ const set = new Set<string>()
234
+
235
+ const dfs = (nodes: Menu[]) => {
236
+ nodes.forEach(node => {
237
+ node.buttonChildren?.forEach(btn => {
238
+ if (btn.appName) set.add(btn.appName)
239
+ })
240
+
241
+ if (node.children) dfs(node.children)
242
+ })
243
+ }
244
+
245
+ dfs(menuTree)
246
+
247
+ return [...set]
248
+ }