feat: complete admin panel and backend admin api migration

This commit is contained in:
s.esmaeili
2026-09-13 21:02:25 +03:30
parent 03119cd655
commit 267fc7b7d1
239 changed files with 25431 additions and 935 deletions
@@ -0,0 +1,89 @@
<template>
<!--
Replacement for legacy TheNavbarVertical.vue / TheNavbarHorizontal.vue.
Legacy carried a Vuexy/Vuesax toolbar with a theme customizer, search overlay,
notification dropdown, bookmarks bar and i18n switcher. Of those, only the
pieces with real backing survive: the sidebar toggle, the page title, and the
user dropdown with logout. The rest were template demo features.
-->
<a-layout-header class="the-navbar">
<div class="nav-left">
<a-button type="text" @click="$emit('toggle-sidebar')">
<MenuUnfoldOutlined v-if="collapsed" />
<MenuFoldOutlined v-else />
</a-button>
<span class="page-title">{{ pageTitle }}</span>
</div>
<div class="nav-right">
<a-dropdown>
<span class="user-chip">
<a-avatar size="small" style="background-color: #1c2d56">
{{ initial }}
</a-avatar>
<span class="user-name">{{ displayName }}</span>
</span>
<template #overlay>
<a-menu>
<a-menu-item key="my-data" @click="router.push('/my-data')">
اطلاعات من
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="onLogout">خروج</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue';
import { useAuthStore } from '../../stores/auth';
defineProps<{ collapsed?: boolean }>();
defineEmits<{ (e: 'toggle-sidebar'): void }>();
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const pageTitle = computed(() => (route.meta?.pageTitle as string) ?? '');
const displayName = computed(() => auth.user?.name ?? auth.user?.fullName ?? 'کاربر');
const initial = computed(() => displayName.value.trim().charAt(0) || 'ک');
function onLogout() {
auth.logout();
router.push('/auth/login');
}
</script>
<style scoped>
.the-navbar {
height: 61px; /* legacy ara/ant.less @layout-header-height */
background: #fff;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
.nav-left,
.nav-right {
display: flex;
align-items: center;
gap: 12px;
}
.page-title {
font-size: 16px;
color: #1c2d56;
}
.user-chip {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
</style>
@@ -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>