- 后台全面引入 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>
165 行
6.2 KiB
Vue
165 行
6.2 KiB
Vue
<template>
|
|
<div style="max-width: 900px;">
|
|
<n-breadcrumb style="margin-bottom: 16px;">
|
|
<n-breadcrumb-item>
|
|
<NuxtLink to="/admin/content/destinations-detail">景点详情</NuxtLink>
|
|
</n-breadcrumb-item>
|
|
<n-breadcrumb-item>{{ form.name || '编辑' }}</n-breadcrumb-item>
|
|
</n-breadcrumb>
|
|
|
|
<n-space justify="space-between" align="center" style="margin-bottom: 24px;">
|
|
<div style="font-size: 20px; font-weight: 600; color: #111827;">{{ form.name || '编辑景点' }}</div>
|
|
<n-space>
|
|
<NuxtLink to="/admin/content/destinations-detail" style="text-decoration: none;">
|
|
<n-button>返回列表</n-button>
|
|
</NuxtLink>
|
|
<n-button type="primary" :loading="saving" :disabled="saving" @click="handleSave">
|
|
{{ saving ? '保存中...' : '保存' }}
|
|
</n-button>
|
|
</n-space>
|
|
</n-space>
|
|
|
|
<n-spin v-if="pageLoading" style="display:block; padding: 60px; text-align: center;" />
|
|
<n-card v-else-if="loadError" style="color: #ef4444; text-align: center; padding: 40px;">{{ loadError }}</n-card>
|
|
<template v-else>
|
|
<n-card title="基本信息" style="margin-bottom: 16px;">
|
|
<n-grid :cols="2" :x-gap="12">
|
|
<n-gi>
|
|
<n-form-item label="ID">
|
|
<n-input v-model:value="form.id" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
<n-gi>
|
|
<n-form-item label="名称">
|
|
<n-input v-model:value="form.name" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
<n-gi>
|
|
<n-form-item label="副标题">
|
|
<n-input v-model:value="form.subtitle" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
<n-gi>
|
|
<n-form-item label="标签">
|
|
<n-input v-model:value="form.tag" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
</n-grid>
|
|
<n-form-item label="描述">
|
|
<n-input v-model:value="form.description" type="textarea" :rows="5" />
|
|
</n-form-item>
|
|
</n-card>
|
|
|
|
<n-card title="亮点与建议" style="margin-bottom: 16px;">
|
|
<n-form-item label="亮点(每行一个)">
|
|
<n-input
|
|
type="textarea"
|
|
:rows="4"
|
|
:value="(form.highlights||[]).join('\n')"
|
|
@input="form.highlights = $event.split('\n').filter(Boolean)"
|
|
/>
|
|
</n-form-item>
|
|
<n-form-item label="旅行建议(每行一个)">
|
|
<n-input
|
|
type="textarea"
|
|
:rows="4"
|
|
:value="(form.tips||[]).join('\n')"
|
|
@input="form.tips = $event.split('\n').filter(Boolean)"
|
|
/>
|
|
</n-form-item>
|
|
<n-form-item v-if="form.seasons" label="可去季节(每行一个)">
|
|
<n-input
|
|
type="textarea"
|
|
:rows="2"
|
|
:value="(form.seasons||[]).join('\n')"
|
|
@input="form.seasons = $event.split('\n').filter(Boolean)"
|
|
/>
|
|
</n-form-item>
|
|
</n-card>
|
|
|
|
<n-card v-if="form.bestSeason" title="最佳季节" style="margin-bottom: 16px;">
|
|
<n-grid :cols="2" :x-gap="12">
|
|
<n-gi>
|
|
<n-form-item label="季节">
|
|
<n-input v-model:value="form.bestSeason.season" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
<n-gi>
|
|
<n-form-item label="原因">
|
|
<n-input v-model:value="form.bestSeason.reason" />
|
|
</n-form-item>
|
|
</n-gi>
|
|
</n-grid>
|
|
</n-card>
|
|
|
|
<n-card v-if="form.seo" title="SEO" style="margin-bottom: 16px;">
|
|
<n-form-item label="SEO 标题">
|
|
<n-input v-model:value="form.seo.title" />
|
|
</n-form-item>
|
|
<n-form-item label="SEO 描述">
|
|
<n-input v-model:value="form.seo.description" type="textarea" :rows="2" />
|
|
</n-form-item>
|
|
<n-form-item v-if="form.seo.keywords" label="关键词">
|
|
<n-input v-model:value="form.seo.keywords" />
|
|
</n-form-item>
|
|
</n-card>
|
|
|
|
<p v-if="formError" style="color: #ef4444; font-size: 13px; margin: 12px 0 0;">{{ formError }}</p>
|
|
<div class="save-bar">
|
|
<NuxtLink to="/admin/content/destinations-detail" style="text-decoration: none;">
|
|
<n-button>返回列表</n-button>
|
|
</NuxtLink>
|
|
<n-button type="primary" :loading="saving" :disabled="saving" @click="handleSave">
|
|
{{ saving ? '保存中...' : '保存' }}
|
|
</n-button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NGrid, NGi, NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
|
|
import { useMessage } from 'naive-ui'
|
|
|
|
definePageMeta({ layout: 'admin', middleware: 'admin', name: 'admin-content-destinations-edit' })
|
|
const route = useRoute()
|
|
const { adminFetch } = useAdmin()
|
|
const message = useMessage()
|
|
const idx = computed(() => Number(route.params.idx))
|
|
const form = ref({})
|
|
const pageLoading = ref(true); const loadError = ref(''); const saving = ref(false); const formError = ref('')
|
|
|
|
async function loadItem() {
|
|
try {
|
|
const res = await adminFetch('/api/admin/site-content?key=destinations-detail')
|
|
const data = res.data
|
|
const arr = Array.isArray(data) ? data : (data.destinations || [])
|
|
if (idx.value >= arr.length) { loadError.value = '索引超出范围'; return }
|
|
form.value = JSON.parse(JSON.stringify(arr[idx.value]))
|
|
} catch (e) { loadError.value = e.data?.message || '加载失败' }
|
|
finally { pageLoading.value = false }
|
|
}
|
|
|
|
async function handleSave() {
|
|
saving.value = true; formError.value = ''
|
|
try {
|
|
const res = await adminFetch('/api/admin/site-content?key=destinations-detail')
|
|
const data = res.data
|
|
if (Array.isArray(data)) {
|
|
data[idx.value] = form.value
|
|
} else {
|
|
data.destinations[idx.value] = form.value
|
|
}
|
|
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: 'destinations-detail', data } })
|
|
message.success('保存成功')
|
|
} catch (e) { formError.value = e.data?.message || '保存失败' }
|
|
finally { saving.value = false }
|
|
}
|
|
|
|
onMounted(loadItem)
|
|
</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>
|