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>
这个提交包含在:
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="activity-card">
|
||||
<h4 class="activity-name">
|
||||
<span class="activity-icon">{{ activity.icon }}</span>
|
||||
{{ activity.name }}
|
||||
</h4>
|
||||
<ul class="activity-points">
|
||||
<li v-for="(point, i) in activity.points" :key="i" class="activity-point">
|
||||
{{ point }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
activity: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.activity-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-lg;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: @font-size-lg;
|
||||
}
|
||||
|
||||
.activity-points {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.activity-point {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-md;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '·';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="guide-checklist">
|
||||
<div
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
class="checklist-item"
|
||||
:class="{ 'checklist-item--important': item.important }"
|
||||
>
|
||||
<span class="checklist-mark">{{ item.important ? '☑' : '☐' }}</span>
|
||||
<span class="checklist-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: { type: Array, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.guide-checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
gap: @space-sm;
|
||||
padding: @space-sm @space-md;
|
||||
border-left: 2px solid @color-border;
|
||||
line-height: @line-height-base;
|
||||
|
||||
&--important {
|
||||
border-left-color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-mark {
|
||||
flex-shrink: 0;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
|
||||
.checklist-item--important & {
|
||||
color: @color-text-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="clothing-card" :class="{ 'clothing-card--highlight': month.highlight }">
|
||||
<div class="clothing-header">
|
||||
<h4 class="clothing-month">{{ month.name }}</h4>
|
||||
<span v-if="month.highlight" class="clothing-tag">大多数家庭选这个月</span>
|
||||
</div>
|
||||
<p class="clothing-desc">{{ month.desc }}</p>
|
||||
<div class="clothing-temps">
|
||||
<span class="temp-badge"><strong>白天</strong> {{ month.tempDay }}</span>
|
||||
<span class="temp-badge"><strong>夜间</strong> {{ month.tempNight }}</span>
|
||||
</div>
|
||||
<div class="clothing-wear">
|
||||
<p class="wear-line"><span class="wear-label">大人</span>{{ month.adultWear }}</p>
|
||||
<p class="wear-line"><span class="wear-label">小朋友</span>{{ month.kidsWear }}</p>
|
||||
</div>
|
||||
<p v-if="month.note" class="clothing-note">{{ month.note }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
month: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.clothing-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-lg;
|
||||
|
||||
&--highlight {
|
||||
border-color: @color-primary-lighter;
|
||||
box-shadow: 0 2px 12px rgba(45, 106, 79, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.clothing-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.clothing-month {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clothing-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-accent;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.clothing-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.clothing-temps {
|
||||
display: flex;
|
||||
gap: @space-lg;
|
||||
margin-bottom: @space-md;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
|
||||
strong {
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.clothing-wear {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.wear-line {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wear-label {
|
||||
display: inline-block;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-primary;
|
||||
margin-right: @space-sm;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.clothing-note {
|
||||
margin-top: @space-md;
|
||||
padding: @space-sm @space-md;
|
||||
background: @color-bg-warm;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-accent;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<nav class="guide-nav">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
class="guide-nav-btn"
|
||||
:class="{ active: active === section.id }"
|
||||
@click="$emit('select', section.id)"
|
||||
>
|
||||
<span class="guide-nav-icon">{{ section.icon }}</span>
|
||||
<span class="guide-nav-text">{{ section.title }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
sections: { type: Array, required: true },
|
||||
active: { type: String, default: '' },
|
||||
})
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.guide-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-2xl;
|
||||
|
||||
.respond-md({
|
||||
justify-content: center;
|
||||
gap: @space-md;
|
||||
});
|
||||
}
|
||||
|
||||
.guide-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: @space-sm @space-md;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
transition: all @transition-fast;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary-light;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @color-primary;
|
||||
border-color: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-nav-icon {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="toddler-section">
|
||||
<!-- 推荐项目 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">{{ data.recommended.title }}</h3>
|
||||
<p class="group-desc">{{ data.recommended.desc }}</p>
|
||||
<div class="toddler-items">
|
||||
<div v-for="item in data.recommended.items" :key="item.name" class="toddler-item toddler-item--green">
|
||||
<span class="toddler-item-icon">{{ item.icon }}</span>
|
||||
<div>
|
||||
<strong class="toddler-item-name">{{ item.name }}</strong>
|
||||
<p class="toddler-item-desc">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视情况选择 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--amber">{{ data.conditional.title }}</h3>
|
||||
<p class="group-desc">{{ data.conditional.desc }}</p>
|
||||
<div class="toddler-items">
|
||||
<div v-for="item in data.conditional.items" :key="item.name" class="toddler-item toddler-item--amber">
|
||||
<span class="toddler-item-icon">{{ item.icon }}</span>
|
||||
<div>
|
||||
<strong class="toddler-item-name">{{ item.name }}</strong>
|
||||
<p class="toddler-item-desc">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不建议参与 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--muted">{{ data.notRecommended.title }}</h3>
|
||||
<p class="group-desc">{{ data.notRecommended.desc }}</p>
|
||||
<div class="toddler-not-items">
|
||||
<div v-for="item in data.notRecommended.items" :key="item.name" class="toddler-not-item">
|
||||
<strong>{{ item.name }}</strong> — {{ item.reason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 装备 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">宝宝出行装备</h3>
|
||||
<div class="toddler-equip">
|
||||
<div v-for="eq in data.equipment" :key="eq.name" class="equip-item" :class="{ 'equip-item--provided': eq.provided }">
|
||||
<strong class="equip-name">{{ eq.name }}</strong>
|
||||
<span v-if="eq.provided" class="equip-badge">呼籁提供</span>
|
||||
<p class="equip-desc">{{ eq.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用餐 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">宝宝用餐小贴士</h3>
|
||||
<ul class="toddler-dining">
|
||||
<li v-for="(tip, i) in data.dining" :key="i">{{ tip }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 温馨提醒 -->
|
||||
<div class="toddler-notes">
|
||||
<div v-for="note in data.notes" :key="note.title" class="toddler-note">
|
||||
<strong>{{ note.title }}</strong>
|
||||
<p>{{ note.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
data: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.toddler-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-2xl;
|
||||
}
|
||||
|
||||
.toddler-group {
|
||||
// group container
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: @font-size-lg;
|
||||
margin: 0 0 @space-sm;
|
||||
|
||||
&--green { color: @color-primary; }
|
||||
&--amber { color: @color-accent; }
|
||||
&--muted { color: @color-text-muted; }
|
||||
}
|
||||
|
||||
.group-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
.toddler-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.toddler-item {
|
||||
display: flex;
|
||||
gap: @space-md;
|
||||
padding: @space-md @space-lg;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-md;
|
||||
border-left: 3px solid @color-border;
|
||||
|
||||
&--green { border-left-color: @color-primary; }
|
||||
&--amber { border-left-color: @color-accent; }
|
||||
}
|
||||
|
||||
.toddler-item-icon {
|
||||
font-size: @font-size-lg;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toddler-item-name {
|
||||
color: @color-text-primary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.toddler-item-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.toddler-not-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.toddler-not-item {
|
||||
padding: @space-sm @space-md;
|
||||
border-left: 2px solid @color-border;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
line-height: @line-height-base;
|
||||
|
||||
strong {
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.toddler-equip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.equip-item {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-md @space-lg;
|
||||
|
||||
&--provided {
|
||||
border-top: 3px solid @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.equip-name {
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
.equip-badge {
|
||||
display: inline-block;
|
||||
margin-left: @space-sm;
|
||||
padding: 2px @space-sm;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-xs;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.equip-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.toddler-dining {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
|
||||
li {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-md;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '☑';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toddler-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.toddler-note {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-md @space-lg;
|
||||
|
||||
strong {
|
||||
color: @color-primary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
在新工单中引用
屏蔽一个用户