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

95 行
3.0 KiB
Vue

<template>
<div style="max-width: 900px;">
<NBreadcrumb style="margin-bottom: 16px;">
<NBreadcrumbItem>
<NuxtLink to="/admin/content/selector">产品选择器</NuxtLink>
</NBreadcrumbItem>
<NBreadcrumbItem>推荐理由</NBreadcrumbItem>
</NBreadcrumb>
<NSpace justify="space-between" align="center" style="margin-bottom: 16px;">
<div style="font-size: 20px; font-weight: 600; color: #111827;">推荐理由</div>
<NSpace :size="8">
<NuxtLink to="/admin/content/selector">
<NButton>返回</NButton>
</NuxtLink>
<NButton type="primary" :loading="saving" @click="handleSave">保存</NButton>
</NSpace>
</NSpace>
<NSpin :show="!loaded">
<template v-if="loaded">
<div style="font-size: 13px; color: #6b7280; margin-bottom: 20px;">
每个产品在不同匹配情况下展示的推荐文案
</div>
<NCard
v-for="(reasons, pid) in matchReasons"
:key="pid"
style="margin-bottom: 16px;"
>
<template #header>
<span style="font-size: 14px; font-weight: 600; color: #374151;">{{ pid }}</span>
</template>
<NForm label-placement="top">
<NFormItem
v-for="(text, rKey) in reasons"
:key="rKey"
:label="rKey"
:label-style="{ fontFamily: 'monospace', fontSize: '12px', color: '#9ca3af' }"
>
<NInput v-model:value="matchReasons[pid][rKey]" />
</NFormItem>
</NForm>
</NCard>
<div class="save-bar">
<NuxtLink to="/admin/content/selector">
<NButton>返回</NButton>
</NuxtLink>
<NButton type="primary" :loading="saving" @click="handleSave">保存</NButton>
</div>
</template>
</NSpin>
</div>
</template>
<script setup>
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
import { useMessage } from 'naive-ui'
definePageMeta({ layout: 'admin', middleware: 'admin' })
const message = useMessage()
const { adminFetch } = useAdmin()
const fullData = ref({})
const matchReasons = ref({})
const loaded = ref(false)
const saving = ref(false)
async function load() {
const res = await adminFetch('/api/admin/site-content?key=selector')
fullData.value = res.data
matchReasons.value = JSON.parse(JSON.stringify(res.data.matchReasons || {}))
loaded.value = true
}
async function handleSave() {
saving.value = true
try {
fullData.value.matchReasons = matchReasons.value
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: 'selector', data: fullData.value } })
message.success('保存成功')
} catch {
message.error('保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
<style scoped>
.save-bar { position: sticky; bottom: 0; display: flex; justify-content: flex-end; gap: 8px; padding: 16px 0; margin-top: 24px; background: #f0f2f5; border-top: 1px solid #e5e7eb; }
</style>