feat(dept): 部门管理改卡片视图,树只到学校层+含子组开关,修复只显示教师组
build / build (push) Successful in 2m22s

- 右侧由表格改为部门卡片网格;有下级部门的部门渲染为跨整行的分组卡(办公室 → 校办/宣传/财务/党建),叶子部门为单卡
- 左侧沿用 zTree,剪枝到「学校」层;学校行尾挂「包含子组」开关(MyTree 新增 attachNodeControl,不污染 node.name、不触发节点选中)
- 按访问级别分视角:超管/局端显示树,学校管理员无树直接列本校部门(多校走顶部下拉)
- 修复「只显示教师组一行」:右侧数据改为从全量组树按 id 回查 children,不再使用 treeLink 中已清空 children 的节点
This commit is contained in:
chenxing
2026-10-21 10:31:48 +08:00
parent e83983b41c
commit 8bbe3c654b
4 changed files with 566 additions and 96 deletions
+6
View File
@@ -215,6 +215,11 @@ function getNodeById(id: number) {
return refMyTree.value?.getNodeById(id);
}
/** 在指定节点后追加自定义控件(见 MyTree.attachNodeControl) */
function attachNodeControl(id: number, className: string, html: string) {
return refMyTree.value?.attachNodeControl(id, className, html);
}
function getCheckedNodes() {
return refMyTree.value?.getCheckedNodes() || [];
}
@@ -257,6 +262,7 @@ defineExpose({
removeNode,
renameNode,
getNodeById,
attachNodeControl,
getCheckedNodes,
checkNodeById,
addNote,
+27
View File
@@ -222,6 +222,33 @@ export default {
if (on) a.addClass('ztree-search-highlight');
else a.removeClass('ztree-search-highlight');
},
/**
* 给指定节点追加自定义 DOM 控件(渲染在节点文字之后)。
* 用途:在 zTree 节点行尾挂业务控件(如开关),不污染 node.name、
* 也不触发节点选中(zTree 的 onClick 绑定在节点 a 标签上,控件在其外部)。
* 树数据重载(initTree)后需重新挂载。
* @param {*} id 节点 id
* @param {string} className 控件容器类名(同时作为去重标识)
* @param {string} html 控件 HTML
* @returns {HTMLElement|null} 控件元素,失败返回 null
*/
attachNodeControl(id, className, html) {
const _domId = this.domId;
const treeObj = $.fn.zTree.getZTreeObj(_domId);
if (!treeObj || !id || !className) return null;
const node = treeObj.getNodesByFilter(n => n.id == id, true);
if (!node) return null;
const li = document.getElementById(node.tId);
if (!li) return null;
li.querySelectorAll("." + className).forEach(el => el.remove());
const box = document.createElement("span");
box.className = className;
box.innerHTML = html;
const a = document.getElementById(node.tId + "_a");
if (a && a.parentNode) a.parentNode.insertBefore(box, a.nextSibling);
else li.appendChild(box);
return box;
},
clearHighlight() {
(this.highlightNodes || []).forEach(n => this.setHighlight(n, false));
this.highlightNodes = [];
+195
View File
@@ -0,0 +1,195 @@
<template>
<article class="dept-card" :class="{ 'is-off': disabled, 'has-kids': showKids }">
<div class="row-1">
<span class="status-dot" :class="{ 'is-off': disabled }"></span>
<h3 class="card-name" :title="dept.name">{{ dept.name }}</h3>
<span class="sw" :class="{ 'is-off': disabled }" title="启用 / 停用" @click.stop="emit('toggle', dept)"></span>
</div>
<div class="row-2">
<span class="sub">{{ subText }}</span>
</div>
<div v-if="showKids" class="kid-list">
<DeptCard v-for="kid in kids" :key="kid.id" :dept="kid" :level="level + 1"
:include-children="includeChildren" @toggle="onKidToggle" />
</div>
</article>
</template>
<script lang="ts" setup>
import { computed } from "vue";
/** 部门口径:教师组(type=7)的直接子节点为部门,下可再挂子部门 */
const TEACHER_GROUP_TYPE = 7;
const props = withDefaults(defineProps<{
dept: any;
/** 层级深度:0 = 一级部门,>0 = 下级部门 */
level?: number;
/** 是否包含下级部门(对应学校上的「包含子组」开关) */
includeChildren?: boolean;
}>(), {
level: 0,
includeChildren: true,
});
const emit = defineEmits<{ (e: "toggle", dept: any): void }>();
const onKidToggle = (dept: any) => emit("toggle", dept);
const kids = computed<any[]>(() => props.dept?.children || []);
const showKids = computed(() => props.includeChildren !== false && kids.value.length > 0);
const disabled = computed(() => props.dept?.status === 1);
const subText = computed(() => {
if (props.dept?.type !== TEACHER_GROUP_TYPE) return "学生组 · 非部门";
if (props.level > 0) return "下级部门";
return kids.value.length ? `${kids.value.length} 个下级部门` : "无下级部门";
});
</script>
<style scoped lang="scss">
.dept-card {
display: flex;
flex-direction: column;
gap: 9px;
padding: 13px 14px;
background: var(--panel, #fff);
border: 1px solid var(--line, #e9edf5);
border-radius: var(--r, 10px);
box-shadow: 0 1px 2px rgba(30, 38, 53, .03);
transition: border-color .15s, box-shadow .15s;
}
.dept-card:hover {
border-color: #dbe2fb;
box-shadow: 0 6px 18px rgba(30, 38, 53, .07);
}
/* 有下级部门:跨整行成组,避免层级被压平 */
.dept-card.has-kids {
grid-column: 1 / -1;
}
.dept-card.is-off {
background: #fbfcfd;
border-style: dashed;
}
.row-1 {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
flex: 0 0 6px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green, #20a162);
}
.status-dot.is-off {
background: #cbd2df;
}
.card-name {
flex: 1;
min-width: 0;
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
color: var(--text, #1e2635);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dept-card.is-off .card-name {
color: var(--text-3, #98a1b2);
}
.row-2 {
display: flex;
align-items: center;
gap: 8px;
min-height: 26px;
padding-top: 9px;
border-top: 1px dashed var(--line-2, #f0f3f9);
font-size: 12px;
color: var(--text-3, #98a1b2);
}
.row-2 .sub {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 启停开关 */
.sw {
position: relative;
flex: 0 0 auto;
width: 36px;
height: 20px;
border-radius: 10px;
background: var(--brand, #577afb);
cursor: pointer;
transition: background .15s;
}
.sw::after {
content: '';
position: absolute;
top: 2px;
left: 18px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(30, 38, 53, .22);
transition: left .15s;
}
.sw.is-off {
background: #d3d9e6;
}
.sw.is-off::after {
left: 2px;
}
/* 下级部门区 */
.kid-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 11px;
padding-top: 11px;
border-top: 1px dashed var(--line-2, #f0f3f9);
}
.kid-list :deep(.dept-card) {
flex: 1 1 168px;
min-width: 150px;
background: #fafbfe;
box-shadow: none;
}
.kid-list :deep(.dept-card:hover) {
background: #fff;
}
.kid-list :deep(.dept-card .card-name) {
font-size: 13.5px;
font-weight: 500;
}
/* 下级部门里若还有下级,占满一行表达更深层级 */
.kid-list :deep(.dept-card.has-kids) {
flex: 1 1 100%;
}
</style>
+338 -96
View File
@@ -1,147 +1,284 @@
<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>
<div v-if="!roleReady" class="dept-loading">加载中…</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 }}】不是部门容器(教师组),部门 = 教师组的直接子节点。
请选择学校下的「教师组」节点。
<template v-else>
<!-- 左侧:zTree 组树(超管 / 局端;只到学校层,学校行带「包含子组」开关) -->
<div v-if="showTree" class="tree-container dept-manage-tree">
<div class="tree-hint">树只到「学校」层;行尾开关控制该校是否含下级部门。</div>
<GroupTreeSearchPanel ref="refTreePanel" v-model:curTreeNode="selectedNode" :treeApi="treeApi"
:defaultSearchVisible="true" @on-change="onTreeChange" @on-tree-data="onTreeData" />
</div>
<!-- 右侧:部门卡片 -->
<div class="dept-manage-main">
<Card dis-hover :bordered="false" class="mb-12">
<template #title>
<div class="panel-title">
<span>部门管理</span>
<Tag v-if="currentSchoolName" color="primary" class="ml-8">{{ currentSchoolName }}</Tag>
<Select v-if="multiSchool" v-model="currentSchoolId" size="small" class="ml-8 school-select">
<Option v-for="s in schoolOptions" :key="s.id" :value="s.id">{{ s.name }}</Option>
</Select>
<span class="inc-ctrl ml-auto" title="打开后列出该校下所有层级的部门">
<span class="inc-label">包含子组</span>
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
</span>
</div>
</template>
<Alert v-if="!currentSchoolName" show-icon type="info">
{{ showTree ? '请在左侧选择学校节点' : '当前账号没有可管理的学校' }}
</Alert>
</template>
<template v-else>
<Alert v-if="deptRows.length === 0" show-icon type="info">
该教师组暂无子节点:按口径「空容器自身算部门」,容器本身即可作为部门使用。
<Alert v-else-if="!deptList.length" 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 v-else class="dept-grid">
<DeptCard v-for="d in deptList" :key="d.id" :dept="d" :include-children="includeOn"
@toggle="onToggle" />
</div>
</Card>
<Alert show-icon type="info" class="usage-note">
说明:停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析)。
关闭「包含子组」后只展示一级部门;打开后连同下级部门(如办公室下的校办 / 宣传 / 财务 / 党建)一并列出。
</Alert>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { cloneDeep } from "lodash";
import Apis from "@/api";
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
import MyConfirm from "@/components/MyConfirm";
import DeptCard from "./DeptCard.vue";
import { Message } from "view-ui-plus";
import { useDefaultStore } from "@/store";
import { UserRoleEnum } from "@/types";
/**
* 部门管理(2026-10-20 部门线 P0 最小批)。
* 口径:部门 = type=7(教师组)的直接子节点;空教师组自身算部门(与 flow-center collectDeptGroups 同源)。
* 只做:只读组树 + 状态徽标 + 停用(确认弹窗)/启用(免确认)。
* 不做(P1~P3):删除入口、占用弹窗、成员区块、「职务」列、新建部门、「已停用」筛选。
* 部门管理(2026-10-21 卡片版)。
* 口径:部门 = 教师组(type=7)的直接子节点,下可再挂子部门。
* 视角:超管 / 局端 → 左侧 zTree 树只到「学校」层,学校行带「包含子组」开关;
* 学校管理员 → 无树,直接列出本校部门(多校时顶部下拉切换)。
*/
const TEACHER_GROUP_TYPE = 7;
const SCHOOL_GROUP_TYPE = 2;
const store = useDefaultStore();
const isSchoolMode = computed(() => store.userRole === UserRoleEnum.School);
/** 超管 / 局端才需要左侧树 */
const showTree = computed(() => !isSchoolMode.value);
/** 角色未落定前不渲染,避免先按默认角色取数再纠正 */
const roleReady = computed(() => store.roleLoaded);
const treeApi = () => Apis.SysGroup.getTree();
const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
const selectedNode = ref<any>({});
const treeData = ref<any[]>([]);
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
const fullTree = ref<any[]>([]);
/** 每个学校的「包含子组」开关(按学校 id 存 localStorage,刷新不丢;未落库到后端) */
const INCLUDE_STORE_KEY = "userbase.dept.include";
const columns = [
{ title: '部门名称', key: 'name', tree: true },
{ title: '状态', slot: 'status', width: 100 },
{ title: '操作', slot: 'action', width: 100 },
];
function loadIncludeMap(): Record<number, boolean> {
try {
return JSON.parse(localStorage.getItem(INCLUDE_STORE_KEY) || "{}") || {};
} catch {
return {};
}
}
const deptContainerName = computed(() =>
selectedNode.value?.type === TEACHER_GROUP_TYPE ? selectedNode.value.name : '');
const includeMap = ref<Record<number, boolean>>(loadIncludeMap());
/** 树上挂载的开关控件,切换时同步外观 */
const controlMap = ref<Record<number, HTMLElement>>({});
/** 当前学校:超管由树选中,学校管理员默认取可管理学校 */
const currentSchoolId = ref<number | undefined>(undefined);
const schoolIds = computed<number[]>(() => store.userScope?.schoolGroupIds ?? []);
const multiSchool = computed(() => isSchoolMode.value && schoolIds.value.length > 1);
const schoolOptions = computed(() => schoolIds.value.map((id) => ({
id,
name: findNodeById(fullTree.value, id)?.name ?? String(id),
})));
const currentSchool = computed(() => findNodeById(fullTree.value, currentSchoolId.value));
const currentSchoolName = computed(() => currentSchool.value?.name || "");
/** 部门 = 该校各教师组(type=7)的直接子节点 */
const deptList = computed<any[]>(() => {
const school = currentSchool.value;
if (!school) return [];
const rows: any[] = [];
(school.children || [])
.filter((c: any) => c.type === TEACHER_GROUP_TYPE)
.forEach((t: any) => rows.push(...(t.children || [])));
return rows;
});
const includeOn = computed(() =>
currentSchoolId.value ? includeMap.value[currentSchoolId.value] !== false : false);
/* ================= 数据加载 ================= */
/** 树数据源:取全量树存起来给右侧用,树上只渲染到学校层 */
async function treeApi() {
const full: any[] = (await Apis.SysGroup.getTree()) || [];
fullTree.value = cloneDeep(full);
return pruneToSchoolLevel(full);
}
/** 剪枝:学校节点保留但不再下钻(超管视角树只到学校层) */
function pruneToSchoolLevel(list: any[]): any[] {
return (list || []).map((n: any) => {
const node: any = { ...n };
if (node.type === SCHOOL_GROUP_TYPE) {
delete node.children;
return node;
}
if (Array.isArray(node.children) && node.children.length) {
node.children = pruneToSchoolLevel(node.children);
}
return node;
});
}
function findNodeById(list: any[], id?: number | null): any {
if (id == null) return null;
for (const n of list || []) {
if (Number(n.id) === Number(id)) return n;
const hit = findNodeById(n.children || [], id);
if (hit) return hit;
}
return null;
}
/* ================= 交互 ================= */
function onTreeChange(node: any) {
selectedNode.value = node || {};
if (node?.type === SCHOOL_GROUP_TYPE) {
currentSchoolId.value = node.id;
} else if (node?.id) {
// 点到中间层(区域 / 学校组等)不作为当前学校
currentSchoolId.value = undefined;
}
}
function onTreeData(tree: any[]) {
treeData.value = tree || [];
/** 树渲染完成后,给每个学校节点挂「包含子组」开关 */
async function onTreeData(tree: any[]) {
await Promise.resolve();
requestAnimationFrame(() => mountSchoolControls(tree));
}
/** 部门口径判定:type=7 教师组是部门容器 */
function isDeptContainer(node: any): boolean {
return node?.type === TEACHER_GROUP_TYPE;
function collectSchools(list: any[], out: any[] = []): any[] {
(list || []).forEach((n) => {
if (n?.type === SCHOOL_GROUP_TYPE) out.push(n);
collectSchools(n?.children, out);
});
return out;
}
/** 教师组无子节点时空容器自身算部门(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 mountSchoolControls(tree: any[]) {
const map: Record<number, HTMLElement> = {};
collectSchools(tree || []).forEach((s) => {
const el = refTreePanel.value?.attachNodeControl(s.id, "dept-inc-ctrl", '<i class="sw-mini"></i>');
if (!el) return;
el.addEventListener("click", (e) => {
e.stopPropagation();
toggleInclude(s.id);
});
map[s.id] = el;
syncIncVisual(s.id, el);
});
controlMap.value = map;
}
function onDisable(row: any) {
/** 同步树上开关外观:容器与内部滑块都要带 is-on */
function syncIncVisual(schoolId: number, el?: HTMLElement) {
const box = el || controlMap.value[schoolId];
if (!box) return;
const on = includeMap.value[schoolId] !== false;
box.classList.toggle("is-on", on);
box.querySelector(".sw-mini")?.classList.toggle("is-on", on);
}
function toggleInclude(schoolId?: number) {
const sid = schoolId ?? currentSchoolId.value;
if (!sid) return;
includeMap.value = { ...includeMap.value, [sid]: includeMap.value[sid] === false };
try {
localStorage.setItem(INCLUDE_STORE_KEY, JSON.stringify(includeMap.value));
} catch {
/* 隐私模式等场景写不进去也不影响使用 */
}
syncIncVisual(sid);
}
function onToggle(dept: any) {
if (!dept?.id) return;
if (dept.status === 1) {
doEnable(dept);
return;
}
MyConfirm({
title: '停用部门',
msg: `确定停用部门【${row.name}】?`,
info: '停用只影响「选」(新建时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。',
title: "停用部门",
msg: `确定停用部门【${dept.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 || '停用失败');
}
await doDisable(dept);
},
});
}
async function onEnable(row: any) {
async function doDisable(dept: any) {
try {
await Apis.SysGroup.enable({ pathParams: { id: row.id } });
Message.success('已启用');
await refTreePanel.value?.refreshTree();
reselect(row.id);
await Apis.SysGroup.disable({ pathParams: { id: dept.id } });
Message.success("已停用");
await reloadData();
} catch (e: any) {
console.warn(e?.message || '启用失败');
console.warn(e?.message || "停用失败");
}
}
/** 刷新后尽量回到原选中节点(找不回则清空,用户重选) */
function reselect(nodeId: number) {
const found = findNodeById(treeData.value, nodeId);
if (found) {
selectedNode.value = found;
refTreePanel.value?.selectNode?.(found);
async function doEnable(dept: any) {
try {
await Apis.SysGroup.enable({ pathParams: { id: dept.id } });
Message.success("已启用");
await reloadData();
} catch (e: any) {
console.warn(e?.message || "启用失败");
}
}
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;
/** 重新取数:有树走树刷新(treeApi 内同步 fullTree),无树自行取 */
async function reloadData() {
if (showTree.value) {
await refTreePanel.value?.refreshTree();
return;
}
const full: any[] = (await Apis.SysGroup.getTree()) || [];
fullTree.value = cloneDeep(full);
}
/* ================= 初始化 ================= */
watch(() => store.roleLoaded, (loaded) => {
if (!loaded || !isSchoolMode.value) return;
loadSchoolScope();
}, { immediate: true });
/** 学校管理员:GetTree 返回的就是各学校子树(多校 = 多根) */
async function loadSchoolScope() {
const full: any[] = (await Apis.SysGroup.getTree()) || [];
fullTree.value = cloneDeep(full);
const ids = schoolIds.value;
if (ids.length) {
currentSchoolId.value = ids[0];
} else if (full.length) {
currentSchoolId.value = full[0]?.id;
}
return null;
}
</script>
@@ -155,6 +292,15 @@ function findNodeById(list: any[], id: number): any {
flex: 0 0 320px;
min-height: 100%;
overflow: auto;
.tree-hint {
font-size: 11.5px;
line-height: 1.6;
color: var(--text-3, #98a1b2);
padding: 0 2px 8px;
border-bottom: 1px dashed var(--line-2, #f0f3f9);
margin-bottom: 6px;
}
}
.dept-manage-main {
@@ -166,4 +312,100 @@ function findNodeById(list: any[], id: number): any {
}
}
}
.dept-loading {
flex: 1;
text-align: center;
padding-top: 80px;
color: var(--text-3, #98a1b2);
}
.panel-title {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.school-select {
width: 180px;
}
/* 面板头的「包含子组」开关 */
.inc-ctrl {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
font-size: 12.5px;
color: var(--text-2, #5a6478);
}
/* 「包含子组」开关:面板头与 zTree 树上共用同一套样式。
树上的控件是运行时创建的 DOM,没有 scoped 标记,只能用 :deep 命中 */
:deep(.sw-mini) {
position: relative;
display: inline-block;
width: 28px;
height: 16px;
border-radius: 8px;
background: #d3d9e6;
transition: background .15s;
&::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 2px rgba(30, 38, 53, .2);
transition: left .15s;
}
&.is-on {
background: var(--brand, #577afb);
&::after {
left: 14px;
}
}
}
/* 部门卡片网格 */
.dept-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
gap: 12px;
align-items: start;
}
/* 树上(zTree)学校节点行尾的开关控件:
校名长时会把开关挤出可视区,故绝对定位固定在行尾,同时给节点文字留出余量 */
:deep(.ztree li) {
position: relative;
}
:deep(.ztree li a) {
display: inline-block;
max-width: calc(100% - 46px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
:deep(.dept-inc-ctrl) {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
cursor: pointer;
z-index: 1;
}
</style>