初始化项目

This commit is contained in:
BBIT-Kai
2026-04-30 10:47:26 +08:00
commit c932419c73
147 changed files with 45298 additions and 0 deletions
+549
View File
@@ -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>