初始化项目
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-card table-fill">
|
||||
<div class="page-toolbar">
|
||||
<n-form inline :show-label="false" class="toolbar-form">
|
||||
<n-form-item>
|
||||
<n-input v-model:value="queryForm.username" clearable placeholder="用户名" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-input v-model:value="queryForm.nickname" clearable placeholder="昵称" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-select
|
||||
v-model:value="queryForm.status"
|
||||
clearable
|
||||
placeholder="状态"
|
||||
:options="statusOptions"
|
||||
style="width: 140px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">查询</n-button>
|
||||
<n-button @click="handleReset">重置</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<PermissionButton permission="system:user:create">
|
||||
<n-button type="primary" @click="openCreate">新增用户</n-button>
|
||||
</PermissionButton>
|
||||
</div>
|
||||
|
||||
<div class="card-body card-body-fill table-fill">
|
||||
<n-data-table
|
||||
remote
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="tableRows"
|
||||
:loading="usersQuery.isLoading.value"
|
||||
:pagination="pagination"
|
||||
:row-key="(row: UserListItem) => row.id"
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<n-modal
|
||||
v-model:show="editModal.visible"
|
||||
preset="card"
|
||||
:title="editModal.title"
|
||||
class="w-[560px]"
|
||||
>
|
||||
<n-form
|
||||
ref="editFormRef"
|
||||
:model="editForm"
|
||||
:rules="editRules"
|
||||
label-placement="left"
|
||||
label-width="90"
|
||||
>
|
||||
<n-form-item label="用户名" path="username">
|
||||
<n-input v-model:value="editForm.username" :disabled="editModal.mode === 'edit'" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="editModal.mode === 'create'" label="密码" path="password">
|
||||
<n-input v-model:value="editForm.password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-form-item label="昵称" path="nickname">
|
||||
<n-input v-model:value="editForm.nickname" />
|
||||
</n-form-item>
|
||||
<n-form-item label="姓名" path="realName">
|
||||
<n-input v-model:value="editForm.realName" />
|
||||
</n-form-item>
|
||||
<n-form-item label="手机号" path="phone">
|
||||
<n-input v-model:value="editForm.phone" />
|
||||
</n-form-item>
|
||||
<n-form-item label="邮箱" path="email">
|
||||
<n-input v-model:value="editForm.email" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="editModal.mode === 'create'" label="状态" path="status">
|
||||
<n-radio-group v-model:value="editForm.status">
|
||||
<n-radio-button value="ENABLED">启用</n-radio-button>
|
||||
<n-radio-button value="DISABLED">禁用</n-radio-button>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<n-button @click="editModal.visible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saveMutation.isPending.value" @click="handleSave"
|
||||
>保存</n-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="passwordModal.visible" preset="card" title="重置密码" class="w-[460px]">
|
||||
<n-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-placement="left"
|
||||
label-width="90"
|
||||
>
|
||||
<n-form-item label="新密码" path="password">
|
||||
<n-input v-model:value="passwordForm.password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<n-button @click="passwordModal.visible = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="passwordMutation.isPending.value"
|
||||
@click="handleResetPassword"
|
||||
>确认</n-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="rolesModal.visible" preset="card" title="分配角色" class="w-[520px]">
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="角色">
|
||||
<n-select
|
||||
v-model:value="rolesModal.roleIds"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
:options="roleOptions"
|
||||
placeholder="请选择角色"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<n-button @click="rolesModal.visible = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="rolesMutation.isPending.value"
|
||||
@click="handleAssignRoles"
|
||||
>确认</n-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
import type { DataTableColumns, FormInst, FormRules } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NSpace, NSwitch, NTag, useMessage } from 'naive-ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import PermissionButton from '@/components/PermissionButton.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { statusLabel, statusTagType } from '@/utils/display'
|
||||
import { renderPagePrefix } from '@/utils/pagination'
|
||||
import {
|
||||
createUserApi,
|
||||
deleteUserApi,
|
||||
getUserDetailApi,
|
||||
listUsersApi,
|
||||
updateUserApi,
|
||||
updateUserPasswordApi,
|
||||
updateUserRolesApi,
|
||||
updateUserStatusApi
|
||||
} from '@/api/system/user'
|
||||
import { listRolesApi } from '@/api/system/role'
|
||||
import type { CreateUserRequest, UpdateUserRequest, UserListItem } from '@/types/system/user'
|
||||
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const queryForm = reactive({
|
||||
username: '',
|
||||
nickname: '',
|
||||
status: null as string | null
|
||||
})
|
||||
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: 'ENABLED' },
|
||||
{ label: '禁用', value: 'DISABLED' }
|
||||
]
|
||||
|
||||
const usersQuery = useQuery({
|
||||
queryKey: computed(() => ['system', 'users', { ...queryForm, ...pager }]),
|
||||
queryFn: () =>
|
||||
listUsersApi({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
username: queryForm.username || undefined,
|
||||
nickname: queryForm.nickname || undefined,
|
||||
status: queryForm.status || undefined
|
||||
})
|
||||
})
|
||||
|
||||
const rolesQuery = useQuery({
|
||||
queryKey: ['system', 'roles', 'enabled'],
|
||||
queryFn: () => listRolesApi({ page: 1, pageSize: 200, status: 'ENABLED' })
|
||||
})
|
||||
|
||||
const roleOptions = computed(() =>
|
||||
(rolesQuery.data.value?.items ?? []).map((role) => ({
|
||||
label: `${role.name} (${role.code})`,
|
||||
value: role.id
|
||||
}))
|
||||
)
|
||||
|
||||
const tableRows = computed(() => usersQuery.data.value?.items ?? [])
|
||||
|
||||
const pagination = computed(() => ({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
itemCount: usersQuery.data.value?.total ?? 0,
|
||||
pageSizes: [10, 20, 50],
|
||||
showSizePicker: true,
|
||||
prefix: renderPagePrefix
|
||||
}))
|
||||
|
||||
const editFormRef = ref<FormInst | null>(null)
|
||||
const editModal = reactive({
|
||||
visible: false,
|
||||
mode: 'create' as 'create' | 'edit',
|
||||
title: '新增用户',
|
||||
id: ''
|
||||
})
|
||||
const editForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
realName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: 'ENABLED'
|
||||
})
|
||||
|
||||
const editRules: FormRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: ['input', 'blur'] }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: ['input', 'blur'] }]
|
||||
}
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (editModal.mode === 'create') {
|
||||
const payload: CreateUserRequest = {
|
||||
username: editForm.username.trim(),
|
||||
password: editForm.password,
|
||||
nickname: editForm.nickname.trim() || undefined,
|
||||
realName: editForm.realName.trim() || undefined,
|
||||
phone: editForm.phone.trim() || undefined,
|
||||
email: editForm.email.trim() || undefined,
|
||||
status: editForm.status
|
||||
}
|
||||
return createUserApi(payload)
|
||||
}
|
||||
const payload: UpdateUserRequest = {
|
||||
nickname: editForm.nickname.trim() || undefined,
|
||||
realName: editForm.realName.trim() || undefined,
|
||||
phone: editForm.phone.trim() || undefined,
|
||||
email: editForm.email.trim() || undefined
|
||||
}
|
||||
return updateUserApi(editModal.id, payload)
|
||||
},
|
||||
onSuccess: async () => {
|
||||
message.success('保存成功')
|
||||
editModal.visible = false
|
||||
await usersQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => deleteUserApi(id),
|
||||
onSuccess: async () => {
|
||||
message.success('删除成功')
|
||||
await usersQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
const statusMutation = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: string }) =>
|
||||
updateUserStatusApi(id, { status }),
|
||||
onSuccess: async () => {
|
||||
message.success('状态更新成功')
|
||||
await usersQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
const passwordFormRef = ref<FormInst | null>(null)
|
||||
const passwordModal = reactive({
|
||||
visible: false,
|
||||
id: ''
|
||||
})
|
||||
const passwordForm = reactive({
|
||||
password: ''
|
||||
})
|
||||
const passwordRules: FormRules = {
|
||||
password: [{ required: true, message: '请输入新密码', trigger: ['input', 'blur'] }]
|
||||
}
|
||||
|
||||
const passwordMutation = useMutation({
|
||||
mutationFn: () => updateUserPasswordApi(passwordModal.id, { password: passwordForm.password }),
|
||||
onSuccess: async () => {
|
||||
message.success('密码重置成功')
|
||||
passwordModal.visible = false
|
||||
passwordForm.password = ''
|
||||
await queryClient.invalidateQueries({ queryKey: ['system', 'users'] })
|
||||
}
|
||||
})
|
||||
|
||||
const rolesModal = reactive({
|
||||
visible: false,
|
||||
id: '',
|
||||
roleIds: [] as string[]
|
||||
})
|
||||
|
||||
const rolesMutation = useMutation({
|
||||
mutationFn: () => updateUserRolesApi(rolesModal.id, { roleIds: rolesModal.roleIds }),
|
||||
onSuccess: async () => {
|
||||
message.success('角色分配成功')
|
||||
rolesModal.visible = false
|
||||
await usersQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
function onPageChange(page: number) {
|
||||
pager.page = page
|
||||
}
|
||||
|
||||
function onPageSizeChange(pageSize: number) {
|
||||
pager.pageSize = pageSize
|
||||
pager.page = 1
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pager.page = 1
|
||||
usersQuery.refetch()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.username = ''
|
||||
queryForm.nickname = ''
|
||||
queryForm.status = null
|
||||
pager.page = 1
|
||||
usersQuery.refetch()
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.username = ''
|
||||
editForm.password = ''
|
||||
editForm.nickname = ''
|
||||
editForm.realName = ''
|
||||
editForm.phone = ''
|
||||
editForm.email = ''
|
||||
editForm.status = 'ENABLED'
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editModal.mode = 'create'
|
||||
editModal.title = '新增用户'
|
||||
editModal.id = ''
|
||||
resetEditForm()
|
||||
editModal.visible = true
|
||||
}
|
||||
|
||||
async function openEdit(row: UserListItem) {
|
||||
editModal.mode = 'edit'
|
||||
editModal.title = `编辑用户 - ${row.username}`
|
||||
editModal.id = row.id
|
||||
const detail = await getUserDetailApi(row.id)
|
||||
editForm.username = detail.username
|
||||
editForm.password = ''
|
||||
editForm.nickname = detail.nickname ?? ''
|
||||
editForm.realName = detail.realName ?? ''
|
||||
editForm.phone = detail.phone ?? ''
|
||||
editForm.email = detail.email ?? ''
|
||||
editForm.status = detail.status
|
||||
editModal.visible = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (editModal.mode === 'create') {
|
||||
await editFormRef.value?.validate()
|
||||
} else {
|
||||
await editFormRef.value?.validate(
|
||||
(errors) => {
|
||||
if (!errors) return
|
||||
throw errors
|
||||
},
|
||||
(rule) => rule?.key !== 'password'
|
||||
)
|
||||
}
|
||||
await saveMutation.mutateAsync()
|
||||
}
|
||||
|
||||
function openResetPassword(row: UserListItem) {
|
||||
passwordModal.id = row.id
|
||||
passwordForm.password = ''
|
||||
passwordModal.visible = true
|
||||
}
|
||||
|
||||
async function handleResetPassword() {
|
||||
await passwordFormRef.value?.validate()
|
||||
await passwordMutation.mutateAsync()
|
||||
}
|
||||
|
||||
async function openAssignRoles(row: UserListItem) {
|
||||
const detail = await getUserDetailApi(row.id)
|
||||
rolesModal.id = row.id
|
||||
rolesModal.roleIds = [...detail.roleIds]
|
||||
rolesModal.visible = true
|
||||
}
|
||||
|
||||
async function handleAssignRoles() {
|
||||
await rolesMutation.mutateAsync()
|
||||
}
|
||||
|
||||
function updateStatus(row: UserListItem, checked: boolean) {
|
||||
statusMutation.mutate({ id: row.id, status: checked ? 'ENABLED' : 'DISABLED' })
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<UserListItem>>(() => [
|
||||
{
|
||||
title: '用户信息',
|
||||
key: 'username',
|
||||
minWidth: 180,
|
||||
render: (row) =>
|
||||
h('div', { class: 'leading-5' }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'font-medium text-slate-900' },
|
||||
row.nickname || row.realName || row.username
|
||||
),
|
||||
h('div', { class: 'text-xs text-slate-500' }, row.username)
|
||||
])
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
key: 'roleCodes',
|
||||
minWidth: 180,
|
||||
render: (row) => (row.roleCodes.length > 0 ? row.roleCodes.join(', ') : '-')
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: statusTagType(row.status), size: 'small' },
|
||||
{ default: () => statusLabel(row.status) }
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '启停',
|
||||
key: 'statusSwitch',
|
||||
width: 110,
|
||||
render: (row) =>
|
||||
authStore.hasPermission('system:user:update')
|
||||
? h(NSwitch, {
|
||||
size: 'small',
|
||||
value: row.status === 'ENABLED',
|
||||
onUpdateValue: (checked: boolean) => updateStatus(row, checked)
|
||||
})
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 360,
|
||||
render: (row) =>
|
||||
h(
|
||||
NSpace,
|
||||
{ size: 6 },
|
||||
{
|
||||
default: () => [
|
||||
authStore.hasPermission('system:user:update')
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
tertiary: true,
|
||||
type: 'primary',
|
||||
class: 'action-btn',
|
||||
onClick: () => openEdit(row)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
)
|
||||
: null,
|
||||
authStore.hasPermission('system:user:update')
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
tertiary: true,
|
||||
type: 'warning',
|
||||
class: 'action-btn',
|
||||
onClick: () => openResetPassword(row)
|
||||
},
|
||||
{ default: () => '重置密码' }
|
||||
)
|
||||
: null,
|
||||
authStore.hasPermission('system:role:assign')
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
tertiary: true,
|
||||
type: 'info',
|
||||
class: 'action-btn',
|
||||
onClick: () => openAssignRoles(row)
|
||||
},
|
||||
{ default: () => '分配角色' }
|
||||
)
|
||||
: null,
|
||||
authStore.hasPermission('system:user:delete')
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => deleteMutation.mutate(row.id)
|
||||
},
|
||||
{
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
tertiary: true,
|
||||
type: 'error',
|
||||
class: 'action-btn',
|
||||
loading: deleteMutation.isPending.value
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
),
|
||||
default: () => `确认删除用户 ${row.username} 吗?`
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
])
|
||||
</script>
|
||||
Reference in New Issue
Block a user