hulai-website/pages/admin/navigation.vue
Mimingguang ee078b61bc feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 Naive UI 组件库,统一 UI 规范
- 前台 usePublicApi 切换到 API 调用(GET /api/content/[key])
- 前后台数据源统一(site_content 表)
- 产品线统一管理(tour/camp/course 三套编辑器)
- 产品数据归一化(itinerary/faq/pricing 格式统一)
- 表单提交 API 改写入 submissions 表
- 新增 submissions 标记已读 API
- site-content PUT 支持 upsert
- 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb)
- 消除 PageHero 类型警告(useSEO reactive 修复)
- 25 个前台页面全部 HTTP 200,展示效果不变

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:48:02 +08:00

113 行
5.5 KiB
Vue

<template>
<div style="max-width: 1100px">
<n-space justify="space-between" align="center" style="margin-bottom: 20px">
<div>
<h1 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; color: #111827">导航菜单管理</h1>
<p style="margin: 0; font-size: 13px; color: #9ca3af">管理官网顶部导航和底部导航的菜单结构</p>
</div>
<n-space>
<n-button secondary @click="loadData">重置</n-button>
<n-button type="primary" :loading="saving" @click="saveNavigation">保存</n-button>
</n-space>
</n-space>
<n-spin :show="!loaded">
<!-- 顶部导航 -->
<n-card title="顶部导航 (Header)">
<template #header-extra>
<n-button size="small" type="primary" @click="addHeaderItem">添加菜单项</n-button>
</template>
<div v-for="(item, idx) in headerItems" :key="idx" style="border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 10px; overflow: hidden;">
<n-space align="center" justify="space-between" style="padding: 10px 12px; background: #fafbfc;">
<n-space>
<n-input v-model:value="item.text" placeholder="文字" size="small" style="width: 140px" />
<n-input v-model:value="item.to" placeholder="链接" size="small" style="width: 160px" />
</n-space>
<n-space size="small">
<n-button size="tiny" quaternary @click="addChild(item)">+ 子菜单</n-button>
<n-button size="tiny" quaternary @click="moveUp(headerItems, idx)" :disabled="idx === 0"></n-button>
<n-button size="tiny" quaternary @click="moveDown(headerItems, idx)" :disabled="idx === headerItems.length - 1"></n-button>
<n-button size="tiny" type="error" quaternary @click="headerItems.splice(idx, 1)">删除</n-button>
</n-space>
</n-space>
<div v-if="item.children?.length" style="padding: 8px 12px 8px 32px; border-top: 1px solid #f0f0f0;">
<n-space v-for="(child, cidx) in item.children" :key="cidx" align="center" style="margin-bottom: 6px;">
<n-input v-model:value="child.text" placeholder="文字" size="small" style="width: 120px" />
<n-input v-model:value="child.to" placeholder="链接" size="small" style="width: 140px" />
<n-input v-model:value="child.season" placeholder="季节" size="small" style="width: 70px" />
<n-button size="tiny" type="error" quaternary @click="item.children.splice(cidx, 1)">删除</n-button>
</n-space>
</div>
</div>
</n-card>
<!-- 底部导航 -->
<n-card title="底部导航 (Footer)" style="margin-top: 16px">
<template #header-extra>
<n-button size="small" type="primary" @click="addFooterGroup">添加分组</n-button>
</template>
<div v-for="(group, gidx) in footerItems" :key="gidx" style="border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 12px; padding: 12px;">
<n-space justify="space-between" align="center" style="margin-bottom: 10px;">
<n-input v-model:value="group.title" placeholder="分组标题" size="small" style="width: 200px" />
<n-button size="tiny" type="error" quaternary @click="footerItems.splice(gidx, 1)">删除分组</n-button>
</n-space>
<div style="padding-left: 16px;">
<n-space v-for="(link, lidx) in group.links" :key="lidx" align="center" style="margin-bottom: 6px;">
<n-input v-model:value="link.text" placeholder="文字" size="small" style="width: 120px" />
<n-input v-model:value="link.to" placeholder="链接" size="small" style="width: 160px" />
<n-button size="tiny" type="error" quaternary @click="group.links.splice(lidx, 1)">删除</n-button>
</n-space>
<n-button size="tiny" dashed @click="group.links.push({ text: '', to: '' })">+ 添加链接</n-button>
</div>
</div>
</n-card>
</n-spin>
</div>
</template>
<script setup>
import { NButton, NCard, NSpace, NInput, NSpin } from 'naive-ui'
import { useMessage } from 'naive-ui'
definePageMeta({ layout: 'admin', middleware: 'admin' })
const { adminFetch } = useAdmin()
const message = useMessage()
const headerItems = ref([])
const footerItems = ref([])
const loaded = ref(false)
const saving = ref(false)
async function loadData() {
loaded.value = false
const data = await adminFetch('/api/admin/site-content?key=navigation')
headerItems.value = JSON.parse(JSON.stringify(data.data.header || []))
footerItems.value = JSON.parse(JSON.stringify(data.data.footer || []))
loaded.value = true
}
function addHeaderItem() { headerItems.value.push({ text: '', to: '/' }) }
function addChild(item) { if (!item.children) item.children = []; item.children.push({ text: '', to: '' }) }
function addFooterGroup() { footerItems.value.push({ title: '', links: [{ text: '', to: '' }] }) }
function moveUp(arr, idx) { if (idx > 0) { [arr[idx - 1], arr[idx]] = [arr[idx], arr[idx - 1]] } }
function moveDown(arr, idx) { if (idx < arr.length - 1) { [arr[idx], arr[idx + 1]] = [arr[idx + 1], arr[idx]] } }
async function saveNavigation() {
saving.value = true
try {
await adminFetch('/api/admin/site-content', {
method: 'PUT',
body: { key: 'navigation', data: { header: headerItems.value, footer: footerItems.value } },
})
message.success('导航菜单已保存')
} catch (e) { message.error('保存失败:' + (e.data?.message || e.message)) }
finally { saving.value = false }
}
onMounted(loadData)
</script>