up
This commit is contained in:
@@ -49,3 +49,12 @@
|
|||||||
2. 查询该组所有成员的 `TabUserGroupBinds` 记录
|
2. 查询该组所有成员的 `TabUserGroupBinds` 记录
|
||||||
3. 提取所有 `UserID` 更新到 `sysAdmins` 缓存切片
|
3. 提取所有 `UserID` 更新到 `sysAdmins` 缓存切片
|
||||||
4. 组不存在或查询失败时清空缓存
|
4. 组不存在或查询失败时清空缓存
|
||||||
|
|
||||||
|
## Web 前端系统管理员入口
|
||||||
|
|
||||||
|
- 后端 `getinfo` 接口返回 `isSysAdmin` 布尔值(不再暴露完整管理员列表)
|
||||||
|
- 后端新增 `POST /users/sysadmins` 接口,仅系统管理员可访问,返回完整 `sysAdmins` 数组
|
||||||
|
- 前端 `userStore`:`isSysAdmin` 改为 ref,直接从后端获取
|
||||||
|
- `AppHeader.vue` 用户菜单:当 `isSysAdmin` 为 true 时显示「系统管理」入口(琥珀色盾牌图标)
|
||||||
|
- 新建 `SysAdminView.vue`:4 个标签页占位符(用户管理、用户组、登录日志、系统配置),页面内调用 `authApi.sysAdmins()` 获取管理员列表
|
||||||
|
- 路由 `/sysadmin`:添加 `requireSysAdmin` 元信息,路由守卫拦截非管理员访问
|
||||||
|
|||||||
@@ -575,8 +575,6 @@ func ApiUser(r *gin.RouterGroup) {
|
|||||||
isAuth, user, _ := AuthenticationAuthority(ctx)
|
isAuth, user, _ := AuthenticationAuthority(ctx)
|
||||||
if isAuth {
|
if isAuth {
|
||||||
//载入用户info
|
//载入用户info
|
||||||
|
|
||||||
//fmt.Println(userInfo)
|
|
||||||
var redata map[string]interface{} = make(map[string]interface{})
|
var redata map[string]interface{} = make(map[string]interface{})
|
||||||
|
|
||||||
info := GetUserInfoFromUserID(user.ID)
|
info := GetUserInfoFromUserID(user.ID)
|
||||||
@@ -586,10 +584,47 @@ func ApiUser(r *gin.RouterGroup) {
|
|||||||
user.Salt = ""
|
user.Salt = ""
|
||||||
redata["user"] = user
|
redata["user"] = user
|
||||||
|
|
||||||
|
// 只返回当前用户是否为系统管理员,不暴露完整列表
|
||||||
|
isSysAdmin := false
|
||||||
|
for _, adminID := range sysAdmins {
|
||||||
|
if adminID == user.ID {
|
||||||
|
isSysAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redata["isSysAdmin"] = isSysAdmin
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", redata)
|
ReturnJson(ctx, "apiOK", redata)
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取系统管理员列表(仅系统管理员可访问)
|
||||||
|
r.POST("/sysadmins", func(ctx *gin.Context) {
|
||||||
|
isAuth, user, _ := AuthenticationAuthority(ctx)
|
||||||
|
if !isAuth {
|
||||||
|
ReturnJson(ctx, "userNoLogin", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为系统管理员
|
||||||
|
isSysAdmin := false
|
||||||
|
for _, adminID := range sysAdmins {
|
||||||
|
if adminID == user.ID {
|
||||||
|
isSysAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isSysAdmin {
|
||||||
|
ReturnJson(ctx, "permission_denied", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var redata map[string]interface{} = make(map[string]interface{})
|
||||||
|
redata["sysAdmins"] = sysAdmins
|
||||||
|
ReturnJson(ctx, "apiOK", redata)
|
||||||
|
})
|
||||||
|
|
||||||
//用户登陆
|
//用户登陆
|
||||||
r.POST("/login", func(ctx *gin.Context) {
|
r.POST("/login", func(ctx *gin.Context) {
|
||||||
var loginuser From_user_login
|
var loginuser From_user_login
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export const authApi = {
|
|||||||
return api.post('/users/getinfo', {})
|
return api.post('/users/getinfo', {})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 获取系统管理员列表(仅管理员可访问) */
|
||||||
|
sysAdmins() {
|
||||||
|
return api.post('/users/sysadmins', {})
|
||||||
|
},
|
||||||
|
|
||||||
/** 修改密码 */
|
/** 修改密码 */
|
||||||
changePassword(oldPass, newPass) {
|
changePassword(oldPass, newPass) {
|
||||||
return api.post('/users/changePassword', { oldpass: oldPass, newpass: newPass })
|
return api.post('/users/changePassword', { oldpass: oldPass, newpass: newPass })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
IconMenu2,
|
IconMenu2,
|
||||||
IconX,
|
IconX,
|
||||||
|
IconShield,
|
||||||
} from "@tabler/icons-vue";
|
} from "@tabler/icons-vue";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
@@ -149,6 +150,15 @@ const navItems = computed(() => [
|
|||||||
<IconSettings :size="16" />
|
<IconSettings :size="16" />
|
||||||
{{ t("message.user_settings") }}
|
{{ t("message.user_settings") }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
v-if="userStore.isSysAdmin"
|
||||||
|
to="/sysadmin"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-900/20"
|
||||||
|
@click="userDropdownOpen = false"
|
||||||
|
>
|
||||||
|
<IconShield :size="16" />
|
||||||
|
系统管理
|
||||||
|
</RouterLink>
|
||||||
<hr class="my-1 border-gray-200 dark:border-dk-muted" />
|
<hr class="my-1 border-gray-200 dark:border-dk-muted" />
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ const router = createRouter({
|
|||||||
name: 'admin',
|
name: 'admin',
|
||||||
component: () => import('@/views/AdminView.vue'),
|
component: () => import('@/views/AdminView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'sysadmin',
|
||||||
|
name: 'sysadmin',
|
||||||
|
component: () => import('@/views/SysAdminView.vue'),
|
||||||
|
meta: { requireSysAdmin: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -172,6 +178,11 @@ router.beforeEach((to) => {
|
|||||||
return { name: 'login', query: { redirect: to.fullPath } }
|
return { name: 'login', query: { redirect: to.fullPath } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 需要系统管理员权限
|
||||||
|
if (to.meta.requireSysAdmin && !userStore.isSysAdmin) {
|
||||||
|
return { name: 'home' }
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const userInfo = ref(null) // TabUserInfo_ 详情
|
const userInfo = ref(null) // TabUserInfo_ 详情
|
||||||
const userCookie = ref(null) // Cookie session
|
const userCookie = ref(null) // Cookie session
|
||||||
const isLoggedIn = ref(false)
|
const isLoggedIn = ref(false)
|
||||||
|
const sysAdmins = ref([]) // 系统管理员 ID 列表
|
||||||
|
|
||||||
// ── Getters ──
|
// ── Getters ──
|
||||||
const cookieValue = computed(() => userCookie.value?.Value ?? '')
|
const cookieValue = computed(() => userCookie.value?.Value ?? '')
|
||||||
@@ -61,6 +62,9 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return `${y}-${m}-${day}`
|
return `${y}-${m}-${day}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 是否系统管理员(后端直接返回)
|
||||||
|
const isSysAdmin = ref(false)
|
||||||
|
|
||||||
// ── Actions ──
|
// ── Actions ──
|
||||||
function login(cookie) {
|
function login(cookie) {
|
||||||
userCookie.value = cookie
|
userCookie.value = cookie
|
||||||
@@ -83,6 +87,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
userCookie.value = null
|
userCookie.value = null
|
||||||
user.value = null
|
user.value = null
|
||||||
userInfo.value = null
|
userInfo.value = null
|
||||||
|
isSysAdmin.value = false
|
||||||
isLoggedIn.value = false
|
isLoggedIn.value = false
|
||||||
removeStorage(STORAGE_KEY_COOKIE)
|
removeStorage(STORAGE_KEY_COOKIE)
|
||||||
}
|
}
|
||||||
@@ -93,6 +98,8 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
if (errCode === 0) {
|
if (errCode === 0) {
|
||||||
user.value = data.user ?? null
|
user.value = data.user ?? null
|
||||||
userInfo.value = data.userInfo ?? null
|
userInfo.value = data.userInfo ?? null
|
||||||
|
// 存储系统管理员状态
|
||||||
|
isSysAdmin.value = data.isSysAdmin === true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 拦截器已处理错误提示
|
// 拦截器已处理错误提示
|
||||||
@@ -117,6 +124,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
userInfo,
|
userInfo,
|
||||||
userCookie,
|
userCookie,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isSysAdmin,
|
||||||
cookieValue,
|
cookieValue,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
birthday,
|
birthday,
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { authApi } from '@/api/auth'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const activeTab = ref('users')
|
||||||
|
const sysAdmins = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'users', label: '用户管理' },
|
||||||
|
{ id: 'groups', label: '用户组' },
|
||||||
|
{ id: 'logs', label: '登录日志' },
|
||||||
|
{ id: 'config', label: '系统配置' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchSysAdmins() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await authApi.sysAdmins()
|
||||||
|
if (res.errCode === 0 && Array.isArray(res.data.sysAdmins)) {
|
||||||
|
sysAdmins.value = res.data.sysAdmins
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 错误已由拦截器处理
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSysAdmins()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 p-6 dark:bg-dk-base">
|
||||||
|
<div class="mx-auto max-w-6xl">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-dk-text">系统管理</h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-dk-subtle">
|
||||||
|
系统管理员专用页面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 rounded-lg bg-amber-100 px-3 py-1.5 dark:bg-amber-900/30">
|
||||||
|
<span class="text-amber-700 dark:text-amber-400">管理员: {{ userStore.user?.Username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="mb-6 border-b border-gray-200 dark:border-dk-muted">
|
||||||
|
<nav class="-mb-px flex gap-6">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
@click="activeTab = tab.id"
|
||||||
|
:class="[
|
||||||
|
'border-b-2 px-1 pb-3 text-sm font-medium transition-colors',
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-dk-subtle dark:hover:text-dk-text',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<!-- 用户管理 -->
|
||||||
|
<div v-if="activeTab === 'users'" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">用户管理</h2>
|
||||||
|
<div class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base">
|
||||||
|
<p class="text-gray-500 dark:text-dk-subtle">sysadmin_users_placeholder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户组 -->
|
||||||
|
<div v-if="activeTab === 'groups'" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">用户组管理</h2>
|
||||||
|
<div class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base">
|
||||||
|
<p class="text-gray-500 dark:text-dk-subtle">sysadmin_groups_placeholder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录日志 -->
|
||||||
|
<div v-if="activeTab === 'logs'" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">登录失败日志</h2>
|
||||||
|
<div class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base">
|
||||||
|
<p class="text-gray-500 dark:text-dk-subtle">sysadmin_logs_placeholder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统配置 -->
|
||||||
|
<div v-if="activeTab === 'config'" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-dk-text">系统配置</h2>
|
||||||
|
<div class="rounded-md bg-gray-50 p-8 text-center dark:bg-dk-base">
|
||||||
|
<p class="text-gray-500 dark:text-dk-subtle">sysadmin_config_placeholder</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SysAdmins List -->
|
||||||
|
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4 dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<h3 class="text-sm font-medium text-gray-700 dark:text-dk-subtle">当前系统管理员列表</h3>
|
||||||
|
<button
|
||||||
|
@click="fetchSysAdmins"
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
{{ loading ? '加载中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="adminId in sysAdmins"
|
||||||
|
:key="adminId"
|
||||||
|
class="rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
ID: {{ adminId }}
|
||||||
|
</span>
|
||||||
|
<span v-if="sysAdmins.length === 0" class="text-sm text-gray-400 dark:text-dk-muted">
|
||||||
|
暂无系统管理员
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user