feat: complete admin panel and backend admin api migration
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
<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"
|
||||
: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` -->
|
||||
<a-menu-item-group v-if="node.header" :title="node.i18n || node.header">
|
||||
<template v-for="(child, j) in visibleChildren(node.items)" :key="child.url || j">
|
||||
<a-sub-menu v-if="child.submenu" :key="subKey(child, j)">
|
||||
<template #title>{{ child.i18n || child.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(child.submenu)"
|
||||
:key="leaf.url || `${j}-${k}`"
|
||||
>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item v-else :key="child.url || `${j}`">
|
||||
{{ child.i18n || child.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu-item-group>
|
||||
|
||||
<!-- group node -->
|
||||
<a-sub-menu v-else-if="node.submenu" :key="subKey(node, i)">
|
||||
<template #title>{{ node.i18n || node.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(node.submenu)"
|
||||
:key="leaf.url || `${i}-${k}`"
|
||||
>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- leaf node -->
|
||||
<a-menu-item v-else :key="node.url || `${i}`">
|
||||
{{ node.i18n || node.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMenu, type NavMenuNode } from '../navMenuItems';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
|
||||
withDefaults(defineProps<{ title?: string; collapsed?: boolean }>(), {
|
||||
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.
|
||||
*/
|
||||
function canSee(node: NavMenuNode): boolean {
|
||||
if (!node.permissionId) return true;
|
||||
return Boolean(users.hasPermission(node.permissionId));
|
||||
}
|
||||
|
||||
function visibleChildren(nodes?: NavMenuNode[]): NavMenuNode[] {
|
||||
return (nodes ?? []).filter(canSee);
|
||||
}
|
||||
|
||||
/** Hides a group whose children are all hidden, as legacy did. */
|
||||
function hasVisibleContent(node: NavMenuNode): 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)),
|
||||
);
|
||||
|
||||
const subKey = (node: NavMenuNode, i: number) => `sub-${node.name ?? ''}-${i}`;
|
||||
|
||||
const selectedKeys = ref<string[]>([route.path]);
|
||||
const openKeys = ref<string[]>([]);
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(p) => {
|
||||
selectedKeys.value = [p];
|
||||
},
|
||||
);
|
||||
|
||||
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 {
|
||||
min-height: 100vh;
|
||||
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>
|
||||
Reference in New Issue
Block a user