Archived
forked from admin/userbase
725 lines
21 KiB
Vue
725 lines
21 KiB
Vue
<template>
|
||
<div class="page-wrapper tree-wrapper">
|
||
<GroupTreeSearchPanel class="tree-container group-auth-tree" v-model:curTreeNode="selectedGroupNode"
|
||
:treeApi="groupTreeApi" :defaultSearchVisible="true" :showCloseButton="false" searchPlaceholder="输入组名称搜索"
|
||
cacheKey="bsch.group-auth.group-node" @on-change="handleGroupChange" />
|
||
|
||
<div class="main-container group-auth-main">
|
||
<div class="search-wrap authz-toolbar">
|
||
<div class="toolbar-left">
|
||
<Select v-model="selectedCategoryId" class="type-filter-select" placeholder="全部应用类型" clearable>
|
||
<Option v-for="opt in categoryOptions" :key="opt.value" :value="opt.value">{{ opt.label }}
|
||
</Option>
|
||
</Select>
|
||
<h5 class="title mb-0">组授权</h5>
|
||
<div class="group-text">
|
||
当前组:
|
||
<span class="group-name">{{ currentGroupName || '未选择组' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar-right">
|
||
<Input v-model="searchKeyword" placeholder="输入应用或模块名称搜索,回车过滤" clearable suffix="ios-search"
|
||
style="width: 260px" @on-enter="onSearch" @on-clear="onClearSearch" />
|
||
</div>
|
||
</div>
|
||
|
||
<Alert v-if="!selectedGroupId" type="warning" show-icon>
|
||
请选择组后再进行授权设置
|
||
</Alert>
|
||
|
||
<div v-else class="authz-panel">
|
||
<Spin fix v-if="panelLoading" />
|
||
|
||
<div v-if="hasVisibleCategories" class="cat-columns">
|
||
<div v-for="(col, ci) in columns" :key="ci" class="cat-col">
|
||
<div v-for="item in col" :key="String(item.cat.id)" class="cat-card">
|
||
<div class="cat-card-header" @click="toggleCollapse(item.cat.id)">
|
||
<Icon class="collapse-icon"
|
||
:type="collapsedMap[String(item.cat.id)] ? 'ios-arrow-forward' : 'ios-arrow-down'" />
|
||
<span class="cat-name" :title="item.cat.name">{{ item.cat.name }}</span>
|
||
<span class="cat-count">{{ item.treeData.length }}个应用</span>
|
||
<span class="inherit-cap">子组继承</span>
|
||
</div>
|
||
<div v-show="!collapsedMap[String(item.cat.id)]" class="cat-card-body">
|
||
<MyTree :treeData="item.treeData" :setting="treeSetting" :autoClick="false"
|
||
@on-tree-check="onTreeCheck" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="empty-tip">
|
||
<template v-if="nonEmptyCategories.length === 0">
|
||
当前组节点未开通任何应用,<router-link class="open-link" :to="{ name: 'AppOpen' }">去开通</router-link>
|
||
</template>
|
||
<template v-else>未找到匹配的应用或模块</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, onMounted, reactive, ref, shallowRef, watch } from "vue";
|
||
import { Message } from "view-ui-plus";
|
||
import Apis from "@/api";
|
||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||
import MyTree from "@/components/MyTree.vue";
|
||
import { useDefaultStore } from "@/store";
|
||
|
||
defineOptions({ name: "GroupAuth" });
|
||
|
||
type AppModuleVo = {
|
||
id?: number;
|
||
name?: string;
|
||
level?: number;
|
||
checked?: boolean;
|
||
inheritFlag?: boolean;
|
||
children?: AppModuleVo[];
|
||
};
|
||
|
||
type AppItem = {
|
||
id?: number;
|
||
name?: string;
|
||
sortNo?: number;
|
||
modules?: AppModuleVo[];
|
||
};
|
||
|
||
type CategoryItem = {
|
||
id?: number;
|
||
name?: string;
|
||
apps?: AppItem[];
|
||
};
|
||
|
||
type DictItem = {
|
||
k: number | string;
|
||
v: string;
|
||
};
|
||
|
||
/** zTree 节点:应用为一级节点,模块为其子节点 */
|
||
type AuthzTreeNode = {
|
||
id: string;
|
||
name: string;
|
||
open: boolean;
|
||
checked: boolean;
|
||
inheritFlag: boolean;
|
||
nodeType: "app" | "module";
|
||
rawId: number;
|
||
appId: number;
|
||
children?: AuthzTreeNode[];
|
||
};
|
||
|
||
type CategoryTreeItem = {
|
||
cat: CategoryItem;
|
||
treeData: AuthzTreeNode[];
|
||
weight: number;
|
||
};
|
||
|
||
const COLUMN_COUNT = 3;
|
||
|
||
const jq = () => (window as any).$;
|
||
|
||
const store = useDefaultStore();
|
||
|
||
const groupTreeApi = () => Apis.SysGroup.getTree({ cacheFor: null });
|
||
|
||
const selectedGroupNode = ref<any>({});
|
||
// shallowRef:节点勾选状态的局部变更不触发树重建,只有整体重新加载才重建
|
||
const categoryList = shallowRef<CategoryItem[]>([]);
|
||
const selectedCategoryId = ref<number | null>(null);
|
||
const pageLoading = ref(false);
|
||
const searchKeyword = ref("");
|
||
const appliedKeyword = ref("");
|
||
const collapsedMap = reactive<Record<string, boolean>>({});
|
||
|
||
const fallbackModeItems: DictItem[] = [
|
||
{ k: 0, v: "追加" },
|
||
{ k: 1, v: "覆盖" },
|
||
{ k: 2, v: "删除" },
|
||
];
|
||
|
||
const selectedGroupId = computed(() => {
|
||
const id = Number(selectedGroupNode.value?.id);
|
||
return Number.isFinite(id) ? id : null;
|
||
});
|
||
const currentGroupName = computed(() => selectedGroupNode.value?.name || "");
|
||
const panelLoading = computed(() => pageLoading.value);
|
||
|
||
const authzModeItems = computed<DictItem[]>(() => {
|
||
const dict = (store.getSysDict as any) || {};
|
||
const preferredKeys = [
|
||
"groupauthzmode",
|
||
"groupAuthzMode",
|
||
"authzmode",
|
||
"authzMode",
|
||
"schoolappmode",
|
||
"schoolAppMode",
|
||
];
|
||
|
||
const isMatch = (list: any) => {
|
||
if (!Array.isArray(list)) return false;
|
||
const keys = list.map((x: any) => Number(x?.k ?? x?.key));
|
||
return keys.includes(0) && keys.includes(1) && keys.includes(2);
|
||
};
|
||
|
||
for (const key of preferredKeys) {
|
||
if (isMatch(dict[key])) {
|
||
return dict[key].map((x: any) => ({ k: Number(x.k ?? x.key), v: String(x.v ?? x.value ?? x.name) }));
|
||
}
|
||
}
|
||
|
||
const firstMatched = Object.values(dict).find(isMatch) as any[] | undefined;
|
||
if (firstMatched) {
|
||
return firstMatched.map((x: any) => ({ k: Number(x.k ?? x.key), v: String(x.v ?? x.value ?? x.name) }));
|
||
}
|
||
|
||
return fallbackModeItems;
|
||
});
|
||
|
||
const addMode = computed(() => Number(authzModeItems.value.find((x) => Number(x.k) === 0)?.k ?? 0));
|
||
const deleteMode = computed(() => Number(authzModeItems.value.find((x) => Number(x.k) === 2)?.k ?? 2));
|
||
|
||
// 仅保留有已开通应用的分类
|
||
const nonEmptyCategories = computed(() => (categoryList.value || []).filter((c) => (c.apps || []).length > 0));
|
||
|
||
const categoryOptions = computed(() =>
|
||
nonEmptyCategories.value.map((c) => ({ value: Number(c.id), label: String(c.name || `应用分类${c.id}`) }))
|
||
);
|
||
|
||
// 切换组后若当前选中的分类已不存在,则回退到"全部"
|
||
watch(categoryOptions, (opts) => {
|
||
if (selectedCategoryId.value != null && !opts.some((o) => o.value === Number(selectedCategoryId.value))) {
|
||
selectedCategoryId.value = null;
|
||
}
|
||
});
|
||
|
||
function moduleNameHit(node: AppModuleVo, kw: string): boolean {
|
||
if ((node.name || "").toLowerCase().includes(kw)) return true;
|
||
return (node.children || []).some((c) => moduleNameHit(c, kw));
|
||
}
|
||
|
||
function countTotalModules(nodes: AppModuleVo[]): number {
|
||
let count = 0;
|
||
for (const node of nodes) {
|
||
count++;
|
||
count += countTotalModules(node.children || []);
|
||
}
|
||
return count;
|
||
}
|
||
|
||
function isAppChecked(app: AppItem): boolean {
|
||
const modules = app.modules || [];
|
||
if (modules.length === 0) return false;
|
||
return modules.every((m) => isModuleAllChecked(m));
|
||
}
|
||
|
||
function isAppInherit(app: AppItem): boolean {
|
||
const modules = app.modules || [];
|
||
if (modules.length === 0) return false;
|
||
return modules.every((m) => isModuleAllInherit(m));
|
||
}
|
||
|
||
function isModuleAllChecked(node: AppModuleVo): boolean {
|
||
if (!node.checked) return false;
|
||
return (node.children || []).every((c) => isModuleAllChecked(c));
|
||
}
|
||
|
||
function isModuleAllInherit(node: AppModuleVo): boolean {
|
||
if (!node.inheritFlag) return false;
|
||
return (node.children || []).every((c) => isModuleAllInherit(c));
|
||
}
|
||
|
||
function buildModuleNode(m: AppModuleVo, appId: number): AuthzTreeNode {
|
||
return {
|
||
id: `mod_${m.id}`,
|
||
name: String(m.name || ""),
|
||
open: true,
|
||
checked: !!m.checked,
|
||
inheritFlag: !!m.inheritFlag,
|
||
nodeType: "module",
|
||
rawId: Number(m.id),
|
||
appId,
|
||
children: (m.children || []).map((c) => buildModuleNode(c, appId)),
|
||
};
|
||
}
|
||
|
||
function buildAppNode(app: AppItem): AuthzTreeNode {
|
||
const appId = Number(app.id);
|
||
return {
|
||
id: `app_${appId}`,
|
||
name: String(app.name || ""),
|
||
open: true,
|
||
checked: isAppChecked(app),
|
||
inheritFlag: isAppInherit(app),
|
||
nodeType: "app",
|
||
rawId: appId,
|
||
appId,
|
||
children: (app.modules || []).map((m) => buildModuleNode(m, appId)),
|
||
};
|
||
}
|
||
|
||
// 分类 -> zTree 数据(应用为一级节点,模块为子节点);weight 用于三列均衡
|
||
const categoryTreeList = computed<CategoryTreeItem[]>(() => {
|
||
const kw = (appliedKeyword.value || "").trim().toLowerCase();
|
||
return nonEmptyCategories.value
|
||
.filter((cat) => selectedCategoryId.value == null || selectedCategoryId.value === "" || Number(cat.id) === Number(selectedCategoryId.value))
|
||
.map((cat) => {
|
||
const apps = (cat.apps || []).filter((app) => {
|
||
if (!kw) return true;
|
||
if ((app.name || "").toLowerCase().includes(kw)) return true;
|
||
return (app.modules || []).some((m) => moduleNameHit(m, kw));
|
||
});
|
||
return { cat, apps };
|
||
})
|
||
.filter((x) => x.apps.length > 0)
|
||
.map(({ cat, apps }) => ({
|
||
cat,
|
||
treeData: apps.map((app) => buildAppNode(app)),
|
||
weight: 2 + apps.reduce((sum, a) => sum + 1 + countTotalModules(a.modules || []), 0),
|
||
}));
|
||
});
|
||
|
||
const hasVisibleCategories = computed(() => categoryTreeList.value.length > 0);
|
||
|
||
// 简易瀑布流:每个分类卡片放入当前累计权重最小的列
|
||
const columns = computed<CategoryTreeItem[][]>(() => {
|
||
const cols: CategoryTreeItem[][] = Array.from({ length: COLUMN_COUNT }, () => []);
|
||
const weights = new Array(COLUMN_COUNT).fill(0);
|
||
for (const item of categoryTreeList.value) {
|
||
let idx = 0;
|
||
for (let i = 1; i < COLUMN_COUNT; i++) {
|
||
if (weights[i] < weights[idx]) idx = i;
|
||
}
|
||
cols[idx].push(item);
|
||
weights[idx] += item.weight;
|
||
}
|
||
return cols;
|
||
});
|
||
|
||
function toggleCollapse(catId: number | undefined) {
|
||
const key = String(catId);
|
||
collapsedMap[key] = !collapsedMap[key];
|
||
}
|
||
|
||
// ---------------- zTree 配置 ----------------
|
||
|
||
/** 在每个节点行尾追加"子组继承"复选框 */
|
||
function addDiyDom(treeId: string, treeNode: any) {
|
||
const $ = jq();
|
||
if (!$) return;
|
||
const inheritId = `${treeNode.tId}_inherit`;
|
||
if ($(`#${inheritId}`).length > 0) return;
|
||
$(`#${treeNode.tId}_a`).after(
|
||
`<span class="inherit-wrap" title="子组继承"><input id="${inheritId}" type="checkbox" class="inherit-chk" /></span>`
|
||
);
|
||
const el = $(`#${inheritId}`);
|
||
el.prop("checked", !!treeNode.inheritFlag);
|
||
el.prop("disabled", !treeNode.checked);
|
||
el.on("change", function (this: HTMLInputElement) {
|
||
onInheritToggle(treeId, treeNode, this.checked);
|
||
});
|
||
}
|
||
|
||
const treeSetting = {
|
||
view: {
|
||
dblClickExpand: false,
|
||
showIcon: false,
|
||
addDiyDom,
|
||
},
|
||
check: {
|
||
enable: true,
|
||
chkStyle: "checkbox",
|
||
// 不自动级联:模块逐条独立授权;应用节点勾选后由代码手动同步子节点
|
||
chkboxType: { Y: "", N: "" },
|
||
},
|
||
callback: {},
|
||
};
|
||
|
||
function getTreeObjByNode(node: any) {
|
||
const $ = jq();
|
||
const treeId = String(node?.tId || "").replace(/_\d+$/, "");
|
||
return $?.fn?.zTree?.getZTreeObj(treeId) || null;
|
||
}
|
||
|
||
/** 模块勾选变化后,按本地数据回算应用节点的勾选/继承态并同步到 zTree */
|
||
function syncAppNodeState(treeObj: any, appId: number) {
|
||
if (!treeObj) return;
|
||
const app = findLocalApp(appId);
|
||
if (!app) return;
|
||
const appNode = treeObj.getNodesByFilter((n: any) => n.id === `app_${appId}`, true);
|
||
if (!appNode) return;
|
||
appNode.checked = isAppChecked(app);
|
||
appNode.inheritFlag = isAppInherit(app);
|
||
treeObj.checkNode(appNode, appNode.checked, false);
|
||
}
|
||
|
||
/** 按 zTree 当前节点状态刷新所有"子组继承"复选框的勾选与禁用态 */
|
||
function syncInheritDoms(treeObj: any) {
|
||
const $ = jq();
|
||
if (!treeObj || !$) return;
|
||
const nodes = treeObj.transformToArray(treeObj.getNodes()) || [];
|
||
nodes.forEach((n: any) => {
|
||
const el = $(`#${n.tId}_inherit`);
|
||
if (!el.length) return;
|
||
el.prop("checked", !!n.inheritFlag);
|
||
el.prop("disabled", !n.checked);
|
||
});
|
||
}
|
||
|
||
// ---------------- 数据加载 ----------------
|
||
|
||
async function ensureSysDict() {
|
||
if (Object.keys(store.getSysDict || {}).length === 0) {
|
||
await store.fetchSysDict();
|
||
}
|
||
}
|
||
|
||
async function loadGroupModuleTree(groupId: number) {
|
||
pageLoading.value = true;
|
||
try {
|
||
const data = await Apis.GroupAuthz.groupModuleTreeByCategory({ params: { groupId }, cacheFor: null });
|
||
categoryList.value = Array.isArray(data) ? data : [];
|
||
} finally {
|
||
pageLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function reloadCurrent() {
|
||
if (selectedGroupId.value) {
|
||
await loadGroupModuleTree(selectedGroupId.value);
|
||
}
|
||
}
|
||
|
||
async function handleGroupChange(node: any) {
|
||
selectedGroupNode.value = node || {};
|
||
const nodeId = Number(node?.id);
|
||
if (!Number.isFinite(nodeId)) {
|
||
categoryList.value = [];
|
||
return;
|
||
}
|
||
await loadGroupModuleTree(nodeId);
|
||
}
|
||
|
||
// ---------------- 本地数据同步(不触发树重建) ----------------
|
||
|
||
function findLocalApp(appId: number): AppItem | null {
|
||
for (const cat of categoryList.value || []) {
|
||
const app = (cat.apps || []).find((a) => Number(a.id) === appId);
|
||
if (app) return app;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function walkModules(nodes: AppModuleVo[], fn: (n: AppModuleVo) => void) {
|
||
for (const node of nodes) {
|
||
fn(node);
|
||
walkModules(node.children || [], fn);
|
||
}
|
||
}
|
||
|
||
function setLocalAppChecked(appId: number, checked: boolean) {
|
||
const app = findLocalApp(appId);
|
||
if (!app) return;
|
||
walkModules(app.modules || [], (n) => {
|
||
n.checked = checked;
|
||
if (!checked) n.inheritFlag = false;
|
||
});
|
||
}
|
||
|
||
function setLocalAppInherit(appId: number, inherit: boolean) {
|
||
const app = findLocalApp(appId);
|
||
if (!app) return;
|
||
walkModules(app.modules || [], (n) => {
|
||
if (n.checked) n.inheritFlag = inherit;
|
||
});
|
||
}
|
||
|
||
function setLocalModuleState(appId: number, moduleId: number, patch: Partial<AppModuleVo>) {
|
||
const app = findLocalApp(appId);
|
||
if (!app) return;
|
||
walkModules(app.modules || [], (n) => {
|
||
if (Number(n.id) === moduleId) Object.assign(n, patch);
|
||
});
|
||
}
|
||
|
||
// ---------------- 授权操作 ----------------
|
||
|
||
/** 勾选/取消勾选:应用节点 => moduleId=0 整应用处理;模块节点 => 单模块处理 */
|
||
async function onTreeCheck(node: any) {
|
||
const groupId = selectedGroupId.value;
|
||
if (!groupId || !node) return;
|
||
|
||
const checked = !!node.checked;
|
||
const isApp = node.nodeType === "app";
|
||
const treeObj = getTreeObjByNode(node);
|
||
|
||
try {
|
||
await Apis.GroupAuthz.setGroupModule({
|
||
data: [{
|
||
groupId,
|
||
appId: node.appId,
|
||
moduleId: isApp ? 0 : node.rawId,
|
||
mode: checked ? addMode.value : deleteMode.value,
|
||
isAll: checked && node.inheritFlag ? 1 : 0,
|
||
}],
|
||
cacheFor: null,
|
||
});
|
||
|
||
if (isApp) {
|
||
setLocalAppChecked(node.appId, checked);
|
||
// 应用节点整批操作:手动同步 zTree 子节点勾选态与继承标记
|
||
if (treeObj) {
|
||
const affected = treeObj.transformToArray([node]) || [];
|
||
affected.forEach((n: any) => {
|
||
if (n !== node) treeObj.checkNode(n, checked, false);
|
||
n.checked = checked;
|
||
if (!checked) n.inheritFlag = false;
|
||
});
|
||
}
|
||
Message.success(checked ? "已授权该应用全部模块" : "已取消该应用全部模块授权");
|
||
} else {
|
||
if (!checked) node.inheritFlag = false;
|
||
setLocalModuleState(node.appId, node.rawId, checked ? { checked: true } : { checked: false, inheritFlag: false });
|
||
syncAppNodeState(treeObj, node.appId);
|
||
}
|
||
syncInheritDoms(treeObj);
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "操作失败");
|
||
await reloadCurrent();
|
||
}
|
||
}
|
||
|
||
/** 子组继承开关:应用节点 => moduleId=0 整应用处理;模块节点 => 单模块处理 */
|
||
async function onInheritToggle(treeId: string, node: any, inherit: boolean) {
|
||
const $ = jq();
|
||
const groupId = selectedGroupId.value;
|
||
const revert = () => $ && $(`#${node.tId}_inherit`).prop("checked", !inherit);
|
||
|
||
if (!groupId || !node.checked) {
|
||
revert();
|
||
return;
|
||
}
|
||
|
||
const isApp = node.nodeType === "app";
|
||
const treeObj = $?.fn?.zTree?.getZTreeObj(treeId) || null;
|
||
|
||
try {
|
||
await Apis.GroupAuthz.setGroupModule({
|
||
data: [{
|
||
groupId,
|
||
appId: node.appId,
|
||
moduleId: isApp ? 0 : node.rawId,
|
||
mode: addMode.value,
|
||
isAll: inherit ? 1 : 0,
|
||
}],
|
||
cacheFor: null,
|
||
});
|
||
|
||
node.inheritFlag = inherit;
|
||
if (isApp) {
|
||
setLocalAppInherit(node.appId, inherit);
|
||
if (treeObj) {
|
||
const affected = treeObj.transformToArray([node]) || [];
|
||
affected.forEach((n: any) => {
|
||
if (n.checked) n.inheritFlag = inherit;
|
||
});
|
||
}
|
||
} else {
|
||
setLocalModuleState(node.appId, node.rawId, { inheritFlag: inherit });
|
||
}
|
||
syncInheritDoms(treeObj);
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "操作失败");
|
||
await reloadCurrent();
|
||
}
|
||
}
|
||
|
||
// ---------------- 搜索 ----------------
|
||
|
||
function onSearch() {
|
||
appliedKeyword.value = searchKeyword.value;
|
||
}
|
||
|
||
function onClearSearch() {
|
||
appliedKeyword.value = "";
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await ensureSysDict();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.group-auth-tree {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.group-auth-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.authz-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.type-filter-select {
|
||
width: 180px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.group-text {
|
||
color: #666;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.group-name {
|
||
color: #2d8cf0;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.authz-panel {
|
||
position: relative;
|
||
flex: 1;
|
||
min-height: 320px;
|
||
padding: 12px 16px;
|
||
background: #fff;
|
||
border-radius: 4px;
|
||
overflow: auto;
|
||
}
|
||
|
||
/* 三列平铺布局 */
|
||
.cat-columns {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.cat-col {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.cat-card {
|
||
border: 1px solid #e8eaec;
|
||
border-radius: 4px;
|
||
background: #fff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.cat-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #e8eaec;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
|
||
&:hover {
|
||
background: #f0f7ff;
|
||
}
|
||
}
|
||
|
||
.collapse-icon {
|
||
color: #999;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.cat-name {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
color: #333;
|
||
margin-left: 6px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cat-count {
|
||
color: #999;
|
||
font-size: 12px;
|
||
margin-left: 8px;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.inherit-cap {
|
||
font-size: 12px;
|
||
color: #999;
|
||
flex-shrink: 0;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.cat-card-body {
|
||
padding: 6px 8px;
|
||
/* 子组继承复选框的绝对定位锚点:保证所有层级节点的复选框右对齐成一列 */
|
||
position: relative;
|
||
}
|
||
|
||
.empty-tip {
|
||
color: #999;
|
||
padding: 48px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.open-link {
|
||
color: #2d8cf0;
|
||
|
||
&:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
/* zTree 节点行尾的"子组继承"复选框:
|
||
float 会跟随节点缩进产生阶梯,改为绝对定位锚定卡片体右边界,所有层级右对齐成一列 */
|
||
:deep(.inherit-wrap) {
|
||
position: absolute;
|
||
right: 14px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 22px;
|
||
}
|
||
|
||
:deep(.inherit-chk) {
|
||
cursor: pointer;
|
||
|
||
&:disabled {
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
:deep(.ztree li a) {
|
||
max-width: calc(100% - 90px);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
vertical-align: middle;
|
||
}
|
||
</style>
|