vue2server7 7.0.44 → 7.0.46

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.
@@ -43,6 +43,7 @@
43
43
  :highlight-current="true"
44
44
  :expand-on-click-node="false"
45
45
  :default-expanded-keys="defaultExpandedKeys"
46
+ :default-expand-all="defaultExpandAll"
46
47
  :filter-node-method="filterNode"
47
48
  :check-strictly="checkStrictly"
48
49
  :show-checkbox="multiple"
@@ -125,6 +126,8 @@ interface OrgTreeSelectProps {
125
126
  checkStrictly?: boolean
126
127
  /** 默认展开的节点 */
127
128
  defaultExpandedKeys?: (string | number)[]
129
+ /** 是否默认展开所有节点 */
130
+ defaultExpandAll?: boolean
128
131
  }
129
132
 
130
133
  /** 组件事件类型定义 */
@@ -152,7 +155,8 @@ const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
152
155
  labelKey: 'name',
153
156
  childrenKey: 'children',
154
157
  checkStrictly: false,
155
- defaultExpandedKeys: () => []
158
+ defaultExpandedKeys: () => [],
159
+ defaultExpandAll: false
156
160
  })
157
161
 
158
162
  const emit = defineEmits<OrgTreeSelectEmits>()
@@ -400,12 +404,16 @@ defineExpose<OrgTreeSelectExpose>({
400
404
  }
401
405
 
402
406
  .org-tree-select__tree-wrapper {
403
- max-height: 400px;
407
+ height: 400px;
404
408
  border: 1px solid var(--el-border-color-light);
405
409
  border-radius: 4px;
406
410
  padding: 8px;
407
411
  }
408
412
 
413
+ .org-tree-select__tree-wrapper :deep(.el-scrollbar__wrap) {
414
+ max-height: 384px;
415
+ }
416
+
409
417
  .org-tree-select__node {
410
418
  display: flex;
411
419
  align-items: center;
@@ -43,6 +43,7 @@
43
43
  :highlight-current="true"
44
44
  :expand-on-click-node="false"
45
45
  :default-expanded-keys="defaultExpandedKeys"
46
+ :default-expand-all="defaultExpandAll"
46
47
  :filter-node-method="filterNode"
47
48
  :check-strictly="true"
48
49
  :show-checkbox="multiple"
@@ -123,6 +124,8 @@ interface OrgTreeSelectProps {
123
124
  childrenKey?: string
124
125
  /** 默认展开的节点 */
125
126
  defaultExpandedKeys?: (string | number)[]
127
+ /** 是否默认展开所有节点 */
128
+ defaultExpandAll?: boolean
126
129
  }
127
130
 
128
131
  /** 组件事件类型定义 */
@@ -149,7 +152,8 @@ const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
149
152
  nodeKey: 'id',
150
153
  labelKey: 'name',
151
154
  childrenKey: 'children',
152
- defaultExpandedKeys: () => []
155
+ defaultExpandedKeys: () => [],
156
+ defaultExpandAll: true
153
157
  })
154
158
 
155
159
  const emit = defineEmits<OrgTreeSelectEmits>()
@@ -261,20 +265,65 @@ function openDialog(): void {
261
265
  selectedValue.value = props.modelValue
262
266
  }
263
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
+
264
304
  /**
265
305
  * 弹窗打开后的回调
266
306
  */
