部门线P0最小批:极简部门管理页(只读树+状态徽标+启停确认,权限管理分组下)
build / build (push) Successful in 1m52s

This commit is contained in:
chenxing
2026-10-20 22:37:43 +08:00
parent 1a011656ec
commit 24ae7b13fe
5 changed files with 227 additions and 0 deletions
+2
View File
@@ -30,6 +30,8 @@ export default {
'SysGroup.move': ['POST', '/api/sysgroup/Move'],
'SysGroup.sysGroupEdit': ['POST', '/api/sysgroup/Edit/{id}'],
'SysGroup.sysGroupRemove': ['POST', '/api/sysgroup/Delete/{id}'],
'SysGroup.disable': ['POST', '/api/sysgroup/Disable/{id}'],
'SysGroup.enable': ['POST', '/api/sysgroup/Enable/{id}'],
'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
'PushSubscription.test': ['POST', '/api/push/Test'],
+46
View File
@@ -1538,6 +1538,52 @@ declare global {
>(
config: Config
): Alova2Method<null, 'SysGroup.sysGroupRemove', Config>;
/**
* ---
*
* [POST]
*
* **path:** /api/sysgroup/Disable/{id}
*
* ---
*
* **Response**
* ```ts
* type Response = null
* ```
*/
disable<
Config extends Alova2MethodConfig<null> & {
pathParams: {
id: number;
};
}
>(
config: Config
): Alova2Method<null, 'SysGroup.disable', Config>;
/**
* ---
*
* [POST]
*
* **path:** /api/sysgroup/Enable/{id}
*
* ---
*
* **Response**
* ```ts
* type Response = null
* ```
*/
enable<
Config extends Alova2MethodConfig<null> & {
pathParams: {
id: number;
};
}
>(
config: Config
): Alova2Method<null, 'SysGroup.enable', Config>;
/**
* ---
*
+1
View File
@@ -48,6 +48,7 @@ export function getMenuList(userRole?: UserRoleEnum): Promise<IModule[]> {
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
{ id: 45, modId: 1106, name: "部门管理", page: "/dept-manage", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
],
},
// 4. 日志管理(父目录)
+9
View File
@@ -12,6 +12,7 @@ import PermissionView from "@/views/PermissionView.vue";
import OperationLogList from "@/views/oplog/OperationLogList.vue";
import OapiLogList from "@/views/oapilog/OapiLogList.vue";
import PushLog from "@/views/push/PushLog.vue";
import DeptManage from "@/views/dept-manage/index.vue";
import NoAuth from "@/views/NoAuth.vue";
// 管理端四级访问控制:meta.roles 为空/未声明 = 登录即可访问
@@ -105,6 +106,14 @@ const routes: RouteRecordRaw[] = [
component: PermissionView,
meta: { title: "权限一览", icon: "icon-list", modId: 1105, roles: ALL_ADMIN },
},
// ===== 部门管理(2026-10-20 部门线 P0 最小批:只读树 + 状态徽标 + 启停)=====
// 挂「权限管理」分组(菜单 id=45);无删除/成员/职务/新建入口——建部门=建组回「组,用户管理」
{
path: "/dept-manage",
name: "DeptManage",
component: DeptManage,
meta: { title: "部门管理", icon: "icon-list", modId: 1106, parentId: 1108, roles: ALL_ADMIN },
},
{
path: "/push-log",
name: "PushLog",
+169
View File
@@ -0,0 +1,169 @@
<template>
<div class="page-wrapper tree-wrapper dept-manage-page">
<!-- 左侧:只读组树(复用 GroupTreeSearchPanel,无新增/编辑/删除操作栏) -->
<div class="tree-container dept-manage-tree">
<GroupTreeSearchPanel ref="refTreePanel" v-model:curTreeNode="selectedNode" :treeApi="treeApi"
:defaultSearchVisible="true" @on-change="onTreeChange" @on-tree-data="onTreeData" />
</div>
<!-- 右侧:部门管理面板(P0 最小批:只读 + 状态徽标 + 启停) -->
<div class="dept-manage-main">
<Card dis-hover :bordered="false" class="mb-12">
<template #title>
<span>部门管理</span>
<Tag v-if="deptContainerName" color="primary" class="ml-8">{{ deptContainerName }}</Tag>
</template>
<Alert v-if="!selectedNode?.id" show-icon type="info">请在左侧选择组织节点</Alert>
<template v-else-if="!isDeptContainer(selectedNode)">
<Alert show-icon type="warning">
当前节点【{{ selectedNode.name }}】不是部门容器(教师组),部门 = 教师组的直接子节点。
请选择学校下的「教师组」节点。
</Alert>
</template>
<template v-else>
<Alert v-if="deptRows.length === 0" show-icon type="info">
该教师组暂无子节点:按口径「空容器自身算部门」,容器本身即可作为部门使用。
</Alert>
<Table v-else :columns="columns" :data="deptRows" size="small" :border="false">
<template #status="{ row }">
<Tag :color="isDisabled(row) ? 'error' : 'success'">{{ isDisabled(row) ? '已停用' : '启用' }}</Tag>
</template>
<template #action="{ row }">
<Button v-if="!isDisabled(row)" size="small" type="error" ghost @click="onDisable(row)">停用</Button>
<Button v-else size="small" type="success" ghost @click="onEnable(row)">启用</Button>
</template>
</Table>
</template>
</Card>
<Alert show-icon type="info" class="usage-note">
说明:停用只影响「选」(新建事项/流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析)。
新建部门请前往「组,用户管理」建组;本页不提供删除。
</Alert>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import Apis from "@/api";
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
import MyConfirm from "@/components/MyConfirm";
import { Message } from "view-ui-plus";
/**
* 部门管理(2026-10-20 部门线 P0 最小批)。
* 口径:部门 = type=7(教师组)的直接子节点;空教师组自身算部门(与 flow-center collectDeptGroups 同源)。
* 只做:只读组树 + 状态徽标 + 停用(确认弹窗)/启用(免确认)。
* 不做(P1~P3):删除入口、占用弹窗、成员区块、「职务」列、新建部门、「已停用」筛选。
*/
const TEACHER_GROUP_TYPE = 7;
const treeApi = () => Apis.SysGroup.getTree();
const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
const selectedNode = ref<any>({});
const treeData = ref<any[]>([]);
const columns = [
{ title: '部门名称', key: 'name', tree: true },
{ title: '状态', slot: 'status', width: 100 },
{ title: '操作', slot: 'action', width: 100 },
];
const deptContainerName = computed(() =>
selectedNode.value?.type === TEACHER_GROUP_TYPE ? selectedNode.value.name : '');
function onTreeChange(node: any) {
selectedNode.value = node || {};
}
function onTreeData(tree: any[]) {
treeData.value = tree || [];
}
/** 部门口径判定:type=7 教师组是部门容器 */
function isDeptContainer(node: any): boolean {
return node?.type === TEACHER_GROUP_TYPE;
}
/** 教师组无子节点时空容器自身算部门(flow-center 同口径) */
const deptRows = computed<any[]>(() => {
if (!isDeptContainer(selectedNode.value)) return [];
const children = selectedNode.value?.children || [];
if (children.length === 0) return [selectedNode.value];
return children;
});
function isDisabled(row: any): boolean {
return row?.status === 1;
}
function onDisable(row: any) {
MyConfirm({
title: '停用部门',
msg: `确定停用部门【${row.name}】?`,
info: '停用只影响「选」(新建时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。',
async onOk() {
try {
await Apis.SysGroup.disable({ pathParams: { id: row.id } });
Message.success('已停用');
await refTreePanel.value?.refreshTree();
reselect(row.id);
} catch (e: any) {
console.warn(e?.message || '停用失败');
}
},
});
}
async function onEnable(row: any) {
try {
await Apis.SysGroup.enable({ pathParams: { id: row.id } });
Message.success('已启用');
await refTreePanel.value?.refreshTree();
reselect(row.id);
} catch (e: any) {
console.warn(e?.message || '启用失败');
}
}
/** 刷新后尽量回到原选中节点(找不回则清空,用户重选) */
function reselect(nodeId: number) {
const found = findNodeById(treeData.value, nodeId);
if (found) {
selectedNode.value = found;
refTreePanel.value?.selectNode?.(found);
}
}
function findNodeById(list: any[], id: number): any {
for (const n of list || []) {
if (n?.id === id) return n;
const hit = findNodeById(n?.children || [], id);
if (hit) return hit;
}
return null;
}
</script>
<style scoped lang="scss">
.dept-manage-page {
display: flex;
gap: 12px;
align-items: stretch;
.dept-manage-tree {
flex: 0 0 320px;
min-height: 100%;
overflow: auto;
}
.dept-manage-main {
flex: 1;
min-width: 0;
.usage-note {
margin-top: 4px;
}
}
}
</style>