hulai-website/pages/admin/content/section-titles.vue

255 行
8.7 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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">统一管理各页面内部每个内容板块的标题与副标题CommonSectionTitle</p>
</div>
<n-space>
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
</n-space>
</n-space>
<n-spin :show="!loaded">
<n-space vertical :size="20">
<n-card
v-for="group in groups"
:key="group.key"
>
<template #header>
<n-space align="center" :size="10">
<span style="font-size: 15px; font-weight: 600; color: #111827">{{ group.label }}</span>
<n-button
v-if="group.preview"
size="tiny"
tag="a"
:href="group.preview"
target="_blank"
quaternary
style="color: #6b7280"
>
查看前台 ↗
</n-button>
</n-space>
</template>
<n-space vertical :size="14">
<div
v-for="item in group.items"
:key="item.key"
style="border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px 14px; background: #fafafa"
>
<n-tag :bordered="false" size="small" style="margin-bottom: 10px">{{ item.label }}</n-tag>
<n-form label-placement="top" :show-feedback="false" style="gap: 10px">
<n-form-item label="主标题">
<n-input
v-model:value="ensureEntry(group.key, item.key).title"
placeholder="主标题"
/>
</n-form-item>
<n-form-item v-if="item.hasLabel" label="英文标签label" style="margin-top: 10px">
<n-input
v-model:value="ensureEntry(group.key, item.key).label"
placeholder="英文标签"
/>
</n-form-item>
<n-form-item label="副标题" style="margin-top: 10px">
<n-input
v-model:value="ensureEntry(group.key, item.key).subtitle"
type="textarea"
:autosize="{ minRows: 1, maxRows: 3 }"
placeholder="副标题"
/>
</n-form-item>
</n-form>
</div>
</n-space>
</n-card>
</n-space>
</n-spin>
</div>
</template>
<script setup>
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NTag } from 'naive-ui'
import { useMessage } from 'naive-ui'
definePageMeta({ layout: 'admin', middleware: 'admin' })
const { adminFetch } = useAdmin()
const message = useMessage()
// 分组配置key 对应数据结构里的顶层分组
const groups = [
{
key: 'home', label: '首页', preview: '/',
items: [
{ key: 'productTree', label: '怎么找到适合你的行程', hasLabel: true },
{ key: 'knowledgeHub', label: '知识库', hasLabel: true },
{ key: 'faq', label: '常见问答', hasLabel: true },
{ key: 'productPreview', label: '旅行产品预览', hasLabel: false },
{ key: 'differentiator', label: '为什么选择呼籁', hasLabel: true },
{ key: 'reviewHighlights', label: '客户怎么说', hasLabel: true },
],
},
{
key: 'products', label: '产品页(额吉的故乡)', preview: '/products',
items: [
{ key: 'v9Series', label: 'V9系列主标题' },
{ key: 'evolutionStats', label: '产品进化' },
{ key: 'upgrades', label: '2026核心升级' },
{ key: 'highlights', label: '更多V9亮点' },
{ key: 'compareV8V9', label: 'V8→V9 对比' },
{ key: 'philosophy', label: '产品哲学' },
{ key: 'pricingPhilosophy', label: '价格透明' },
{ key: 'selectionGuide', label: '选版指南' },
],
},
{
key: 'productsAutumn', label: '秋季产品', preview: '/products/autumn',
items: [
{ key: 'highlights', label: '秋季不可错过' },
{ key: 'southLine', label: '南线产品' },
{ key: 'southHighlights', label: '南线独有体验' },
{ key: 'northLine', label: '北线产品' },
{ key: 'northHighlights', label: '北线独有体验' },
{ key: 'selection', label: '选版指南' },
],
},
{
key: 'productsWinter', label: '冬季产品', preview: '/products/winter',
items: [
{ key: 'versions', label: 'V3系列' },
{ key: 'highlights', label: '冬季行程亮点' },
{ key: 'south', label: '南线特色' },
{ key: 'north', label: '北线特色' },
],
},
{
key: 'summerCamp', label: '夏令营', preview: '/summer-camp',
items: [
{ key: 'principles', label: '行程设计原则' },
{ key: 'itinerary', label: '行程概览' },
{ key: 'activities', label: '核心体验' },
{ key: 'compare', label: '营 vs 额吉的故乡' },
{ key: 'faq', label: '常见问题' },
],
},
{
key: 'winterCamp', label: '冬令营', preview: '/winter-camp',
items: [
{ key: 'whyHulunbuir', label: '为什么是呼伦贝尔' },
{ key: 'advantages', label: '亲子营优势' },
{ key: 'hotels', label: '住宿安排' },
{ key: 'itinerary', label: '行程概览' },
{ key: 'photographer', label: '随行摄影师' },
{ key: 'clothing', label: '穿衣指南' },
{ key: 'serviceConfig', label: '服务配置' },
{ key: 'faq', label: '常见问题' },
],
},
{
key: 'qualifications', label: '资质荣誉', preview: '/qualifications',
items: [
{ key: 'safety', label: '服务保障' },
{ key: 'awards', label: '荣誉与认证' },
],
},
{
key: 'courses', label: '研学课程', preview: '/courses',
items: [
{ key: 'byAge', label: '按年龄找课程' },
{ key: 'modules', label: '6大课程模块' },
{ key: 'mentors', label: '课程导师团队' },
{ key: 'faq', label: '常见问题' },
],
},
{
key: 'partners', label: '合作伙伴', preview: '/partners',
items: [
{ key: 'platform', label: '平台认证' },
{ key: 'government', label: '政府资质' },
{ key: 'universities', label: '校企合作' },
{ key: 'scenic', label: '景区合作' },
{ key: 'media', label: '媒体报道' },
],
},
{
key: 'contact', label: '联系我们', preview: '/contact',
items: [
{ key: 'primary', label: '立即联系' },
{ key: 'more', label: '更多联系方式' },
],
},
{
key: 'gallery', label: '相册', preview: '/gallery',
items: [
{ key: 'intro', label: '开篇' },
{ key: 'works', label: '作品墙' },
],
},
{
key: 'pricing', label: '价格页', preview: '/pricing',
items: [
{ key: 'reference', label: '价格参考' },
{ key: 'explanation', label: '费用说明' },
],
},
{
key: 'aboutPage', label: '关于我们(页内)', preview: '/about',
items: [
{ key: 'culture', label: '呼籁文化理念' },
{ key: 'xiaohongshu', label: '小红书上的呼籁' },
{ key: 'team', label: '我们的团队' },
{ key: 'copyright', label: '知识产权' },
{ key: 'qualCert', label: '资质认证' },
{ key: 'qualService', label: '服务保障(页内)' },
{ key: 'cultureValues', label: '企业文化' },
{ key: 'mascot', label: '品牌IP·籁小咩' },
],
},
]
const data = ref({})
const loaded = ref(false)
const saving = ref(false)
function ensureEntry(groupKey, itemKey) {
if (!data.value[groupKey]) data.value[groupKey] = {}
if (!data.value[groupKey][itemKey]) data.value[groupKey][itemKey] = { title: '', subtitle: '', label: '' }
// 补齐字段,避免 v-model 访问 undefined
const e = data.value[groupKey][itemKey]
if (e.title === undefined) e.title = ''
if (e.subtitle === undefined) e.subtitle = ''
if (e.label === undefined) e.label = ''
return e
}
async function load() {
try {
const res = await adminFetch('/api/admin/site-content?key=section-titles')
data.value = res.data || {}
} catch (e) {
data.value = {}
} finally {
loaded.value = true
}
}
async function save() {
saving.value = true
try {
await adminFetch('/api/admin/site-content', {
method: 'PUT',
body: { key: 'section-titles', data: data.value },
})
message.success('已保存')
} catch (e) {
message.error('保存失败:' + (e.data?.message || ''))
} finally {
saving.value = false
}
}
onMounted(load)
</script>