Administrator 8117958bd6 feat: add user center with RBAC, OAuth2 multi-mode and collapsible sidebar
- Add user management with roles and permissions (RBAC)
- Implement OAuth2 service provider supporting 4 grant types:
  authorization_code, password, client_credentials, refresh_token
- Add JWT authentication with 7-day expiry
- Add admin API for users, roles and OAuth clients management
- Add CLI tool for user management (scripts/user-cli.js)
- Add collapsible sidebar layout with login dialog
- Add user management page and OAuth client management page
- Add server middleware for auth token verification
- Add seed script for initial data (admin/admin123)
2026-03-19 17:19:57 +08:00

316 lines
8.3 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="users-container">
<el-card>
<template #header>
<div class="card-header">
<span>用户管理</span>
<el-button type="primary" @click="showAddDialog = true">添加用户</el-button>
</div>
</template>
<el-form :inline="true" class="filter-form">
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable @change="loadUsers">
<el-option label="正常" value="active" />
<el-option label="停用" value="inactive" />
<el-option label="封禁" value="banned" />
</el-select>
</el-form-item>
</el-form>
<el-table :data="users" border stripe v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="realName" label="姓名" />
<el-table-column prop="roleName" label="角色" width="120">
<template #default="{ row }">
<el-tag :type="row.roleName === 'admin' ? 'danger' : 'info'">
{{ row.roleName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastLogin" label="最后登录" width="180">
<template #default="{ row }">
{{ row.lastLogin ? new Date(row.lastLogin).toLocaleString('zh-CN') : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editUser(row)">编辑</el-button>
<el-button
type="danger"
size="small"
@click="deleteUser(row)"
:disabled="row.roleName === 'admin'"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="showAddDialog" title="添加用户" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
<div class="form-tip">密码至少6位不能为纯数字</div>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱(可选)" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="form.realName" placeholder="请输入姓名(可选)" />
</el-form-item>
<el-form-item label="角色" prop="roleId">
<el-select v-model="form.roleId" placeholder="请选择角色">
<el-option
v-for="role in roles"
:key="role.id"
:label="role.description || role.name"
:value="role.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDialog = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default'
})
const users = ref<any[]>([])
const roles = ref<any[]>([])
const loading = ref(false)
const showAddDialog = ref(false)
const filters = ref({ status: '' })
const form = ref({
id: null as number | null,
username: '',
password: '',
email: '',
realName: '',
roleId: null as number | null
})
const formRef = ref()
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名2-20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6位', trigger: 'blur' }
],
roleId: [
{ required: true, message: '请选择角色', trigger: 'change' }
]
}
const getStatusType = (status: string) => {
const types: Record<string, string> = {
active: 'success',
inactive: 'warning',
banned: 'danger'
}
return types[status] || 'info'
}
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
active: '正常',
inactive: '停用',
banned: '封禁'
}
return texts[status] || status
}
const loadUsers = async () => {
const token = localStorage.getItem('token')
if (!token) {
ElMessage.warning('请先登录')
navigateTo('/')
return
}
loading.value = true
try {
const params = new URLSearchParams()
if (filters.value.status) params.append('status', filters.value.status)
const res = await $fetch(`/api/admin/users?${params}`, {
headers: { Authorization: `Bearer ${token}` }
})
users.value = res.data
} catch (error: any) {
ElMessage.error(error.data?.message || '加载用户失败')
} finally {
loading.value = false
}
}
const loadRoles = async () => {
const token = localStorage.getItem('token')
if (!token) return
try {
const res = await $fetch('/api/admin/roles', {
headers: { Authorization: `Bearer ${token}` }
})
roles.value = res.data.roles
} catch (error) {
console.error('Failed to load roles:', error)
}
}
const editUser = (user: any) => {
form.value = {
id: user.id,
username: user.username,
password: '',
email: user.email || '',
realName: user.realName || '',
roleId: user.roleId
}
showAddDialog.value = true
}
const deleteUser = async (user: any) => {
const token = localStorage.getItem('token')
if (!token) return
try {
await ElMessageBox.confirm(`确定删除用户 "${user.username}" 吗?`, '提示', {
type: 'warning'
})
await $fetch(`/api/admin/users/${user.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
})
ElMessage.success('删除成功')
loadUsers()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.data?.message || '删除失败')
}
}
}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
const token = localStorage.getItem('token')
if (!token) return
try {
if (form.value.id) {
await $fetch(`/api/admin/users/${form.value.id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
body: {
email: form.value.email,
realName: form.value.realName,
roleId: form.value.roleId
}
})
ElMessage.success('更新成功')
} else {
await $fetch('/api/admin/users', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: form.value
})
ElMessage.success('添加成功')
}
showAddDialog.value = false
resetForm()
loadUsers()
} catch (error: any) {
ElMessage.error(error.data?.message || '操作失败')
}
})
}
const resetForm = () => {
form.value = {
id: null,
username: '',
password: '',
email: '',
realName: '',
roleId: null
}
formRef.value?.resetFields()
}
onMounted(() => {
loadUsers()
loadRoles()
})
</script>
<style scoped>
.users-container {
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-form {
margin-bottom: 20px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
@media (max-width: 900px) {
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.filter-form :deep(.el-form-item) {
margin-right: 0;
width: 100%;
}
.filter-form :deep(.el-select) {
width: 100%;
}
}
</style>