feat: 增加部门管理

This commit is contained in:
chansee97 2025-08-31 23:44:33 +08:00
parent 53282f9453
commit 82933cfa22
28 changed files with 608 additions and 85 deletions

View File

@ -49,6 +49,9 @@ const propOverrides = {
labelPlacement: 'left',
preset: 'card',
},
ProDataTable: {
paginateSinglePage: false,
},
}
</script>

View File

@ -5,3 +5,4 @@ export * from './system/user'
export * from './system/menu'
export * from './system/role'
export * from './system/dict'
export * from './system/dept'

51
src/api/system/dept.ts Normal file
View File

@ -0,0 +1,51 @@
import { request } from '../../utils/alova'
export type SearchQuery = Partial<Pick<Entity.Dept, 'deptName' | 'status'>>
/**
*
* POST /dept
*/
export function createDept(data: Partial<Entity.Dept>) {
return request.Post<Api.Response<Entity.Dept>>('/dept', data)
}
/**
*
* GET /dept
*/
export function getDeptList(params?: SearchQuery) {
return request.Get<Api.Response<Entity.Dept[]>>('/dept', { params })
}
/**
*
* GET /dept/options
*/
export function getDeptOptions() {
return request.Get<Api.Response<Entity.Dept[]>>('/dept/options')
}
/**
*
* GET /dept/{id}
*/
export function getDeptById(id: number) {
return request.Get<Api.Response<Entity.Dept>>(`/dept/${id}`)
}
/**
*
* PUT /dept/{id}
*/
export function updateDept(id: number, data: Partial<Entity.Dept>) {
return request.Put<Api.Response<Entity.Dept>>(`/dept/${id}`, data)
}
/**
*
* DELETE /dept/{id}
*/
export function deleteDept(id: number) {
return request.Delete<Api.Response<boolean>>(`/dept/${id}`)
}

View File

