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>
这个提交包含在:
Mimingguang
2026-03-24 17:48:02 +08:00
共同撰写人 Claude Opus 4.6
当前提交 ee078b61bc
共修改 444 个文件,包含 59890 行新增和 0 行删除
+612
查看文件
@@ -0,0 +1,612 @@
<template>
<div v-if="note" class="page-xhs-detail">
<LayoutPageHero
:title="note.title"
:subtitle="`来自小红书用户 @${note.author} 的真实旅行分享`"
:breadcrumbs="[{ text: '小红书游记', to: '/xiaohongshu' }, { text: note.title }]"
compact
/>
<section class="section">
<div class="container">
<div class="detail-layout">
<!-- 主内容 -->
<main class="detail-main">
<!-- 封面图 -->
<div class="detail-cover" :class="{ 'detail-cover--wide': note.ratio === 'horizontal' }">
<img :src="note.cover" :alt="note.title" />
</div>
<!-- 笔记正文预览 -->
<article class="detail-content">
<div class="detail-author-bar">
<div class="detail-author-bar__left">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" opacity="0.5"><circle cx="12" cy="8" r="4"/><path d="M12 14c-6 0-8 3-8 3v1h16v-1s-2-3-8-3z"/></svg>
<span class="detail-author-bar__name">{{ note.author }}</span>
</div>
<span class="detail-author-bar__likes">
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#ff2442"/></svg>
{{ formatLikes(note.likes) }} 赞
</span>
</div>
<h2 class="detail-content__title">{{ note.title }}</h2>
<div class="detail-content__body">
<p>{{ note.summary }}</p>
</div>
<!-- 标签 -->
<div class="detail-tags">
<span v-for="tag in note.tags" :key="tag" class="detail-tag">#{{ tag }}</span>
</div>
</article>
<!-- 查看原文 CTA -->
<div class="detail-xhs-cta">
<div class="detail-xhs-cta__icon">
<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="12" r="11" fill="#ff2442" /><text x="12" y="16" text-anchor="middle" fill="white" font-size="10" font-weight="bold">书</text></svg>
</div>
<div class="detail-xhs-cta__text">
<p class="detail-xhs-cta__hint">以上为笔记摘要,查看完整图文内容请前往小红书</p>
<a
:href="note.url"
target="_blank"
rel="noopener nofollow"
class="detail-xhs-cta__btn"
>
在小红书查看原文
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</div>
<!-- 前后导航 -->
<div class="detail-nav">
<NuxtLink
v-if="prevNote"
:to="`/xiaohongshu/${prevNote.id}`"
class="detail-nav__item detail-nav__item--prev"
>
<span class="detail-nav__label">上一篇</span>
<span class="detail-nav__title">{{ prevNote.title }}</span>
</NuxtLink>
<div v-else />
<NuxtLink
v-if="nextNote"
:to="`/xiaohongshu/${nextNote.id}`"
class="detail-nav__item detail-nav__item--next"
>
<span class="detail-nav__label">下一篇</span>
<span class="detail-nav__title">{{ nextNote.title }}</span>
</NuxtLink>
</div>
</main>
<!-- 侧边栏 -->
<aside class="detail-sidebar">
<!-- 小红书账号卡 -->
<div class="sidebar-card sidebar-card--xhs">
<div class="sidebar-card__account">
<img src="/images/xiaohongshu/profile.jpg" alt="呼籁旅行" class="sidebar-card__avatar" />
<div>
<h3 class="sidebar-card__name">{{ wallData.account.name }}</h3>
<span class="sidebar-card__badge">蓝V认证 &middot; 旅行社</span>
</div>
</div>
<div class="sidebar-card__stats">
<span>粉丝 <strong>{{ wallData.account.followers }}</strong></span>
<span>获赞 <strong>{{ wallData.account.likes }}</strong></span>
</div>
<a
:href="wallData.account.url"
target="_blank"
rel="noopener nofollow"
class="sidebar-card__follow"
>
关注小红书
</a>
</div>
<!-- 咨询 CTA -->
<div class="sidebar-card sidebar-card--cta">
<h3 class="sidebar-card__title">想要同款旅行体验?</h3>
<p class="sidebar-card__desc">联系呼籁定制师,一对一规划专属行程</p>
<CommonInternalLink to="/contact" text="免费咨询定制师" />
</div>
<!-- 更多笔记 -->
<div class="sidebar-card">
<h3 class="sidebar-card__title">更多客户笔记</h3>
<ul class="sidebar-notes">
<li
v-for="n in relatedNotes"
:key="n.id"
class="sidebar-note"
:class="{ 'sidebar-note--active': n.id === note.id }"
>
<NuxtLink :to="`/xiaohongshu/${n.id}`" class="sidebar-note__link">
<img :src="n.cover" :alt="n.title" class="sidebar-note__thumb" />
<div class="sidebar-note__info">
<span class="sidebar-note__title">{{ n.title }}</span>
<span class="sidebar-note__author">@{{ n.author }}</span>
</div>
</NuxtLink>
</li>
</ul>
<NuxtLink to="/xiaohongshu" class="sidebar-card__more">查看全部笔记 &rarr;</NuxtLink>
</div>
</aside>
</div>
</div>
</section>
<!-- CTA -->
<CommonSectionCTA
title="想拥有同款草原体验?"
description="联系定制师,开始规划您的呼伦贝尔之旅"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
]"
/>
</div>
<!-- 404 -->
<div v-else class="page-not-found">
<div class="container">
<h1>笔记不存在</h1>
<NuxtLink to="/xiaohongshu">返回小红书游记</NuxtLink>
</div>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data: wallData } = await usePublicApi('xiaohongshu-wall')
const route = useRoute()
const allNotes = computed(() => wallData.value?.notes || [])
const note = computed(() => allNotes.value.find(n => n.id === route.params.id))
if (!note.value) {
throw createError({ statusCode: 404, statusMessage: '笔记不存在' })
}
// SEO
useHead({
title: `${note.value.title} - 客户旅行笔记 | 呼籁旅行`,
meta: [
{ name: 'description', content: note.value.summary || `来自小红书用户 @${note.value.author} 的呼伦贝尔旅行笔记:${note.value.title}` },
{ name: 'keywords', content: `呼伦贝尔,${note.value.tags.join(',')},呼籁旅行,小红书,旅行笔记,${note.value.author}` },
{ name: 'author', content: note.value.author },
{ property: 'og:title', content: note.value.title },
{ property: 'og:description', content: note.value.summary },
{ property: 'og:type', content: 'article' },
{ property: 'og:image', content: note.value.cover },
{ property: 'og:url', content: `https://1814.love/xiaohongshu/${note.value.id}` },
{ property: 'og:site_name', content: '呼籁旅行' },
],
link: [
{ rel: 'canonical', href: `https://1814.love/xiaohongshu/${note.value.id}` },
],
})
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小红书游记', to: '/xiaohongshu' },
{ text: note.value.title },
])
// 上下篇
const currentIndex = computed(() => allNotes.value.findIndex(n => n.id === note.value?.id))
const prevNote = computed(() => currentIndex.value > 0 ? allNotes.value[currentIndex.value - 1] : null)
const nextNote = computed(() => currentIndex.value < allNotes.value.length - 1 ? allNotes.value[currentIndex.value + 1] : null)
// 相关笔记:同标签优先,取6条
const relatedNotes = computed(() => {
if (!note.value) return []
const sameTag = allNotes.value.filter(n => n.id !== note.value.id && n.tags.some(t => note.value.tags.includes(t)))
const others = allNotes.value.filter(n => n.id !== note.value.id && !sameTag.includes(n))
return [...sameTag, ...others].slice(0, 6)
})
function formatLikes(num) {
if (num >= 10000) return (num / 10000).toFixed(1) + 'w'
if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
return num
}
</script>
<style lang="less" scoped>
.detail-layout {
display: grid;
grid-template-columns: 1fr;
gap: @space-2xl;
.respond-lg({
grid-template-columns: 1fr 320px;
align-items: start;
});
}
.detail-main {
min-width: 0;
}
// ===== 封面 =====
.detail-cover {
border-radius: @border-radius-lg;
overflow: hidden;
margin-bottom: @space-xl;
aspect-ratio: 3 / 4;
max-height: 560px;
background: @gradient-cool;
&--wide {
aspect-ratio: 4 / 3;
max-height: 480px;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
// ===== 正文 =====
.detail-content {
margin-bottom: @space-xl;
}
.detail-author-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: @space-lg;
padding-bottom: @space-md;
border-bottom: 1px solid @color-divider;
&__left {
display: flex;
align-items: center;
gap: @space-sm;
}
&__name {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-secondary;
}
&__likes {
display: flex;
align-items: center;
gap: 4px;
font-size: @font-size-sm;
color: @color-text-muted;
svg { flex-shrink: 0; }
}
}
.detail-content__title {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-lg;
line-height: @line-height-tight;
.respond-md({
font-size: @font-size-3xl;
});
}
.detail-content__body {
p {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-relaxed;
margin: 0 0 @space-md;
.respond-md({
font-size: @font-size-md;
});
}
}
.detail-tags {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
margin-top: @space-lg;
}
.detail-tag {
padding: 4px 14px;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-full;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
}
// ===== 小红书原文 CTA =====
.detail-xhs-cta {
display: flex;
align-items: center;
gap: @space-lg;
padding: @space-xl;
background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%);
border-radius: @border-radius-lg;
border: 1px solid rgba(255, 36, 66, 0.1);
margin-bottom: @space-xl;
&__icon {
flex-shrink: 0;
svg {
display: block;
}
}
&__hint {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-sm;
}
&__btn {
display: inline-flex;
align-items: center;
gap: @space-sm;
padding: 10px @space-xl;
background: #ff2442;
color: #fff;
border-radius: @border-radius-full;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
transition: all @transition-base;
white-space: nowrap;
svg {
transition: transform @transition-fast;
}
&:hover {
background: #e6203b;
box-shadow: 0 4px 16px rgba(255, 36, 66, 0.3);
svg {
transform: translateX(3px);
}
}
}
}
// ===== 前后导航 =====
.detail-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: @space-md;
padding-top: @space-lg;
border-top: 1px solid @color-border;
&__item {
padding: @space-md;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
text-decoration: none;
transition: all @transition-base;
display: flex;
flex-direction: column;
gap: @space-xs;
&:hover {
border-color: @color-primary;
background: @color-primary-bg;
}
&--next {
text-align: right;
}
}
&__label {
font-size: @font-size-xs;
color: @color-text-muted;
}
&__title {
font-size: @font-size-sm;
color: @color-text-primary;
line-height: @line-height-tight;
.text-clamp(2);
}
}
// ===== 侧边栏 =====
.detail-sidebar {
display: flex;
flex-direction: column;
gap: @space-lg;
position: sticky;
top: 90px;
}
.sidebar-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-lg;
&--xhs {
border-color: rgba(255, 36, 66, 0.15);
background: linear-gradient(180deg, #fff5f7 0%, #fff 40%);
}
&--cta {
background: @color-primary-bg;
border-color: @color-primary-lighter;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0 0 @space-md;
}
&__more {
display: block;
text-align: center;
margin-top: @space-md;
padding-top: @space-md;
border-top: 1px solid @color-divider;
font-size: @font-size-sm;
color: @color-primary;
text-decoration: none;
font-weight: @font-weight-medium;
&:hover {
color: @color-primary-dark;
}
}
}
.sidebar-card__account {
display: flex;
align-items: center;
gap: @space-md;
margin-bottom: @space-md;
}
.sidebar-card__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(255, 36, 66, 0.15);
background: #fff;
}
.sidebar-card__name {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 2px;
}
.sidebar-card__badge {
font-size: 11px;
color: #ff2442;
font-weight: @font-weight-medium;
}
.sidebar-card__stats {
display: flex;
gap: @space-lg;
margin-bottom: @space-md;
font-size: @font-size-sm;
color: @color-text-muted;
strong {
color: @color-text-primary;
font-weight: @font-weight-bold;
}
}
.sidebar-card__follow {
display: block;
text-align: center;
padding: @space-sm;
background: #ff2442;
color: #fff;
border-radius: @border-radius-full;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
transition: background @transition-fast;
&:hover {
background: #e6203b;
}
}
.sidebar-notes {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.sidebar-note {
&--active .sidebar-note__link {
background: @color-primary-bg;
}
&__link {
display: flex;
gap: @space-sm;
padding: @space-sm;
border-radius: @border-radius-md;
text-decoration: none;
transition: background @transition-fast;
&:hover {
background: @color-bg-gray;
}
}
&__thumb {
width: 48px;
height: 48px;
border-radius: @border-radius-sm;
object-fit: cover;
flex-shrink: 0;
}
&__info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
&__title {
font-size: @font-size-sm;
color: @color-text-primary;
line-height: @line-height-tight;
.text-clamp(2);
}
&__author {
font-size: 11px;
color: @color-text-muted;
}
}
// 404
.page-not-found {
text-align: center;
padding: @space-3xl 0;
h1 { margin-bottom: @space-lg; }
a { color: @color-primary; }
}
</style>
+653
查看文件
@@ -0,0 +1,653 @@
<template>
<div class="page-xiaohongshu">
<LayoutPageHero
:title="seo.h1 || '小红书上的呼籁'"
subtitle="来自真实客户的旅行笔记,点击查看原文"
:breadcrumbs="[{ text: '小红书游记' }]"
background-image="/images/seasons/summer-river.jpg"
/>
<!-- TL;DR -->
<section class="page-tldr" aria-label="小红书摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行小红书官方账号精选笔记:来自真实客户的旅行分享和体验记录,点击可查看原文。</p>
</div>
</div>
</section>
<!-- 账号名片 -->
<section v-if="wallData" class="section section--account">
<div class="container">
<div class="account-card">
<div class="account-card__inner">
<div class="account-card__left">
<div class="account-avatar">
<img src="/images/xiaohongshu/profile.jpg" alt="呼籁旅行小红书头像" />
<span class="account-avatar__badge">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</span>
</div>
<div class="account-meta">
<div class="account-meta__name">
<h2>{{ wallData.account.name }}</h2>
<span class="account-meta__verified">蓝V认证 &middot; 旅行社</span>
</div>
<div class="account-meta__stats">
<div class="stat-item">
<span class="stat-item__value">{{ wallData.account.followers }}</span>
<span class="stat-item__label">粉丝</span>
</div>
<span class="stat-divider"></span>
<div class="stat-item">
<span class="stat-item__value">{{ wallData.account.likes }}</span>
<span class="stat-item__label">获赞与收藏</span>
</div>
<span class="stat-divider"></span>
<div class="stat-item">
<span class="stat-item__value">{{ wallData.notes.length }}</span>
<span class="stat-item__label">精选笔记</span>
</div>
</div>
</div>
</div>
<a
:href="wallData.account.url"
target="_blank"
rel="noopener nofollow"
class="follow-btn"
>
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="11" fill="#fff" /><text x="12" y="16" text-anchor="middle" fill="#ff2442" font-size="10" font-weight="bold">书</text></svg>
关注我们
</a>
</div>
</div>
</div>
</section>
<!-- 标签筛选 -->
<section class="section section--tags">
<div class="container">
<div class="tag-bar">
<button
class="tag-pill"
:class="{ 'tag-pill--active': activeTag === null }"
@click="activeTag = null"
>
全部
</button>
<button
v-for="tag in allTags"
:key="tag"
class="tag-pill"
:class="{ 'tag-pill--active': activeTag === tag }"
@click="activeTag = activeTag === tag ? null : tag"
>
{{ tag }}
</button>
</div>
</div>
</section>
<!-- 瀑布流照片墙 -->
<section class="section section--grid">
<div class="container">
<div class="masonry">
<div v-for="(col, ci) in columns" :key="ci" class="masonry__col">
<NuxtLink
v-for="note in col"
:key="note.id"
:to="`/xiaohongshu/${note.id}`"
class="note-card"
>
<div class="note-card__cover" :class="{ 'note-card__cover--wide': note.ratio === 'horizontal' }">
<img
:src="note.cover"
:alt="note.title"
loading="lazy"
@error="handleImageError"
/>
<div class="note-card__overlay">
<span class="note-card__read">查看原文</span>
</div>
<div class="note-card__tags">
<span v-for="tag in note.tags.slice(0, 2)" :key="tag" class="note-card__tag">{{ tag }}</span>
</div>
</div>
<div class="note-card__body">
<h3 class="note-card__title">{{ note.title }}</h3>
<div class="note-card__footer">
<span class="note-card__author">
<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" opacity="0.4"><circle cx="12" cy="8" r="4"/><path d="M12 14c-6 0-8 3-8 3v1h16v-1s-2-3-8-3z"/></svg>
{{ note.author }}
</span>
<span class="note-card__likes">
<svg viewBox="0 0 24 24" width="13" height="13"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#ff2442"/></svg>
{{ formatLikes(note.likes) }}
</span>
</div>
</div>
</NuxtLink>
</div>
</div>
<div class="grid-footer">
<p class="grid-footer__notice">以上内容来自小红书平台客户自发分享,点击卡片查看详情并跳转原文</p>
<a
:href="wallData.account.url"
target="_blank"
rel="noopener nofollow"
class="grid-footer__more"
>
在小红书查看更多
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</div>
</section>
<!-- CTA -->
<CommonSectionCTA
title="想拥有同款草原体验?"
description="联系定制师,开始规划您的呼伦贝尔之旅"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
]"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data: wallData } = await usePublicApi('xiaohongshu-wall')
const seo = useSEO('xiaohongshu')
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小红书游记' },
])
const activeTag = ref(null)
// 提取高频标签(按出现次数排序,最多显示8个)
const allTags = computed(() => {
const tagCount = {}
;(wallData.value?.notes || []).forEach(n => n.tags?.forEach(t => {
tagCount[t] = (tagCount[t] || 0) + 1
}))
return Object.entries(tagCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([tag]) => tag)
})
// 筛选笔记
const filteredNotes = computed(() => {
if (!activeTag.value) return wallData.value?.notes || []
return (wallData.value?.notes || []).filter(n => n.tags?.includes(activeTag.value))
})
// 瀑布流分列 - 移动2列,桌面3列(通过CSS控制第3列显隐)
const columns = computed(() => {
const cols = [[], [], []]
filteredNotes.value.forEach((note, i) => {
cols[i % 3].push(note)
})
return cols
})
function formatLikes(num) {
if (num >= 10000) return (num / 10000).toFixed(1) + 'w'
if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
return num
}
function handleImageError(e) {
e.target.closest('.note-card__cover').classList.add('note-card__cover--fallback')
}
</script>
<style lang="less" scoped>
// ===== 账号名片区 =====
.section--account {
padding-top: 0;
padding-bottom: 0;
margin-top: -@space-xl;
position: relative;
z-index: 2;
}
.account-card {
background: @color-bg-white;
border-radius: @border-radius-xl;
box-shadow: @shadow-lg;
overflow: hidden;
}
.account-card__inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: @space-lg @space-xl;
gap: @space-md;
flex-wrap: wrap;
.respond-md({
padding: @space-xl @space-2xl;
});
}
.account-card__left {
display: flex;
align-items: center;
gap: @space-md;
.respond-md({
gap: @space-lg;
});
}
.account-avatar {
position: relative;
width: 56px;
height: 56px;
flex-shrink: 0;
.respond-md({
width: 72px;
height: 72px;
});
img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 3px solid @color-bg-white;
box-shadow: 0 2px 12px rgba(255, 36, 66, 0.15);
background: @color-bg-white;
}
}
.account-avatar__badge {
position: absolute;
bottom: -2px;
right: -2px;
width: 22px;
height: 22px;
background: #ff2442;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border: 2px solid @color-bg-white;
.respond-md({
width: 26px;
height: 26px;
});
}
.account-meta__name {
display: flex;
align-items: center;
gap: @space-sm;
flex-wrap: wrap;
margin-bottom: @space-xs;
h2 {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0;
line-height: 1;
.respond-md({
font-size: @font-size-xl;
});
}
}
.account-meta__verified {
display: inline-block;
padding: 2px 10px;
background: linear-gradient(135deg, #ff2442 0%, #ff6b81 100%);
color: #fff;
font-size: 11px;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
letter-spacing: 0.3px;
}
.account-meta__stats {
display: flex;
align-items: center;
gap: @space-md;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-item__value {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
line-height: 1;
.respond-md({
font-size: @font-size-lg;
});
}
.stat-item__label {
font-size: 11px;
color: @color-text-muted;
line-height: 1;
}
.stat-divider {
width: 1px;
height: 28px;
background: @color-border;
}
.follow-btn {
display: inline-flex;
align-items: center;
gap: @space-sm;
padding: 10px @space-xl;
background: linear-gradient(135deg, #ff2442 0%, #ff6b81 100%);
color: #fff;
border-radius: @border-radius-full;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
transition: all @transition-base;
white-space: nowrap;
box-shadow: 0 4px 16px rgba(255, 36, 66, 0.25);
letter-spacing: 0.5px;
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(255, 36, 66, 0.35);
}
}
// ===== 标签筛选 =====
.section--tags {
padding-top: @space-xl;
padding-bottom: 0;
}
.tag-bar {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
justify-content: center;
}
.tag-pill {
padding: 6px 18px;
border: 1.5px solid @color-border;
border-radius: @border-radius-full;
background: @color-bg-white;
color: @color-text-secondary;
font-size: @font-size-sm;
cursor: pointer;
transition: all @transition-fast;
font-family: inherit;
line-height: 1.4;
&:hover {
border-color: @color-text-muted;
color: @color-text-primary;
}
&--active {
background: @color-primary;
border-color: @color-primary;
color: #fff;
font-weight: @font-weight-medium;
&:hover {
background: @color-primary-dark;
border-color: @color-primary-dark;
color: #fff;
}
}
}
// ===== 瀑布流网格 =====
.section--grid {
padding-top: @space-xl;
}
.masonry {
display: flex;
gap: @space-md;
.respond-md({
gap: @space-lg;
});
}
.masonry__col {
flex: 1;
display: flex;
flex-direction: column;
gap: @space-md;
.respond-md({
gap: @space-lg;
});
// 移动端隐藏第3列
&:nth-child(3) {
display: none;
.respond-md({
display: flex;
});
}
}
// ===== 笔记卡片(精致版) =====
.note-card {
display: block;
background: @color-bg-white;
border-radius: @border-radius-lg;
overflow: hidden;
text-decoration: none;
box-shadow: @shadow-sm;
transition: all @transition-base;
&:hover {
box-shadow: @shadow-card-hover;
transform: translateY(-4px);
.note-card__overlay {
opacity: 1;
}
.note-card__cover img {
transform: scale(1.05);
}
}
}
.note-card__cover {
position: relative;
overflow: hidden;
aspect-ratio: 3 / 4;
background: @gradient-cool;
&--wide {
aspect-ratio: 4 / 3;
}
&--fallback {
display: flex;
align-items: center;
justify-content: center;
&::after {
content: '暂无封面';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: @font-size-sm;
color: @color-text-muted;
background: @gradient-cool;
}
img {
display: none;
}
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
}
.note-card__overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 50%);
opacity: 0;
transition: opacity @transition-base;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: @space-xl;
}
.note-card__read {
padding: 6px 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: @border-radius-full;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
color: @color-text-primary;
backdrop-filter: blur(4px);
}
.note-card__tags {
position: absolute;
top: @space-sm;
left: @space-sm;
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.note-card__tag {
padding: 3px 10px;
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(8px);
border-radius: @border-radius-full;
font-size: 11px;
color: @color-text-secondary;
font-weight: @font-weight-medium;
}
.note-card__body {
padding: 14px @space-md;
}
.note-card__title {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 10px;
line-height: @line-height-tight;
.text-clamp(2);
}
.note-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.note-card__author {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: @color-text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 60%;
}
.note-card__likes {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: @color-text-muted;
svg {
flex-shrink: 0;
}
}
// ===== 底部 =====
.grid-footer {
text-align: center;
margin-top: @space-3xl;
padding-top: @space-xl;
border-top: 1px solid @color-divider;
}
.grid-footer__notice {
font-size: @font-size-sm;
color: @color-text-muted;
margin-bottom: @space-lg;
}
.grid-footer__more {
display: inline-flex;
align-items: center;
gap: @space-sm;
padding: 12px @space-2xl;
border: 2px solid #ff2442;
color: #ff2442;
border-radius: @border-radius-full;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
transition: all @transition-base;
letter-spacing: 0.3px;
svg {
transition: transform @transition-fast;
}
&:hover {
background: #ff2442;
color: #fff;
box-shadow: 0 4px 16px rgba(255, 36, 66, 0.25);
svg {
stroke: #fff;
transform: translateX(3px);
}
}
}
</style>