admin-panel: render sidebar icons, fix openKeys, persist collapse state
- VerticalNavMenu.vue now renders the icon: values that navMenuItems.ts already carried on 33 nodes but were never displayed, via the shared resolveIcon() map. Sub-menus and leaf items get a #icon slot each; ADV4's Menu shows icons only (labels hidden) once :inline-collapsed is true, which is now wired to the collapsed prop. - Fixed openKeys: it previously started at [] and was only ever updated by user clicks, so a direct link or page refresh landed with the active item's parent submenu collapsed. It's now derived from the current route on load and on every navigation. - Sidebar collapse state moves from a local ref in Main.vue to stores/layout.ts's existing (previously unused) sidebarCollapsed, persisted to localStorage so it survives a reload. Verified: pnpm build passes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
02d92fc465
commit
a6d8993d01
@@ -24,25 +24,31 @@
|
||||
<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` -->
|
||||
<!-- 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). -->
|
||||
<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>
|
||||
@@ -50,17 +56,20 @@
|
||||
|
||||
<!-- 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>
|
||||
</template>
|
||||
@@ -73,6 +82,7 @@ import { computed, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMenu, type NavMenuNode } from '../navMenuItems';
|
||||
import { useUsersStore } from '../../stores/users';
|
||||
import { resolveIcon } from '../../components/icons';
|
||||
|
||||
withDefaults(defineProps<{ title?: string; collapsed?: boolean }>(), {
|
||||
title: 'هنزا گلد',
|
||||
@@ -113,13 +123,45 @@ const visibleMenu = computed(() =>
|
||||
|
||||
const subKey = (node: NavMenuNode, i: number) => `sub-${node.name ?? ''}-${i}`;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
if (child.submenu && visibleChildren(child.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(child, j)];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.submenu && visibleChildren(node.submenu).some((leaf) => leaf.url === path)) {
|
||||
return [subKey(node, i)];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedKeys = ref<string[]>([route.path]);
|
||||
const openKeys = ref<string[]>([]);
|
||||
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];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<VerticalNavMenu :collapsed="collapsed" />
|
||||
|
||||
<a-layout>
|
||||
<TheNavbar :collapsed="collapsed" @toggle-sidebar="collapsed = !collapsed" />
|
||||
<TheNavbar :collapsed="collapsed" @toggle-sidebar="layout.setSidebarCollapsed(!collapsed)" />
|
||||
|
||||
<a-layout-content id="content-area">
|
||||
<div class="content-wrapper">
|
||||
@@ -40,6 +40,7 @@ import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import VerticalNavMenu from '../components/VerticalNavMenu.vue';
|
||||
import TheNavbar from '../components/TheNavbar.vue';
|
||||
import { useLayoutStore } from '../../stores/layout';
|
||||
|
||||
interface Crumb {
|
||||
title: string;
|
||||
@@ -48,7 +49,10 @@ interface Crumb {
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const collapsed = ref(false);
|
||||
const layout = useLayoutStore();
|
||||
// Persisted via stores/layout.ts (localStorage) so the sidebar stays
|
||||
// collapsed/expanded across reloads instead of resetting every time.
|
||||
const collapsed = computed(() => layout.sidebarCollapsed);
|
||||
|
||||
const pageTitle = computed(() => route.meta?.pageTitle as string | undefined);
|
||||
const breadcrumb = computed(() => route.meta?.breadcrumb as Crumb[] | undefined);
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import themeConfig, { type ThemeMode } from '../themeConfig';
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = 'admin-panel:sidebarCollapsed';
|
||||
|
||||
/** Reads the persisted collapse state, falling back to themeConfig's default
|
||||
* when localStorage is unavailable or has never been set. */
|
||||
function readPersistedCollapsed(): boolean {
|
||||
if (typeof window === 'undefined') return themeConfig.sidebarCollapsed;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
|
||||
return raw === null ? themeConfig.sidebarCollapsed : raw === '1';
|
||||
} catch {
|
||||
return themeConfig.sidebarCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
export const useLayoutStore = defineStore('layout', {
|
||||
state: () => ({
|
||||
windowWidth: typeof window !== 'undefined' ? window.innerWidth : 0,
|
||||
@@ -12,7 +26,7 @@ export const useLayoutStore = defineStore('layout', {
|
||||
theme: themeConfig.theme as ThemeMode,
|
||||
// legacy state.js: sidebar visibility for the vertical nav menu
|
||||
isSidebarActive: true,
|
||||
sidebarCollapsed: themeConfig.sidebarCollapsed,
|
||||
sidebarCollapsed: readPersistedCollapsed(),
|
||||
}),
|
||||
getters: {
|
||||
// legacy used breakpoints of 1200 (theme tour) and 992 (nav menu collapse)
|
||||
@@ -32,6 +46,13 @@ export const useLayoutStore = defineStore('layout', {
|
||||
},
|
||||
setSidebarCollapsed(value: boolean) {
|
||||
this.sidebarCollapsed = value;
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, value ? '1' : '0');
|
||||
} catch {
|
||||
// localStorage unavailable (private mode, disabled site data, ...) —
|
||||
// collapse state just won't persist across reloads.
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user