@ -1,5 +1,6 @@
import { request } from '../../utils/alova'
export type SearchQuery = Partial<Pick<Entity.Menu, 'title' | 'status'>>
/**
*
* POST /menu
@ -12,8 +13,8 @@ export function createMenu(data: Partial<Entity.Menu>) {
*
* GET /menu
*/
export function getMenuList() {
return request.Get<Api.Response<Entity.Menu[]>>('/menu')
export function getMenuList(params?: SearchQuery) {
return request.Get<Api.Response<Entity.Menu[]>>('/menu', { params })
}
/**

View File

@ -15,7 +15,7 @@ export const i18n = createI18n({
enUS,
},
// 缺失国际化键警告
// missingWarn: false,
missingWarn: false,
// 缺失回退内容警告
fallbackWarn: false,

View File

@ -331,7 +331,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
},
{
name: 'setting',
path: '/setting',
path: '/system',
title: '系统设置',
requiresAuth: true,
icon: 'icon-park-outline:setting',
@ -342,44 +342,54 @@ export const staticRoutes: AppRoute.RowRoute[] = [
},
{
name: 'accountSetting',
path: '/setting/user',
path: '/system/user',
title: '用户设置',
requiresAuth: true,
icon: 'icon-park-outline:every-user',
component: '/setting/user/index.vue',
component: '/system/user/index.vue',
id: 701,
parentId: 7,
},
{
name: 'roleSetting',
path: '/setting/role',
path: '/system/role',
title: '角色设置',
requiresAuth: true,
icon: 'icon-park-outline:every-user',
component: '/setting/role/index.vue',
component: '/system/role/index.vue',
id: 702,
parentId: 7,
},
{
name: 'dictionarySetting',
path: '/setting/dict',
path: '/system/dict',
title: '字典设置',
requiresAuth: true,
icon: 'icon-park-outline:book-one',
component: '/setting/dict/index.vue',
component: '/system/dict/index.vue',
id: 703,
parentId: 7,
},
{
name: 'menuSetting',
path: '/setting/menu',
path: '/system/menu',
title: '菜单设置',
requiresAuth: true,
icon: 'icon-park-outline:application-menu',
component: '/setting/menu/index.vue',
component: '/system/menu/index.vue',
id: 704,
parentId: 7,
},
{
name: 'deptSetting',
path: '/system/dept',
title: '部门管理',
requiresAuth: true,
icon: 'icon-park-outline:application-menu',
component: '/system/dept/index.vue',
id: 705,
parentId: 7,
},
{
name: 'about',
path: '/about',

44
src/typings/entities/dept.d.ts vendored Normal file
View File

@ -0,0 +1,44 @@
/// <reference path="../global.d.ts"/>
/* 数据库表字段 */
namespace Entity {
interface Dept {
/**
*
*/
ancestors?: string
/**
*
*/
deptName: string
/**
*
*/
email?: string
/**
*
*/
leader?: string
/**
* ID
*/
parentId?: number
/**
*
*/
phone?: string
/**
*
*/
remark?: string
/**
*
*/
sort?: number
/**
*
*/
status?: number
[property: string]: any
}
}

View File

@ -61,7 +61,7 @@ namespace Entity {
/**
*
*/
status?: number
status: number
/**
*
*/

View File

@ -0,0 +1,122 @@
import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { renderProCopyableText, renderProDateText } from 'pro-naive-ui'
interface DeptColumnActions {
onEdit: (row: Entity.Dept) => void
onDelete: (id: number) => void
onAdd: (row: Entity.Dept) => void
}
export const deptSearchColumns: ProSearchFormColumns<Entity.Dept> = [
{
title: '部门名称',
path: 'deptName',
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
options: [
{ label: '正常', value: 0 },
{ label: '停用', value: 1 },
],
},
},
]
export function createDeptColumns(actions: DeptColumnActions): DataTableColumns<Entity.Dept> {
return [
{
key: 'deptName',
title: '部门名称',
align: 'left',
minWidth: 150,
ellipsis: {
tooltip: true,
},
},
{
key: 'sort',
title: '排序',
align: 'center',
width: 80,
},
{
key: 'status',
title: '状态',
align: 'center',
width: 100,
render: (row: Entity.Dept) => (
<NTag type={row.status === 0 ? 'success' : 'error'} size="small">
{row.status === 0 ? '正常' : '停用'}
</NTag>
),
},
{
key: 'leader',
title: '负责人',
align: 'center',
width: 120,
render: (row: Entity.Dept) => row.leader || '-',
},
{
key: 'phone',
title: '联系电话',
align: 'center',
render: (row: Entity.Dept) => row.phone ? renderProCopyableText(row.phone) : '-',
},
{
key: 'email',
title: '邮箱',
align: 'center',
render: (row: Entity.Dept) => row.email ? renderProCopyableText(row.email) : '-',
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
width: 180,
render: (row: Entity.Dept) => {
return row.createTime ? renderProDateText(row.createTime) : '-'
},
},
{
key: 'actions',
title: '操作',
align: 'center',
width: 200,
render: (row: Entity.Dept) => (
<NSpace justify="center">
<NButton
type="info"
text
onClick={() => actions.onAdd(row)}
>
</NButton>
<NButton
type="primary"
text
onClick={() => actions.onEdit(row)}
>
</NButton>
<NPopconfirm
onPositiveClick={() => actions.onDelete(row.id!)}
v-slots={{
trigger: () => (
<NButton type="error" text>
</NButton>
),
default: () => `确定删除部门"${row.deptName}"吗?`,
}}
/>
</NSpace>
),
},
]
}

View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { createDept, getDeptById, getDeptOptions, updateDept } from '@/api'
import { createProModalForm } from 'pro-naive-ui'
import { Regex } from '@/constants'
interface Props {
modalName?: string
}
const {
modalName = '',
} = defineProps<Props>()
const emit = defineEmits<{
success: []
}>()
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
//
const deptOptions = ref<Entity.Dept[]>([])
const modalForm = createProModalForm<Partial<Entity.Dept>>({
omitEmptyString: false,
initialValues: {
sort: 0,
status: 0,
},
onSubmit: submitModal,
})
type ModalType = 'add' | 'edit'
const modalType = shallowRef<ModalType>('add')
const modalTitle = computed(() => {
const titleMap: Record<ModalType, string> = {
add: '添加',
edit: '编辑',
}
return `${titleMap[modalType.value]}${modalName}`
})
//
async function loadDeptOptions() {
try {
const { data } = await getDeptOptions()
deptOptions.value = data
}
catch {
console.error('加载部门选项失败')
}
}
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Dept>) {
modalType.value = type
modalForm.open()
loadDeptOptions()
const handlers = {
async add() {
if (data?.id) {
modalForm.values.value.parentId = data.id
}
},
async edit() {
if (!data?.id)
return
try {
const { data: dept } = await getDeptById(data.id)
modalForm.values.value = dept
}
catch {
window.$message.error('获取部门信息失败')
}
},
}
await handlers[type]()
}
async function submitModal(fieldValues: Partial<Entity.Dept>) {
const handlers = {
async add() {
try {
await createDept(fieldValues)
window.$message.success('部门创建成功')
return true
}
catch (error) {
console.error('创建部门失败', error)
return false
}
},
async edit() {
try {
await updateDept(modalForm.values.value.id!, fieldValues)
window.$message.success('部门更新成功')
return true
}
catch (error) {
console.error('更新部门失败', error)
return false
}
},
}
startLoading()
const success = await handlers[modalType.value]()
endLoading()
if (success) {
emit('success')
modalForm.close()
}
}
defineExpose({
openModal,
})
</script>
<template>
<pro-modal-form
:title="modalTitle"
:form="modalForm"
:loading="submitLoading"
width="700px"
>
<div class="grid grid-cols-2">
<pro-input
required
title="部门名称"
path="deptName"
/>
<pro-tree-select
title="上级部门"
path="parentId"
:field-props="{
options: deptOptions,
clearable: true,
keyField: 'value',
}"
/>
<pro-digit
title="显示排序"
path="sort"
:field-props="{ min: 0, max: 999 }"
/>
<pro-input
title="负责人"
path="leader"
/>
<pro-input
title="联系电话"
path="phone"
:field-props="{ maxlength: 11 }"
/>
<pro-input
title="邮箱"
path="email"
:rule="[
{
pattern: new RegExp(Regex.Email),
message: '请输入正确的邮箱格式',
trigger: 'blur',
},
]"
/>
<pro-switch
title="部门状态"
path="status"
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
/>
<div />
<pro-textarea
class="col-span-2"
title="备注"
path="remark"
/>
</div>
</pro-modal-form>
</template>

