v-permission 指令示范
本页演示如何在项目中中展示自定义指令 v-permission,并详细说明用法与行为。
提示
建议绑定在单根真实 dom 元素上,直接绑定到非原生 html 元素时,插件可能无法正常工作,建议仅在原生 html 元素上使用。 目前已知不会挂载指令的元素有:
templateel-dropdown-item
权限注入方式
- 适合全局共享的权限集合,一次注入,所有组件均可用。
ts
import Directives from '@quiteer/directives'
import { createApp } from 'vue'
const app = createApp(App)
app.use(Directives, {
permission: []
})1
2
3
4
5
6
7
2
3
4
5
6
7
- 权限修改为响应式 , 权限修改界面不刷新即可重新鉴权
ts
import { getPermissionManager } from '@quiteer/directives'
const permissionManager = getPermissionManager()
permissionManager.addPermissions([
'sys:user:list',
'sys:user:add',
'sys:user:edit',
'sys:user:delete'
])1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
查看代码
vue
<script setup lang="ts">
import { getPermissionManager } from '@quiteer/directives'
import { NAlert, NButton, NCard, NFlex, NGrid, NGridItem, NSelect, NTag } from 'naive-ui'
import { computed, readonly, ref, watch } from 'vue'
/**
* 模拟API调用,从服务器获取权限
*/
async function fetchUserPermissions(userId: string): Promise<string[]> {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 300))
// 模拟不同用户的权限
const permissionMap: Record<string, string[]> = {
admin: [
'sys:user:admin',
'sys:user:add',
'sys:user:edit',
'sys:user:delete',
'sys:role:manage',
'sys:log:view'
],
editor: [
'sys:user:add',
'sys:user:edit',
'sys:log:view'
],
viewer: [
'sys:log:view'
]
}
return permissionMap[userId] || []
}
/**
* 权限服务Hook - 确保响应式更新
*/
function usePermissionService() {
const loading = ref(false)
const error = ref<Error | null>(null)
const permissionsManager = getPermissionManager()
/**
* 加载用户权限
*/
const loadPermissions = async (userId: string) => {
try {
loading.value = true
error.value = null
const perms = await fetchUserPermissions(userId)
// 确保创建新的Set实例,触发响应式更新
permissionsManager?.setPermissions(new Set(perms))
return perms
}
catch (err) {
error.value = err instanceof Error ? err : new Error('Failed to load permissions')
throw error.value
}
finally {
loading.value = false
}
}
/**
* 模拟用户登录
*/
const loginAsUser = async (userId: string) => {
return loadPermissions(userId)
}
/**
* 退出登录
*/
const logout = () => {
// 创建空的Set实例
permissionsManager?.setPermissions(new Set())
}
// 使用本地ref而不是直接引用manager.permissions
const permissions = ref(permissionsManager?.permissions.value)
// 监听权限变化,确保响应式更新
watch(
() => permissionsManager?.permissions.value,
(newPerms) => {
permissions.value = new Set(newPerms) // 创建新引用
},
{ deep: true }
)
return {
loading: readonly(loading),
error: readonly(error),
permissions: readonly(permissions),
loginAsUser,
logout,
hasPermission: permissionsManager?.hasPermission.bind(permissionsManager),
hasAnyPermission: permissionsManager?.hasAnyPermission.bind(permissionsManager),
hasAllPermissions: permissionsManager?.hasAllPermissions.bind(permissionsManager)
}
}
// 使用权限服务
const {
permissions,
loading,
error,
loginAsUser,
logout,
hasAnyPermission
} = usePermissionService()
// 当前用户
const currentUser = ref<string | null>(null)
// 模拟用户选项
const userOptions = [
{ label: '管理员 (admin)', value: 'admin' },
{ label: '编辑者 (editor)', value: 'editor' },
{ label: '查看者 (viewer)', value: 'viewer' },
{ label: '无权限用户', value: 'none' }
]
// 登录为指定用户
async function handleLogin(userId: string) {
try {
currentUser.value = userId
await loginAsUser(userId)
}
catch (err) {
console.error('登录失败:', err)
}
}
// 退出登录
function handleLogout() {
logout()
currentUser.value = null
}
// 检查特定权限
const canManageUsers = computed(() => hasAnyPermission?.(['sys:user:add', 'sys:user:edit', 'sys:user:delete']))
// 关键添加:强制更新UI的计数器
const forceUpdateKey = ref(0)
// 监听权限变化,强制重新渲染
watch(permissions, () => {
forceUpdateKey.value++
}, { deep: true })
// 监听当前用户变化
watch(currentUser, (newUser) => {
if (newUser && newUser !== 'none') {
handleLogin(newUser)
}
else if (newUser === 'none') {
logout()
}
}, { immediate: true })
// 处理删除点击
function handleDeleteClick() {
// eslint-disable-next-line no-alert
alert('删除功能已触发!')
}
</script>
<template>
<div class="permission-demo-container">
<!-- 用户控制面板 -->
<NCard title="权限控制面板" class="control-panel">
<NFlex vertical :size="16">
<div>
<strong>当前用户:</strong>
<span v-if="currentUser">{{ currentUser }}</span>
<span v-else class="text-muted">未登录</span>
<NButton v-if="currentUser" size="small" type="error" class="logout-btn" @click="handleLogout">
退出登录
</NButton>
</div>
<div>
<strong>可用权限:</strong>
<NSelect
v-model:value="currentUser"
:options="userOptions"
placeholder="选择用户类型"
style="width: 240px"
:loading="loading"
/>
</div>
<div>
<strong>当前权限:</strong>
<div class="permissions-list">
<NTag
v-for="perm, i in Array.from(permissions || [])"
:key="i"
type="success"
size="small"
class="permission-tag"
>
{{ perm }}
</NTag>
<NTag v-if="permissions?.size === 0" type="warning" size="small">
无权限
</NTag>
</div>
</div>
<NAlert v-if="error" type="error" title="错误">
{{ error.message }}
</NAlert>
<!-- 调试信息 -->
<div class="debug-info">
<small>UI更新计数: {{ forceUpdateKey }}</small>
</div>
</NFlex>
</NCard>
<!-- 关键修改:添加:key强制重新渲染整个演示区域 -->
<NGrid :key="forceUpdateKey" cols="1 480:2 768:3 1024:4" x-gap="16" y-gap="16" class="demo-grid">
<!-- 基础用法 -->
<NGridItem>
<NCard title="基础用法" class="demo-card">
<p class="demo-desc">
v-permission="'sys:user:add'"
</p>
<NFlex vertical :size="12">
<NButton v-permission="'sys:user:add'" type="primary" block>
新增用户 (sys:user:add)
</NButton>
<NButton v-permission="'sys:user:edit'" type="info" block>
编辑用户 (sys:user:edit)
</NButton>
<NButton v-permission="'sys:user:delete'" type="error" block>
删除用户 (sys:user:delete)
</NButton>
</NFlex>
</NCard>
</NGridItem>
<!-- 任意命中模式 -->
<NGridItem>
<NCard title="任意命中模式 (.any)" class="demo-card">
<p class="demo-desc">
v-permission.any="['sys:user:add', 'sys:user:edit']"
</p>
<NButton v-permission.any="['sys:user:add', 'sys:user:edit']" type="success" block>
有新增或编辑权限时显示
</NButton>
</NCard>
</NGridItem>
<!-- 全部命中模式 -->
<NGridItem>
<NCard title="全部命中模式 (.all)" class="demo-card">
<p class="demo-desc">
v-permission.all="['sys:user:add', 'sys:user:edit']"
</p>
<NButton v-permission.all="['sys:user:add', 'sys:user:edit']" type="warning" block>
必须同时拥有新增和编辑权限
</NButton>
</NCard>
</NGridItem>
<!-- 禁用效果 -->
<NGridItem>
<NCard title="禁用效果 (:disable/.disable)" class="demo-card">
<p class="demo-desc">
v-permission.disable="'sys:user:delete'"
</p>
<NButton v-permission.disable="'sys:user:delete'" type="error" block @click="handleDeleteClick">
删除用户 (未授权时禁用)
</NButton>
</NCard>
</NGridItem>
<!-- 移除效果 -->
<NGridItem>
<NCard title="移除效果 (:remove/.remove)" class="demo-card">
<p class="demo-desc">
v-permission:remove="'sys:user:admin'"
</p>
<div class="remove-demo-container">
<NButton v-permission:remove="'sys:user:admin'" type="primary">
管理员专属按钮 (无权限时移除)
</NButton>
<!-- 这个span用于演示元素被移除的效果 -->
<span v-if="false" class="remove-placeholder">按钮位置</span>
</div>
</NCard>
</NGridItem>
<!-- 隐藏效果 -->
<NGridItem>
<NCard title="隐藏效果 (:hide/.hide)" class="demo-card">
<p class="demo-desc">
v-permission:hide="'sys:user:admin'"
</p>
<NButton v-permission:hide="'sys:user:admin'" type="primary" block>
仅管理员可见 (无权限时隐藏)
</NButton>
</NCard>
</NGridItem>
<!-- 组合用法 -->
<NGridItem>
<NCard title="组合用法" class="demo-card">
<p class="demo-desc">
v-permission:disable.any="['sys:user:add', 'sys:user:edit']"
</p>
<NButton
v-permission:disable.any="['sys:user:add', 'sys:user:edit']"
type="success"
block
>
有新增或编辑权限时启用
</NButton>
<p class="demo-desc mt-12">
v-permission:remove.all="['sys:user:admin', 'sys:role:manage']"
</p>
<NButton
v-permission:remove.all="['sys:user:admin', 'sys:role:manage']"
type="error"
block
>
必须同时拥有管理员和角色管理权限
</NButton>
</NCard>
</NGridItem>
<!-- 条件渲染对比 -->
<NGridItem>
<NCard title="与 v-if 对比" class="demo-card">
<p class="demo-desc">
使用 v-if 手动控制
</p>
<NButton
v-if="canManageUsers"
type="primary"
block
>
通过JS条件判断 ({{ canManageUsers ? '显示' : '隐藏' }})
</NButton>
<p class="demo-desc mt-12">
使用指令控制
</p>
<NButton
v-permission.any="['sys:user:add', 'sys:user:edit', 'sys:user:delete']"
type="primary"
block
>
通过指令判断 (相同条件)
</NButton>
<p class="demo-desc mt-12">
说明: 指令方式更声明式,且支持多种效果
</p>
</NCard>
</NGridItem>
</NGrid>
</div>
</template>
<style scoped>
.permission-demo-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.control-panel {
margin-bottom: 24px;
}
.logout-btn {
margin-left: 12px;
}
.permissions-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.permission-tag {
margin-right: 4px;
margin-bottom: 4px;
}
.demo-grid {
margin-top: 16px;
}
.demo-card {
height: 100%;
}
.demo-desc {
color: #666;
font-size: 14px;
margin-bottom: 12px;
font-family: monospace;
background-color: #f5f5f5;
padding: 6px 10px;
border-radius: 4px;
}
.mt-12 {
margin-top: 12px;
}
.remove-demo-container {
display: flex;
justify-content: center;
align-items: center;
height: 60px;
border: 1px dashed #ddd;
border-radius: 6px;
position: relative;
}
.remove-placeholder {
color: #999;
font-style: italic;
}
.text-muted {
color: #999;
}
.debug-info {
margin-top: 8px;
color: #666;
font-size: 12px;
}
/* 权限禁用状态的样式 */
.permission-disabled {
opacity: 0.6;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
v-permission 指令说明
- 功能:根据用户权限控制元素的可见性或交互性。
- 指令值:
- 字符串:单一权限码,如
'sys:user:add' - 数组:多个权限码,如
['sys:user:add','sys:user:edit'] - 对象:
{ codes, mode, effect }
- 字符串:单一权限码,如
- 修饰符:
any:任意命中即可,如v-permission.any="[...]"all:必须全部命中,如v-permission.all="[...]"hide:未授权时隐藏(默认效果)disable:未授权时禁用remove:未授权时移除
- 参数:
- 可用
:hide/:disable/:remove - 指定效果
v-permission:remove="'sys:user:add'"
- 可用
代码示例
vue
<!-- 任意命中 -->
<button v-permission.any="['sys:user:add','sys:user:edit']">
新增或编辑
</button>
<!-- 全部命中 -->
<button v-permission.all="['sys:user:add','sys:user:edit']">
新增并编辑
</button>
<!-- 禁用(未授权时) -->
<button v-permission.disable="'sys:user:delete'">
删除用户
</button>
<!-- 移除(未授权时) -->
<button v-permission:remove="'sys:user:admin'">
管理员操作
</button>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
基础用法(默认隐藏)
- 功能:未授权时隐藏元素,授权则显示
- 示例:
'sys:user:add'已授权、'sys:user:edit'未授权
查看代码
vue
<!-- 命中则显示 -->
<button v-permission="'sys:user:add'">
新增用户(有权限)
</button>
<!-- 未命中则隐藏 -->
<button v-permission="'sys:user:edit'">
编辑用户(无权限,默认隐藏)
</button>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
任意命中(any)
- 功能:只要命中其中一个权限码即可显示
- 示例:当前注入有
'sys:user:add',无'sys:user:edit'
查看代码
vue
<button v-permission.any="['sys:user:add','sys:user:edit']">
新增或编辑(任意命中)
</button>1
2
3
2
3
全部命中(all)
- 功能:必须同时拥有多个权限码才显示
- 示例:当前注入仅
'sys:user:add',缺少'sys:user:edit',因此不显示
查看代码
vue
<button v-permission.all="['sys:user:add','sys:user:edit']">
新增并编辑(全部命中)
</button>1
2
3
2
3
禁用效果(disable)
- 功能:未授权时不隐藏,而是禁用交互(添加
aria-disabled、pointer-events: none等) - 示例:删除按钮在未授权时禁用
查看代码
vue
<button v-permission.disable="'sys:user:delete'">
删除用户(未授权时禁用)
</button>1
2
3
2
3
移除效果(remove)
- 功能:未授权时直接从 DOM 中移除
- 示例:管理员操作在未授权时不渲染
查看代码
vue
<button v-permission:remove="'sys:user:admin'">
管理员操作(未授权时移除)
</button>1
2
3
2
3
对象值(codes/mode/effect)
- 功能:通过对象统一配置权限码、匹配模式和效果
- 示例:全部命中且未授权时禁用
查看代码
vue
<button
v-permission="{
codes: ['sys:user:add','sys:user:edit'],
mode: 'all',
effect: 'disable'
}"
>
组合配置(全部命中且未授权时禁用)
</button>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
修饰符与参数优先级
- 修饰符优先(
.any/.all与.hide/.disable/.remove) - 参数次之(
:hide/:disable/:remove) - 选项对象最后(
{ mode, effect })
查看代码
vue
<!-- 修饰符优先:强制 all + remove -->
<button v-permission.all:remove="['sys:user:add','sys:user:edit']">
强制全部命中且未授权时移除
</button>
<!-- 参数优先于对象:效果变为 disable -->
<button v-permission:disable="{ codes: 'sys:user:edit', effect: 'hide' }">
优先级演示(最终禁用)
</button>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9