- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore) - 实现服务端API代理功能,支持文件、模块和服务器API转发 - 创建文章详情页、栏目文章列表页和单页内容展示页面 - 集成Ant Design Vue组件库并实现SSR样式提取功能 - 定义API响应数据结构类型和应用布局组件 - 开发开发者应用中心和文章管理页面 - 实现CMS导航菜单获取和多租户切换功能
248 lines
6.4 KiB
TypeScript
248 lines
6.4 KiB
TypeScript
/**
|
||
* 按钮级权限控制
|
||
*
|
||
* 参考 mp-vue/src/utils/permission.ts
|
||
* 当前项目未引入 Pinia 的 user store,因此改为从 Nuxt state / localStorage 读取。
|
||
*/
|
||
import type { App } from 'vue'
|
||
import { useState } from '#imports'
|
||
import type { User } from '@/api/system/user/model'
|
||
import type { Role } from '@/api/system/role/model'
|
||
import type { Menu } from '@/api/system/menu/model'
|
||
|
||
type AuthzState = {
|
||
roles: string[]
|
||
authorities: string[]
|
||
}
|
||
|
||
const AUTHZ_STORAGE_KEY = 'Authz'
|
||
|
||
function uniqNonEmpty(values: Array<string | undefined | null>) {
|
||
const seen = new Set<string>()
|
||
for (const v of values) {
|
||
const s = typeof v === 'string' ? v.trim() : ''
|
||
if (!s || s === 'null' || s === 'undefined') continue
|
||
seen.add(s)
|
||
}
|
||
return Array.from(seen)
|
||
}
|
||
|
||
function safeJsonParse(value: string): unknown {
|
||
try {
|
||
return JSON.parse(value)
|
||
} catch {
|
||
return undefined
|
||
}
|
||
}
|
||
|
||
function normalizeStringArray(value: unknown): string[] {
|
||
if (Array.isArray(value)) {
|
||
return uniqNonEmpty(value.filter((v): v is string => typeof v === 'string'))
|
||
}
|
||
if (typeof value === 'string') {
|
||
return uniqNonEmpty(
|
||
value
|
||
.split(',')
|
||
.map((v) => v.trim())
|
||
.filter(Boolean)
|
||
)
|
||
}
|
||
return []
|
||
}
|
||
|
||
function readAuthzFromStorage(): AuthzState {
|
||
if (!import.meta.client) return { roles: [], authorities: [] }
|
||
try {
|
||
const raw = localStorage.getItem(AUTHZ_STORAGE_KEY)
|
||
if (!raw) return { roles: [], authorities: [] }
|
||
const parsed = safeJsonParse(raw)
|
||
if (!parsed || typeof parsed !== 'object') return { roles: [], authorities: [] }
|
||
const obj = parsed as Record<string, unknown>
|
||
return {
|
||
roles: normalizeStringArray(obj.roles),
|
||
authorities: normalizeStringArray(obj.authorities)
|
||
}
|
||
} catch {
|
||
return { roles: [], authorities: [] }
|
||
}
|
||
}
|
||
|
||
function writeAuthzToStorage(next: AuthzState) {
|
||
if (!import.meta.client) return
|
||
try {
|
||
localStorage.setItem(AUTHZ_STORAGE_KEY, JSON.stringify(next))
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function getAuthzStateRef() {
|
||
try {
|
||
return useState<AuthzState>('authz', () => readAuthzFromStorage())
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function getAuthzSnapshot(): AuthzState {
|
||
const state = getAuthzStateRef()
|
||
return state?.value ?? readAuthzFromStorage()
|
||
}
|
||
|
||
export function setAuthz(next: Partial<AuthzState>) {
|
||
const current = getAuthzSnapshot()
|
||
const merged: AuthzState = {
|
||
roles: normalizeStringArray(next.roles ?? current.roles),
|
||
authorities: normalizeStringArray(next.authorities ?? current.authorities)
|
||
}
|
||
|
||
const state = getAuthzStateRef()
|
||
if (state) state.value = merged
|
||
writeAuthzToStorage(merged)
|
||
}
|
||
|
||
export function clearAuthz() {
|
||
const state = getAuthzStateRef()
|
||
if (state) state.value = { roles: [], authorities: [] }
|
||
if (!import.meta.client) return
|
||
try {
|
||
localStorage.removeItem(AUTHZ_STORAGE_KEY)
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function getRoleCodesFromUser(user?: User | null) {
|
||
const roleCodes: string[] = []
|
||
|
||
const fromUserRoleCode = normalizeStringArray(user?.roleCode)
|
||
roleCodes.push(...fromUserRoleCode)
|
||
|
||
const roles: Role[] | undefined = user?.roles
|
||
if (Array.isArray(roles)) {
|
||
for (const role of roles) {
|
||
if (!role) continue
|
||
roleCodes.push(role.roleCode)
|
||
}
|
||
}
|
||
|
||
return uniqNonEmpty(roleCodes)
|
||
}
|
||
|
||
function getAuthoritiesFromUser(user?: User | null) {
|
||
const authorities: string[] = []
|
||
const list: Menu[] | undefined = user?.authorities
|
||
if (Array.isArray(list)) {
|
||
for (const item of list) {
|
||
if (!item) continue
|
||
authorities.push(item.authority)
|
||
}
|
||
}
|
||
return uniqNonEmpty(authorities)
|
||
}
|
||
|
||
export function setAuthzFromUser(user?: User | null) {
|
||
setAuthz({
|
||
roles: getRoleCodesFromUser(user),
|
||
authorities: getAuthoritiesFromUser(user)
|
||
})
|
||
}
|
||
|
||
/* 判断数组是否有某些值(全包含) */
|
||
function normalizeNeedles(value: string | string[]) {
|
||
if (Array.isArray(value)) return uniqNonEmpty(value)
|
||
const s = typeof value === 'string' ? value.trim() : ''
|
||
return s ? [s] : []
|
||
}
|
||
|
||
function normalizeHaystack(array: (string | undefined)[]) {
|
||
return uniqNonEmpty(array)
|
||
}
|
||
|
||
function arrayHas(array: (string | undefined)[], value: string | string[]): boolean {
|
||
if (!value) return true
|
||
if (!array) return false
|
||
const needles = normalizeNeedles(value)
|
||
if (needles.length === 0) return true
|
||
const haystack = new Set(normalizeHaystack(array))
|
||
for (let i = 0; i < needles.length; i++) {
|
||
if (!haystack.has(needles[i])) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/* 判断数组是否有任意值(任一包含) */
|
||
function arrayHasAny(array: (string | undefined)[], value: string | string[]): boolean {
|
||
if (!value) return true
|
||
if (!array) return false
|
||
const needles = normalizeNeedles(value)
|
||
if (needles.length === 0) return true
|
||
const haystack = new Set(normalizeHaystack(array))
|
||
for (let i = 0; i < needles.length; i++) {
|
||
if (haystack.has(needles[i])) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 是否有某些角色
|
||
* @param value 角色字符或字符数组
|
||
*/
|
||
export function hasRole(value: string | string[]): boolean {
|
||
const { roles } = getAuthzSnapshot()
|
||
return arrayHas(roles, value)
|
||
}
|
||
|
||
/**
|
||
* 是否有任意角色
|
||
* @param value 角色字符或字符数组
|
||
*/
|
||
export function hasAnyRole(value: string | string[]): boolean {
|
||
const { roles } = getAuthzSnapshot()
|
||
return arrayHasAny(roles, value)
|
||
}
|
||
|
||
/**
|
||
* 是否有某些权限
|
||
* @param value 权限字符或字符数组
|
||
*/
|
||
export function hasPermission(value: string | string[]): boolean {
|
||
const { authorities } = getAuthzSnapshot()
|
||
return arrayHas(authorities, value)
|
||
}
|
||
|
||
/**
|
||
* 是否有任意权限
|
||
* @param value 权限字符或字符数组
|
||
*/
|
||
export function hasAnyPermission(value: string | string[]): boolean {
|
||
const { authorities } = getAuthzSnapshot()
|
||
return arrayHasAny(authorities, value)
|
||
}
|
||
|
||
export default {
|
||
install(app: App) {
|
||
// 添加自定义指令
|
||
app.directive('role', {
|
||
mounted: (el, binding) => {
|
||
if (!hasRole(binding.value)) el.parentNode?.removeChild(el)
|
||
}
|
||
})
|
||
app.directive('any-role', {
|
||
mounted: (el, binding) => {
|
||
if (!hasAnyRole(binding.value)) el.parentNode?.removeChild(el)
|
||
}
|
||
})
|
||
app.directive('permission', {
|
||
mounted: (el, binding) => {
|
||
if (!hasPermission(binding.value)) el.parentNode?.removeChild(el)
|
||
}
|
||
})
|
||
app.directive('any-permission', {
|
||
mounted: (el, binding) => {
|
||
if (!hasAnyPermission(binding.value)) el.parentNode?.removeChild(el)
|
||
}
|
||
})
|
||
}
|
||
}
|