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:
@@ -4,7 +4,8 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.models.user import User
|
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.schemas.common import ResponseModel, ListResponse, PageParams
|
||||||
from app.services import user_service
|
from app.services import user_service
|
||||||
from app.api.deps import get_current_user
|
from app.api.deps import get_current_user
|
||||||
@@ -62,3 +63,21 @@ def delete_user(
|
|||||||
):
|
):
|
||||||
user_service.delete_user(db, user_id)
|
user_service.delete_user(db, user_id)
|
||||||
return ResponseModel(message="删除成功")
|
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])
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -7,9 +7,12 @@
|
|||||||
<div class="table-card card-shadow">
|
<div class="table-card card-shadow">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<el-input v-model="userKeyword" placeholder="搜索用户" clearable style="width: 220px" />
|
<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-icon><Search /></el-icon>查询
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="success" size="small" @click="openAdd">
|
||||||
|
<el-icon><Plus /></el-icon>新增用户
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="userList" v-loading="userLoading" stripe size="default">
|
<el-table :data="userList" v-loading="userLoading" stripe size="default">
|
||||||
<el-table-column prop="username" label="用户名" min-width="120" />
|
<el-table-column prop="username" label="用户名" min-width="120" />
|
||||||
@@ -34,7 +37,24 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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>
|
</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>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
@@ -44,27 +64,101 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
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 activeTab = ref('users')
|
||||||
const userList = ref<any[]>([])
|
const userList = ref<UserItem[]>([])
|
||||||
const userLoading = ref(false)
|
const userLoading = ref(false)
|
||||||
const userKeyword = ref('')
|
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() {
|
async function fetchUsers() {
|
||||||
userLoading.value = true
|
userLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/v1/users?keyword=${encodeURIComponent(userKeyword.value)}`, {
|
const res: any = await getUsers({
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('dp_token') || ''}` },
|
page: page.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
keyword: userKeyword.value || undefined,
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
userList.value = res.data || []
|
||||||
userList.value = data.data || []
|
total.value = res.total || 0
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.message || '加载失败')
|
ElMessage.error(e?.message || '加载失败')
|
||||||
} finally {
|
} 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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -99,4 +294,10 @@ onMounted(fetchUsers)
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user