- 后台全面引入 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>
113 行
5.5 KiB
Vue
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>
|