feat: user management - add/edit/delete users

Backend:
- Add GET /users/roles endpoint (list all roles)
- Add GET /users/depts endpoint (list all departments)

Frontend:
- Add user.ts API client (getUsers, createUser, updateUser, deleteUser, getRoles, getDepts)
- Rewrite System.vue with full user CRUD:
  - User list table with pagination
  - Add User button + dialog form
  - Edit user (username disabled, password hidden)
  - Delete user (disabled for superadmin)
  - Role and department dropdowns
  - Status radio buttons
This commit is contained in:
hiderfong
2026-04-23 11:32:45 +08:00
parent 9d38180745
commit 377e9cba22
3 changed files with 292 additions and 10 deletions
+20 -1
View File
@@ -4,7 +4,8 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate, UserOut
from app.models.user import Role, Dept
from app.schemas.user import UserCreate, UserUpdate, UserOut, RoleOut, DeptOut
from app.schemas.common import ResponseModel, ListResponse, PageParams
from app.services import user_service
from app.api.deps import get_current_user
@@ -62,3 +63,21 @@ def delete_user(
):
user_service.delete_user(db, user_id)
return ResponseModel(message="删除成功")
@router.get("/roles", response_model=ResponseModel[list[RoleOut]])
def list_roles(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
items = db.query(Role).order_by(Role.id).all()
return ResponseModel(data=[RoleOut.model_validate(r) for r in items])
@router.get("/depts", response_model=ResponseModel[list[DeptOut]])
def list_depts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
items = db.query(Dept).order_by(Dept.sort_order).all()
return ResponseModel(data=[DeptOut.model_validate(d) for d in items])
+62
View File
@@ -0,0 +1,62 @@
import request from './request'
export interface UserItem {
id: number
username: string
email?: string
real_name?: string
phone?: string
dept_id?: number
is_active: boolean
is_superuser: boolean
dept?: { id: number; name: string }
roles: { id: number; name: string; code: string }[]
created_at: string
}
export interface RoleItem {
id: number
name: string
code: string
description?: string
}
export interface DeptItem {
id: number
name: string
parent_id?: number
sort_order: number
}
export function getUsers(params?: { page?: number; page_size?: number; keyword?: string }) {
return request.get('/users', { params })
}
export function createUser(data: {
username: string
password: string
email?: string
real_name?: string
phone?: string
dept_id?: number
is_active?: boolean
role_ids?: number[]
}) {
return request.post('/users', data)
}
export function updateUser(id: number, data: Partial<UserItem> & { role_ids?: number[] }) {
return request.put(`/users/${id}`, data)
}
export function deleteUser(id: number) {
return request.delete(`/users/${id}`)
}
export function getRoles() {
return request.get('/users/roles')
}
export function getDepts() {
return request.get('/users/depts')
}
+210 -9
View File
@@ -7,9 +7,12 @@
<div class="table-card card-shadow">
<div class="table-header">
<el-input v-model="userKeyword" placeholder="搜索用户" clearable style="width: 220px" />
<el-button type="primary" size="small" @click="fetchUsers">
<el-button type="primary" size="small" @click="handleSearch">
<el-icon><Search /></el-icon>查询
</el-button>
<el-button type="success" size="small" @click="openAdd">
<el-icon><Plus /></el-icon>新增用户
</el-button>
</div>
<el-table :data="userList" v-loading="userLoading" stripe size="default">
<el-table-column prop="username" label="用户名" min-width="120" />
@@ -34,7 +37,24 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" size="small" :disabled="row.is_superuser" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-bar">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
@change="fetchUsers"
/>
</div>
</div>
</el-tab-pane>
@@ -44,27 +64,101 @@
</div>
</el-tab-pane>
</el-tabs>
<!-- User Form Dialog -->
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="520px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" :disabled="isEdit" placeholder="请输入用户名" />
</el-form-item>
<el-form-item v-if="!isEdit" label="密码" prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="真实姓名" prop="real_name">
<el-input v-model="form.real_name" placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="部门" prop="dept_id">
<el-select v-model="form.dept_id" placeholder="选择部门" clearable style="width: 100%">
<el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
</el-form-item>
<el-form-item label="角色" prop="role_ids">
<el-select v-model="form.role_ids" multiple placeholder="选择角色" style="width: 100%">
<el-option v-for="r in roles" :key="r.id" :label="r.name" :value="r.id" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="is_active">
<el-radio-group v-model="form.is_active">
<el-radio :label="true">正常</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus } from '@element-plus/icons-vue'
import { getUsers, createUser, updateUser, deleteUser } from '@/api/user'
import { getRoles, getDepts } from '@/api/user'
import type { UserItem, RoleItem, DeptItem } from '@/api/user'
const activeTab = ref('users')
const userList = ref<any[]>([])
const userList = ref<UserItem[]>([])
const userLoading = ref(false)
const userKeyword = ref('')
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const dialogVisible = ref(false)
const isEdit = ref(false)
const submitting = ref(false)
const formRef = ref()
const roles = ref<RoleItem[]>([])
const depts = ref<DeptItem[]>([])
const form = ref({
id: undefined as number | undefined,
username: '',
password: '',
real_name: '',
email: '',
phone: '',
dept_id: undefined as number | undefined,
role_ids: [] as number[],
is_active: true,
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
real_name: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
}
async function fetchUsers() {
userLoading.value = true
try {
const res = await fetch(`/api/v1/users?keyword=${encodeURIComponent(userKeyword.value)}`, {
headers: { Authorization: `Bearer ${localStorage.getItem('dp_token') || ''}` },
const res: any = await getUsers({
page: page.value,
page_size: pageSize.value,
keyword: userKeyword.value || undefined,
})
const data = await res.json()
userList.value = data.data || []
userList.value = res.data || []
total.value = res.total || 0
} catch (e: any) {
ElMessage.error(e?.message || '加载失败')
} finally {
@@ -72,7 +166,108 @@ async function fetchUsers() {
}
}
onMounted(fetchUsers)
async function fetchMeta() {
try {
const [rRes, dRes] = await Promise.all([getRoles(), getDepts()])
roles.value = (rRes as any)?.data || []
depts.value = (dRes as any)?.data || []
} catch (e) {
// ignore
}
}
function handleSearch() {
page.value = 1
fetchUsers()
}
function resetForm() {
form.value = {
id: undefined,
username: '',
password: '',
real_name: '',
email: '',
phone: '',
dept_id: undefined,
role_ids: [],
is_active: true,
}
}
function openAdd() {
resetForm()
isEdit.value = false
dialogVisible.value = true
}
function openEdit(row: UserItem) {
resetForm()
isEdit.value = true
form.value.id = row.id
form.value.username = row.username
form.value.real_name = row.real_name || ''
form.value.email = row.email || ''
form.value.phone = row.phone || ''
form.value.dept_id = row.dept_id
form.value.role_ids = row.roles.map(r => r.id)
form.value.is_active = row.is_active
dialogVisible.value = true
}
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
if (isEdit.value && form.value.id) {
await updateUser(form.value.id, {
real_name: form.value.real_name,
email: form.value.email,
phone: form.value.phone,
dept_id: form.value.dept_id,
is_active: form.value.is_active,
role_ids: form.value.role_ids,
})
ElMessage.success('更新成功')
} else {
await createUser({
username: form.value.username,
password: form.value.password,
real_name: form.value.real_name,
email: form.value.email,
phone: form.value.phone,
dept_id: form.value.dept_id,
is_active: form.value.is_active,
role_ids: form.value.role_ids,
})
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchUsers()
} catch (e: any) {
ElMessage.error(e?.message || '操作失败')
} finally {
submitting.value = false
}
}
async function handleDelete(row: UserItem) {
try {
await ElMessageBox.confirm(`确认删除用户 ${row.username}`, '提示', { type: 'warning' })
await deleteUser(row.id)
ElMessage.success('删除成功')
fetchUsers()
} catch (e: any) {
if (e !== 'cancel') ElMessage.error(e?.message || '删除失败')
}
}
onMounted(() => {
fetchUsers()
fetchMeta()
})
</script>
<style scoped lang="scss">
@@ -99,4 +294,10 @@ onMounted(fetchUsers)
margin-bottom: 16px;
}
}
.pagination-bar {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>