feat(admin): 优化用户权限分级·统一头像·数据分析面板修复
- fix(DataAnalysisPanel): 活跃用户卡片固定 400px + overflow-y 独立滚动容器,底部渐变遮罩提示滚动;列表项 tooltip 增加 max-height 与 overflow 截断;新增圆形头像显示与 @error 回退首字母 - feat(admin): 新增 getUserAvatarUrl 统一头像策略链(OAuth identity→Gravatar→DiceBear 7.x initials),stats/realtime、active-users、users/index 三处接口返回 avatarUrl - fix(admin): 建立 SUPER_ADMIN(100)>ADMIN(80)>USER(10) 分级常量与 canChangeRoleOf/canResetPasswordFor 等谓词;reset-password、[id].put、index.post 全面应用级别守卫;前端 UserManager 按钮按权限禁用、availableRoles 按操作者级别过滤 SUPER_ADMIN/ADMIN 等高角色
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
interface AvatarUserState {
|
||||
avatarProvider?: string | null
|
||||
avatarProviderUserId?: string | null
|
||||
email?: string | null
|
||||
username?: string | null
|
||||
name?: string | null
|
||||
id?: string | number | null
|
||||
}
|
||||
|
||||
interface IdentityRow {
|
||||
@@ -8,6 +12,7 @@ interface IdentityRow {
|
||||
providerUserId: string
|
||||
avatar?: string | null
|
||||
createdAt: Date | string
|
||||
providerUsername?: string | null
|
||||
}
|
||||
|
||||
export interface AvatarSource {
|
||||
@@ -43,3 +48,75 @@ export function resolveAvatarSource(
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function md5Hash(str: string): string {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(8, '0') + Math.abs(hash * 7).toString(16).padStart(8, '0') +
|
||||
Math.abs(hash * 13).toString(16).padStart(8, '0') + Math.abs(hash * 17).toString(16).padStart(8, '0')
|
||||
}
|
||||
|
||||
export function generateGravatarUrl(email?: string | null): string | null {
|
||||
if (!email) return null
|
||||
const normalizedEmail = email.trim().toLowerCase()
|
||||
const hash = md5Hash(normalizedEmail)
|
||||
return `https://www.gravatar.com/avatar/${hash}?d=404&s=128`
|
||||
}
|
||||
|
||||
export function generateDiceBearUrl(seed: string): string {
|
||||
const encodedSeed = encodeURIComponent(seed || 'user')
|
||||
return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedSeed}&backgroundType=gradientLinear&fontSize=40&fontWeight=700`
|
||||
}
|
||||
|
||||
export function buildAvatarFromSource(source: AvatarSource | null, identities: IdentityRow[] = []): string | null {
|
||||
if (!source || !source.provider) return null
|
||||
|
||||
const identity = identities.find(
|
||||
(id) => id.provider === source.provider && id.providerUserId === source.providerUserId
|
||||
)
|
||||
|
||||
if (identity?.avatar) {
|
||||
return identity.avatar
|
||||
}
|
||||
|
||||
switch (source.provider.toLowerCase()) {
|
||||
case 'github':
|
||||
if (identity?.providerUsername) {
|
||||
return `https://github.com/${identity.providerUsername}.png?size=128`
|
||||
}
|
||||
if (source.providerUserId) {
|
||||
return `https://avatars.githubusercontent.com/u/${source.providerUserId}?size=128`
|
||||
}
|
||||
return null
|
||||
case 'google':
|
||||
return null
|
||||
case 'casdoor':
|
||||
return null
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserAvatarUrl(
|
||||
user: AvatarUserState,
|
||||
identities: IdentityRow[] = []
|
||||
): string {
|
||||
try {
|
||||
const source = resolveAvatarSource(user, identities)
|
||||
const sourceAvatar = source ? buildAvatarFromSource(source, identities) : null
|
||||
if (sourceAvatar) return sourceAvatar
|
||||
|
||||
const gravatar = generateGravatarUrl(user.email)
|
||||
if (gravatar) return gravatar
|
||||
|
||||
const seed = (user.name || user.username || user.id?.toString() || 'User')
|
||||
return generateDiceBearUrl(seed)
|
||||
} catch {
|
||||
const seed = (user?.name || user?.username || user?.id?.toString() || 'User')
|
||||
return generateDiceBearUrl(seed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user