Files
hedena_gold/apps/admin-panel/src/layouts/components/VerticalNavMenu.vue
T

193 lines
6.3 KiB
Vue
Raw Normal View History

<template>
<!--
Replacement for legacy henza_admin/src/layouts/components/vertical-nav-menu/
(VerticalNavMenu.vue + VerticalNavMenuGroup/Item/Header, ~700 lines of Vuesax-
specific markup). Vuesax has no Vue 3 build, so the tree is rendered with
ant-design-vue's a-menu instead.
What is preserved exactly: the menu DATA (navMenuItems.ts, byte-exact), the
permission filtering rule, the header/group/item nesting, and the Farsi labels.
What changes: the DOM/classes, because the underlying component library changed.
-->
<a-layout-sider
:collapsed="collapsed"
:trigger="null"
collapsible
width="260"
class="vertical-nav-menu"
theme="light"
>
<div class="nav-header">
<span v-if="!collapsed" class="nav-title">{{ title }}</span>
</div>
<a-menu
mode="inline"
theme="light"
:inline-collapsed="collapsed"
:selected-keys="selectedKeys"
:open-keys="openKeys"
@update:openKeys="(k: string[]) => (openKeys = k)"
@click="onClick"
>
<template v-for="(node, i) in visibleMenu" :key="node.url || node.header || i">
<!-- header node: a section divider carrying `items` (ant has no icon
slot on a menu-item-group; it's a label-only divider, so its
2026-09-19 12:05:59 +03:30
children still carry their own icons below). Each child renders
through NavMenuNode, which recurses into however many submenu
levels it has. -->
<a-menu-item-group v-if="node.header" :title="node.i18n || node.header">
2026-09-19 12:05:59 +03:30
<NavMenuNode
v-for="(child, j) in visibleChildren(node.items)"
:key="child.url || `${i}-${j}`"
:node="child"
:menu-key="child.url || subKey(child, j)"
/>
</a-menu-item-group>
2026-09-19 12:05:59 +03:30
<!-- group / leaf node -->
<NavMenuNode v-else :node="node" :menu-key="node.url || subKey(node, i)" />
</template>
</a-menu>
</a-layout-sider>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
2026-09-19 12:05:59 +03:30
import { getMenu, type NavMenuNode as NavMenuNodeData } from '../navMenuItems';
import { useUsersStore } from '../../stores/users';
import { resolveIcon } from '../../components/icons';
2026-09-19 12:05:59 +03:30
import NavMenuNode from './NavMenuNode.vue';
withDefaults(defineProps<{ title?: string; collapsed?: boolean }>(), {
2026-09-16 11:59:35 +03:30
title: 'منتزر',
collapsed: false,
});
const route = useRoute();
const router = useRouter();
const users = useUsersStore();
/**
* Legacy's visibility rule, from the v-if on each nav item:
* a node with no permissionId is ALWAYS visible; otherwise it needs the rid.
*
* That "no permissionId = public" rule is why the banker-list and shopkeeper-list
* items (whose permissionId is commented out in the legacy data) show for everyone.
* Preserved deliberately — see docs/ADMIN-PORT-PLAN.md.
*/
2026-09-19 12:05:59 +03:30
function canSee(node: NavMenuNodeData): boolean {
if (!node.permissionId) return true;
return Boolean(users.hasPermission(node.permissionId));
}
2026-09-19 12:05:59 +03:30
function visibleChildren(nodes?: NavMenuNodeData[]): NavMenuNodeData[] {
return (nodes ?? []).filter(canSee);
}
/** Hides a group whose children are all hidden, as legacy did. */
2026-09-19 12:05:59 +03:30
function hasVisibleContent(node: NavMenuNodeData): boolean {
if (node.header) return visibleChildren(node.items).length > 0;
if (node.submenu) return visibleChildren(node.submenu).length > 0;
return true;
}
const visibleMenu = computed(() =>
getMenu().filter((n) => canSee(n) && hasVisibleContent(n)),
);
2026-09-19 12:05:59 +03:30
const subKey = (node: NavMenuNodeData, i: number) => `sub-${node.name ?? ''}-${i}`;
/**
* Finds the chain of sub-menu keys that must be open so the current route's
* menu item is visible, at any nesting depth. Mirrors the key each
* NavMenuNode instance is actually given (node.url, or menuKey-derived for
* url-less group nodes), so a match here really is a key ant's a-menu knows
* about.
*/
function findOpenKeysIn(nodes: NavMenuNodeData[], path: string, parentKey: string): string[] | null {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const key = node.url || `${parentKey}-${i}`;
if (!node.submenu) {
if (node.url === path) return [];
continue;
}
const nested = findOpenKeysIn(visibleChildren(node.submenu), path, key);
if (nested) return [key, ...nested];
}
return null;
}
/**
* Finds the sub-menu key(s) that must be open so the current route's menu
* item is visible. Previously openKeys only ever changed via user clicks
* (@update:openKeys), so a direct link or a page refresh landed with every
* submenu collapsed even when the active item was nested inside one.
*/
function findOpenKeysFor(path: string): string[] {
for (let i = 0; i < visibleMenu.value.length; i++) {
const node = visibleMenu.value[i];
if (node.header) {
const children = visibleChildren(node.items);
for (let j = 0; j < children.length; j++) {
const child = children[j];
2026-09-19 12:05:59 +03:30
if (!child.submenu) continue;
const childKey = child.url || subKey(child, j);
const nested = findOpenKeysIn(visibleChildren(child.submenu), path, childKey);
if (nested) return [childKey, ...nested];
}
continue;
}
2026-09-19 12:05:59 +03:30
if (node.submenu) {
const nodeKey = node.url || subKey(node, i);
const nested = findOpenKeysIn(visibleChildren(node.submenu), path, nodeKey);
if (nested) return [nodeKey, ...nested];
}
}
return [];
}
const selectedKeys = ref<string[]>([route.path]);
const openKeys = ref<string[]>(findOpenKeysFor(route.path));
watch(
() => route.path,
(p) => {
selectedKeys.value = [p];
const needed = findOpenKeysFor(p);
for (const key of needed) {
if (!openKeys.value.includes(key)) openKeys.value = [...openKeys.value, key];
}
},
);
function onClick({ key }: { key: string | number }) {
const path = String(key);
if (path.startsWith('/') && path !== route.path) {
router.push(path);
}
}
</script>
<style scoped>
.vertical-nav-menu {
2026-09-19 12:05:59 +03:30
height: 100vh;
overflow-y: auto; /* pinned in place; its own menu scrolls if it's taller than the viewport */
border-left: 1px solid #f0f0f0;
}
.nav-header {
height: 61px; /* legacy ara/ant.less @layout-header-height */
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 18px;
color: #1c2d56;
}
</style>