View File

@ -0,0 +1,103 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { deleteDept, getDeptList } from '@/api'
import type { SearchQuery } from '@/api'
import { createDeptColumns, deptSearchColumns } from './columns'
import DeptModal from './components/DeptModal.vue'
import arrayToTree from 'array-to-tree'
import { createProSearchForm } from 'pro-naive-ui'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const deptModalRef = ref<InstanceType<typeof DeptModal>>()
//
const searchForm = createProSearchForm<SearchQuery>({
initialValues: {},
onSubmit: getAllDepts,
onReset: getAllDepts,
})
//
async function deleteDeptItem(id: number) {
try {
await deleteDept(id)
window.$message.success('部门删除成功')
searchForm.submit()
}
catch {
window.$message.error('部门删除失败')
}
}
//
const deptColumns = createDeptColumns({
onAdd: (row: Entity.Dept) => deptModalRef.value?.openModal('add', row),
onEdit: (row: Entity.Dept) => deptModalRef.value?.openModal('edit', row),
onDelete: deleteDeptItem,
})
const tableData = ref<Entity.Dept[]>([])
//
async function getAllDepts(params?: SearchQuery) {
startLoading()
try {
const { data } = await getDeptList(params)
tableData.value = arrayToTree(data, {
parentProperty: 'parentId',
})
}
catch {
window.$message.error('获取部门列表失败')
}
finally {
endLoading()
}
}
onMounted(() => {
searchForm.submit()
})
</script>
<template>
<div class="h-full">
<!-- 搜索表单 -->
<n-card class="mb-4">
<pro-search-form
:form="searchForm"
:columns="deptSearchColumns"
:collapse-button-props="false"
/>
</n-card>
<!-- 数据表格 -->
<pro-data-table
row-key="id"
:columns="deptColumns"
:data="tableData"
:loading="loading"
>
<template #title>
<n-button
type="primary"
@click="deptModalRef?.openModal('add')"
>
<template #icon>
<icon-park-outline-plus />
</template>
新增部门
</n-button>
</template>
</pro-data-table>
<!-- 部门弹窗 -->
<DeptModal
ref="deptModalRef"
modal-name="部门"
@success="searchForm.submit"
/>
</div>
</template>

View File

