Files
userbase/front/src/components/ModuleAuthzTree.vue
T

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>