hulai-website/components/admin/SeasonProduct.vue
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

111 行
6.8 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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}">&#9656;</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}">&#9656;</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]}">&#9656;</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>