@ -1,7 +1,7 @@
import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { renderProCopyableText, renderProTags } from 'pro-naive-ui'
import { renderProCopyableText } from 'pro-naive-ui'
interface DictDataColumnActions {
onEdit: (row: Entity.DictData) => void
@ -13,18 +13,14 @@ export const dictDataSearchColumns: ProSearchFormColumns<Entity.DictData> = [
title: '数据名称',
path: 'name',
},
{
title: '数据键值',
path: 'value',
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
options: [
{ label: '正常', value: 1 },
{ label: '停用', value: 0 },
{ label: '正常', value: 0 },
{ label: '停用', value: 1 },
],
},
},
@ -53,7 +49,7 @@ export function createDictDataColumns(actions: DictDataColumnActions): DataTable
key: 'dictType',
title: '字典类型',
align: 'center',
render: row => renderProTags(row.dictType),
render: row => renderProCopyableText(row.dictType),
},
{
key: 'sort',
@ -61,11 +57,27 @@ export function createDictDataColumns(actions: DictDataColumnActions): DataTable
align: 'center',
width: 100,
},
{
key: 'status',
title: '状态',
align: 'center',
width: 100,
render: (row: Entity.DictData) => (
<NTag type={row.status === 0 ? 'success' : 'error'} bordered={false}>
{row.status === 0 ? '正常' : '停用'}
</NTag>
),
},
{
key: 'remark',
title: '备注',
align: 'center',
},
{
key: 'updateTime',
title: '更新时间',
align: 'center',
},
{
key: 'actions',
title: '操作',

View File

@ -130,7 +130,7 @@ defineExpose({
<pro-switch
title="状态"
path="status"
:field-props="{ checkedValue: 1, uncheckedValue: 0 }"
:field-props="{ checkedValue: 0, uncheckedValue: 1 }"
/>
<pro-textarea
class="col-span-2"

View File

@ -2,7 +2,7 @@
import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace } from 'naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import { createProSearchForm, renderProCopyableText, useNDataTable } from 'pro-naive-ui'
import { createProSearchForm, useNDataTable } from 'pro-naive-ui'
import { deleteDictType, getDictTypeList } from '@/api'
import DictTypeModal from './DictTypeModal.vue'
@ -22,10 +22,6 @@ const dictTypeSearchColumns: ProSearchFormColumns<Entity.DictType> = [
title: '字典名称',
path: 'name',
},
{
title: '字典类型',
path: 'type',
},
]
// 使 useNDataTable
@ -80,11 +76,6 @@ const columns: DataTableColumns<Entity.DictType> = [
tooltip: true,
},
},
{
key: 'type',
title: '字典类型',
render: row => renderProCopyableText(row.type),
},
{
key: 'actions',
title: '操作',
@ -131,7 +122,7 @@ const columns: DataTableColumns<Entity.DictType> = [
:columns="dictTypeSearchColumns"
:collapse-button-props="false"
v-bind="dictTypeSearchProps"
:cols="3"
:cols="2"
/>
</n-card>

View File

@ -11,7 +11,6 @@ const dictTypeListRef = ref<InstanceType<typeof DictTypeList>>()
const dictDataSearchForm = createProSearchForm<Partial<Entity.DictData>>({
initialValues: {},
})
const { values } = dictDataSearchForm
const {
table: {
@ -79,7 +78,7 @@ async function getDictDataPage({ current, pageSize }: any, formData: Partial<Ent
<template>
<div class="flex h-full gap-2">
<!-- 左侧字典类型列表 -->
<div class="w-1/3">
<div class="w-1/5">
<DictTypeList
ref="dictTypeListRef"
@select="handleDictTypeSelect"
@ -103,9 +102,9 @@ async function getDictDataPage({ current, pageSize }: any, formData: Partial<Ent
>
<template #title>
<n-button
v-if="values.dictType"
v-if="currentDictType?.type"
type="primary"
@click="dictDataModalRef?.openModal('add', { dictType: values.dictType })"
@click="dictDataModalRef?.openModal('add', { dictType: currentDictType.type })"
>
<template #icon>
<icon-park-outline-plus />

View File

@ -2,6 +2,8 @@ import type { DataTableColumns } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import { createIcon } from '@/utils'
import { renderProCopyableText } from 'pro-naive-ui'
import type { ProSearchFormColumns } from 'pro-naive-ui'
import type { SearchQuery } from '@/api'
// 菜单管理columns配置函数
interface MenuColumnActions {
@ -10,6 +12,23 @@ interface MenuColumnActions {
onAdd: (row: Entity.Menu) => void
}
export const searchColumns: ProSearchFormColumns<SearchQuery> = [
{
title: '菜单名称',
path: 'title',
},
{
title: '状态',
path: 'status',
field: 'select',
fieldProps: {
options: [
{ label: '正常', value: 0 },
{ label: '停用', value: 1 },
],
},
},
]
export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<Entity.Menu> {
const { onEdit, onDelete, onAdd } = actions
@ -23,7 +42,7 @@ export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<
title: '图标',
align: 'center',
key: 'icon',
width: '6em',
width: '100px',
render: (row) => {
return row.icon && createIcon(row.icon, { size: 20 })
},
@ -85,7 +104,7 @@ export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<
{row.menuType !== 'permission' && (
<NButton
text
type="primary"
type="info"
onClick={() => onAdd(row)}
>
@ -93,6 +112,7 @@ export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<
)}
<NButton
text
type="primary"
onClick={() => onEdit(row)}
>

View File

@ -1,14 +1,23 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { deleteMenu, getMenuList } from '@/api'
import { createMenuColumns } from './columns'
import type { SearchQuery } from '@/api'
import { createMenuColumns, searchColumns } from './columns'
import MenuModal from './components/MenuModal.vue'
import arrayToTree from 'array-to-tree'
import { createProSearchForm } from 'pro-naive-ui'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const menuModalRef = ref()
//
const searchForm = createProSearchForm<SearchQuery>({
initialValues: {},
onSubmit: getAllRoutes,
onReset: getAllRoutes,
})
// columns
const columns = createMenuColumns({
onEdit: row => menuModalRef.value.openModal('edit', row),
@ -20,40 +29,22 @@ async function deleteData(id: number) {
try {
await deleteMenu(id)
window.$message.success('删除菜单成功')
getAllRoutes() //
searchForm.submit()
}
catch (error) {
console.error('删除菜单失败', error)
}
}
//
function sortMenuTree(menus: Entity.Menu[]): Entity.Menu[] {
// sort
const sortedMenus = menus.sort((a, b) => {
const sortA = a.sort || 0
const sortB = b.sort || 0
return sortA - sortB
})
//
return sortedMenus.map(menu => ({
...menu,
children: menu.children ? sortMenuTree(menu.children) : undefined,
}))
}
const tableData = ref<Entity.Menu[]>([])
async function getAllRoutes() {
async function getAllRoutes(params?: SearchQuery) {
startLoading()
try {
const { data } = await getMenuList()
const treeData = arrayToTree(data, {
const { data } = await getMenuList(params)
tableData.value = arrayToTree(data, {
parentProperty: 'parentId',
})
// sort
tableData.value = sortMenuTree(treeData)
}
catch {
window.$message.error('获取菜单列表失败')
@ -64,12 +55,21 @@ async function getAllRoutes() {
}
onMounted(() => {
getAllRoutes()
searchForm.submit()
})
</script>
<template>
<div>
<!-- 搜索表单 -->
<n-card class="mb-4">
<pro-search-form
:form="searchForm"
:columns="searchColumns"
:collapse-button-props="false"
/>
</n-card>
<pro-data-table
row-key="id"
:columns="columns"
@ -84,17 +84,8 @@ onMounted(() => {
新增
</n-button>
</template>
<template #toolbar>
<n-button type="primary" secondary @click="getAllRoutes">
<template #icon>
<icon-park-outline-refresh />
</template>
刷新
</n-button>
</template>
</pro-data-table>
<MenuModal ref="menuModalRef" modal-name="菜单" @success="getAllRoutes" />
<MenuModal ref="menuModalRef" modal-name="菜单" @success="searchForm.submit" />
</div>
</template>

View File

@ -9,7 +9,7 @@ export const searchColumns: ProSearchFormColumns<Entity.Role> = [
path: 'roleName',
},
{
title: '权限标识',
title: '角色标识',
path: 'roleKey',
},
{
@ -46,7 +46,7 @@ export function createRoleColumns(actions: RoleColumnActions): DataTableColumns<
key: 'roleName',
},
{
title: '权限标识',
title: '角色标识',
align: 'center',
key: 'roleKey',
render: row => renderProCopyableText(row.roleKey),

View File

@ -151,14 +151,7 @@ defineExpose({
<pro-input
title="手机号"
path="phone"
placeholder="11位手机号"
:rule="[
{
pattern: new RegExp(Regex.Phone),
message: '请输入正确的手机号格式',
trigger: 'blur',
},
]"
max-length="11"
/>
<pro-select
title="角色"