Browse Source

feat(page): add admin user list with role guard

main
Mohan 1 month ago
parent
commit
77df158026
  1. 8
      src/router/index.ts
  2. 11
      src/stores/auth.ts
  3. 1
      src/views/admin/AdminLayout.vue
  4. 165
      src/views/admin/UserListView.vue

8
src/router/index.ts

@ -59,6 +59,12 @@ const router = createRouter({
path: 'tags', path: 'tags',
name: 'AdminTagList', name: 'AdminTagList',
component: () => import('@/views/admin/TagListView.vue') component: () => import('@/views/admin/TagListView.vue')
},
{
path: 'users',
name: 'AdminUserList',
component: () => import('@/views/admin/UserListView.vue'),
meta: { requiresAdmin: true }
} }
] ]
} }
@ -72,6 +78,8 @@ router.beforeEach((to, _from, next) => {
next('/login') next('/login')
} else if (to.meta.guestOnly && auth.isLoggedIn) { } else if (to.meta.guestOnly && auth.isLoggedIn) {
next('/admin') next('/admin')
} else if (to.meta.requiresAdmin && !auth.isAdmin) {
next('/admin/articles')
} else { } else {
next() next()
} }

11
src/stores/auth.ts

@ -8,6 +8,16 @@ export const useAuthStore = defineStore('auth', () => {
const refreshToken = ref<string | null>(localStorage.getItem('refreshToken')) const refreshToken = ref<string | null>(localStorage.getItem('refreshToken'))
const username = ref<string | null>(localStorage.getItem('username')) const username = ref<string | null>(localStorage.getItem('username'))
const isLoggedIn = computed(() => !!token.value) const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => {
if (!token.value) return false
try {
const payload = JSON.parse(atob(token.value.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
const roles: string[] = payload.roles ?? []
return roles.includes('ADMIN')
} catch {
return false
}
})
async function register(request: RegisterRequest) { async function register(request: RegisterRequest) {
const resp = await authApi.apiAuthRegisterPost(request) const resp = await authApi.apiAuthRegisterPost(request)
@ -54,6 +64,7 @@ export const useAuthStore = defineStore('auth', () => {
refreshToken, refreshToken,
username, username,
isLoggedIn, isLoggedIn,
isAdmin,
register, register,
login, login,
logout, logout,

1
src/views/admin/AdminLayout.vue

@ -22,6 +22,7 @@ async function handleLogout() {
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<router-link to="/admin/articles">文章管理</router-link> <router-link to="/admin/articles">文章管理</router-link>
<router-link to="/admin/tags">标签管理</router-link> <router-link to="/admin/tags">标签管理</router-link>
<router-link v-if="auth.isAdmin" to="/admin/users">用户管理</router-link>
</nav> </nav>
</aside> </aside>
<div class="content-wrap"> <div class="content-wrap">

165
src/views/admin/UserListView.vue

@ -0,0 +1,165 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/client'
import type { UserInfo } from '@/generated/api'
const users = ref<UserInfo[]>([])
const page = ref(1)
const size = 10
const total = ref(0)
const loading = ref(false)
const error = ref<string | null>(null)
function roleLabel(role: string): string {
switch (role) {
case 'ADMIN':
return '管理员'
case 'EDITOR':
return '编辑'
case 'VISITOR':
return '访客'
default:
return role
}
}
function formatDate(iso: string): string {
const d = new Date(iso)
if (isNaN(d.getTime())) return iso
return d.toLocaleString('zh-CN', { hour12: false })
}
async function load() {
loading.value = true
error.value = null
try {
const resp = await userApi.apiAdminUsersGet(page.value, size)
const data = resp.data.data
users.value = data?.list as UserInfo[] ?? []
total.value = Number(data?.total ?? 0)
} catch (e) {
console.error(e)
error.value = '用户列表加载失败'
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<div>
<h1 class="page-title">用户管理</h1>
<p v-if="error" class="error-text">{{ error }}</p>
<p v-else-if="loading" class="hint-text">加载中...</p>
<p v-else-if="users.length === 0" class="hint-text">暂无用户</p>
<table v-else class="user-table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email || '-' }}</td>
<td>
<span class="role-badge" :class="user.role.toLowerCase()">{{ roleLabel(user.role) }}</span>
</td>
<td>
<span class="status-badge" :class="user.enabled ? 'enabled' : 'disabled'">
{{ user.enabled ? '启用' : '禁用' }}
</span>
</td>
<td>{{ formatDate(user.createdAt) }}</td>
</tr>
</tbody>
</table>
<p class="total-hint"> {{ total }} 位用户</p>
</div>
</template>
<style scoped>
.page-title {
margin: 0 0 16px;
font-size: 20px;
}
.user-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #e5e5ea;
border-radius: 8px;
overflow: hidden;
}
.user-table th,
.user-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.user-table th {
background: #f7f8fa;
color: #5c6470;
}
.role-badge {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
}
.role-badge.admin {
background: #f8d7da;
color: #8f1f2b;
}
.role-badge.editor {
background: #cce5ff;
color: #004a99;
}
.role-badge.visitor {
background: #e9ecef;
color: #6c757d;
}
.status-badge {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
}
.status-badge.enabled {
background: #d4edda;
color: #1e7e34;
}
.status-badge.disabled {
background: #e9ecef;
color: #6c757d;
}
.total-hint {
color: #8a93a3;
font-size: 13px;
}
.error-text {
color: #d33c3c;
}
.hint-text {
color: #8a93a3;
}
</style>
Loading…
Cancel
Save