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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
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
|
+
- 可扩展
|