- 后台全面引入 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>
111 行
6.8 KiB
Vue
111 行
6.8 KiB
Vue
<template>
|
||
<div>
|
||
<div v-if="!loaded" class="loading-card">加载中...</div>
|
||
<template v-else>
|
||
<!-- 基本信息 -->
|
||
<div class="card">
|
||
<div class="card-header" @click="open.base=!open.base"><h2>基本信息</h2><i class="arrow" :class="{open:open.base}">▸</i></div>
|
||
<div v-show="open.base" class="card-body grid-2">
|
||
<div class="field"><label>品牌</label><input v-model="data.brand" /></div>
|
||
<div class="field"><label>版本</label><input v-model="data.version" /></div>
|
||
<div class="field"><label>季节</label><input v-model="data.season" /></div>
|
||
<div class="field"><label>风格</label><input v-model="data.style" /></div>
|
||
<div class="field full"><label>叙事</label><textarea v-model="data.narrative" rows="3" class="w-full"></textarea></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 亮点 -->
|
||
<div class="card">
|
||
<div class="card-header" @click="open.hl=!open.hl"><h2>产品亮点 ({{ data.highlights?.length || 0 }})</h2><i class="arrow" :class="{open:open.hl}">▸</i></div>
|
||
<div v-show="open.hl" class="card-body">
|
||
<div v-for="(h, i) in data.highlights" :key="i" class="inline-row">
|
||
<input v-model="h.title" placeholder="标题" style="width:150px" />
|
||
<input v-model="h.description" placeholder="描述" style="flex:1" />
|
||
<button class="btn btn-xs btn-danger" @click="data.highlights.splice(i,1)">删</button>
|
||
</div>
|
||
<button class="btn btn-xs btn-primary" @click="data.highlights.push({title:'',description:''})">+ 添加亮点</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 产品版本 -->
|
||
<div v-for="(v, vi) in data.versions" :key="vi" class="card">
|
||
<div class="card-header" @click="open['v'+vi]=!open['v'+vi]">
|
||
<h2>{{ v.name || '版本 '+(vi+1) }}</h2>
|
||
<div class="card-meta">
|
||
<span class="tag">{{ v.days }}天{{ v.nights }}晚</span>
|
||
<span class="tag">{{ v.line || v.route || '' }}</span>
|
||
<i class="arrow" :class="{open:open['v'+vi]}">▸</i>
|
||
</div>
|
||
</div>
|
||
<div v-show="open['v'+vi]" class="card-body">
|
||
<div class="grid-2">
|
||
<div class="field"><label>ID</label><input v-model="v.id" /></div>
|
||
<div class="field"><label>名称</label><input v-model="v.name" /></div>
|
||
<div class="field"><label>线路</label><input v-model="v.line" /></div>
|
||
<div class="field"><label>路线</label><input v-model="v.route" /></div>
|
||
<div class="field"><label>天数</label><input v-model.number="v.days" type="number" /></div>
|
||
<div class="field"><label>晚数</label><input v-model.number="v.nights" type="number" /></div>
|
||
<div class="field"><label>受众</label><input v-model="v.audience" /></div>
|
||
<div class="field"><label>标签</label><input v-model="v.tag" /></div>
|
||
</div>
|
||
<div class="field"><label>描述</label><textarea v-model="v.description" rows="3" class="w-full"></textarea></div>
|
||
<div class="field"><label>亮点(每行一个)</label>
|
||
<textarea :value="(v.highlights||[]).join('\n')" @input="v.highlights=$event.target.value.split('\n').filter(Boolean)" rows="3" class="w-full"></textarea>
|
||
</div>
|
||
<div class="sub-section"><h3>每日行程</h3>
|
||
<div v-for="(day, di) in (v.itinerary||[])" :key="di" class="day-card">
|
||
<div class="day-num">D{{ day.day || di+1 }}</div>
|
||
<div class="day-fields">
|
||
<input v-model="day.title" placeholder="当日标题" />
|
||
<textarea v-model="day.summary" rows="2" placeholder="行程概要"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
const props = defineProps({ data: Object, loaded: Boolean })
|
||
const open = reactive({ base: false })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.loading-card { background: #fff; border-radius: 8px; padding: 60px; text-align: center; color: #999; }
|
||
.card { background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); margin-bottom: 12px; overflow: hidden; }
|
||
.card-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; cursor: pointer; user-select: none; background: #fafafa; border-bottom: 1px solid #f0f0f0; }
|
||
.card-header:hover { background: #f5f5f5; }
|
||
.card-header h2 { margin: 0; font-size: 15px; color: #333; }
|
||
.card-meta { display: flex; align-items: center; gap: 8px; }
|
||
.card-body { padding: 16px 20px; }
|
||
.arrow { font-style: normal; font-size: 12px; transition: transform 0.2s; color: #999; }
|
||
.arrow.open { transform: rotate(90deg); }
|
||
.tag { font-size: 11px; background: #e6f7e9; color: #3a7d44; padding: 2px 8px; border-radius: 4px; }
|
||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
|
||
.full { grid-column: span 2; }
|
||
.field { margin-bottom: 10px; }
|
||
.field label { display: block; font-size: 12px; color: #888; margin-bottom: 3px; }
|
||
.field input, .field textarea { width: 100%; padding: 6px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; box-sizing: border-box; }
|
||
.field input:focus, .field textarea:focus { border-color: #3a7d44; outline: none; }
|
||
.field textarea { resize: vertical; }
|
||
.w-full { width: 100%; box-sizing: border-box; }
|
||
.inline-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||
.inline-row input { padding: 6px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; }
|
||
.inline-row input:focus { border-color: #3a7d44; outline: none; }
|
||
.sub-section { margin-top: 14px; border-top: 1px solid #f0f0f0; padding-top: 10px; }
|
||
.sub-section h3 { margin: 0 0 10px; font-size: 14px; color: #555; }
|
||
.day-card { display: flex; gap: 12px; margin-bottom: 8px; padding: 10px; background: #fafbfc; border-radius: 6px; }
|
||
.day-num { width: 36px; height: 36px; background: #3a7d44; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0; }
|
||
.day-fields { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
||
.day-fields input, .day-fields textarea { padding: 6px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; width: 100%; box-sizing: border-box; }
|
||
.day-fields input:focus, .day-fields textarea:focus { border-color: #3a7d44; outline: none; }
|
||
.day-fields textarea { resize: vertical; }
|
||
.btn { padding: 6px 14px; border: 1px solid #d9d9d9; border-radius: 4px; cursor: pointer; font-size: 13px; background: #fff; }
|
||
.btn-primary { background: #3a7d44; color: #fff; border-color: #3a7d44; }
|
||
.btn-xs { padding: 3px 8px; font-size: 11px; }
|
||
.btn-danger { color: #e74c3c; border-color: #e74c3c; }
|
||
.btn-danger:hover { background: #e74c3c; color: #fff; }
|
||
</style>
|