覆盖最新版本代码,包含: - 新增品牌百科、公司百科页面及CMS管理 - 新增微信引导组件(WechatCTA) - 新增品牌/公司图片资源 - 全站组件/页面/数据/样式更新 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
258 行
12 KiB
Vue
258 行
12 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 tag="a" href="/brand" target="_blank" secondary>查看前台 ↗</n-button>
|
||
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
|
||
</n-space>
|
||
</n-space>
|
||
|
||
<n-spin :show="!loaded">
|
||
<n-collapse :default-expanded-names="['basic']">
|
||
<!-- 基本信息 -->
|
||
<n-collapse-item title="基本信息" name="basic">
|
||
<n-card>
|
||
<n-form label-placement="top">
|
||
<n-grid :cols="2" :x-gap="12">
|
||
<n-gi><n-form-item label="标题"><n-input v-model:value="data.title" /></n-form-item></n-gi>
|
||
<n-gi><n-form-item label="Logo 路径"><n-input v-model:value="data.logo" /></n-form-item></n-gi>
|
||
</n-grid>
|
||
<n-form-item label="副标题">
|
||
<n-input v-model:value="data.subtitle" />
|
||
</n-form-item>
|
||
<n-form-item label="摘要">
|
||
<n-input v-model:value="data.summary" type="textarea" :autosize="{ minRows: 3, maxRows: 8 }" />
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-card>
|
||
</n-collapse-item>
|
||
|
||
<!-- 信息框 -->
|
||
<n-collapse-item title="信息框" name="infoBox">
|
||
<n-card>
|
||
<div style="width: 100%">
|
||
<div v-for="(_, key) in data.infoBox" :key="key" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
|
||
<n-input :value="key" placeholder="字段名" style="width: 160px" @update:value="v => renameKey(data.infoBox, key, v)" />
|
||
<n-input v-model:value="data.infoBox[key]" placeholder="值" style="flex: 1" />
|
||
<n-button size="tiny" type="error" quaternary @click="delete data.infoBox[key]">删</n-button>
|
||
</div>
|
||
<n-button size="small" type="primary" dashed @click="addInfoBoxItem">添加字段</n-button>
|
||
</div>
|
||
</n-card>
|
||
</n-collapse-item>
|
||
|
||
<!-- 内容章节 -->
|
||
<n-collapse-item title="内容章节" name="sections">
|
||
<n-card>
|
||
<div style="width: 100%">
|
||
<n-card v-for="(sec, i) in data.sections" :key="i" size="small" style="margin-bottom: 16px">
|
||
<template #header>
|
||
<n-space align="center">
|
||
<span style="font-size: 14px; font-weight: 500">{{ sec.title || '新章节' }}</span>
|
||
<n-tag :bordered="false" size="small" type="info">{{ getSectionType(sec) }}</n-tag>
|
||
<n-button size="tiny" type="error" quaternary @click="data.sections.splice(i, 1)">删除</n-button>
|
||
</n-space>
|
||
</template>
|
||
<n-form label-placement="top">
|
||
<n-form-item label="章节标题">
|
||
<n-input v-model:value="sec.title" />
|
||
</n-form-item>
|
||
|
||
<!-- 纯文本型 -->
|
||
<template v-if="getSectionType(sec) === '文本'">
|
||
<n-form-item label="内容">
|
||
<n-input v-model:value="sec.content" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" />
|
||
</n-form-item>
|
||
<n-form-item v-if="sec.image !== undefined" label="图片路径">
|
||
<n-input v-model:value="sec.image" />
|
||
</n-form-item>
|
||
</template>
|
||
|
||
<!-- 子章节型(品牌历史) -->
|
||
<template v-if="getSectionType(sec) === '子章节'">
|
||
<n-form-item label="子章节">
|
||
<div style="width: 100%">
|
||
<n-card v-for="(sub, j) in sec.subsections" :key="j" size="small" style="margin-bottom: 8px">
|
||
<template #header>
|
||
<n-space align="center">
|
||
<span style="font-size: 13px; color: #6b7280">{{ j + 1 }}.</span>
|
||
<n-button size="tiny" type="error" quaternary @click="sec.subsections.splice(j, 1)">删</n-button>
|
||
</n-space>
|
||
</template>
|
||
<n-form-item label="标题"><n-input v-model:value="sub.title" /></n-form-item>
|
||
<n-form-item label="内容"><n-input v-model:value="sub.content" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /></n-form-item>
|
||
</n-card>
|
||
<n-button size="small" type="primary" dashed @click="sec.subsections.push({ title: '', content: '' })">添加子章节</n-button>
|
||
</div>
|
||
</n-form-item>
|
||
</template>
|
||
|
||
<!-- 产品型 -->
|
||
<template v-if="getSectionType(sec) === '产品'">
|
||
<n-form-item label="简介">
|
||
<n-input v-model:value="sec.intro" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" />
|
||
</n-form-item>
|
||
<n-form-item label="产品列表">
|
||
<div style="width: 100%">
|
||
<n-card v-for="(prod, j) in sec.products" :key="j" size="small" style="margin-bottom: 8px">
|
||
<template #header>
|
||
<n-space align="center">
|
||
<span style="font-size: 13px; color: #6b7280">{{ prod.name || '新产品' }}</span>
|
||
<n-button size="tiny" type="error" quaternary @click="sec.products.splice(j, 1)">删</n-button>
|
||
</n-space>
|
||
</template>
|
||
<n-form-item label="名称"><n-input v-model:value="prod.name" /></n-form-item>
|
||
<n-form-item label="描述"><n-input v-model:value="prod.description" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /></n-form-item>
|
||
</n-card>
|
||
<n-button size="small" type="primary" dashed @click="sec.products.push({ name: '', description: '' })">添加产品</n-button>
|
||
</div>
|
||
</n-form-item>
|
||
</template>
|
||
|
||
<!-- 步骤型(服务流程) -->
|
||
<template v-if="getSectionType(sec) === '步骤'">
|
||
<n-form-item label="步骤">
|
||
<div style="width: 100%">
|
||
<div v-for="(step, j) in sec.steps" :key="j" style="display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px">
|
||
<n-input-number v-model:value="step.step" placeholder="序号" style="width: 80px" :min="1" />
|
||
<n-input v-model:value="step.name" placeholder="名称" style="width: 140px" />
|
||
<n-input v-model:value="step.detail" placeholder="详情" style="flex: 1" />
|
||
<n-button size="tiny" type="error" quaternary @click="sec.steps.splice(j, 1)">删</n-button>
|
||
</div>
|
||
<n-button size="small" type="primary" dashed @click="sec.steps.push({ step: sec.steps.length + 1, name: '', detail: '' })">添加步骤</n-button>
|
||
</div>
|
||
</n-form-item>
|
||
</template>
|
||
|
||
<!-- 键值对型(文化理念) -->
|
||
<template v-if="getSectionType(sec) === '键值对'">
|
||
<n-form-item label="条目">
|
||
<div style="width: 100%">
|
||
<div v-for="(item, j) in sec.items" :key="j" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
|
||
<n-input v-model:value="item.label" placeholder="标签" style="width: 160px" />
|
||
<n-input v-model:value="item.value" placeholder="内容" style="flex: 1" />
|
||
<n-button size="tiny" type="error" quaternary @click="sec.items.splice(j, 1)">删</n-button>
|
||
</div>
|
||
<n-button size="small" type="primary" dashed @click="sec.items.push({ label: '', value: '' })">添加条目</n-button>
|
||
</div>
|
||
</n-form-item>
|
||
</template>
|
||
</n-form>
|
||
</n-card>
|
||
|
||
<n-space style="margin-top: 12px">
|
||
<n-button size="small" type="primary" dashed @click="addSection('text')">+ 文本章节</n-button>
|
||
<n-button size="small" type="primary" dashed @click="addSection('subsections')">+ 子章节</n-button>
|
||
<n-button size="small" type="primary" dashed @click="addSection('products')">+ 产品章节</n-button>
|
||
<n-button size="small" type="primary" dashed @click="addSection('steps')">+ 步骤章节</n-button>
|
||
<n-button size="small" type="primary" dashed @click="addSection('items')">+ 键值对章节</n-button>
|
||
</n-space>
|
||
</div>
|
||
</n-card>
|
||
</n-collapse-item>
|
||
|
||
<!-- 图库 -->
|
||
<n-collapse-item title="图库" name="gallery">
|
||
<n-card>
|
||
<n-dynamic-input v-model:value="data.gallery" :min="0" placeholder="图片路径,如 /images/brand/xxx.jpg" />
|
||
</n-card>
|
||
</n-collapse-item>
|
||
|
||
<!-- SEO -->
|
||
<n-collapse-item title="SEO" name="seo">
|
||
<n-card>
|
||
<n-form label-placement="top">
|
||
<n-form-item label="页面标题">
|
||
<n-input v-model:value="data.seo.title" />
|
||
</n-form-item>
|
||
<n-form-item label="描述">
|
||
<n-input v-model:value="data.seo.description" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
|
||
</n-form-item>
|
||
<n-form-item label="关键词">
|
||
<n-input v-model:value="data.seo.keywords" />
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-card>
|
||
</n-collapse-item>
|
||
</n-collapse>
|
||
</n-spin>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NGrid, NGi, NCollapse, NCollapseItem, NDynamicInput, NInputNumber, NTag } from 'naive-ui'
|
||
import { useMessage } from 'naive-ui'
|
||
|
||
definePageMeta({ layout: 'admin', middleware: 'admin' })
|
||
const { adminFetch } = useAdmin()
|
||
const message = useMessage()
|
||
|
||
const data = ref({
|
||
title: '', subtitle: '', summary: '', logo: '',
|
||
infoBox: {},
|
||
sections: [],
|
||
gallery: [],
|
||
seo: { title: '', description: '', keywords: '' }
|
||
})
|
||
const loaded = ref(false)
|
||
const saving = ref(false)
|
||
|
||
function getSectionType(sec) {
|
||
if (sec.subsections) return '子章节'
|
||
if (sec.products) return '产品'
|
||
if (sec.steps) return '步骤'
|
||
if (sec.items) return '键值对'
|
||
return '文本'
|
||
}
|
||
|
||
function addSection(type) {
|
||
const templates = {
|
||
text: { title: '', content: '' },
|
||
subsections: { title: '', subsections: [{ title: '', content: '' }] },
|
||
products: { title: '', intro: '', products: [{ name: '', description: '' }] },
|
||
steps: { title: '', steps: [{ step: 1, name: '', detail: '' }] },
|
||
items: { title: '', items: [{ label: '', value: '' }] },
|
||
}
|
||
data.value.sections.push(templates[type])
|
||
}
|
||
|
||
function addInfoBoxItem() {
|
||
const key = `新字段${Object.keys(data.value.infoBox).length + 1}`
|
||
data.value.infoBox[key] = ''
|
||
}
|
||
|
||
function renameKey(obj, oldKey, newKey) {
|
||
if (newKey && newKey !== oldKey && !(newKey in obj)) {
|
||
const val = obj[oldKey]
|
||
delete obj[oldKey]
|
||
obj[newKey] = val
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
const res = await adminFetch('/api/admin/site-content?key=brand-wiki')
|
||
data.value = res.data
|
||
if (!data.value.seo) data.value.seo = { title: '', description: '', keywords: '' }
|
||
if (!data.value.gallery) data.value.gallery = []
|
||
loaded.value = true
|
||
}
|
||
|
||
async function save() {
|
||
saving.value = true
|
||
try {
|
||
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: 'brand-wiki', data: data.value } })
|
||
message.success('已保存')
|
||
} catch (e) {
|
||
message.error('保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|