- 后台全面引入 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>
444 行
9.6 KiB
Vue
444 行
9.6 KiB
Vue
<template>
|
||
<section v-if="data" class="section">
|
||
<div class="container">
|
||
<CommonSectionTitle :title="data.title" :subtitle="data.subtitle" />
|
||
<p class="compare-intro">{{ data.intro }}</p>
|
||
|
||
<!-- 桌面端:紧凑对比表格 -->
|
||
<div class="compare-table-desktop">
|
||
<div class="compare-grid">
|
||
<!-- 表头 -->
|
||
<div class="grid-header">
|
||
<div class="grid-label-col"></div>
|
||
<div
|
||
v-for="dest in data.destinations"
|
||
:key="dest.id"
|
||
class="grid-dest-col"
|
||
:class="{ 'grid-dest-col--highlight': dest.highlight }"
|
||
>
|
||
<span v-if="dest.highlight" class="recommend-badge">推荐</span>
|
||
<h3 class="dest-name">{{ dest.name }}</h3>
|
||
<span class="dest-tag">{{ dest.tag }}</span>
|
||
</div>
|
||
</div>
|
||
<!-- 数据行 -->
|
||
<div v-for="dim in data.dimensions" :key="dim.label" class="grid-row">
|
||
<div class="grid-label-col">
|
||
<span class="dim-icon">{{ dim.icon }}</span>
|
||
<span class="dim-label">{{ dim.label }}</span>
|
||
</div>
|
||
<div
|
||
v-for="(val, i) in dim.values"
|
||
:key="i"
|
||
class="grid-value-col"
|
||
:class="{ 'grid-value-col--highlight': data.destinations[i]?.highlight }"
|
||
>
|
||
{{ val }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 坦诚说明 - 桌面端紧贴表格底部 -->
|
||
<div class="honest-strip">
|
||
<div class="honest-strip-header">
|
||
<span class="honest-icon">💬</span>
|
||
<span class="honest-label">{{ data.honestNote.title }}</span>
|
||
<span class="honest-dash">—</span>
|
||
<span class="honest-sub">{{ data.honestNote.subtitle }}</span>
|
||
</div>
|
||
<div class="honest-strip-items">
|
||
<span v-for="(item, i) in data.honestNote.items" :key="i" class="honest-item">
|
||
{{ item }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 移动端:Tab切换卡片 -->
|
||
<div class="compare-mobile">
|
||
<div class="tab-bar">
|
||
<button
|
||
v-for="(dest, i) in data.destinations"
|
||
:key="dest.id"
|
||
class="tab-btn"
|
||
:class="{ 'tab-btn--active': activeTab === i, 'tab-btn--highlight': dest.highlight }"
|
||
@click="activeTab = i"
|
||
>
|
||
<span v-if="dest.highlight && activeTab !== i" class="tab-dot"></span>
|
||
{{ dest.name }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="dest-card">
|
||
<p class="dest-card-tag">{{ data.destinations[activeTab].tag }}</p>
|
||
<div class="dest-card-grid">
|
||
<div v-for="dim in data.dimensions" :key="dim.label" class="dest-card-item">
|
||
<div class="dest-card-dim">
|
||
<span class="dest-card-icon">{{ dim.icon }}</span>
|
||
<span class="dest-card-label">{{ dim.label }}</span>
|
||
</div>
|
||
<p class="dest-card-value">{{ dim.values[activeTab] }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 坦诚说明 - 移动端 -->
|
||
<div v-if="data.destinations[activeTab]?.highlight" class="honest-mobile">
|
||
<p class="honest-mobile-title">
|
||
<span class="honest-icon-sm">💬</span>
|
||
{{ data.honestNote.title }}
|
||
</p>
|
||
<ul class="honest-mobile-list">
|
||
<li v-for="(item, i) in data.honestNote.items" :key="i">{{ item }}</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 数据来源 -->
|
||
<p class="data-source">{{ data.dataSources }}</p>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
|
||
const { data } = await usePublicApi('destinations')
|
||
|
||
const activeTab = ref(0)
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.compare-intro {
|
||
max-width: 720px;
|
||
margin: 0 auto @space-lg;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
text-align: center;
|
||
}
|
||
|
||
// ========== 桌面端表格 ==========
|
||
.compare-table-desktop {
|
||
display: none;
|
||
|
||
.respond-md({
|
||
display: block;
|
||
border-radius: @border-radius-lg;
|
||
overflow: hidden;
|
||
box-shadow: @shadow-card;
|
||
margin-bottom: @space-xl;
|
||
});
|
||
}
|
||
|
||
.compare-grid {
|
||
background: @color-bg-white;
|
||
}
|
||
|
||
.grid-header {
|
||
display: grid;
|
||
grid-template-columns: 100px repeat(3, 1fr);
|
||
background: @color-primary-bg;
|
||
}
|
||
|
||
.grid-label-col {
|
||
padding: @space-md @space-md;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
|
||
.grid-dest-col {
|
||
padding: @space-lg @space-md;
|
||
text-align: center;
|
||
position: relative;
|
||
|
||
&--highlight {
|
||
background: fadeout(@color-primary, 92%);
|
||
}
|
||
}
|
||
|
||
.recommend-badge {
|
||
display: inline-block;
|
||
padding: 2px 10px;
|
||
font-size: 11px;
|
||
font-weight: @font-weight-bold;
|
||
color: #fff;
|
||
background: @color-primary;
|
||
border-radius: @border-radius-full;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.dest-name {
|
||
font-size: @font-size-md;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.dest-tag {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
// 数据行
|
||
.grid-row {
|
||
display: grid;
|
||
grid-template-columns: 100px repeat(3, 1fr);
|
||
border-top: 1px solid @color-divider;
|
||
|
||
&:nth-child(even) {
|
||
background: @color-bg-gray;
|
||
}
|
||
}
|
||
|
||
.dim-icon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.dim-label {
|
||
font-size: @font-size-xs;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.grid-value-col {
|
||
padding: 12px @space-md;
|
||
font-size: 13px;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-normal;
|
||
border-left: 1px solid @color-divider;
|
||
|
||
&--highlight {
|
||
color: @color-text-primary;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
}
|
||
|
||
// ========== 坦诚说明条 - 桌面 ==========
|
||
.honest-strip {
|
||
background: @color-bg-warm;
|
||
padding: @space-md @space-lg;
|
||
border-top: 1px solid fadeout(@color-accent, 80%);
|
||
}
|
||
|
||
.honest-strip-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: @space-sm;
|
||
font-size: @font-size-sm;
|
||
}
|
||
|
||
.honest-icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.honest-label {
|
||
font-weight: @font-weight-bold;
|
||
color: @color-accent;
|
||
}
|
||
|
||
.honest-dash {
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
.honest-sub {
|
||
color: @color-text-muted;
|
||
font-size: @font-size-xs;
|
||
}
|
||
|
||
.honest-strip-items {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px @space-md;
|
||
}
|
||
|
||
.honest-item {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-normal;
|
||
position: relative;
|
||
padding-left: 12px;
|
||
|
||
&::before {
|
||
content: '·';
|
||
position: absolute;
|
||
left: 0;
|
||
color: @color-accent;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
top: 1px;
|
||
}
|
||
}
|
||
|
||
// ========== 移动端 Tab + 卡片 ==========
|
||
.compare-mobile {
|
||
display: block;
|
||
|
||
.respond-md({
|
||
display: none;
|
||
});
|
||
}
|
||
|
||
.tab-bar {
|
||
display: flex;
|
||
gap: 0;
|
||
background: @color-bg-gray;
|
||
border-radius: @border-radius-md;
|
||
padding: 3px;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.tab-btn {
|
||
flex: 1;
|
||
padding: 10px 4px;
|
||
border: none;
|
||
background: none;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-medium;
|
||
color: @color-text-muted;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all @transition-fast;
|
||
position: relative;
|
||
|
||
&--active {
|
||
background: @color-bg-white;
|
||
color: @color-text-primary;
|
||
font-weight: @font-weight-bold;
|
||
box-shadow: @shadow-xs;
|
||
}
|
||
|
||
&--highlight:not(.tab-btn--active) {
|
||
color: @color-primary;
|
||
}
|
||
}
|
||
|
||
.tab-dot {
|
||
display: inline-block;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: @color-primary;
|
||
border-radius: 50%;
|
||
margin-right: 4px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.dest-card {
|
||
background: @color-bg-white;
|
||
border-radius: @border-radius-lg;
|
||
box-shadow: @shadow-card;
|
||
padding: @space-md;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.dest-card-tag {
|
||
text-align: center;
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
margin-bottom: @space-md;
|
||
padding-bottom: @space-sm;
|
||
border-bottom: 1px solid @color-divider;
|
||
}
|
||
|
||
.dest-card-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1px;
|
||
background: @color-divider;
|
||
border-radius: @border-radius-md;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dest-card-item {
|
||
padding: 12px;
|
||
background: @color-bg-white;
|
||
|
||
// 让最后一个奇数项占满整行
|
||
&:last-child:nth-child(odd) {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.dest-card-dim {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.dest-card-icon {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dest-card-label {
|
||
font-size: 11px;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-muted;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.dest-card-value {
|
||
font-size: 13px;
|
||
color: @color-text-primary;
|
||
line-height: @line-height-normal;
|
||
}
|
||
|
||
// ========== 坦诚说明 - 移动端 ==========
|
||
.honest-mobile {
|
||
background: @color-bg-warm;
|
||
border-radius: @border-radius-md;
|
||
padding: @space-md;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.honest-mobile-title {
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-accent;
|
||
margin-bottom: @space-sm;
|
||
}
|
||
|
||
.honest-icon-sm {
|
||
font-size: 14px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.honest-mobile-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
li {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-normal;
|
||
padding-left: 14px;
|
||
position: relative;
|
||
|
||
&::before {
|
||
content: '·';
|
||
position: absolute;
|
||
left: 0;
|
||
color: @color-accent;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
top: -1px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 数据来源 ==========
|
||
.data-source {
|
||
text-align: center;
|
||
font-size: @font-size-xs;
|
||
color: @color-text-placeholder;
|
||
margin-top: @space-sm;
|
||
}
|
||
</style>
|