diff --git a/src/api/auth.ts b/src/api/auth.ts index 4192fff..7c59b37 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -5,3 +5,8 @@ import type { ResourceNode } from '@/types/resource' export function fetchMenus(): Promise { return http.get('/auth/menus') } + +/** 当前用户权限码与超管标志(供按钮/路由级权限控制)。 */ +export function fetchPermissions(): Promise<{ super: boolean; permissions: string[] }> { + return http.get<{ super: boolean; permissions: string[] }>('/auth/permissions') +} diff --git a/src/api/user.ts b/src/api/user.ts index af2e65a..4090bc1 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -71,3 +71,13 @@ export function deleteUser(id: string): Promise { export function resetPassword(id: string, password: string): Promise { return http.patch(`/users/${id}/password`, { password }) } + +/** 用户已分配的角色 code 列表。 */ +export function listUserRoles(userId: string, signal?: AbortSignal): Promise { + return http.get(`/users/${userId}/roles`, { signal }) +} + +/** 分配角色(整体替换)。 */ +export function assignUserRoles(userId: string, roleCodes: string[]): Promise { + return http.put(`/users/${userId}/roles`, { roleCodes }) +} diff --git a/src/components/ty-crud.vue b/src/components/ty-crud.vue index c8db0d0..f6e0384 100644 --- a/src/components/ty-crud.vue +++ b/src/components/ty-crud.vue @@ -12,7 +12,9 @@
- 新增{{ entityName }} + + 新增{{ entityName }} +
@@ -80,9 +82,12 @@ import type { import TyForm from '@/components/ty-form.vue' import { showDialog } from '@/utils/dialog' import { useRequest } from '@/composables/useRequest' +import { usePermissionStore } from '@/stores/modules/permission' defineOptions({ name: 'TyCrud' }) +const permissionStore = usePermissionStore() + const props = withDefaults( defineProps<{ /** 实体名(用于标题与删除确认,如「用户」)。 */ @@ -115,6 +120,8 @@ const props = withDefaults( collapseCount?: number /** 是否启用多选列(配合 #toolbar-right 做批量操作)。 */ selection?: boolean + /** 新增按钮所需权限码(无该权限且非超管时隐藏新增按钮)。 */ + createPermission?: string actionsWidth?: number | string }>(), { @@ -229,7 +236,11 @@ const resolvedActions = computed(() => ) function visibleActions(row: Record): CrudActionItem[] { - return resolvedActions.value.filter((a) => !a.show || a.show(row)) + return resolvedActions.value.filter((a) => { + if (a.show && !a.show(row)) return false + if (a.permission && !permissionStore.has(a.permission)) return false + return true + }) } function displayActions(row: Record): CrudActionItem[] { diff --git a/src/directives/permission.ts b/src/directives/permission.ts new file mode 100644 index 0000000..2d76097 --- /dev/null +++ b/src/directives/permission.ts @@ -0,0 +1,25 @@ +import { watchEffect, type Directive } from 'vue' +import { usePermissionStore } from '@/stores/modules/permission' + +const stops = new WeakMap void>() + +/** + * v-permission 指令:当前用户不具备指定权限码(且非超管)时隐藏元素。 + * 用法:v-permission="'user:create'" 或 v-permission="['user:create', 'user:update']"(任一满足即可)。 + * 权限异步加载:用 watchEffect 响应式隐藏/恢复,权限加载完成后自动显示。 + */ +export const vPermission: Directive = { + mounted(el, binding) { + const store = usePermissionStore() + const required = (Array.isArray(binding.value) ? binding.value : [binding.value]).filter(Boolean) as string[] + if (required.length === 0) return + const stop = watchEffect(() => { + el.style.display = store.hasAny(required) ? '' : 'none' + }) + stops.set(el, stop) + }, + unmounted(el) { + stops.get(el)?.() + stops.delete(el) + }, +} diff --git a/src/layouts/default.vue b/src/layouts/default.vue index 7064ed7..8b9b212 100644 --- a/src/layouts/default.vue +++ b/src/layouts/default.vue @@ -74,6 +74,7 @@ import { HOME_PATH, useTabsStore } from '@/stores/modules/tabs' import { useAuthStore } from '@/stores/modules/auth' import { useMenuStore } from '@/stores/modules/menu' import { useDictStore } from '@/stores/modules/dict' +import { usePermissionStore } from '@/stores/modules/permission' import logo from '@/assets/images/logo.png' import type { LayoutSettings } from '@/config/layout' import type { ResourceNode } from '@/types/resource' @@ -85,6 +86,7 @@ const tabsStore = useTabsStore() const auth = useAuthStore() const menuStore = useMenuStore() const dictStore = useDictStore() +const permissionStore = usePermissionStore() const route = useRoute() const router = useRouter() const { isFullscreen, toggle: toggleFullscreen } = useFullscreen() @@ -94,10 +96,11 @@ const HEADER_HEIGHT = 56 const menus = computed(() => menuStore.menus) const settingsOpen = ref(false) -// 登录后拉取菜单与字典 +// 登录后拉取菜单、字典与权限码 onMounted(() => { menuStore.fetch() dictStore.fetch() + permissionStore.fetch() }) const activePath = computed(() => route.path) @@ -223,6 +226,7 @@ async function handleLogout(): Promise { await auth.logout() menuStore.reset() dictStore.reset() + permissionStore.reset() tabsStore.removeAll() router.push('/login') } diff --git a/src/main.ts b/src/main.ts index 008887d..de75331 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,10 +10,14 @@ import { installI18n } from '@/i18n' import { setAuthExpiredHandler } from '@/api/http' import { useMenuStore } from '@/stores/modules/menu' import { useDictStore } from '@/stores/modules/dict' +import { usePermissionStore } from '@/stores/modules/permission' import { useTabsStore } from '@/stores/modules/tabs' +import { vPermission } from '@/directives/permission' const app = createApp(App) +app.directive('permission', vPermission) + installRouter(app) installStore(app) @@ -21,6 +25,7 @@ installStore(app) setAuthExpiredHandler(() => { useMenuStore(pinia).reset() useDictStore(pinia).reset() + usePermissionStore(pinia).reset() useTabsStore(pinia).removeAll() if (router.currentRoute.value.path !== '/login') { router.push('/login') diff --git a/src/pages/system/user/assign-roles-form.vue b/src/pages/system/user/assign-roles-form.vue new file mode 100644 index 0000000..c5e6380 --- /dev/null +++ b/src/pages/system/user/assign-roles-form.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/pages/system/user/index.vue b/src/pages/system/user/index.vue index af5df58..c6c318f 100644 --- a/src/pages/system/user/index.vue +++ b/src/pages/system/user/index.vue @@ -11,10 +11,14 @@ :update="update" :remove="remove" :actions="actions" + create-permission="user:create" id-key="id" name-key="username" :edit-readonly-keys="['username']" > +