179 lines
4.4 KiB
Vue
179 lines
4.4 KiB
Vue
<template>
|
|
<div class="module-authz-tree">
|
|
<div v-for="node in visibleNodes" :key="node.id" class="tree-node"
|
|
:style="{ paddingLeft: `${(node.level || 1) * 18 - 18}px` }"
|
|
:class="{ 'is-leaf': !node.children?.length, 'is-expanded': node.expand !== false }">
|
|
<div class="node-main">
|
|
<span v-if="node.children?.length" class="expand-icon" @click.stop="toggleExpand(node)">
|
|
<Icon :type="node.expand === false ? 'ios-add' : 'ios-remove'" />
|
|
</span>
|
|
<span v-else class="expand-placeholder"></span>
|
|
<Checkbox :model-value="!!node.checked"
|
|
:disabled="loadingId === node.id && loadingType === 'check'"
|
|
@on-change="(checked: boolean) => onCheckChange(node, checked)">
|
|
<span class="node-name" :title="node.name">{{ node.name }}</span>
|
|
</Checkbox>
|
|
</div>
|
|
<div class="node-inherit">
|
|
<Checkbox :model-value="!!node.inheritFlag"
|
|
:disabled="!node.checked || (loadingId === node.id && loadingType === 'inherit')"
|
|
@on-change="(checked: boolean) => onInheritChange(node, checked)">
|
|
</Checkbox>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, reactive } from "vue";
|
|
|
|
export interface ModuleAuthzNode {
|
|
id: number;
|
|
name: string;
|
|
level?: number;
|
|
checked?: boolean;
|
|
inheritFlag?: boolean;
|
|
expand?: boolean;
|
|
children?: ModuleAuthzNode[];
|
|
}
|
|
|
|
const props = defineProps<{
|
|
nodes: ModuleAuthzNode[];
|
|
appId: number;
|
|
groupId: number;
|
|
loadingId?: number | null;
|
|
loadingType?: "check" | "inherit" | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: "module-change", payload: {
|
|
appId: number;
|
|
groupId: number;
|
|
moduleId: number;
|
|
checked: boolean;
|
|
inheritFlag: boolean;
|
|
type: "check" | "inherit";
|
|
}): void;
|
|
}>();
|
|
|
|
const expandMap = reactive<Record<number, boolean>>({});
|
|
|
|
function buildLocalNodes(nodes: ModuleAuthzNode[]): ModuleAuthzNode[] {
|
|
return (nodes || []).map((n) => ({
|
|
...n,
|
|
expand: expandMap[n.id] ?? true,
|
|
children: buildLocalNodes(n.children || []),
|
|
}));
|
|
}
|
|
|
|
const localNodes = computed<ModuleAuthzNode[]>(() => buildLocalNodes(props.nodes));
|
|
|
|
const visibleNodes = computed<ModuleAuthzNode[]>(() => {
|
|
const result: ModuleAuthzNode[] = [];
|
|
const walk = (list: ModuleAuthzNode[]) => {
|
|
for (const node of list) {
|
|
result.push(node);
|
|
if (node.children?.length && node.expand !== false) {
|
|
walk(node.children);
|
|
}
|
|
}
|
|
};
|
|
walk(localNodes.value);
|
|
return result;
|
|
});
|
|
|
|
function toggleExpand(node: ModuleAuthzNode) {
|
|
expandMap[node.id] = node.expand === false;
|
|
}
|
|
|
|
function onCheckChange(node: ModuleAuthzNode, checked: boolean) {
|
|
node.checked = checked;
|
|
if (!checked) {
|
|
node.inheritFlag = false;
|
|
}
|
|
emit("module-change", {
|
|
appId: props.appId,
|
|
groupId: props.groupId,
|
|
moduleId: node.id,
|
|
checked,
|
|
inheritFlag: !!node.inheritFlag,
|
|
type: "check",
|
|
});
|
|
}
|
|
|
|
function onInheritChange(node: ModuleAuthzNode, checked: boolean) {
|
|
node.inheritFlag = checked;
|
|
emit("module-change", {
|
|
appId: props.appId,
|
|
groupId: props.groupId,
|
|
moduleId: node.id,
|
|
checked: !!node.checked,
|
|
inheritFlag: checked,
|
|
type: "inherit",
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.module-authz-tree {
|
|
user-select: none;
|
|
}
|
|
|
|
.tree-node {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 4px 0;
|
|
min-height: 32px;
|
|
border-bottom: 1px dashed #f0f0f0;
|
|
|
|
&:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
&:hover {
|
|
background-color: #f7f7f7;
|
|
}
|
|
}
|
|
|
|
.node-main {
|
|
display: flex;
|
|
align-items: center;
|
|
flex: 1;
|
|
min-width: 0;
|
|
gap: 4px;
|
|
}
|
|
|
|
.expand-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
color: #999;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
|
|
&:hover {
|
|
color: #2d8cf0;
|
|
}
|
|
}
|
|
|
|
.expand-placeholder {
|
|
width: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.node-name {
|
|
font-size: 13px;
|
|
color: #333;
|
|
}
|
|
|
|
.node-inherit {
|
|
width: 80px;
|
|
text-align: center;
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|