- 后台全面引入 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>
171 行
3.2 KiB
Vue
171 行
3.2 KiB
Vue
<template>
|
||
<!--
|
||
SEO关键设计:
|
||
1. 用<details>/<summary>确保内容在HTML中始终存在(百度蜘蛛可抓取)
|
||
2. 问题用<h3>标签,语义化
|
||
3. 答案用<p>纯文本,AI直接引用
|
||
-->
|
||
<details class="faq-item" :open="initialOpen || undefined">
|
||
<summary class="faq-item-question">
|
||
<h3 class="faq-item-q-text">{{ question }}</h3>
|
||
<span class="faq-item-icon" aria-hidden="true"></span>
|
||
</summary>
|
||
<div class="faq-item-answer">
|
||
<p class="faq-item-a-text">{{ answer }}</p>
|
||
<div v-if="relatedLinks && relatedLinks.length" class="faq-item-links">
|
||
<NuxtLink
|
||
v-for="link in relatedLinks"
|
||
:key="link.url"
|
||
:to="link.url"
|
||
class="faq-item-link"
|
||
>
|
||
{{ link.text }} →
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</template>
|
||
|
||
<script setup>
|
||
defineProps({
|
||
question: { type: String, required: true },
|
||
answer: { type: String, required: true },
|
||
relatedLinks: { type: Array, default: () => [] },
|
||
initialOpen: { type: Boolean, default: false },
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.faq-item {
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-md;
|
||
overflow: hidden;
|
||
transition: border-color @transition-fast;
|
||
|
||
&[open] {
|
||
border-color: @color-primary-lighter;
|
||
|
||
.faq-item-icon {
|
||
transform: rotate(180deg);
|
||
}
|
||
}
|
||
|
||
&:hover {
|
||
border-color: @color-primary-lighter;
|
||
}
|
||
}
|
||
|
||
.faq-item-question {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: @space-lg;
|
||
cursor: pointer;
|
||
list-style: none;
|
||
touch-action: manipulation;
|
||
|
||
&::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
&::marker {
|
||
display: none;
|
||
content: '';
|
||
}
|
||
|
||
&:hover {
|
||
background: @color-bg-gray;
|
||
}
|
||
}
|
||
|
||
.faq-item-q-text {
|
||
font-size: @font-size-md;
|
||
font-weight: @font-weight-medium;
|
||
color: @color-text-primary;
|
||
flex: 1;
|
||
padding-right: @space-md;
|
||
}
|
||
|
||
.faq-item-icon {
|
||
width: 20px;
|
||
height: 20px;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
transition: transform @transition-base;
|
||
|
||
&::before,
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
background: @color-text-muted;
|
||
transition: opacity @transition-fast;
|
||
}
|
||
|
||
&::before {
|
||
top: 50%;
|
||
left: 2px;
|
||
right: 2px;
|
||
height: 2px;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
&::after {
|
||
left: 50%;
|
||
top: 2px;
|
||
bottom: 2px;
|
||
width: 2px;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
details[open] & {
|
||
&::after {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 答案展开动画
|
||
.faq-item-answer {
|
||
padding: 0 @space-lg @space-lg;
|
||
animation: fadeSlideIn @transition-base;
|
||
}
|
||
|
||
@keyframes fadeSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.faq-item-a-text {
|
||
font-size: @font-size-base;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
}
|
||
|
||
.faq-item-links {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: @space-md;
|
||
margin-top: @space-md;
|
||
padding-top: @space-md;
|
||
border-top: 1px solid @color-border;
|
||
}
|
||
|
||
.faq-item-link {
|
||
font-size: @font-size-sm;
|
||
color: @color-primary;
|
||
text-decoration: none;
|
||
font-weight: @font-weight-medium;
|
||
|
||
&:hover {
|
||
color: @color-primary-light;
|
||
}
|
||
}
|
||
</style>
|