- 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)
316 lines
8.3 KiB
Vue
316 lines
8.3 KiB
Vue
<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>
|