初始化2
This commit is contained in:
283
app/composables/useAppPermission.ts
Normal file
283
app/composables/useAppPermission.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 应用级权限管理 composable
|
||||
*
|
||||
* 管理当前用户在各应用中的角色权限,提供:
|
||||
* 1. 开发者中心访问检查
|
||||
* 2. 可访问应用列表 + 角色映射
|
||||
* 3. 按角色判断某应用的具体权限
|
||||
*/
|
||||
|
||||
import type { AppRole, AppPermissionInfo } from '@/api/app/appUser/model'
|
||||
|
||||
// ============ 角色层级 ============
|
||||
|
||||
/** 角色层级:数字越大权限越高 */
|
||||
export const ROLE_HIERARCHY: Record<AppRole, number> = {
|
||||
viewer: 1,
|
||||
developer: 2,
|
||||
admin: 3,
|
||||
owner: 4,
|
||||
}
|
||||
|
||||
/** 角色中文名 */
|
||||
export const ROLE_LABEL: Record<AppRole, string> = {
|
||||
owner: '所有者',
|
||||
admin: '管理员',
|
||||
developer: '开发者',
|
||||
viewer: '只读',
|
||||
}
|
||||
|
||||
/** 角色颜色(用于 RoleTag) */
|
||||
export const ROLE_COLOR: Record<AppRole, string> = {
|
||||
owner: 'gold',
|
||||
admin: 'blue',
|
||||
developer: 'green',
|
||||
viewer: 'default',
|
||||
}
|
||||
|
||||
// ============ 权限定义 ============
|
||||
|
||||
export interface AppPermission {
|
||||
appId: number
|
||||
productName: string
|
||||
productCode?: string
|
||||
icon?: string
|
||||
role: AppRole
|
||||
isOwner: boolean
|
||||
canManageMembers: boolean // owner | admin
|
||||
canEditApp: boolean // owner | admin
|
||||
canDeleteApp: boolean // owner only
|
||||
canSubmitReview: boolean // owner | admin
|
||||
canEditResource: boolean // owner | admin | developer
|
||||
canViewSensitive: boolean // owner | admin | developer
|
||||
canCreateApiKey: boolean // owner | admin | developer
|
||||
canEditConfig: boolean // owner | admin
|
||||
canTriggerBuild: boolean // owner | admin | developer
|
||||
}
|
||||
|
||||
/** 根据 role 计算权限字段 */
|
||||
function buildPermission(info: AppPermissionInfo): AppPermission {
|
||||
const { role } = info
|
||||
const isOwner = role === 'owner'
|
||||
const isAtLeastAdmin = isOwner || role === 'admin'
|
||||
const isAtLeastDeveloper = isAtLeastAdmin || role === 'developer'
|
||||
|
||||
return {
|
||||
appId: info.appId,
|
||||
productName: info.productName,
|
||||
productCode: info.productCode,
|
||||
icon: info.icon,
|
||||
role,
|
||||
isOwner,
|
||||
canManageMembers: isAtLeastAdmin,
|
||||
canEditApp: isAtLeastAdmin,
|
||||
canDeleteApp: isOwner,
|
||||
canSubmitReview: isAtLeastAdmin,
|
||||
canEditResource: isAtLeastDeveloper,
|
||||
canViewSensitive: isAtLeastDeveloper,
|
||||
canCreateApiKey: isAtLeastDeveloper,
|
||||
canEditConfig: isAtLeastAdmin,
|
||||
canTriggerBuild: isAtLeastDeveloper,
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 全局状态(模块级单例) ============
|
||||
|
||||
const appPermissionsMap = ref<Map<number, AppPermission>>(new Map())
|
||||
const isPlatformDeveloper = ref(false)
|
||||
const hasCheckedAccess = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// ============ 核心方法 ============
|
||||
|
||||
/**
|
||||
* 检查用户是否有开发者中心访问权限
|
||||
* 并自动加载可访问的应用列表
|
||||
*/
|
||||
async function checkDeveloperAccess(): Promise<{
|
||||
accessible: boolean
|
||||
isPlatformDeveloper: boolean
|
||||
hasJoinedApps: boolean
|
||||
}> {
|
||||
if (loading.value) return { accessible: false, isPlatformDeveloper: false, hasJoinedApps: false }
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 尝试获取可访问应用列表(后端接口:GET /api/app/app-user/check-access)
|
||||
const { checkAppAccess } = await import('@/api/app/appUser')
|
||||
const res = await checkAppAccess()
|
||||
|
||||
if (res?.accessible) {
|
||||
isPlatformDeveloper.value = res.isPlatformDeveloper ?? false
|
||||
hasCheckedAccess.value = true
|
||||
|
||||
// 如果返回了可访问应用列表,直接缓存
|
||||
if (res.apps?.length) {
|
||||
const newMap = new Map<number, AppPermission>()
|
||||
for (const info of res.apps) {
|
||||
newMap.set(info.appId, buildPermission(info))
|
||||
}
|
||||
appPermissionsMap.value = newMap
|
||||
}
|
||||
|
||||
return {
|
||||
accessible: true,
|
||||
isPlatformDeveloper: res.isPlatformDeveloper ?? false,
|
||||
hasJoinedApps: (res.apps?.length ?? 0) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { accessible: false, isPlatformDeveloper: false, hasJoinedApps: false }
|
||||
}
|
||||
catch {
|
||||
// 接口失败时降级:如果 localStorage 有 UserId 且是 type=2,仍允许访问
|
||||
if (import.meta.client) {
|
||||
const userType = localStorage.getItem('UserType')
|
||||
if (userType === '2') {
|
||||
isPlatformDeveloper.value = true
|
||||
hasCheckedAccess.value = true
|
||||
return { accessible: true, isPlatformDeveloper: true, hasJoinedApps: false }
|
||||
}
|
||||
}
|
||||
return { accessible: false, isPlatformDeveloper: false, hasJoinedApps: false }
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户所有可访问应用的权限
|
||||
* 后端接口:GET /api/app/product/accessible
|
||||
*/
|
||||
async function loadAppPermissions(): Promise<Map<number, AppPermission>> {
|
||||
try {
|
||||
const { getMyAccessibleApps } = await import('@/api/app/appProduct')
|
||||
const apps = await getMyAccessibleApps()
|
||||
|
||||
const newMap = new Map<number, AppPermission>()
|
||||
for (const app of apps) {
|
||||
const role = (app.myRole as AppRole) || 'viewer'
|
||||
newMap.set(app.productId!, {
|
||||
appId: app.productId!,
|
||||
productName: app.productName || '',
|
||||
productCode: app.productCode,
|
||||
icon: app.icon,
|
||||
role,
|
||||
...(buildPermission({ appId: app.productId!, productName: app.productName || '', role } as AppPermissionInfo)),
|
||||
})
|
||||
}
|
||||
appPermissionsMap.value = newMap
|
||||
return newMap
|
||||
}
|
||||
catch {
|
||||
console.warn('[useAppPermission] 加载应用权限失败')
|
||||
return appPermissionsMap.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单个应用的权限缓存
|
||||
*/
|
||||
function setAppPermission(info: AppPermissionInfo) {
|
||||
const permission = buildPermission(info)
|
||||
appPermissionsMap.value.set(info.appId, permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定应用的权限
|
||||
*/
|
||||
function getAppPermission(appId: number | undefined | null): AppPermission | null {
|
||||
if (!appId) return null
|
||||
return appPermissionsMap.value.get(appId) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定应用是否拥有某项权限
|
||||
*/
|
||||
function hasPermission(
|
||||
appId: number | undefined | null,
|
||||
permission: keyof AppPermission,
|
||||
): boolean {
|
||||
const perm = getAppPermission(appId)
|
||||
if (!perm) return false
|
||||
return !!perm[permission]
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前用户在指定应用中的角色是否 >= 某个级别
|
||||
*/
|
||||
function hasRole(appId: number | undefined | null, minRole: AppRole): boolean {
|
||||
const perm = getAppPermission(appId)
|
||||
if (!perm) return false
|
||||
return ROLE_HIERARCHY[perm.role] >= ROLE_HIERARCHY[minRole]
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空权限缓存(登出时调用)
|
||||
*/
|
||||
function clearPermissions() {
|
||||
appPermissionsMap.value.clear()
|
||||
isPlatformDeveloper.value = false
|
||||
hasCheckedAccess.value = false
|
||||
}
|
||||
|
||||
// ============ Computed ============
|
||||
|
||||
/** 可访问的应用 ID 列表 */
|
||||
const accessibleAppIds = computed(() => [...appPermissionsMap.value.keys()])
|
||||
|
||||
/** 是否已检查过访问权限 */
|
||||
const isChecked = computed(() => hasCheckedAccess.value)
|
||||
|
||||
/** 是否正在加载 */
|
||||
const isLoading = computed(() => loading.value)
|
||||
|
||||
/** 权限不足时的提示文字 */
|
||||
function getNoPermissionTip(role?: AppRole): string {
|
||||
const roleName = role ? ROLE_LABEL[role] : '当前角色'
|
||||
return `当前身份为「${roleName}」,无法执行此操作。如需更多权限,请联系应用管理员。`
|
||||
}
|
||||
|
||||
// ============ 敏感信息遮罩 ============
|
||||
|
||||
/**
|
||||
* 遮罩敏感字段值
|
||||
* @param value 原始值
|
||||
* @param canView 是否有查看权限
|
||||
* @param maskLength 遮罩长度,默认 6 个 *
|
||||
*/
|
||||
function maskSensitiveValue(value: string | null | undefined, canView: boolean, maskLength = 6): string {
|
||||
if (canView) return value || ''
|
||||
if (!value) return ''
|
||||
const mask = '*'.repeat(maskLength)
|
||||
// 对于较短的值直接全部遮罩,较长的值显示前几个字符
|
||||
if (value.length <= maskLength) return mask
|
||||
return value.slice(0, 4) + mask
|
||||
}
|
||||
|
||||
// ============ 组合式函数导出 ============
|
||||
|
||||
export function useAppPermission() {
|
||||
return {
|
||||
// 状态
|
||||
appPermissions: appPermissionsMap,
|
||||
isPlatformDeveloper: readonly(isPlatformDeveloper),
|
||||
isChecked: readonly(isChecked),
|
||||
isLoading: readonly(isLoading),
|
||||
|
||||
// 方法
|
||||
checkDeveloperAccess,
|
||||
loadAppPermissions,
|
||||
setAppPermission,
|
||||
getAppPermission,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
clearPermissions,
|
||||
getNoPermissionTip,
|
||||
|
||||
// 工具
|
||||
accessibleAppIds,
|
||||
maskSensitiveValue,
|
||||
}
|
||||
}
|
||||
165
app/composables/useNotificationCenter.ts
Normal file
165
app/composables/useNotificationCenter.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
getUnreadCount,
|
||||
listRecentNotification,
|
||||
markRead,
|
||||
markAllRead,
|
||||
} from '@/api/app/notification'
|
||||
import type { Notification, UnreadCountResult, NotificationType } from '@/api/app/notification/model'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
/** 轮询间隔:30 秒 */
|
||||
const POLL_INTERVAL = 30_000
|
||||
|
||||
/** 全局未读数(跨组件共享) */
|
||||
const unreadTotal = ref(0)
|
||||
const unreadByType = ref<Partial<Record<NotificationType, number>>>({})
|
||||
const recentList = ref<Notification[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
/** 轮询定时器 */
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 防止重复轮询的计数器 */
|
||||
let pollRefCount = 0
|
||||
|
||||
/** 通知类型 → 显示名称 & 图标 */
|
||||
export const notificationTypeMap: Record<NotificationType, { label: string; icon: string; color: string }> = {
|
||||
ticket: { label: '工单通知', icon: '🎫', color: '#1890ff' },
|
||||
review: { label: '审核通知', icon: '✅', color: '#52c41a' },
|
||||
system: { label: '系统通知', icon: '📢', color: '#faad14' },
|
||||
resource: { label: '资源通知', icon: '🖥️', color: '#722ed1' },
|
||||
permission: { label: '权限通知', icon: '🔐', color: '#eb2f96' },
|
||||
member: { label: '成员通知', icon: '👥', color: '#13c2c2' },
|
||||
payment: { label: '账单通知', icon: '💳', color: '#fa8c16' },
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知中心 composable(全局共享状态)
|
||||
*/
|
||||
export function useNotificationCenter() {
|
||||
// ---------- 加载未读数 ----------
|
||||
async function fetchUnreadCount() {
|
||||
try {
|
||||
const data = await getUnreadCount()
|
||||
unreadTotal.value = data.total ?? 0
|
||||
unreadByType.value = { ...data } as Partial<Record<NotificationType, number>>
|
||||
;(unreadByType.value as Record<string, unknown>).total = undefined // 去掉 total
|
||||
} catch {
|
||||
// 静默失败,不打扰用户
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 加载最近通知 ----------
|
||||
async function fetchRecentNotifications(limit = 10) {
|
||||
loading.value = true
|
||||
try {
|
||||
recentList.value = await listRecentNotification({ limit })
|
||||
} catch {
|
||||
// 静默失败
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 标记单条已读 ----------
|
||||
async function markNotificationRead(id: number) {
|
||||
try {
|
||||
await markRead(id)
|
||||
// 乐观更新
|
||||
const item = recentList.value.find((n) => n.id === id)
|
||||
if (item) item.isRead = 1
|
||||
if (unreadTotal.value > 0) unreadTotal.value--
|
||||
} catch {
|
||||
message.error('标记失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 全部已读 ----------
|
||||
async function markAllAsRead(type?: string) {
|
||||
try {
|
||||
await markAllRead(type ? { type } : undefined)
|
||||
recentList.value.forEach((n) => {
|
||||
if (!type || n.type === type) n.isRead = 1
|
||||
})
|
||||
if (type) {
|
||||
unreadTotal.value -= (unreadByType.value[type as NotificationType] ?? 0)
|
||||
unreadByType.value[type as NotificationType] = 0
|
||||
} else {
|
||||
unreadTotal.value = 0
|
||||
Object.keys(unreadByType.value).forEach((k) => {
|
||||
unreadByType.value[k as NotificationType] = 0
|
||||
})
|
||||
}
|
||||
if (unreadTotal.value < 0) unreadTotal.value = 0
|
||||
message.success('已全部标记为已读')
|
||||
} catch {
|
||||
message.error('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 启动 / 停止轮询 ----------
|
||||
function startPolling() {
|
||||
pollRefCount++
|
||||
if (pollTimer) return
|
||||
// 立即拉取一次
|
||||
fetchUnreadCount()
|
||||
pollTimer = setInterval(fetchUnreadCount, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
pollRefCount = Math.max(0, pollRefCount - 1)
|
||||
if (pollRefCount === 0 && pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 时间格式化 ----------
|
||||
function formatTime(time?: string): string {
|
||||
if (!time) return ''
|
||||
const date = new Date(time)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffSec = Math.floor(diffMs / 1000)
|
||||
const diffMin = Math.floor(diffSec / 60)
|
||||
const diffHour = Math.floor(diffMin / 60)
|
||||
const diffDay = Math.floor(diffHour / 24)
|
||||
|
||||
if (diffSec < 60) return '刚刚'
|
||||
if (diffMin < 60) return `${diffMin} 分钟前`
|
||||
if (diffHour < 24) return `${diffHour} 小时前`
|
||||
if (diffDay < 7) return `${diffDay} 天前`
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// ---------- 获取通知跳转链接 ----------
|
||||
function getNotificationLink(n: Notification): string {
|
||||
if (n.linkUrl) return n.linkUrl
|
||||
switch (n.refType) {
|
||||
case 'ticket':
|
||||
return `/console/tickets`
|
||||
case 'permission_request':
|
||||
return `/developer/requests`
|
||||
default:
|
||||
return '/console/notifications'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
unreadTotal,
|
||||
unreadByType,
|
||||
recentList,
|
||||
loading,
|
||||
// 方法
|
||||
fetchUnreadCount,
|
||||
fetchRecentNotifications,
|
||||
markNotificationRead,
|
||||
markAllAsRead,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
// 工具
|
||||
formatTime,
|
||||
getNotificationLink,
|
||||
}
|
||||
}
|
||||
37
app/composables/usePageSeo.ts
Normal file
37
app/composables/usePageSeo.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useHead, useRequestURL, useSeoMeta } from '#app'
|
||||
|
||||
type SeoInput = {
|
||||
title: string
|
||||
description: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
function getSiteOrigin() {
|
||||
if (import.meta.client) return window.location.origin
|
||||
try {
|
||||
return useRequestURL().origin
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function usePageSeo(input: SeoInput) {
|
||||
const origin = getSiteOrigin()
|
||||
const url = input.path && origin ? new URL(input.path, origin).toString() : undefined
|
||||
|
||||
useSeoMeta({
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
ogTitle: input.title,
|
||||
ogDescription: input.description,
|
||||
ogType: 'website',
|
||||
...(url ? { ogUrl: url } : {}),
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
|
||||
if (url) {
|
||||
useHead({
|
||||
link: [{ rel: 'canonical', href: url }]
|
||||
})
|
||||
}
|
||||
}
|
||||
33
app/composables/useQRCode.ts
Normal file
33
app/composables/useQRCode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
/**
|
||||
* 将 URL 转换为二维码图片的 data URL
|
||||
*/
|
||||
export async function generateQrCodeDataUrl(url: string, options?: QRCode.QRCodeToDataURLOptions): Promise<string> {
|
||||
const defaultOptions: QRCode.QRCodeToDataURLOptions = {
|
||||
width: 280,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff'
|
||||
}
|
||||
}
|
||||
|
||||
return QRCode.toDataURL(url, { ...defaultOptions, ...options })
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 URL 转换为二维码图片的 Canvas 元素
|
||||
*/
|
||||
export async function generateQrCodeCanvas(url: string, canvas: HTMLCanvasElement, options?: QRCode.QRCodeToCanvasOptions): Promise<void> {
|
||||
const defaultOptions: QRCode.QRCodeToCanvasOptions = {
|
||||
width: 280,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff'
|
||||
}
|
||||
}
|
||||
|
||||
return QRCode.toCanvas(canvas, url, { ...defaultOptions, ...options })
|
||||
}
|
||||
134
app/composables/useResourceAccess.ts
Normal file
134
app/composables/useResourceAccess.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 资源中心协作权限 composable
|
||||
*
|
||||
* 权限级别:
|
||||
* 0 - 无权限
|
||||
* 1 - 基础查看:名称/IP/端口/状态(所有团队成员)
|
||||
* 2 - 连接查看:用户名、Host、连接方式(技术负责人及以上)
|
||||
* 3 - 完全权限:密码、私钥、编辑/删除(仅资源 Owner)
|
||||
*/
|
||||
import type { AppResource } from '@/api/app/appResource/model'
|
||||
|
||||
export type ResourceAccessLevel = 0 | 1 | 2 | 3
|
||||
|
||||
/**
|
||||
* 判断当前用户是否是资源所有者
|
||||
* 后端在返回时会带 ownerUserId 字段,前端基于 localStorage UserId 判断
|
||||
*/
|
||||
export function isResourceOwner(resource: AppResource): boolean {
|
||||
if (!import.meta.client) return false
|
||||
const currentUserId = localStorage.getItem('UserId')
|
||||
if (!currentUserId) return false
|
||||
return Number(currentUserId) === Number(resource.ownerUserId)
|
||||
|| Number(currentUserId) === Number(resource.userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户对某资源的访问级别
|
||||
* - 后端若已计算 accessLevel,直接使用
|
||||
* - 否则前端保底逻辑:Owner=3,其余=1
|
||||
*/
|
||||
export function getResourceAccessLevel(resource: AppResource): ResourceAccessLevel {
|
||||
// 后端已计算的权限级别,优先使用
|
||||
if (resource.accessLevel !== undefined) {
|
||||
return resource.accessLevel
|
||||
}
|
||||
// 降级:判断是否是 owner
|
||||
return isResourceOwner(resource) ? 3 : 1
|
||||
}
|
||||
|
||||
/** 是否有基础查看权限(所有人) */
|
||||
export function canViewBasic(resource: AppResource): boolean {
|
||||
return getResourceAccessLevel(resource) >= 1
|
||||
}
|
||||
|
||||
/** 是否有连接信息查看权限(用户名、Host 等) */
|
||||
export function canViewConnection(resource: AppResource): boolean {
|
||||
return getResourceAccessLevel(resource) >= 2
|
||||
}
|
||||
|
||||
/** 是否有完整权限(编辑、删除、查看密码/私钥) */
|
||||
export function canViewSensitive(resource: AppResource): boolean {
|
||||
return getResourceAccessLevel(resource) >= 3
|
||||
}
|
||||
|
||||
/** 是否能编辑/删除该资源 */
|
||||
export function canManageResource(resource: AppResource): boolean {
|
||||
return getResourceAccessLevel(resource) >= 3
|
||||
}
|
||||
|
||||
/** 脱敏占位符:判断某字段是否已被后端脱敏 */
|
||||
export function isMaskedValue(value: string | null | undefined): boolean {
|
||||
if (!value) return false
|
||||
return value === '******' || value === '***' || /^\*{3,}$/.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源权限 composable(reactive,接受 computed/ref 的资源对象)
|
||||
*/
|
||||
export function useResourceAccess(resource: AppResource | (() => AppResource | null | undefined)) {
|
||||
const getResource = (): AppResource | null | undefined =>
|
||||
typeof resource === 'function' ? resource() : resource
|
||||
|
||||
const accessLevel = computed<ResourceAccessLevel>(() => {
|
||||
const r = getResource()
|
||||
if (!r) return 0
|
||||
return getResourceAccessLevel(r)
|
||||
})
|
||||
|
||||
const isOwner = computed(() => {
|
||||
const r = getResource()
|
||||
if (!r) return false
|
||||
return isResourceOwner(r)
|
||||
})
|
||||
|
||||
const canBasic = computed(() => accessLevel.value >= 1)
|
||||
const canConnection = computed(() => accessLevel.value >= 2)
|
||||
const canSensitive = computed(() => accessLevel.value >= 3)
|
||||
const canManage = computed(() => accessLevel.value >= 3)
|
||||
|
||||
/** 权限级别对应的文字说明 */
|
||||
const accessLevelText = computed(() => {
|
||||
switch (accessLevel.value) {
|
||||
case 3: return '完全权限'
|
||||
case 2: return '连接权限'
|
||||
case 1: return '查看权限'
|
||||
default: return '无权限'
|
||||
}
|
||||
})
|
||||
|
||||
/** 权限不足时的提示文字 */
|
||||
const noPermissionTip = computed(() =>
|
||||
isOwner.value ? '' : '如需查看完整信息,请联系资源创建者',
|
||||
)
|
||||
|
||||
return {
|
||||
accessLevel,
|
||||
isOwner,
|
||||
canBasic,
|
||||
canConnection,
|
||||
canSensitive,
|
||||
canManage,
|
||||
accessLevelText,
|
||||
noPermissionTip,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理资源列表,为每个资源附加 isOwner 字段
|
||||
* 调用时机:后端未返回 accessLevel 时的前端降级处理
|
||||
*/
|
||||
export function enrichResourcesWithPermission<T extends AppResource>(resources: T[]): T[] {
|
||||
if (!import.meta.client) return resources
|
||||
const currentUserId = localStorage.getItem('UserId')
|
||||
if (!currentUserId) return resources
|
||||
return resources.map(r => ({
|
||||
...r,
|
||||
isOwner: Number(currentUserId) === Number(r.ownerUserId) || Number(currentUserId) === Number(r.userId),
|
||||
accessLevel: r.accessLevel ?? (
|
||||
(Number(currentUserId) === Number(r.ownerUserId) || Number(currentUserId) === Number(r.userId))
|
||||
? 3
|
||||
: 1
|
||||
),
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user