wx ac90205395 feat: 官网代码全量更新
覆盖最新版本代码,包含:
- 新增品牌百科、公司百科页面及CMS管理
- 新增微信引导组件(WechatCTA)
- 新增品牌/公司图片资源
- 全站组件/页面/数据/样式更新

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:21:11 +08:00

299 行
8.1 KiB
Vue

<template>
<client-only>
<n-config-provider :theme-overrides="themeOverrides">
<n-message-provider>
<n-dialog-provider>
<div class="admin-layout">
<aside class="admin-sidebar">
<div class="admin-logo">
<div class="logo-icon">H</div>
<div class="logo-text">
<h2>呼籁后台</h2>
<span>hulai.com</span>
</div>
</div>
<div class="menu-wrap">
<n-menu
:value="activeKey"
:options="menuOptions"
:expanded-keys="expandedKeys"
:indent="16"
inverted
@update:value="handleMenuSelect"
@update:expanded-keys="handleExpand"
/>
</div>
<div class="admin-user">
<div class="user-info">
<n-avatar :size="28" round style="background: #3a7d44; font-size: 12px;">A</n-avatar>
<span>管理员</span>
</div>
<n-button size="tiny" quaternary @click="logout" style="color: rgba(255,255,255,0.55);">
退出
</n-button>
</div>
</aside>
<main class="admin-main">
<slot />
</main>
</div>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</client-only>
</template>
<script setup>
import { h } from 'vue'
import {
NConfigProvider, NMessageProvider, NDialogProvider,
NMenu, NAvatar, NButton, NBadge,
} from 'naive-ui'
const route = useRoute()
const { logout, adminFetch } = useAdmin()
const unreadCount = ref(0)
onMounted(async () => {
try {
const data = await adminFetch('/api/admin/submissions/unread')
unreadCount.value = data.count
} catch {}
})
// ── Naive UI 主题 ──────────────────────────────
const themeOverrides = {
common: {
primaryColor: '#3a7d44',
primaryColorHover: '#2d6235',
primaryColorPressed: '#245c2b',
primaryColorSuppl: '#3a7d44',
},
}
// ── 菜单配置 ──────────────────────────────────
function menuLabel(text) {
return () => text
}
function submissionLabel() {
return () => h('span', { style: 'display:flex;align-items:center;justify-content:space-between;width:100%' }, [
'表单提交',
unreadCount.value > 0
? h(NBadge, { value: unreadCount.value, max: 99, type: 'error' })
: null,
])
}
const menuOptions = computed(() => {
return [
{ label: menuLabel('数据概览'), key: '/admin' },
{ label: menuLabel('产品线管理'), key: '/admin/products' },
{ label: menuLabel('产品选择器'), key: '/admin/content/selector' },
{
label: menuLabel('目的地'), key: 'g-destinations',
children: [
{ label: menuLabel('景点对比'), key: '/admin/content/destinations' },
{ label: menuLabel('景点详情'), key: '/admin/content/destinations-detail' },
{ label: menuLabel('旅行日历'), key: '/admin/content/calendar' },
{ label: menuLabel('出行指南'), key: '/admin/content/guides' },
],
},
{
label: menuLabel('客户口碑'), key: 'g-reviews',
children: [
{ label: menuLabel('客户评价'), key: '/admin/reviews' },
{ label: menuLabel('小红书游记'), key: '/admin/content/xiaohongshu-wall' },
{ label: menuLabel('客户游记'), key: '/admin/content/youji' },
{ label: menuLabel('呼籁旅拍'), key: '/admin/gallery' },
{ label: menuLabel('客户故事'), key: '/admin/stories' },
],
},
{ label: menuLabel('常见问答'), key: '/admin/faqs' },
{
label: menuLabel('关于我们'), key: 'g-about',
children: [
{ label: menuLabel('关于呼籁'), key: '/admin/content/about' },
{ label: menuLabel('呼籁旅行百科'), key: '/admin/content/brand-wiki' },
{ label: menuLabel('呼籁文旅集团'), key: '/admin/content/company-wiki' },
{ label: menuLabel('最新动态'), key: '/admin/articles' },
{ label: menuLabel('资质荣誉'), key: '/admin/content/qualifications' },
{ label: menuLabel('合作伙伴'), key: '/admin/content/partners' },
],
},
{ label: menuLabel('联系我们'), key: '/admin/content/contact' },
{ label: menuLabel('博客文章'), key: '/admin/blogs' },
{ label: submissionLabel(), key: '/admin/submissions' },
{ type: 'divider' },
{
label: menuLabel('系统设置'), key: 'g-settings',
children: [
{ label: menuLabel('导航菜单'), key: '/admin/navigation' },
{ label: menuLabel('SEO 配置'), key: '/admin/content/seo' },
{ label: menuLabel('品牌信息'), key: '/admin/content/brand' },
{ label: menuLabel('图片配置'), key: '/admin/content/images' },
{ label: menuLabel('版本历史'), key: '/admin/content/versions' },
{ label: menuLabel('定制页配置'), key: '/admin/content/customize' },
],
},
]})
// ── 路由映射到菜单 key ────────────────────────
const activeKey = computed(() => {
const path = route.path
// 精确匹配
if (path === '/admin') return '/admin'
// 子路由匹配到父菜单
const allKeys = []
function collectKeys(items) {
for (const item of items) {
if (item.key && !item.key.startsWith('g-')) allKeys.push(item.key)
if (item.children) collectKeys(item.children)
}
}
collectKeys(menuOptions.value)
// 最长前缀匹配
let best = ''
for (const k of allKeys) {
if (path.startsWith(k) && k.length > best.length) best = k
}
return best || path
})
// ── 自动展开分组 ──────────────────────────────
const expandedKeys = ref([])
const groupMappings = {
'g-products': [],
'g-destinations': ['destinations', 'calendar', 'guides'],
'g-reviews': ['reviews', 'xiaohongshu', 'youji', 'gallery', 'stories'],
'g-about': ['about', 'articles', 'qualifications', 'partners', 'brand-wiki', 'company-wiki'],
'g-settings': ['navigation', 'seo', 'brand', 'images', 'versions', 'customize'],
}
watchEffect(() => {
const path = route.path
const keys = new Set(expandedKeys.value)
for (const [group, keywords] of Object.entries(groupMappings)) {
if (keywords.some((kw) => path.includes(kw))) {
keys.add(group)
}
}
expandedKeys.value = [...keys]
})
function handleMenuSelect(key) {
if (!key.startsWith('g-')) {
navigateTo(key)
}
}
function handleExpand(keys) {
expandedKeys.value = keys
}
</script>
<style scoped>
.admin-layout {
display: flex;
min-height: 100vh;
background: #f0f2f5;
}
.admin-sidebar {
width: 240px;
background: #001529;
color: #fff;
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 100;
overflow: hidden;
}
.admin-logo {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 16px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.logo-icon {
width: 36px;
height: 36px;
background: #3a7d44;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 18px;
flex-shrink: 0;
}
.logo-text h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 1.2;
}
.logo-text span {
font-size: 11px;
color: rgba(255,255,255,0.35);
}
.menu-wrap {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.menu-wrap::-webkit-scrollbar {
width: 4px;
}
.menu-wrap::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.15);
border-radius: 2px;
}
.admin-user {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: rgba(255,255,255,0.65);
}
.admin-main {
flex: 1;
margin-left: 240px;
padding: 24px 28px;
min-height: 100vh;
}
</style>
<style>
/* NMenu inverted 在 #001529 背景下的微调 */
.admin-sidebar .n-menu.n-menu--vertical {
background: transparent;
}
</style>