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:
@@ -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-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>
|
||||
|
||||
Reference in New Issue
Block a user