build / build (push) Successful in 3m51s
- 系统部门:自写 table 换成 iView Table(border + 表头灰底 + 行 hover), 层级仍用名称列缩进 + 折叠;状态改为可点击的边框 Tag(已启用/已停用) - 标题行:去掉 SYSTEM / CUSTOM 英文,「包含子组」移到来源切换左侧并整体右对齐, 切换视图时标签位置不再左右跳动 - 自定义部门:同样换 iView Table;表格内不再拖行,改为「排序管理」按钮 + 弹框拖动, 保存后按新顺序提交 customDeptSort;操作列保留 编辑/成员/删除 - 修正 Tag 上的 @click.native(Vue 3 已移除 .native)
731 lines
24 KiB
Vue
731 lines
24 KiB
Vue
<template>
|
||
<div class="page-wrapper tree-wrapper dept-manage-page">
|
||
<div v-if="!roleReady" class="dept-loading">加载中…</div>
|
||
|
||
<template v-else>
|
||
<!-- 左侧:zTree 组树(超管 / 局端;只到学校层) -->
|
||
<div v-if="showTree" class="tree-container dept-manage-tree">
|
||
<GroupTreeSearchPanel class="tree-inner" ref="refTreePanel" v-model:curTreeNode="selectedNode"
|
||
:treeApi="treeApi" :defaultSearchVisible="true" @on-change="onTreeChange" />
|
||
</div>
|
||
|
||
<!-- 右侧:部门表格(白卡由主题的 .main-container 提供) -->
|
||
<div class="main-container dept-manage-main">
|
||
<div class="panel-title">
|
||
<span class="panel-caption">部门管理</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>
|
||
<!-- 来源切换:只切右侧管理视图,学校上下文共用,不改底层组织树 -->
|
||
<div class="title-tail">
|
||
<span v-if="source === 'SYSTEM'" class="inc-ctrl" title="打开后列出该校下所有层级的部门"
|
||
@click="toggleInclude">
|
||
<span class="inc-label">包含子组</span>
|
||
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
||
</span>
|
||
<div class="source-tabs">
|
||
<span class="source-tab" :class="{ 'is-active': source === 'SYSTEM' }"
|
||
@click="source = 'SYSTEM'">系统部门</span>
|
||
<span class="source-tab" :class="{ 'is-active': source === 'CUSTOM' }"
|
||
@click="source = 'CUSTOM'">自定义部门</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="source === 'SYSTEM'">
|
||
<Alert show-icon type="success" class="usage-note">
|
||
<b>设置说明:</b>
|
||
停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析);
|
||
关闭「包含子组」只展示一级部门,打开后连同下级部门一并列出。
|
||
</Alert>
|
||
|
||
<Alert v-if="!currentSchoolName" show-icon type="info" class="state-tip">
|
||
{{ showTree ? '请在左侧选择学校节点' : '当前账号没有可管理的学校' }}
|
||
</Alert>
|
||
<Alert v-else-if="!deptRows.length" show-icon type="info" class="state-tip">
|
||
该校教师组下暂无部门。部门 = 教师组的直接子节点,可前往「组,用户管理」建组。
|
||
</Alert>
|
||
|
||
<template v-else>
|
||
<div class="row-sum">{{ deptRows.length }} 个一级部门 · 共 {{ flatRows.length }} 行</div>
|
||
<Table class="dept-itable" border :columns="sysColumns" :data="flatRows"
|
||
:row-class-name="rowClassName" :loading="false" no-data-text="暂无部门">
|
||
<template #name="{ row }">
|
||
<span class="nmcell" :style="{ paddingLeft: row.level * 20 + 'px' }">
|
||
<i v-if="row.kidCount" class="tw" @click.stop="toggleRow(row.id)">{{ row.open ? '▾' : '▸' }}</i>
|
||
<i v-else class="tw blank"></i>
|
||
<span class="nm" :class="{ root: row.level === 0 }" :title="row.name">{{ row.name }}</span>
|
||
<span v-if="row.kidCount" class="cnt">{{ row.kidCount }} 个下级</span>
|
||
<span v-if="row.isStudent" class="cnt grey">学生组</span>
|
||
</span>
|
||
</template>
|
||
<template #members="{ row }">
|
||
<Tooltip v-if="row.memberCount" placement="top" max-width="360" :content="memberTip(row)">
|
||
<span class="mem-cell" @click="openMembers(row)">{{ memberText(row) }}</span>
|
||
</Tooltip>
|
||
<span v-else class="dim">无成员</span>
|
||
</template>
|
||
<template #status="{ row }">
|
||
<Tag type="border" :color="row.status === 1 ? 'error' : 'success'" class="sta-tag"
|
||
:title="row.status === 1 ? '点击启用' : '点击停用'" @click="onToggle(row.raw)">
|
||
{{ row.status === 1 ? '已停用' : '已启用' }}
|
||
</Tag>
|
||
</template>
|
||
</Table>
|
||
<div class="table-foot">共 {{ flatRows.length }} 个部门</div>
|
||
</template>
|
||
</template>
|
||
|
||
<CustomDeptPanel v-else :school-id="currentSchoolId" />
|
||
|
||
<!-- 系统部门人员:预览之外看全部,走 /api/sysuser/List?groupId= -->
|
||
<Modal v-model="memModal.visible" :title="`部门人员 · ${memModal.deptName}`" width="560"
|
||
:mask-closable="true" footer-hide>
|
||
<div class="mem-modal-body">
|
||
<div v-if="memModal.loading" class="mem-empty">加载中…</div>
|
||
<div v-else-if="!memModal.rows.length" class="mem-empty">该部门暂无直属成员</div>
|
||
<ul v-else class="mem-list">
|
||
<li v-for="u in memModal.rows" :key="u.id">
|
||
<span class="mname">{{ u.realName || u.account }}</span>
|
||
<span class="macc">{{ u.account }}</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
<div v-if="memModal.total > memModal.pageSize" class="mem-pager">
|
||
<Page :current="memModal.page" :total="memModal.total" :page-size="memModal.pageSize"
|
||
size="small" show-total @on-change="onMemPageChange" />
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, reactive, ref, watch } from "vue";
|
||
import { cloneDeep } from "lodash";
|
||
import Apis from "@/api";
|
||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||
import MyConfirm from "@/components/MyConfirm";
|
||
import CustomDeptPanel from "./CustomDeptPanel.vue";
|
||
import { Message } from "view-ui-plus";
|
||
import { useDefaultStore } from "@/store";
|
||
import { UserRoleEnum } from "@/types";
|
||
|
||
/**
|
||
* 部门管理(2026-10-23 表格版)。
|
||
* 口径:部门 = 教师组(type=7)的直接子节点,下可再挂子部门(只有教师组下才可能有多级)。
|
||
* 视角:超管 / 局端 → 左侧 zTree 树只到「学校」层,右侧是该学校的部门表格;
|
||
* 学校管理员 → 无树,直接列出本校部门(多校时顶部下拉切换)。
|
||
* 表格:层级用缩进 + 折叠表达;不提供新增 / 排序 / 行内操作,只保留启停与「包含子组」。
|
||
* 人员:getTree 已带 memberCount + memberNames(前 50),超出走 /api/sysuser/List 看全部。
|
||
*/
|
||
const TEACHER_GROUP_TYPE = 7;
|
||
const STUDENT_GROUP_TYPE = 8;
|
||
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 refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
|
||
const selectedNode = ref<any>({});
|
||
/** 部门来源页签:SYSTEM=原组织树视图(行为零改动);CUSTOM=学校自建分类/部门 */
|
||
const source = ref<"SYSTEM" | "CUSTOM">("SYSTEM");
|
||
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
|
||
const fullTree = ref<any[]>([]);
|
||
/** 当前学校:超管由树选中,学校管理员默认取可管理学校 */
|
||
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 || "");
|
||
|
||
/** 折叠状态:默认全展开,只记「被收起的 id」 */
|
||
const closedIds = ref<Set<number>>(new Set());
|
||
function toggleRow(id: number) {
|
||
const next = new Set(closedIds.value);
|
||
if (next.has(id)) next.delete(id);
|
||
else next.add(id);
|
||
closedIds.value = next;
|
||
}
|
||
|
||
interface DeptRow {
|
||
id: number;
|
||
name: string;
|
||
status: number;
|
||
parentName: string;
|
||
level: number;
|
||
isStudent: boolean;
|
||
memberCount: number;
|
||
memberNames: string[];
|
||
kidCount: number;
|
||
open: boolean;
|
||
no: number;
|
||
raw: any;
|
||
children: DeptRow[];
|
||
}
|
||
|
||
/** 表格树:一级部门来自各教师组下的直接子节点,子级递归;「包含子组」关闭时不带子级 */
|
||
const deptRows = computed<DeptRow[]>(() => {
|
||
const school = currentSchool.value;
|
||
if (!school) return [];
|
||
const rows: DeptRow[] = [];
|
||
(school.children || [])
|
||
.filter((c: any) => c.type === TEACHER_GROUP_TYPE)
|
||
.forEach((t: any) => {
|
||
(t.children || []).forEach((d: any) => rows.push(toRow(d, t.name, 0)));
|
||
});
|
||
return rows;
|
||
});
|
||
|
||
function toRow(d: any, parentName: string, level: number): DeptRow {
|
||
const kids: any[] = includeOn.value ? (d.children || []) : [];
|
||
return {
|
||
id: d.id,
|
||
name: d.name,
|
||
status: d.status ?? 0,
|
||
parentName,
|
||
level,
|
||
isStudent: d.type === STUDENT_GROUP_TYPE,
|
||
memberCount: Number(d.memberCount ?? 0),
|
||
memberNames: Array.isArray(d.memberNames) ? d.memberNames : [],
|
||
kidCount: kids.length,
|
||
open: !closedIds.value.has(d.id),
|
||
no: 0,
|
||
raw: d,
|
||
children: kids.map((k) => toRow(k, d.name, level + 1)),
|
||
};
|
||
}
|
||
|
||
/** 拍平成可渲染行(按折叠状态裁剪子级),并顺序编号 */
|
||
const flatRows = computed<DeptRow[]>(() => {
|
||
const out: DeptRow[] = [];
|
||
const walk = (list: DeptRow[]) => {
|
||
list.forEach((row) => {
|
||
out.push(row);
|
||
if (row.kidCount && row.open) walk(row.children);
|
||
});
|
||
};
|
||
walk(deptRows.value);
|
||
out.forEach((row, i) => (row.no = i + 1));
|
||
return out;
|
||
});
|
||
|
||
/** iView Table 列定义:层级用「部门名称」列的缩进 + 折叠表达,不用 Table 自带树(避免样式不可控) */
|
||
const sysColumns: any[] = [
|
||
{ title: "序号", type: "index", width: 80, align: "center" },
|
||
{ title: "部门名称", slot: "name", minWidth: 300 },
|
||
{ title: "上级", key: "parentName", width: 160 },
|
||
{ title: "人员", slot: "members", minWidth: 320 },
|
||
{ title: "状态", slot: "status", width: 120, align: "center" },
|
||
];
|
||
/** 停用行整行灰化 */
|
||
function rowClassName(row: any) {
|
||
return row?.status === 1 ? "is-off" : "";
|
||
}
|
||
|
||
/** 人员列:前 3 个姓名 + 剩余数;无姓名时退回「N 人」 */
|
||
function memberText(row: DeptRow) {
|
||
if (!row.memberNames.length) return `${row.memberCount} 人`;
|
||
const head = row.memberNames.slice(0, 3).join("、");
|
||
const rest = row.memberCount - Math.min(3, row.memberNames.length);
|
||
return rest > 0 ? `${head} +${rest}` : head;
|
||
}
|
||
function memberTip(row: DeptRow) {
|
||
const list = row.memberNames.join("、");
|
||
const more = row.memberCount > row.memberNames.length ? `…(仅预览前 ${row.memberNames.length} 人)` : "";
|
||
return `共 ${row.memberCount} 人:${list}${more} 点击查看全部`;
|
||
}
|
||
|
||
/* ================= 人员弹窗(查看全部,分页) ================= */
|
||
const memModal = reactive({
|
||
visible: false,
|
||
loading: false,
|
||
deptId: 0,
|
||
deptName: "",
|
||
rows: [] as any[],
|
||
total: 0,
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
function openMembers(row: DeptRow) {
|
||
Object.assign(memModal, {
|
||
visible: true, loading: true, deptId: row.id, deptName: row.name,
|
||
rows: [], total: 0, page: 1,
|
||
});
|
||
loadMembers();
|
||
}
|
||
function onMemPageChange(page: number) {
|
||
memModal.page = page;
|
||
loadMembers();
|
||
}
|
||
async function loadMembers() {
|
||
memModal.loading = true;
|
||
try {
|
||
const res: any = await Apis.SysUser.list({
|
||
params: { groupId: memModal.deptId, pageIndex: memModal.page, pageSize: memModal.pageSize },
|
||
cacheFor: null,
|
||
});
|
||
memModal.rows = res?.content ?? [];
|
||
memModal.total = Number(res?.total ?? 0);
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "加载部门人员失败");
|
||
memModal.rows = [];
|
||
memModal.total = 0;
|
||
} finally {
|
||
memModal.loading = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 当前学校的「包含子组」:读学校节点上的 includeChildren(1=含子组 / 0=仅本级)。
|
||
* 历史数据或旧库该列为 null 时按「含子组」兜底,与升级前表现一致。
|
||
*/
|
||
const includeOn = computed(() => {
|
||
const school = currentSchool.value;
|
||
return school ? school.includeChildren !== 0 : 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;
|
||
closedIds.value = new Set();
|
||
} else if (node?.id) {
|
||
// 点到中间层(区域 / 学校组等)不作为当前学校
|
||
currentSchoolId.value = undefined;
|
||
}
|
||
}
|
||
|
||
/** 切换「包含子组」:落库到学校节点,成功后就地更新,避免整树重绘丢失选中态 */
|
||
async function toggleInclude() {
|
||
const school = currentSchool.value;
|
||
if (!school?.id) return;
|
||
const next = !includeOn.value;
|
||
try {
|
||
await Apis.SysGroup.setIncludeChildren({
|
||
pathParams: { id: school.id },
|
||
data: { includeChildren: next },
|
||
});
|
||
school.includeChildren = next ? 1 : 0;
|
||
closedIds.value = new Set();
|
||
Message.success(next ? "已包含下级部门" : "仅显示一级部门");
|
||
} catch (e: any) {
|
||
// 全局 alova 拦截器(alova-iview 的 onBizError/onHttpError)已统一 Message.error,此处只记日志避免二次弹窗;
|
||
// 本地字段只在写成功后改,失败即自然停在原值,无需回滚
|
||
console.warn(e?.message || "设置包含子组失败");
|
||
}
|
||
}
|
||
|
||
function onToggle(dept: any) {
|
||
if (!dept?.id) return;
|
||
if (dept.status === 1) {
|
||
doEnable(dept);
|
||
return;
|
||
}
|
||
MyConfirm({
|
||
title: "停用部门",
|
||
msg: `确定停用部门【${dept.name}】?`,
|
||
info: "停用只影响「选」(新建时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。",
|
||
async onOk() {
|
||
await doDisable(dept);
|
||
},
|
||
});
|
||
}
|
||
|
||
async function doDisable(dept: any) {
|
||
try {
|
||
await Apis.SysGroup.disable({ pathParams: { id: dept.id } });
|
||
Message.success("已停用");
|
||
await syncTreeData();
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "停用失败");
|
||
}
|
||
}
|
||
|
||
async function doEnable(dept: any) {
|
||
try {
|
||
await Apis.SysGroup.enable({ pathParams: { id: dept.id } });
|
||
Message.success("已启用");
|
||
await syncTreeData();
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "启用失败");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 静默同步组树数据:只替换右侧数据源,**不重建左侧 zTree**。<br>
|
||
* 启停改的是部门自身状态,而树只渲染到学校层,部门节点根本不在树上——
|
||
* 走 refreshTree 会让整棵树重绘、展开态与滚动位置一起丢失。
|
||
*/
|
||
async function syncTreeData() {
|
||
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;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.dept-manage-page {
|
||
/* 骨架交由主题 .tree-wrapper(flex + gap 16 + padding 16)+ .main-container(白卡);
|
||
这里只声明两卡等高,间距/留白一律不覆盖 */
|
||
align-items: stretch;
|
||
box-sizing: border-box;
|
||
|
||
/* 树:外层不滚(滚动交给内部 zTree),否则内容未超高也会因 1~2px 溢出出现滚动条 */
|
||
.dept-manage-tree {
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
.tree-inner {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 右侧白卡内部纵向排布:标题行 / 说明条 / 表格 */
|
||
.dept-manage-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.panel-title {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.panel-caption {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--text, #1e2635);
|
||
}
|
||
|
||
/* 标题行右侧:包含子组 + 来源切换,整体右对齐;「包含子组」放左边,切视图时 tab 位置不跳 */
|
||
.title-tail {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.school-select {
|
||
width: 180px;
|
||
}
|
||
|
||
/* 来源切换:同页两个视图,不新增菜单项 */
|
||
.source-tabs {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 2px;
|
||
background: #f2f4f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.source-tab {
|
||
padding: 3px 10px;
|
||
font-size: 12.5px;
|
||
line-height: 18px;
|
||
border-radius: 6px;
|
||
color: var(--text-2, #5a6478);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.source-tab.is-active {
|
||
background: var(--panel, #fff);
|
||
color: var(--brand, #577afb);
|
||
font-weight: 600;
|
||
box-shadow: 0 1px 2px rgba(30, 38, 53, .08);
|
||
}
|
||
|
||
/* 面板内顶部说明条的间距(外观完全交给 iView Alert 自身,不覆写配色) */
|
||
.usage-note {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
/* 空态 / 无权限等临时提示 */
|
||
.state-tip {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.dept-loading {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding-top: 80px;
|
||
color: var(--text-3, #98a1b2);
|
||
}
|
||
|
||
/* 面板头的「包含子组」开关 */
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ================= 部门表格(iView Table) ================= */
|
||
.row-sum {
|
||
flex: 0 0 auto;
|
||
margin-bottom: 8px;
|
||
font-size: 11.5px;
|
||
color: var(--text-3, #98a1b2);
|
||
}
|
||
|
||
/* 表格本体外观全部交给 iView:只补单元格内部的排版细节 */
|
||
.dept-itable {
|
||
:deep(.ivu-table-cell) {
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
}
|
||
|
||
:deep(.ivu-table-row.is-off td) {
|
||
background: #fcfcfd;
|
||
color: var(--text-3, #98a1b2);
|
||
}
|
||
|
||
:deep(.ivu-table-row.is-off:hover td) {
|
||
background: #f8f9fb;
|
||
}
|
||
}
|
||
|
||
.nmcell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.nmcell .tw {
|
||
flex: 0 0 auto;
|
||
width: 14px;
|
||
text-align: center;
|
||
font-style: normal;
|
||
font-size: 9px;
|
||
color: #aeb6c2;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.nmcell .tw.blank {
|
||
visibility: hidden;
|
||
}
|
||
|
||
.nmcell .nm {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.nmcell .nm.root {
|
||
font-weight: 600;
|
||
color: var(--text, #1e2635);
|
||
}
|
||
|
||
.nmcell .cnt {
|
||
flex: 0 0 auto;
|
||
padding: 0 5px;
|
||
line-height: 16px;
|
||
border-radius: 4px;
|
||
background: #f1f3f7;
|
||
color: #8a94a6;
|
||
font-size: 10.5px;
|
||
}
|
||
|
||
.nmcell .cnt.grey {
|
||
background: #f6f7fa;
|
||
color: #a3adbd;
|
||
}
|
||
|
||
.mem-cell {
|
||
cursor: pointer;
|
||
color: var(--text-2, #5a6478);
|
||
border-bottom: 1px dashed transparent;
|
||
}
|
||
|
||
.mem-cell:hover {
|
||
color: var(--brand, #577afb);
|
||
border-bottom-color: #c9d4f7;
|
||
}
|
||
|
||
.dim {
|
||
color: var(--text-3, #98a1b2);
|
||
font-size: 11.5px;
|
||
}
|
||
|
||
/* 状态标签可点:hover 给一点反馈,暗示能切换 */
|
||
.sta-tag {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sta-tag:hover {
|
||
opacity: .82;
|
||
}
|
||
|
||
|
||
.table-foot {
|
||
flex: 0 0 auto;
|
||
padding: 10px 2px;
|
||
text-align: right;
|
||
font-size: 12px;
|
||
color: var(--text-3, #98a1b2);
|
||
}
|
||
|
||
/* ================= 人员弹窗 ================= */
|
||
.mem-modal-body {
|
||
max-height: 380px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.mem-empty {
|
||
padding: 26px 0;
|
||
text-align: center;
|
||
font-size: 12.5px;
|
||
color: var(--text-3, #98a1b2);
|
||
}
|
||
|
||
.mem-list {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
|
||
li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
height: 34px;
|
||
padding: 0 4px;
|
||
border-bottom: 1px solid #f2f5f9;
|
||
font-size: 12.5px;
|
||
|
||
&:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.mname {
|
||
color: var(--text, #1e2635);
|
||
}
|
||
|
||
.macc {
|
||
margin-left: auto;
|
||
color: var(--text-3, #98a1b2);
|
||
font-size: 11.5px;
|
||
}
|
||
}
|
||
|
||
.mem-pager {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 8px;
|
||
}
|
||
</style>
|