363 lines
10 KiB
Vue
363 lines
10 KiB
Vue
<template>
|
|
<n-config-provider
|
|
:style-mount-target="styleMountTarget"
|
|
:theme-overrides="adminThemeOverrides"
|
|
>
|
|
<n-message-provider>
|
|
<n-dialog-provider>
|
|
<n-notification-provider>
|
|
<n-layout v-if="!isMobile" class="aksrt-admin-layout" has-sider>
|
|
<n-layout-sider
|
|
class="aksrt-admin-sider"
|
|
bordered
|
|
collapse-mode="width"
|
|
:collapsed-width="64"
|
|
:width="232"
|
|
show-collapse-trigger
|
|
:collapsed="collapsed"
|
|
:native-scrollbar="false"
|
|
@collapse="collapsed = true"
|
|
@expand="collapsed = false"
|
|
>
|
|
<div class="aksrt-logo">
|
|
<span class="logo-icon">AK</span>
|
|
<span v-if="!collapsed" class="aksrt-logo__copy">
|
|
<span class="logo-text">AKSRT</span>
|
|
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<n-menu
|
|
class="aksrt-admin-menu"
|
|
:value="activeMenuKey"
|
|
:collapsed="collapsed"
|
|
:collapsed-width="64"
|
|
:options="menuOptions"
|
|
@update:value="handleMenuClick"
|
|
/>
|
|
</n-layout-sider>
|
|
|
|
<n-layout :native-scrollbar="false" class="aksrt-admin-content">
|
|
<div class="aksrt-admin-content__inner">
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="fade-slide" mode="out-in">
|
|
<div class="aksrt-page-wrapper" :key="route.fullPath">
|
|
<component :is="Component" />
|
|
</div>
|
|
</transition>
|
|
</router-view>
|
|
|
|
<div v-if="pageError" class="aksrt-error-fallback">
|
|
<p class="aksrt-error-title">页面渲染异常</p>
|
|
<p class="aksrt-error-msg">{{ pageError }}</p>
|
|
<n-button size="small" @click="pageError = null">重试</n-button>
|
|
</div>
|
|
</div>
|
|
</n-layout>
|
|
</n-layout>
|
|
|
|
<div v-else class="aksrt-admin-mobile">
|
|
<MobileTopBar :title="currentPageTitle" @toggle="drawerActive = true" />
|
|
|
|
<n-drawer v-model:show="drawerActive" placement="left" :width="280">
|
|
<n-drawer-content :native-scrollbar="false" body-content-style="padding:0;">
|
|
<div class="aksrt-logo aksrt-logo--drawer">
|
|
<span class="logo-icon">AK</span>
|
|
<span class="aksrt-logo__copy">
|
|
<span class="logo-text">AKSRT</span>
|
|
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
|
|
</span>
|
|
</div>
|
|
<n-menu
|
|
class="aksrt-admin-menu"
|
|
:value="activeMenuKey"
|
|
:options="menuOptions"
|
|
@update:value="handleMenuClick"
|
|
/>
|
|
</n-drawer-content>
|
|
</n-drawer>
|
|
|
|
<div class="aksrt-admin-mobile__body">
|
|
<router-view v-slot="{ Component }">
|
|
<transition name="fade-slide" mode="out-in">
|
|
<div class="aksrt-page-wrapper" :key="route.fullPath">
|
|
<component :is="Component" />
|
|
</div>
|
|
</transition>
|
|
</router-view>
|
|
|
|
<div v-if="pageError" class="aksrt-error-fallback">
|
|
<p class="aksrt-error-title">页面渲染异常</p>
|
|
<p class="aksrt-error-msg">{{ pageError }}</p>
|
|
<n-button size="small" @click="pageError = null">重试</n-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</n-notification-provider>
|
|
</n-dialog-provider>
|
|
</n-message-provider>
|
|
</n-config-provider>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onBeforeUnmount, onErrorCaptured, onMounted, provide, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { NConfigProvider } from 'naive-ui'
|
|
import MobileTopBar from './components/MobileTopBar.vue'
|
|
import { styleMountTarget } from './style-target'
|
|
import { ADMIN_SECTIONS } from './constants/admin-sections'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const collapsed = ref(false)
|
|
const pageError = ref(null)
|
|
const isMobile = ref(false)
|
|
const drawerActive = ref(false)
|
|
const themeVersion = window.aksrtAdminData?.themeVersion || window.aksrtAdminData?.version || '9.0'
|
|
const MOBILE_BREAKPOINT = 960
|
|
|
|
const adminThemeOverrides = {
|
|
common: {
|
|
primaryColor: '#18A058',
|
|
primaryColorHover: '#36AD6A',
|
|
primaryColorPressed: '#0C7A43',
|
|
primaryColorSuppl: '#36AD6A',
|
|
bodyColor: '#F5F7F6',
|
|
cardColor: '#FFFFFF',
|
|
modalColor: '#FFFFFF',
|
|
popoverColor: '#FFFFFF',
|
|
textColorBase: '#1F2923',
|
|
textColor1: '#1F2923',
|
|
textColor2: '#465149',
|
|
textColor3: '#6F7A72',
|
|
borderColor: '#E2E8E3',
|
|
dividerColor: '#E2E8E3',
|
|
borderRadius: '8px',
|
|
borderRadiusSmall: '6px',
|
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
},
|
|
Button: {
|
|
borderRadiusMedium: '8px',
|
|
heightMedium: '36px',
|
|
fontWeight: '600',
|
|
},
|
|
Card: {
|
|
borderRadius: '12px',
|
|
borderColor: '#E2E8E3',
|
|
},
|
|
DataTable: {
|
|
thColor: '#F7F9F7',
|
|
thTextColor: '#465149',
|
|
tdColorHover: '#F7FBF8',
|
|
borderColor: '#E2E8E3',
|
|
},
|
|
Input: {
|
|
borderRadius: '8px',
|
|
heightMedium: '36px',
|
|
},
|
|
Menu: {
|
|
itemColorActive: '#EAF7EF',
|
|
itemColorActiveHover: '#EAF7EF',
|
|
itemColorHover: '#F2F8F4',
|
|
itemTextColorActive: '#168A4B',
|
|
itemTextColorActiveHover: '#168A4B',
|
|
itemTextColorHover: '#168A4B',
|
|
},
|
|
Switch: {
|
|
railColorActive: '#18A058',
|
|
},
|
|
}
|
|
|
|
function checkMobile() {
|
|
isMobile.value = window.innerWidth < MOBILE_BREAKPOINT
|
|
}
|
|
|
|
onMounted(() => {
|
|
checkMobile()
|
|
window.addEventListener('resize', checkMobile)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', checkMobile)
|
|
})
|
|
|
|
provide('collapsed', collapsed)
|
|
|
|
onErrorCaptured((err) => {
|
|
console.error('[AKSRT] 组件渲染错误:', err)
|
|
pageError.value = err?.message || String(err)
|
|
return false
|
|
})
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Build key → path lookup from ADMIN_SECTIONS */
|
|
/* ------------------------------------------------------------------ */
|
|
const keyToPath = {}
|
|
for (const section of ADMIN_SECTIONS) {
|
|
if (section.children) {
|
|
for (const child of section.children) {
|
|
keyToPath[child.key] = child.route
|
|
}
|
|
}
|
|
// For placeholder sections (no children), the section key maps to its route
|
|
if (section.placeholder) {
|
|
keyToPath[section.key] = section.route
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Build NaiveUI menu options from ADMIN_SECTIONS */
|
|
/* ------------------------------------------------------------------ */
|
|
const menuOptions = ADMIN_SECTIONS.filter((section) => !section.hidden).map((section) => {
|
|
if (section.placeholder) {
|
|
// Placeholder sections have no children — render as a leaf menu item
|
|
return {
|
|
label: section.label,
|
|
key: section.key,
|
|
icon: section.icon,
|
|
}
|
|
}
|
|
return {
|
|
label: section.label,
|
|
key: section.key,
|
|
icon: section.icon,
|
|
children: section.children.map((child) => ({
|
|
label: child.secondaryEntry ? `${child.label} · 次级` : child.label,
|
|
key: child.key,
|
|
props: child.secondaryEntry ? { class: 'aksrt-menu-item--secondary' } : undefined,
|
|
})),
|
|
}
|
|
})
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Active menu key resolution */
|
|
/* ------------------------------------------------------------------ */
|
|
const activeMenuKey = computed(() => {
|
|
const path = route.path
|
|
|
|
// Find the child whose route matches the current path
|
|
for (const section of ADMIN_SECTIONS) {
|
|
if (section.children) {
|
|
for (const child of section.children) {
|
|
if (path === child.route || path.startsWith(child.route + '/')) {
|
|
return child.key
|
|
}
|
|
}
|
|
}
|
|
// Placeholder section (no children)
|
|
if (section.placeholder && (path === section.route || path.startsWith(section.route + '/'))) {
|
|
return section.key
|
|
}
|
|
}
|
|
|
|
// Default: first section's first child
|
|
return ADMIN_SECTIONS[0]?.children?.[0]?.key || 'global-common'
|
|
})
|
|
|
|
const currentPageTitle = computed(() => route.meta?.title || 'AKSRT')
|
|
|
|
function handleMenuClick(key) {
|
|
if (keyToPath[key]) {
|
|
router.push(keyToPath[key])
|
|
if (isMobile.value) {
|
|
drawerActive.value = false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aksrt-logo {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
height: 64px;
|
|
gap: 11px;
|
|
padding: 0 18px;
|
|
border-bottom: 1px solid var(--aksrt-color-border);
|
|
}
|
|
|
|
.aksrt-logo--drawer {
|
|
height: 64px;
|
|
}
|
|
|
|
.logo-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
flex: 0 0 28px;
|
|
border-radius: 7px;
|
|
background: var(--aksrt-color-primary);
|
|
color: #fff;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.aksrt-logo__copy {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 7px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: var(--aksrt-color-text);
|
|
letter-spacing: .08em;
|
|
}
|
|
|
|
.aksrt-logo__version {
|
|
color: var(--aksrt-color-muted);
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
font-size: 10px;
|
|
letter-spacing: .02em;
|
|
}
|
|
|
|
.aksrt-admin-content__inner {
|
|
width: 100%;
|
|
max-width: 1236px;
|
|
margin: 0 auto;
|
|
padding: 26px 28px 42px;
|
|
}
|
|
|
|
.aksrt-admin-mobile {
|
|
background: var(--aksrt-color-canvas);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.aksrt-admin-mobile__body {
|
|
padding: 14px 12px 32px;
|
|
}
|
|
|
|
.aksrt-page-wrapper {
|
|
min-height: calc(100vh - 120px);
|
|
}
|
|
|
|
.aksrt-error-fallback {
|
|
padding: 48px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.aksrt-error-title {
|
|
margin: 0 0 10px;
|
|
font-size: 16px;
|
|
color: #d03050;
|
|
}
|
|
|
|
.aksrt-error-msg {
|
|
margin: 0 0 16px;
|
|
font-size: 13px;
|
|
color: #777;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.aksrt-admin-content__inner {
|
|
padding: 22px 20px 38px;
|
|
}
|
|
}
|
|
</style>
|