add user management interface

This commit is contained in:
2026-03-18 15:25:17 +08:00
parent 257668f3f3
commit d64c098a36
5 changed files with 875 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
<script setup lang="ts">
import { api } from '@/utils/request'
import { useUserManageStore, type UserRecord } from '@/stores/user-manage'
import UserFormModal from '@/components/UserFormModal.vue'
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
} from '@ant-design/icons-vue'
const store = useUserManageStore()
// Detail drawer
const drawerVisible = ref(false)
const drawerLoading = ref(false)
const currentUser = ref<UserRecord | null>(null)
// Form modal
const modalOpen = ref(false)
const modalMode = ref<'create' | 'edit'>('create')
const editingUser = ref<UserRecord | null>(null)
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username' },
{ title: '邮箱', dataIndex: 'email' },
{ title: '状态', dataIndex: 'status', width: 100 },
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
{ title: '操作', key: 'action', width: 200 },
]
onMounted(() => {
store.fetchUsers()
})
function handleSearch() {
store.pagination.page = 1
store.fetchUsers()
}
function handleReset() {
store.resetFilters()
store.fetchUsers()
}
function handlePageChange(page: number, pageSize: number) {
store.pagination.page = page
store.pagination.per_page = pageSize
store.fetchUsers()
}
async function handleViewDetail(record: UserRecord) {
drawerVisible.value = true
drawerLoading.value = true
try {
currentUser.value = await api.get<UserRecord>(`/api/v1/users/${record.id}`)
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '获取详情失败'
message.error(msg)
} finally {
drawerLoading.value = false
}
}
function handleCreate() {
modalMode.value = 'create'
editingUser.value = null
modalOpen.value = true
}
function handleEdit(record: UserRecord) {
modalMode.value = 'edit'
editingUser.value = record
modalOpen.value = true
}
async function handleToggleStatus(record: UserRecord) {
const newStatus = record.status === 1 ? 0 : 1
try {
await api.patch(`/api/v1/users/${record.id}/status`, { status: newStatus })
message.success('操作成功')
store.fetchUsers()
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : '操作失败'
message.error(msg)
}
}
function handleModalSuccess() {
store.fetchUsers()
}
function formatTime(time: string) {
return time ? time.replace('T', ' ').substring(0, 19) : '-'
}
</script>
<template>
<div>
<h2 class="text-xl font-semibold mb-4">用户管理</h2>
<!-- Filter area -->
<a-card class="mb-4">
<a-form layout="inline" @submit.prevent="handleSearch">
<a-form-item label="用户名">
<a-input
v-model:value="store.filters.username"
placeholder="请输入用户名"
allow-clear
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="邮箱">
<a-input
v-model:value="store.filters.email"
placeholder="请输入邮箱"
allow-clear
@press-enter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="store.filters.status"
placeholder="全部"
allow-clear
style="width: 120px"
>
<a-select-option :value="1">启用</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<!-- Table -->
<a-card>
<div class="mb-4">
<a-button type="primary" @click="handleCreate">
<template #icon><PlusOutlined /></template>
新建用户
</a-button>
</div>
<a-table
:columns="columns"
:data-source="store.users"
:loading="store.loading"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'created_at'">
{{ formatTime(record.created_at) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleViewDetail(record as UserRecord)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record as UserRecord)">
编辑
</a-button>
<a-popconfirm
:title="`确定要${record.status === 1 ? '禁用' : '启用'}该用户吗?`"
@confirm="handleToggleStatus(record as UserRecord)"
>
<a-button type="link" size="small" :danger="record.status === 1">
{{ record.status === 1 ? '禁用' : '启用' }}
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<div class="mt-4 flex justify-end">
<a-pagination
:current="store.pagination.page"
:page-size="store.pagination.per_page"
:total="store.pagination.total"
show-size-changer
show-quick-jumper
:show-total="(total: number) => `${total}`"
@change="handlePageChange"
/>
</div>
</a-card>
<!-- Detail drawer -->
<a-drawer
title="用户详情"
:open="drawerVisible"
:width="480"
@close="drawerVisible = false"
>
<a-spin :spinning="drawerLoading">
<a-descriptions v-if="currentUser" :column="1" bordered>
<a-descriptions-item label="ID">{{ currentUser.id }}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ currentUser.username }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ currentUser.email }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="currentUser.status === 1 ? 'green' : 'red'">
{{ currentUser.status === 1 ? '启用' : '禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="角色 ID">{{ currentUser.role_id }}</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formatTime(currentUser.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="更新时间">
{{ formatTime(currentUser.updated_at) }}
</a-descriptions-item>
<a-descriptions-item label="扩展信息">
<pre class="m-0 text-xs">{{
currentUser.ext ? JSON.stringify(currentUser.ext, null, 2) : '-'
}}</pre>
</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-drawer>
<!-- Create/Edit modal -->
<UserFormModal
v-model:open="modalOpen"
:mode="modalMode"
:user-data="editingUser"
@success="handleModalSuccess"
/>
</div>
</template>