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
+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>