Files
crawler-plugin/admin-frontend-vue/src/layout/AdminLayout.vue
T
huangzd1997 83b935c11b task-15(壳层/路由): 实现页面标题与面包屑规则
title-breadcrumb 提供 resolveDocumentTitle('页面 - 数富AI')、updateDocumentTitle
(非浏览器静默)、crumbsForActiveRoute(分组->页面);AdminLayout 用 watch 同步
document.title 并在层级>1 时渲染 el-breadcrumb。
2026-09-05 14:43:54 +08:00

128 lines
4.9 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAdminSessionStore } from '@/stores/admin-session'
import { joinAdminPath } from '@/config/app'
import { pageTitleOf, topbarUserOf } from '@/layout/topbar-model'
import { groupKeysForActive, toSidebarEntries } from '@/layout/menu-mapper'
import {
EMPTY_MENU_DESCRIPTION,
EMPTY_MENU_TITLE,
shouldShowEmptyMenu,
} from '@/layout/empty-state'
import GlobalErrorContainer from '@/layout/GlobalErrorContainer.vue'
import { initialCollapsed, toggleCollapsed, writeCollapsePreference } from '@/layout/sidebar-collapse'
import { crumbsForActiveRoute, resolveDocumentTitle, updateDocumentTitle } from '@/layout/title-breadcrumb'
const route = useRoute()
const router = useRouter()
const session = useAdminSessionStore()
const collapsed = ref(readInitialCollapse())
function readInitialCollapse(): boolean {
const storage = typeof window !== 'undefined' ? window.localStorage : null
const width = typeof window !== 'undefined' ? window.innerWidth : 0
return initialCollapsed(width, storage)
}
function setCollapsed(value: boolean): void {
collapsed.value = value
if (typeof window !== 'undefined') writeCollapsePreference(window.localStorage, value)
}
function toggleSidebar(): void {
setCollapsed(toggleCollapsed(collapsed.value))
}
const activePath = computed(() => route.path)
const menuEntries = computed(() => toSidebarEntries(session.menuTree))
const openedKeys = computed(() => groupKeysForActive(menuEntries.value, route.path))
const emptyMenu = computed(() => shouldShowEmptyMenu(menuEntries.value.length > 0, session.loading, session.initialized))
const pageTitle = computed(() => pageTitleOf(route.meta.title))
const userVm = computed(() => topbarUserOf(session.user))
const breadcrumbs = computed(() => crumbsForActiveRoute(session.menuTree, route.path))
const logoUrl = joinAdminPath('assets', 'logo.jpg')
watch(
pageTitle,
(title) => updateDocumentTitle(resolveDocumentTitle(title), typeof document !== 'undefined' ? document : null),
{ immediate: true },
)
async function signOut() {
try {
await session.signOut()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '退出登录失败')
}
}
</script>
<template>
<div class="admin-shell" :class="{ 'is-collapsed': collapsed }">
<aside class="admin-sidebar">
<div class="admin-brand">
<img :src="logoUrl" alt="数富AI" class="admin-brand-logo" />
<div v-if="!collapsed" class="admin-brand-copy">
<strong>数富AI</strong>
<span>电商运营管理后台</span>
</div>
</div>
<el-scrollbar class="admin-menu-scroll">
<el-menu :default-active="activePath" :default-openeds="openedKeys" :collapse="collapsed" router class="admin-menu">
<template v-for="entry in menuEntries" :key="entry.key">
<el-sub-menu v-if="entry.kind === 'group'" :index="entry.key">
<template #title>
<span class="menu-group-title">{{ entry.name }}</span>
</template>
<el-menu-item v-for="item in entry.children" :key="item.key" :index="item.route">
{{ item.name }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :key="entry.key" :index="entry.route">
{{ entry.name }}
</el-menu-item>
</template>
</el-menu>
<el-empty v-if="emptyMenu" :description="EMPTY_MENU_TITLE" :image-size="72" />
</el-scrollbar>
<button class="sidebar-collapse" type="button" @click="toggleSidebar">
{{ collapsed ? '展开菜单' : '收起菜单' }}
</button>
</aside>
<section class="admin-main">
<header class="admin-topbar">
<div>
<div class="admin-kicker">运营管理控制台</div>
<h1>{{ pageTitle }}</h1>
</div>
<div class="admin-user">
<div class="admin-user-meta">
<strong>{{ userVm.username }}</strong>
<span v-if="userVm.role">{{ userVm.role }}</span>
</div>
<el-button text type="danger" @click="signOut">退出登录</el-button>
</div>
</header>
<main class="admin-content">
<el-breadcrumb v-if="breadcrumbs.length > 1" class="admin-breadcrumb" separator="/">
<el-breadcrumb-item v-for="crumb in breadcrumbs" :key="crumb.key">{{ crumb.name }}</el-breadcrumb-item>
</el-breadcrumb>
<GlobalErrorContainer />
<el-alert v-if="session.error" :title="session.error" type="error" show-icon :closable="false" />
<el-empty
v-if="emptyMenu && route.path === '/'"
:title="EMPTY_MENU_TITLE"
:description="EMPTY_MENU_DESCRIPTION"
:image-size="96"
/>
<RouterView v-else />
</main>
</section>
</div>
</template>