admin panel beter ui
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<!--
|
||||
Recursive renderer for one navMenuItems.ts node, used by VerticalNavMenu.vue.
|
||||
Extracted so a submenu can itself contain a submenu (e.g. "پیکربندی" nesting
|
||||
"کیف پول" nesting "لیست کیف پولهای شارژ شده") — ant-design-vue's a-sub-menu
|
||||
nests natively, the fixed two-level template just didn't recurse into it.
|
||||
-->
|
||||
<a-sub-menu v-if="node.submenu" :key="menuKey">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
<template #title>{{ node.i18n || node.name }}</template>
|
||||
<NavMenuNode
|
||||
v-for="(child, k) in visibleChildren(node.submenu)"
|
||||
:key="child.url || `${menuKey}-${k}`"
|
||||
:node="child"
|
||||
:menu-key="child.url || `${menuKey}-${k}`"
|
||||
/>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-menu-item v-else :key="menuKey">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
{{ node.i18n || node.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { resolveIcon } from '../../components/icons';
|
||||
import type { NavMenuNode as NavMenuNodeType } from '../navMenuItems';
|
||||
|
||||
defineProps<{ node: NavMenuNodeType; menuKey: string }>();
|
||||
|
||||
const users = useUsersStore();
|
||||
|
||||
// Same visibility rule as VerticalNavMenu.vue: no permissionId = always visible.
|
||||
function canSee(node: NavMenuNodeType): boolean {
|
||||
if (!node.permissionId) return true;
|
||||
return Boolean(users.hasPermission(node.permissionId));
|
||||
}
|
||||
|
||||
function visibleChildren(nodes?: NavMenuNodeType[]): NavMenuNodeType[] {
|
||||
return (nodes ?? []).filter(canSee);
|
||||
}
|
||||
|
||||
defineOptions({ name: 'NavMenuNode' });
|
||||
</script>
|
||||
@@ -13,6 +13,14 @@
|
||||
<MenuFoldOutlined v-else />
|
||||
</a-button>
|
||||
<span class="page-title">{{ pageTitle }}</span>
|
||||
<a-breadcrumb v-if="breadcrumb" class="breadcrumb">
|
||||
<a-breadcrumb-item v-for="(crumb, i) in breadcrumb" :key="i">
|
||||
<router-link v-if="crumb.url && !crumb.active" :to="crumb.url">
|
||||
{{ crumb.title }}
|
||||
</router-link>
|
||||
<span v-else>{{ crumb.title }}</span>
|
||||
</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
@@ -43,6 +51,12 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
interface Crumb {
|
||||
title: string;
|
||||
url?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
defineProps<{ collapsed?: boolean }>();
|
||||
defineEmits<{ (e: 'toggle-sidebar'): void }>();
|
||||
|
||||
@@ -51,6 +65,7 @@ const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const pageTitle = computed(() => (route.meta?.pageTitle as string) ?? '');
|
||||
const breadcrumb = computed(() => route.meta?.breadcrumb as Crumb[] | undefined);
|
||||
const displayName = computed(() => auth.user?.name ?? auth.user?.fullName ?? 'کاربر');
|
||||
const initial = computed(() => displayName.value.trim().charAt(0) || 'ک');
|
||||
|
||||
@@ -80,6 +95,9 @@ function onLogout() {
|
||||
font-size: 16px;
|
||||
color: #1c2d56;
|
||||
}
|
||||
.breadcrumb {
|
||||
font-size: 13px;
|
||||
}
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -33,45 +33,20 @@
|
||||
<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
|
||||
children still carry their own icons below). -->
|
||||
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">
|
||||
<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 #icon><component :is="resolveIcon(child.icon)" v-if="child.icon" /></template>
|
||||
<template #title>{{ child.i18n || child.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(child.submenu)"
|
||||
:key="leaf.url || `${j}-${k}`"
|
||||
>
|
||||
<template #icon><component :is="resolveIcon(leaf.icon)" v-if="leaf.icon" /></template>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item v-else :key="child.url || `${j}`">
|
||||
<template #icon><component :is="resolveIcon(child.icon)" v-if="child.icon" /></template>
|
||||
{{ child.i18n || child.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<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>
|
||||
|
||||
<!-- group node -->
|
||||
<a-sub-menu v-else-if="node.submenu" :key="subKey(node, i)">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
<template #title>{{ node.i18n || node.name }}</template>
|
||||
<a-menu-item
|
||||
v-for="(leaf, k) in visibleChildren(node.submenu)"
|
||||
:key="leaf.url || `${i}-${k}`"
|
||||
>
|
||||
<template #icon><component :is="resolveIcon(leaf.icon)" v-if="leaf.icon" /></template>
|
||||
{{ leaf.i18n || leaf.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- leaf node -->
|
||||
<a-menu-item v-else :key="node.url || `${i}`">
|
||||
<template #icon><component :is="resolveIcon(node.icon)" v-if="node.icon" /></template>
|
||||
{{ node.i18n || node.name }}
|
||||
</a-menu-item>
|
||||
<!-- group / leaf node -->
|
||||
<NavMenuNode v-else :node="node" :menu-key="node.url || subKey(node, i)" />
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
@@ -80,9 +55,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMenu, type NavMenuNode } from '../navMenuItems';
|
||||
import { getMenu, type NavMenuNode as NavMenuNodeData } from '../navMenuItems';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { resolveIcon } from '../../components/icons';
|
||||
import NavMenuNode from './NavMenuNode.vue';
|
||||
|
||||
withDefaults(defineProps<{ title?: string; collapsed?: boolean }>(), {
|
||||
title: 'منتزر',
|
||||
@@ -101,17 +77,17 @@ const users = useUsersStore();
|
||||
* 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 {
|
||||
function canSee(node: NavMenuNodeData): boolean {
|
||||
if (!node.permissionId) return true;
|
||||
return Boolean(users.hasPermission(node.permissionId));
|
||||
}
|
||||
|
||||
function visibleChildren(nodes?: NavMenuNode[]): NavMenuNode[] {
|
||||
function visibleChildren(nodes?: NavMenuNodeData[]): NavMenuNodeData[] {
|
||||
return (nodes ?? []).filter(canSee);
|
||||
}
|
||||
|
||||
/** Hides a group whose children are all hidden, as legacy did. */
|
||||
function hasVisibleContent(node: NavMenuNode): boolean {
|
||||
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;
|
||||
@@ -121,7 +97,28 @@ const visibleMenu = computed(() =>
|
||||
getMenu().filter((n) => canSee(n) && hasVisibleContent(n)),
|
||||
);
|
||||
|
||||
const subKey = (node: NavMenuNode, i: number) => `sub-${node.name ?? ''}-${i}`;
|
||||
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
|
||||
@@ -137,15 +134,18 @@ function findOpenKeysFor(path: string): string[] {
|
||||
const children = visibleChildren(node.items);
|
||||
for (let j = 0; j < children.length; j++) {
|
||||
const child = children[j];
|
||||
if (child.submenu && visibleChildren(child.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(child, j)];
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (node.submenu && visibleChildren(node.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(node, i)];
|
||||
if (node.submenu) {
|
||||
const nodeKey = node.url || subKey(node, i);
|
||||
const nested = findOpenKeysIn(visibleChildren(node.submenu), path, nodeKey);
|
||||
if (nested) return [nodeKey, ...nested];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -175,7 +175,8 @@ function onClick({ key }: { key: string | number }) {
|
||||
|
||||
<style scoped>
|
||||
.vertical-nav-menu {
|
||||
min-height: 100vh;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user