267
307
  function onDialogOpened(): void {
268
- nextTick(() => {
308
+ nextTick(async () => {
269
309
  updateSelectedNodes()
270
310
  if (!treeRef.value) return
271
311
 
312
+ // 先恢复选中状态
272
313
  if (props.multiple) {
273
314
  const keys = Array.isArray(selectedValue.value) ? selectedValue.value : []
274
315
  treeRef.value.setCheckedKeys(keys as (string | number)[])
275
316
  } else if (selectedValue.value !== null) {
276
317
  treeRef.value.setCurrentKey(selectedValue.value as string | number)
277
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
+ }
278
327
  })
279
328
  }
280
329
 
@@ -451,12 +500,16 @@ defineExpose<OrgTreeSelectExpose>({
451
500
  }
452
501
 
453
502
  .org-tree-select__tree-wrapper {
454
- max-height: 400px;
503
+ height: 400px;
455
504
  border: 1px solid var(--el-border-color-light);
456
505
  border-radius: 4px;
457
506
  padding: 8px;
458
507
  }
459
508
 
509
+ .org-tree-select__tree-wrapper :deep(.el-scrollbar__wrap) {
510
+ max-height: 384px;
511
+ }
512
+
460
513
  .org-tree-select__node {
461
514
  display: flex;
462
515
  align-items: center;
@@ -78,6 +78,23 @@
78
78
  当前值:<code>{{ JSON.stringify(cascadeValue) }}</code>
79
79
  </div>
80
80
  </el-card>
81
+
82
+ <el-card class="demo-card" shadow="never">
83
+ <template #header>
84
+ <span>默认全部展开 <code>default-expand-all</code></span>
85
+ </template>
86
+ <OrgTreeSelect
87
+ v-model="expandAllValue"
88
+ :data="orgData"
89
+ multiple
90
+ default-expand-all
91
+ placeholder="默认展开所有节点"
92
+ class="demo-control"
93
+ />
94
+ <div class="demo-output">
95
+ 当前值:<code>{{ JSON.stringify(expandAllValue) }}</code>
96
+ </div>
97
+ </el-card>
81
98
  </section>
82
99
  </template>
83
100
 
@@ -86,171 +103,79 @@ import { ref } from 'vue'
86
103
  import OrgTreeSelect, { type OrgTreeNode } from '../components/OrgTreeSelect.vue'
87
104
  import OrgTreeSelectCascade from '../components/OrgTreeSelectCascade.vue'
88
105
 
89
- // 机构树数据(模拟)
90
- const orgData = ref<OrgTreeNode[]>([
91
- {
92
- id: '25001',
93
- code: '25001',
94
- name: '北京银行总行核算中心',
95
- icon: 'OfficeBuilding',
96
- children: [
97
- {
98
- id: '00019',
99
- code: '00019',
100
- name: '北京分行',
101
- icon: 'OfficeBuilding',
102
- children: []
103
- },
104
- {
105
- id: '00023',
106
- code: '00023',
107
- name: '中关村分行',
108
- icon: 'OfficeBuilding',
109
- children: []
110
- },
111
- {
112
- id: '00025',
113
- code: '00025',
114
- name: '城市副中心分行',
115
- icon: 'OfficeBuilding',
116
- children: []
117
- },
118
- {
119
- id: '25101',
120
- code: '25101',
121
- 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]}`,
122
144
  icon: 'OfficeBuilding',
123
145
  children: []
124
146
  }
125
- ]
126
- },
127
- {
128
- id: '25111',
129
- code: '25111',
130
- name: '天津核算中心',
131
- icon: 'OfficeBuilding',
132
- children: [
133
- {
134
- id: '25311',
135
- code: '25311',
136
- name: '天津分行会计部',
137
- icon: 'Document',
138
- children: []
139
- },
140
- {
141
- id: '30001',
142
- code: '30001',
143
- name: '天津营业部',
144
- icon: 'Document',
145
- children: []
146
- },
147
- {
148
- id: '30011',
149
- code: '30011',
150
- name: '天津河西支行营业部',
151
- icon: 'Document',
152
- children: []
153
- },
154
- {
155
- id: '30021',
156
- code: '30021',
157
- name: '天津开发区支行营业部',
158
- icon: 'Document',
159
- children: []
160
- },
161
- {
162
- id: '30031',
163
- code: '30031',
164
- name: '天津南开支行营业部',
165
- icon: 'Document',
166
- children: []
167
- },
168
- {
169
- id: '30041',
170
- code: '30041',
171
- name: '北京银行股份有限公司天津八纬路支行',
172
- icon: 'Document',
173
- children: []
174
- },
175
- {
176
- id: '30051',
177
- code: '30051',
178
- name: '天津梅江支行营业部',
179
- icon: 'Document',
180
- children: []
181
- },
182
- {
183
- id: '30061',
184
- code: '30061',
185
- name: '天津津南支行营业部',
186
- icon: 'Document',
187
- children: []
188
- },
189
- {
190
- id: '30071',
191
- code: '30071',
192
- name: '天津河北支行营业部',
193
- icon: 'Document',
194
- children: []
195
- },
196
- {
197
- id: '30081',
198
- code: '30081',
199
- name: '北京银行股份有限公司天津河东支行',
200
- icon: 'Document',
201
- children: []
202
- },
203
- {
204
- id: '30091',
205
- code: '30091',
206
- name: '北京银行股份有限公司天津和平支行',
207
- icon: 'Document',
208
- children: []
209
- },
210
- {
211
- id: '30101',
212
- code: '30101',
213
- name: '天津滨海支行营业部',
214
- icon: 'Document',
215
- children: []
216
- },
217
- {
218
- id: '30111',
219
- code: '30111',
220
- name: '天津西青支行营业部',
221
- icon: 'Document',
222
- children: []
223
- },
224
- {
225
- id: '30121',
226
- code: '30121',
227
- name: '北京银行股份有限公司天津科创支行',
228
- icon: 'Document',
229
- children: []
230
- },
231
- {
232
- id: '30131',
233
- code: '30131',
234
- name: '北京银行股份有限公司天津北辰支行',
235
- icon: 'Document',
236
- children: []
237
- },
238
- {
239
- id: '30141',
240
- code: '30141',
241
- name: '北京银行天津中新生态城绿色支行',
242
- icon: 'Document',
243
- 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
+ })
244
160
  }
245
- ]
161
+
162
+ bank.children!.push(branch)
163
+ }
164
+
165
+ data.push(bank)
246
166
  }
247
- ])
167
+
168
+ return data
169
+ }
170
+
171
+ const orgData = ref<OrgTreeNode[]>(generateOrgData())
248
172
 
249
173
  const singleValue = ref<string | null>(null)
250
174
  const multipleValue = ref<string[]>([])
251
175
  const disabledValue = ref<string>('25001')
252
176
  const expandedValue = ref<string | null>(null)
253
177
  const cascadeValue = ref<string[]>([])
178
+ const expandAllValue = ref<string[]>([])
254
179
 
255
180
  function onSingleChange(value: string | number | (string | number)[] | null, node?: OrgTreeNode | OrgTreeNode[]) {
256
181
  console.log('单选变化:', value, node)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.44",
3
+ "version": "7.0.46",
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,220 @@
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
+ - 可扩展