- 后台全面引入 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>
109 行
4.0 KiB
Vue
109 行
4.0 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">{{ contentLabel }}</h1>
|
|
<p style="margin: 0; font-size: 13px; color: #9ca3af">编辑「{{ contentLabel }}」的数据内容</p>
|
|
</div>
|
|
<n-space>
|
|
<n-button v-if="previewUrl" tag="a" :href="previewUrl" target="_blank" secondary>查看前台</n-button>
|
|
<n-button secondary @click="resetContent">重置</n-button>
|
|
<n-button type="primary" :disabled="!!jsonError" :loading="saving" @click="saveContent">保存</n-button>
|
|
</n-space>
|
|
</n-space>
|
|
|
|
<n-spin :show="!loaded">
|
|
<n-card v-if="loaded">
|
|
<n-space style="margin-bottom: 12px" size="small">
|
|
<n-button size="small" @click="formatJSON">格式化</n-button>
|
|
<n-button size="small" @click="collapseJSON">压缩</n-button>
|
|
<span style="margin-left: auto; font-size: 12px; color: #9ca3af">此配置结构较复杂,使用 JSON 编辑器</span>
|
|
</n-space>
|
|
|
|
<n-input
|
|
v-model:value="jsonContent"
|
|
type="textarea"
|
|
:rows="30"
|
|
font-family="monospace"
|
|
:status="jsonError ? 'error' : undefined"
|
|
@input="validateJSON"
|
|
/>
|
|
|
|
<n-space justify="space-between" align="center" style="margin-top: 12px">
|
|
<n-text v-if="jsonError" type="error" style="font-size: 13px">{{ jsonError }}</n-text>
|
|
<n-text v-else type="success" style="font-size: 13px">JSON 格式正确</n-text>
|
|
</n-space>
|
|
</n-card>
|
|
</n-spin>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { NButton, NCard, NSpace, NInput, NSpin, NText } from 'naive-ui'
|
|
import { useMessage } from 'naive-ui'
|
|
|
|
definePageMeta({ layout: 'admin', middleware: 'admin' })
|
|
|
|
const route = useRoute()
|
|
const { adminFetch } = useAdmin()
|
|
const message = useMessage()
|
|
|
|
const key = computed(() => route.params.key)
|
|
const contentLabel = ref('')
|
|
const jsonContent = ref('')
|
|
const originalContent = ref('')
|
|
const jsonError = ref('')
|
|
const loaded = ref(false)
|
|
const saving = ref(false)
|
|
|
|
const previewMap = {
|
|
products: '/products', 'autumn-products': '/products/autumn', 'winter-products': '/products/winter',
|
|
courses: '/summer-camp', 'winter-camp': '/winter-camp', destinations: '/destinations',
|
|
'destinations-detail': '/destinations', calendar: '/calendar', guides: '/guides',
|
|
'xiaohongshu-wall': '/xiaohongshu', youji: '/youji', about: '/about',
|
|
qualifications: '/qualifications', partners: '/partners', contact: '/contact',
|
|
pricing: '/pricing', brand: '/', seo: '/', navigation: '/',
|
|
}
|
|
const previewUrl = computed(() => previewMap[key.value] || '')
|
|
|
|
async function loadData() {
|
|
loaded.value = false
|
|
const data = await adminFetch(`/api/admin/site-content?key=${key.value}`)
|
|
contentLabel.value = data.label
|
|
jsonContent.value = JSON.stringify(data.data, null, 2)
|
|
originalContent.value = jsonContent.value
|
|
jsonError.value = ''
|
|
loaded.value = true
|
|
}
|
|
|
|
function validateJSON() {
|
|
try { JSON.parse(jsonContent.value); jsonError.value = '' }
|
|
catch (e) { jsonError.value = e.message }
|
|
}
|
|
|
|
function formatJSON() {
|
|
try { jsonContent.value = JSON.stringify(JSON.parse(jsonContent.value), null, 2); jsonError.value = '' }
|
|
catch (e) { jsonError.value = e.message }
|
|
}
|
|
|
|
function collapseJSON() {
|
|
try { jsonContent.value = JSON.stringify(JSON.parse(jsonContent.value)); jsonError.value = '' }
|
|
catch (e) { jsonError.value = e.message }
|
|
}
|
|
|
|
function resetContent() { jsonContent.value = originalContent.value; jsonError.value = '' }
|
|
|
|
async function saveContent() {
|
|
if (jsonError.value) return
|
|
saving.value = true
|
|
try {
|
|
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: key.value, data: JSON.parse(jsonContent.value) } })
|
|
originalContent.value = jsonContent.value
|
|
message.success('保存成功')
|
|
} catch (e) { message.error('保存失败:' + (e.data?.message || e.message)) }
|
|
finally { saving.value = false }
|
|
}
|
|
|
|
watch(key, loadData, { immediate: true })
|
|
</script>
|