department management

This commit is contained in:
mizhexiaoxiao 2024-05-20 14:41:32 +08:00
parent b9fa6d92ce
commit 05919d8654
14 changed files with 2415 additions and 3744 deletions

View File

@ -7,6 +7,7 @@ from .base import base_router
from .menus import menus_router
from .roles import roles_router
from .users import users_router
from .depts import depts_router
v1_router = APIRouter()
@ -15,3 +16,4 @@ v1_router.include_router(users_router, prefix="/user", dependencies=[DependPermi
v1_router.include_router(roles_router, prefix="/role", dependencies=[DependPermisson])
v1_router.include_router(menus_router, prefix="/menu", dependencies=[DependPermisson])
v1_router.include_router(apis_router, prefix="/api", dependencies=[DependPermisson])
v1_router.include_router(depts_router, prefix="/dept", dependencies=[DependPermisson])

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from .depts import router
depts_router = APIRouter()
depts_router.include_router(router, tags=["部门模块"])
__all__ = ["depts_router"]

49
app/api/v1/depts/depts.py Normal file
View File

@ -0,0 +1,49 @@
from fastapi import APIRouter, Query
from app.controllers.dept import dept_controller
from app.log import logger
from app.schemas import Success, SuccessExtra
from app.schemas.depts import *
router = APIRouter()
@router.get("/list", summary="查看部门列表")
async def list_dept(
name: str = Query(None, description="部门名称"),
):
dept_tree = await dept_controller.get_dept_tree(name)
return Success(data=dept_tree)
@router.get("/get", summary="查看部门")
async def get_dept(
id: int = Query(..., description="部门ID"),
):
dept_obj = await dept_controller.get(id=id)
data = await dept_obj.to_dict()
return Success(data=data)
@router.post("/create", summary="创建部门")
async def create_dept(
dept_in: DeptCreate,
):
await dept_controller.create_dept(obj_in=dept_in)
return Success(msg="Created Successfully")
@router.post("/update", summary="更新部门")
async def update_dept(
dept_in: DeptUpdate,
):
await dept_controller.update_dept(obj_in=dept_in)
return Success(msg="Update Successfully")
@router.delete("/delete", summary="删除部门")
async def delete_dept(
dept_id: int = Query(..., description="部门ID"),
):
await dept_controller.delete_dept(dept_id=dept_id)
return Success(msg="Deleted Success")

View File

@ -8,6 +8,7 @@ from app.controllers.user import UserController
from app.core.dependency import DependPermisson
from app.schemas.base import Success, SuccessExtra
from app.schemas.users import *
from app.controllers.dept import dept_controller
logger = logging.getLogger(__name__)
@ -20,6 +21,7 @@ async def list_user(
page_size: int = Query(10, description="每页数量"),
username: str = Query("", description="用户名称,用于搜索"),
email: str = Query("", description="邮箱地址"),
dept_id: int = Query(None, description="部门ID")
):
user_controller = UserController()
q = Q()
@ -27,8 +29,14 @@ async def list_user(
q &= Q(username__contains=username)
if email:
q &= Q(email__contains=email)
if dept_id is not None:
q &= Q(dept_id=dept_id)
total, user_objs = await user_controller.list(page=page, page_size=page_size, search=q)
data = [await obj.to_dict(m2m=True, exclude_fields=["password"]) for obj in user_objs]
for item in data:
dept_id = item.pop("dept_id", None)
item["dept"] = await (await dept_controller.get(id=dept_id)).to_dict() if dept_id else {}
return SuccessExtra(data=data, total=total, page=page, page_size=page_size)

95
app/controllers/dept.py Normal file
View File

@ -0,0 +1,95 @@
from app.core.crud import CRUDBase
from app.models.admin import Dept, DeptClosure
from app.schemas.depts import DeptCreate, DeptUpdate
from tortoise.transactions import atomic
from tortoise.expressions import Q
class DeptController(CRUDBase[Dept, DeptCreate, DeptUpdate]):
def __init__(self):
super().__init__(model=Dept)
async def get_dept_tree(self, name):
q = Q()
# 获取所有未被软删除的部门
q &= Q(is_deleted=False)
if name:
q &= Q(name__contains=name)
all_depts = await self.model.filter(q).order_by('order')
# 辅助函数,用于递归构建部门树
def build_tree(parent_id):
return [
{
'id': dept.id,
'name': dept.name,
'desc': dept.desc,
'order': dept.order,
'parent_id': dept.parent_id,
'children': build_tree(dept.id) # 递归构建子部门
}
for dept in all_depts if dept.parent_id == parent_id
]
# 从顶级部门parent_id=0开始构建部门树
dept_tree = build_tree(0)
return dept_tree
async def get_dept_info(self):
pass
async def update_dept_closure(self, obj: Dept):
parent_depts = await DeptClosure.filter(descendant=obj.parent_id)
for i in parent_depts:
print(i.ancestor, i.descendant)
dept_closure_objs: list[DeptClosure] = []
# 插入父级关系
for item in parent_depts:
dept_closure_objs.append(
DeptClosure(
ancestor=item.ancestor,
descendant=obj.id,
level=item.level + 1
)
)
# 插入自身x
dept_closure_objs.append(
DeptClosure(
ancestor=obj.id,
descendant=obj.id,
level=0
)
)
# 创建关系
await DeptClosure.bulk_create(dept_closure_objs)
@atomic()
async def create_dept(self, obj_in: DeptCreate):
# 创建
if obj_in.parent_id != 0:
await self.get(id=obj_in.parent_id)
new_obj = await self.create(obj_in=obj_in)
await self.update_dept_closure(new_obj)
@atomic()
async def update_dept(self, obj_in: DeptUpdate):
dept_obj = await self.get(id=obj_in.id)
# 更新部门关系
if dept_obj.parent_id != obj_in.parent_id:
await DeptClosure.filter(ancestor=dept_obj.id).delete()
await DeptClosure.filter(descendant=dept_obj.id).delete()
await self.update_dept_closure(dept_obj)
# 更新部门信息
dept_obj.update_from_dict(obj_in.model_dump(exclude_unset=True))
await dept_obj.save()
@atomic()
async def delete_dept(self, dept_id: int):
# 删除部门
obj = await self.get(id=dept_id)
obj.is_deleted = True
await obj.save()
# 删除关系
await DeptClosure.filter(descendant=dept_id).delete()
dept_controller = DeptController()

View File

@ -16,6 +16,7 @@ class User(BaseModel, TimestampMixin):
is_superuser = fields.BooleanField(default=False, description="是否为超级管理员")
last_login = fields.DatetimeField(null=True, description="最后登录时间")
roles = fields.ManyToManyField("models.Role", related_name="user_roles")
dept_id = fields.IntField(null=True, description="部门ID")
class Meta:
table = "user"
@ -65,10 +66,16 @@ class Menu(BaseModel, TimestampMixin):
class Dept(BaseModel, TimestampMixin):
name = fields.CharField(max_length=20, unique=True, description="部门名称")
desc = fields.CharField(max_length=500, null=True, blank=True, description="菜单描述")
desc = fields.CharField(max_length=500, null=True, blank=True, description="备注")
is_deleted = fields.BooleanField(default=False, description="软删除标记")
order = fields.IntField(default=0, description="排序")
parent_id = fields.IntField(default=0, max_length=10, description="父部门ID")
class Meta:
table = "dept"
class DeptClosure(BaseModel, TimestampMixin):
ancestor = fields.IntField(description="父代")
descendant = fields.IntField(description="子代")
level = fields.IntField(default=0, description="深度")

19
app/schemas/depts.py Normal file
View File

@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
class BaseDept(BaseModel):
name: str = Field(..., description="部门名称", example="研发中心")
desc: str = Field("", description="备注", example="研发中心")
order: int = Field(0, description="排序")
parent_id: int = Field(0, description="父部门ID")
class DeptCreate(BaseDept):
...
class DeptUpdate(BaseDept):
id: int
def update_dict(self):
return self.model_dump(exclude_unset=True, exclude={"id"})

View File

@ -23,6 +23,7 @@ class UserCreate(BaseModel):
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
roles: Optional[List[int]] = []
dept_id: Optional[int] = Field(0, description="部门ID")
def create_dict(self):
return self.model_dump(exclude_unset=True, exclude={"roles"})
@ -35,6 +36,7 @@ class UserUpdate(BaseModel):
is_active: Optional[bool] = True
is_superuser: Optional[bool] = False
roles: Optional[List[int]] = []
dept_id: Optional[int] = 0
def update_dict(self):
return self.model_dump(exclude_unset=True, exclude={"roles", "id"})

5483
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -31,4 +31,9 @@ export default {
updateApi: (data = {}) => request.post('/api/update', data),
deleteApi: (params = {}) => request.delete('/api/delete', { params }),
refreshApi: (data = {}) => request.post('/api/refresh', data),
// depts
getDepts: (params = {}) => request.get('/dept/list', { params }),
createDept: (data = {}) => request.post('/dept/create', data),
updateDept: (data = {}) => request.post('/dept/update', data),
deleteDept: (params = {}) => request.delete('/dept/delete', { params }),
}

View File

@ -128,5 +128,6 @@ function onChecked(rowKeys) {
defineExpose({
handleSearch,
handleReset,
tableData,
})
</script>

View File

@ -0,0 +1,216 @@
<script setup>
import { h, onMounted, ref, resolveDirective, withDirectives } from 'vue'
import { NButton, NForm, NFormItem, NInput, NInputNumber, NPopconfirm, NTreeSelect } from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue'
import QueryBarItem from '@/components/query-bar/QueryBarItem.vue'
import CrudModal from '@/components/table/CrudModal.vue'
import CrudTable from '@/components/table/CrudTable.vue'
import TheIcon from '@/components/icon/TheIcon.vue'
import { renderIcon } from '@/utils'
import { useCRUD } from '@/composables'
// import { loginTypeMap, loginTypeOptions } from '@/constant/data'
import api from '@/api'
defineOptions({ name: '部门管理' })
const $table = ref(null)
const queryItems = ref({})
const vPermission = resolveDirective('permission')
const {
modalVisible,
modalTitle,
modalLoading,
handleSave,
modalForm,
modalFormRef,
handleEdit,
handleDelete,
handleAdd,
} = useCRUD({
name: 'API',
initForm: { order: 0 },
doCreate: api.createDept,
doUpdate: api.updateDept,
doDelete: api.deleteDept,
refresh: () => $table.value?.handleSearch(),
})
const deptOption = ref([])
const isDisabled = ref(false)
onMounted(() => {
$table.value?.handleSearch()
api.getDepts().then((res) => (deptOption.value = res.data))
})
const deptRules = {
name: [
{
required: true,
message: '请输入部门名称',
trigger: ['input', 'blur', 'change'],
},
],
}
async function addDepts() {
isDisabled.value = false
handleAdd()
}
const columns = [
{
title: '部门名称',
key: 'name',
width: 'auto',
align: 'center',
ellipsis: { tooltip: true },
},
{
title: '备注',
key: 'desc',
align: 'center',
width: 'auto',
ellipsis: { tooltip: true },
},
{
title: '操作',
key: 'actions',
width: 'auto',
align: 'center',
fixed: 'right',
render(row) {
return [
withDirectives(
h(
NButton,
{
size: 'small',
type: 'primary',
style: 'margin-right: 8px;',
onClick: () => {
console.log('row', row.parent_id)
if (row.parent_id === 0) {
isDisabled.value = true
} else {
isDisabled.value = false
}
handleEdit(row)
},
},
{
default: () => '编辑',
icon: renderIcon('material-symbols:edit', { size: 16 }),
}
),
[[vPermission, 'post/api/v1/dept/update']]
),
h(
NPopconfirm,
{
onPositiveClick: () => handleDelete({ dept_id: row.id }, false),
onNegativeClick: () => {},
},
{
trigger: () =>
withDirectives(
h(
NButton,
{
size: 'small',
type: 'error',
},
{
default: () => '删除',
icon: renderIcon('material-symbols:delete-outline', { size: 16 }),
}
),
[[vPermission, 'delete/api/v1/dept/delete']]
),
default: () => h('div', {}, '确定删除该部门吗?'),
}
),
]
},
},
]
</script>
<template>
<!-- 业务页面 -->
<CommonPage show-footer title="部门列表">
<template #action>
<div>
<NButton
v-permission="'post/api/v1/dept/create'"
class="float-right mr-15"
type="primary"
@click="addDepts"
>
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />新建部门
</NButton>
</div>
</template>
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:get-data="api.getDepts"
>
<template #queryBar>
<QueryBarItem label="部门名称" :label-width="80">
<NInput
v-model:value="queryItems.name"
clearable
type="text"
placeholder="请输入部门名称"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
</CrudTable>
<!-- 新增/编辑 弹窗 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
@save="handleSave"
>
<NForm
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
:rules="deptRules"
>
<NFormItem label="父级部门" path="parent_id">
<NTreeSelect
v-model:value="modalForm.parent_id"
:options="deptOption"
key-field="id"
label-field="name"
placeholder="请选择父级部门"
clearable
default-expand-all
:disabled="isDisabled"
></NTreeSelect>
</NFormItem>
<NFormItem label="部门名称" path="name">
<NInput v-model:value="modalForm.name" clearable placeholder="请输入部门名称" />
</NFormItem>
<NFormItem label="备注" path="desc">
<NInput v-model:value="modalForm.desc" type="textarea" clearable />
</NFormItem>
<NFormItem label="排序" path="order">
<NInputNumber v-model:value="modalForm.order" min="0"></NInputNumber>
</NFormItem>
</NForm>
</CrudModal>
</CommonPage>
</template>

View File

@ -25,7 +25,6 @@ import { formatDate, renderIcon } from '@/utils'
import { useCRUD } from '@/composables'
import api from '@/api'
import TheIcon from '@/components/icon/TheIcon.vue'
import { uniq } from 'lodash-es'
defineOptions({ name: '角色管理' })

View File

@ -12,6 +12,10 @@ import {
NSwitch,
NTag,
NPopconfirm,
NLayout,
NLayoutSider,
NLayoutContent,
NTreeSelect,
} from 'naive-ui'
import CommonPage from '@/components/page/CommonPage.vue'
@ -53,10 +57,12 @@ const {
})
const roleOption = ref([])
const deptOption = ref([])
onMounted(() => {
$table.value?.handleSearch()
api.getRoleList({ page: 1, page_size: 9999 }).then((res) => (roleOption.value = res.data))
api.getDepts().then((res) => (deptOption.value = res.data))
})
const columns = [
@ -104,6 +110,13 @@ const columns = [
return h('span', group)
},
},
{
title: '部门',
key: 'dept.name',
align: 'center',
width: 40,
ellipsis: { tooltip: true },
},
{
title: '超级用户',
key: 'is_superuser',
@ -169,6 +182,7 @@ const columns = [
onClick: () => {
// roles => role_ids
handleEdit(row)
modalForm.value.dept_id = row.dept?.id
modalForm.value.roles = row.roles.map((e) => (e = e.id))
},
},
@ -225,6 +239,7 @@ async function handleUpdateDisable(row) {
role_ids.push(e.id)
})
row.roles = role_ids
row.dept_id = row.dept?.id
try {
await api.updateUser(row)
$message?.success(row.is_active ? '已取消禁用该用户' : '已禁用该用户')
@ -237,6 +252,16 @@ async function handleUpdateDisable(row) {
}
}
const nodeProps = ({ option }) => {
return {
onClick() {
api.getUserList({ dept_id: option.id }).then((res) => {
$table.value.tableData = res.data
})
},
}
}
const validateAddUser = {
username: [
{
@ -299,110 +324,138 @@ const validateAddUser = {
</script>
<template>
<!-- 业务页面 -->
<CommonPage show-footer title="用户列表">
<template #action>
<NButton v-permission="'post/api/v1/user/create'" type="primary" @click="handleAdd">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />新建用户
</NButton>
</template>
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:get-data="api.getUserList"
>
<template #queryBar>
<QueryBarItem label="名称" :label-width="40">
<NInput
v-model:value="queryItems.username"
clearable
type="text"
placeholder="请输入用户名称"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
<QueryBarItem label="邮箱" :label-width="40">
<NInput
v-model:value="queryItems.email"
clearable
type="text"
placeholder="请输入邮箱"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
</CrudTable>
<!-- 新增/编辑 弹窗 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
@save="handleSave"
>
<NForm
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
:rules="validateAddUser"
<NLayout has-sider wh-full>
<NLayoutSider bordered content-style="padding: 24px;">
<h1>部门管理</h1>
<br />
<NTree
block-line
:data="deptOption"
key-field="id"
label-field="name"
default-expand-all
:node-props="nodeProps"
>
<NFormItem label="用户名称" path="username">
<NInput v-model:value="modalForm.username" clearable placeholder="请输入用户名称" />
</NFormItem>
<NFormItem label="邮箱" path="email">
<NInput v-model:value="modalForm.email" clearable placeholder="请输入邮箱" />
</NFormItem>
<NFormItem v-if="modalAction === 'add'" label="密码" path="password">
<NInput
v-model:value="modalForm.password"
show-password-on="mousedown"
type="password"
clearable
placeholder="请输入密码"
/>
</NFormItem>
<NFormItem v-if="modalAction === 'add'" label="确认密码" path="confirmPassword">
<NInput
v-model:value="modalForm.confirmPassword"
show-password-on="mousedown"
type="password"
clearable
placeholder="请确认密码"
/>
</NFormItem>
<NFormItem label="角色" path="roles">
<NCheckboxGroup v-model:value="modalForm.roles">
<NSpace item-style="display: flex;">
<NCheckbox
v-for="item in roleOption"
:key="item.id"
:value="item.id"
:label="item.name"
</NTree>
</NLayoutSider>
<NLayoutContent>
<CommonPage show-footer title="用户列表">
<template #action>
<NButton v-permission="'post/api/v1/user/create'" type="primary" @click="handleAdd">
<TheIcon icon="material-symbols:add" :size="18" class="mr-5" />新建用户
</NButton>
</template>
<!-- 表格 -->
<CrudTable
ref="$table"
v-model:query-items="queryItems"
:columns="columns"
:get-data="api.getUserList"
>
<template #queryBar>
<QueryBarItem label="名称" :label-width="40">
<NInput
v-model:value="queryItems.username"
clearable
type="text"
placeholder="请输入用户名称"
@keypress.enter="$table?.handleSearch()"
/>
</NSpace>
</NCheckboxGroup>
</NFormItem>
<NFormItem label="超级用户" path="is_superuser">
<NSwitch
v-model:value="modalForm.is_superuser"
size="small"
:checked-value="true"
:unchecked-value="false"
></NSwitch>
</NFormItem>
<NFormItem label="禁用" path="is_active">
<NSwitch
v-model:value="modalForm.is_active"
:checked-value="false"
:unchecked-value="true"
:default-value="true"
/>
</NFormItem>
</NForm>
</CrudModal>
</CommonPage>
</QueryBarItem>
<QueryBarItem label="邮箱" :label-width="40">
<NInput
v-model:value="queryItems.email"
clearable
type="text"
placeholder="请输入邮箱"
@keypress.enter="$table?.handleSearch()"
/>
</QueryBarItem>
</template>
</CrudTable>
<!-- 新增/编辑 弹窗 -->
<CrudModal
v-model:visible="modalVisible"
:title="modalTitle"
:loading="modalLoading"
@save="handleSave"
>
<NForm
ref="modalFormRef"
label-placement="left"
label-align="left"
:label-width="80"
:model="modalForm"
:rules="validateAddUser"
>
<NFormItem label="用户名称" path="username">
<NInput v-model:value="modalForm.username" clearable placeholder="请输入用户名称" />
</NFormItem>
<NFormItem label="邮箱" path="email">
<NInput v-model:value="modalForm.email" clearable placeholder="请输入邮箱" />
</NFormItem>
<NFormItem v-if="modalAction === 'add'" label="密码" path="password">
<NInput
v-model:value="modalForm.password"
show-password-on="mousedown"
type="password"
clearable
placeholder="请输入密码"
/>
</NFormItem>
<NFormItem v-if="modalAction === 'add'" label="确认密码" path="confirmPassword">
<NInput
v-model:value="modalForm.confirmPassword"
show-password-on="mousedown"
type="password"
clearable
placeholder="请确认密码"
/>
</NFormItem>
<NFormItem label="角色" path="roles">
<NCheckboxGroup v-model:value="modalForm.roles">
<NSpace item-style="display: flex;">
<NCheckbox
v-for="item in roleOption"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</NSpace>
</NCheckboxGroup>
</NFormItem>
<NFormItem label="超级用户" path="is_superuser">
<NSwitch
v-model:value="modalForm.is_superuser"
size="small"
:checked-value="true"
:unchecked-value="false"
></NSwitch>
</NFormItem>
<NFormItem label="禁用" path="is_active">
<NSwitch
v-model:value="modalForm.is_active"
:checked-value="false"
:unchecked-value="true"
:default-value="true"
/>
</NFormItem>
<NFormItem label="部门" path="dept_id">
<NTreeSelect
v-model:value="modalForm.dept_id"
:options="deptOption"
key-field="id"
label-field="name"
placeholder="请选择部门"
clearable
default-expand-all
></NTreeSelect>
</NFormItem>
</NForm>
</CrudModal>
</CommonPage>
</NLayoutContent>
</NLayout>
<!-- 业务页面 -->
</template>