feat(frontend): collapsible payment submenu in sidebar

Group the 4 payment admin pages under a single "订单管理" parent menu
item that expands/collapses to show sub-items with indented tree line.
This commit is contained in:
erio
2026-04-06 14:40:37 +08:00
parent fa00ad06d2
commit f2b3cf628e
+106 -28
View File
@@ -29,30 +29,64 @@
<template v-if="isAdmin">
<!-- Admin Section -->
<div class="sidebar-section">
<router-link
v-for="item in adminNavItems"
:key="item.path"
:to="item.path"
class="sidebar-link mb-1"
:class="{ 'sidebar-link-active': isActive(item.path) }"
:title="sidebarCollapsed ? item.label : undefined"
:id="
item.path === '/admin/accounts'
? 'sidebar-channel-manage'
: item.path === '/admin/groups'
? 'sidebar-group-manage'
: item.path === '/admin/redeem'
? 'sidebar-wallet'
: undefined
"
@click="handleMenuItemClick(item.path)"
>
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade">
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
</transition>
</router-link>
<template v-for="item in adminNavItems" :key="item.path">
<!-- Collapsible group (has children) -->
<template v-if="item.children?.length">
<button
type="button"
class="sidebar-link mb-1 w-full"
:class="{ 'sidebar-link-active': isGroupActive(item) && !isGroupExpanded(item) }"
:title="sidebarCollapsed ? item.label : undefined"
@click="sidebarCollapsed ? undefined : toggleGroup(item)"
>
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade">
<span v-if="!sidebarCollapsed" class="flex flex-1 items-center justify-between">
<span>{{ item.label }}</span>
<ChevronDownIcon class="h-4 w-4 flex-shrink-0 transition-transform duration-200" :class="isGroupExpanded(item) ? 'rotate-180' : ''" />
</span>
</transition>
</button>
<!-- Children -->
<div v-if="!sidebarCollapsed && isGroupExpanded(item)" class="mb-1 ml-4 border-l border-gray-200 pl-2 dark:border-dark-600">
<router-link
v-for="child in item.children"
:key="child.path"
:to="child.path"
class="sidebar-link mb-0.5 py-1.5 text-sm"
:class="{ 'sidebar-link-active': route.path === child.path }"
@click="handleMenuItemClick(child.path)"
>
<component :is="child.icon" class="h-4 w-4 flex-shrink-0" />
<span>{{ child.label }}</span>
</router-link>
</div>
</template>
<!-- Normal item (no children) -->
<router-link
v-else
:to="item.path"
class="sidebar-link mb-1"
:class="{ 'sidebar-link-active': isActive(item.path) }"
:title="sidebarCollapsed ? item.label : undefined"
:id="
item.path === '/admin/accounts'
? 'sidebar-channel-manage'
: item.path === '/admin/groups'
? 'sidebar-group-manage'
: item.path === '/admin/redeem'
? 'sidebar-wallet'
: undefined
"
@click="handleMenuItemClick(item.path)"
>
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
<transition name="fade">
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
</transition>
</router-link>
</template>
</div>
<!-- Personal Section for Admin (hidden in simple mode) -->
@@ -160,6 +194,7 @@ interface NavItem {
icon: unknown
iconSvg?: string
hideInSimpleMode?: boolean
children?: NavItem[]
}
const { t } = useI18n()
@@ -175,6 +210,9 @@ const mobileOpen = computed(() => appStore.mobileOpen)
const isAdmin = computed(() => authStore.isAdmin)
const isDark = ref(document.documentElement.classList.contains('dark'))
// Track which parent nav groups are expanded
const expandedGroups = ref<Set<string>>(new Set())
// Site settings from appStore (cached, no flicker)
const siteName = computed(() => appStore.siteName)
const siteLogo = computed(() => appStore.siteLogo)
@@ -513,6 +551,21 @@ const ChevronDoubleRightIcon = {
)
}
const ChevronDownIcon = {
render: () =>
h(
'svg',
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
[
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
d: 'm19.5 8.25-7.5 7.5-7.5-7.5'
})
]
)
}
// User navigation items (for regular users)
const userNavItems = computed((): NavItem[] => {
const items: NavItem[] = [
@@ -622,10 +675,18 @@ const adminNavItems = computed((): NavItem[] => {
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
...(adminSettingsStore.paymentEnabled
? [
{ path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon, hideInSimpleMode: true },
{ path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon, hideInSimpleMode: true },
{ path: '/admin/orders/config', label: t('nav.paymentConfig'), icon: CogIcon, hideInSimpleMode: true },
{ path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon, hideInSimpleMode: true },
{
path: '/admin/orders',
label: t('nav.orderManagement'),
icon: OrderIcon,
hideInSimpleMode: true,
children: [
{ path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon },
{ path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon },
{ path: '/admin/orders/config', label: t('nav.paymentConfig'), icon: CogIcon },
{ path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon },
],
},
]
: []),
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon }
@@ -689,6 +750,23 @@ function isActive(path: string): boolean {
return route.path === path || route.path.startsWith(path + '/')
}
function isGroupActive(item: NavItem): boolean {
if (!item.children) return false
return item.children.some(child => route.path === child.path)
}
function isGroupExpanded(item: NavItem): boolean {
return expandedGroups.value.has(item.path) || isGroupActive(item)
}
function toggleGroup(item: NavItem) {
if (expandedGroups.value.has(item.path)) {
expandedGroups.value.delete(item.path)
} else {
expandedGroups.value.add(item.path)
}
}
// Initialize theme
const savedTheme = localStorage.getItem('theme')
if (