feat: rebuild miniapp reference home

This commit is contained in:
Codex
2026-09-14 06:45:36 +08:00
parent df47543501
commit 36b15d9ca8
6 changed files with 189 additions and 253 deletions
+36 -8
View File
@@ -2,7 +2,7 @@
* 首页 - V1.1 新版(U18)
* 结构:品牌区 → 用户状态条 → Banner → 8入口 → 推荐 → 服务建议 → 活动 → 服务站 → 安全提醒
*/
const { authStore, appStore } = require('../../stores')
const { authStore, appStore, matchStore } = require('../../stores')
const api = require('../../utils/api')
const ENTRY_VISUALS = {
@@ -129,7 +129,12 @@ Page({
statusChecks: checks,
banners: banners || [],
quickEntries: quickEntries?.length
? quickEntries.map((entry) => ({ ...entry, ...ENTRY_VISUALS[entry.code] }))
? QUICK_ENTRIES.map((defaultEntry) => {
const serverEntry = quickEntries.find((entry) => entry.code === defaultEntry.code)
return serverEntry
? { ...defaultEntry, ...serverEntry, ...ENTRY_VISUALS[defaultEntry.code] }
: { ...defaultEntry, enabled: false, disabledReason: '服务暂未开放' }
})
: QUICK_ENTRIES,
recommendations: recommendations || [],
recState: (recommendations && recommendations.length > 0) ? 'loaded' : 'empty',
@@ -198,18 +203,41 @@ Page({
if (userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${userId}` })
},
onLike(e) {
async onLike(e) {
const userId = e.detail?.userId
if (!userId) return
if (!userId || this.data.actionLoadingUserId) return
this.setData({ actionLoadingUserId: userId })
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
try {
const result = await matchStore.like(userId)
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) })
if (result?.mutualMatch) {
appStore.toastSuccess('双向喜欢!可以开始聊天了')
if (result.matchedConversationId) {
wx.navigateTo({ url: `/pages/chat/chat?id=${result.matchedConversationId}` })
}
} else {
appStore.toastSuccess('已表达喜欢')
}
} catch (err) {
appStore.toastError(err instanceof Error ? err.message : '操作失败')
} finally {
this.setData({ actionLoadingUserId: '' })
}
},
onPass(e) {
async onPass(e) {
const userId = e.detail?.userId
if (!userId) return
if (!userId || this.data.actionLoadingUserId) return
this.setData({ actionLoadingUserId: userId })
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
try {
await matchStore.pass(userId)
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) })
appStore.toastSuccess('已略过')
} catch (err) {
appStore.toastError(err instanceof Error ? err.message : '操作失败')
} finally {
this.setData({ actionLoadingUserId: '' })
}
},
onEmptyAction(e) {
+80 -47
View File
@@ -1,58 +1,91 @@
<view class="page app-page reference-page">
<!-- 品牌区 -->
<view class="brand-bar">
<view><text class="brand-name">善缘丹阳</text><text class="brand-slogan">真实相识 · 公益牵线</text></view>
<view class="brand-mark">公益服务</view>
<view class="bell-btn" bindtap="onMessages"><text>消息</text><text wx:if="{{unreadCount}}" class="badge">{{unreadCount}}</text></view>
</view>
<view wx:if="{{previewMode}}" class="preview-note">界面预览数据</view>
<!-- 用户状态条 -->
<view class="status-strip">
<view wx:if="{{isOnboarding}}" class="status-main"><text class="status-title">完善资料,开启安心相遇</text><text class="status-desc">{{onboardingMessage}}</text><view class="status-action" bindtap="onEmptyAction" data-event="{{onboardingActions[0].event}}">{{onboardingActions[0].text}}</view></view>
<view wx:elif="{{isPaused}}" class="status-main"><text class="status-title">已暂停新的对象推荐</text><view class="status-action" bindtap="onResume">恢复寻找</view></view>
<view wx:elif="{{isSuccessful}}" class="status-main"><text class="status-title">祝福你们正在认真了解彼此</text><text class="status-desc">愿真诚与尊重陪伴你们继续前行</text></view>
<view wx:else class="status-checks"><text wx:for="{{statusChecks}}" wx:key="label" class="status-check {{item.done ? 'done' : ''}}">{{item.done ? '✓' : '○'}} {{item.label}}</text></view>
</view>
<!-- Banner 轮播 -->
<view class="banner-section"><swiper class="banner-swiper" indicator-dots autoplay circular interval="4500" indicator-active-color="#E96B6B"><swiper-item wx:for="{{banners}}" wx:key="id"><view class="banner-card" bindtap="onBannerTap" data-item="{{item}}"><image wx:if="{{item.imageFileId}}" src="{{item.imageFileId}}" mode="aspectFill" class="banner-img"/><view class="banner-art"><view class="heart-line">♡</view></view><view class="banner-copy"><text class="banner-title">{{item.title}}</text><text class="banner-subtitle">{{item.subtitle}}</text><view class="banner-btn">{{item.buttonText || '查看详情'}} <text>›</text></view></view></view></swiper-item></swiper></view>
<!-- 8个核心入口 -->
<view class="entries-card"><view class="entries-grid"><view wx:for="{{quickEntries}}" wx:key="code" class="entry-item {{!item.enabled ? 'disabled' : ''}}" bindtap="onEntryTap" data-entry="{{item}}"><view class="entry-icon tone-{{item.tone}}">{{item.icon}}</view><text class="entry-label">{{item.label}}</text></view></view></view>
<view wx:if="{{loadError}}" class="section"><ui-error-state title="暂时无法加载首页" description="请检查网络后重试" bind:retry="onRetry"/></view>
<block wx:else>
<!-- 今日推荐 -->
<view class="section">
<ui-section-header title="今日推荐" subtitle="根据双方条件,为你认真筛选" action="查看更多" bind:action="onSeeMore"/>
<block wx:if="{{loading}}">
<ui-loading mode="skeleton" rows="2"/>
</block>
<block wx:elif="{{recommendations.length}}">
<view wx:for="{{recommendations}}" wx:key="userId" class="rec-wrap">
<ui-recommend-card recommendation="{{item}}" action-loading="{{actionLoadingUserId === item.userId}}" bind:view="onViewDetail" bind:like="onLike" bind:pass="onPass"/>
<view class="night-shell">
<view class="brand-bar">
<view class="brand-lockup">
<image class="brand-logo" src="/assets/brand/logo-mark.png" mode="aspectFit" />
<view class="brand-copy">
<text class="brand-name">善缘丹阳</text>
<text class="brand-slogan">公益婚恋服务平台</text>
</view>
</block>
<block wx:else>
<ui-empty illustration="heart" title="暂时没有完全符合双方条件的人" description="可以调整择偶条件,或申请公益红娘协助推荐" actions="{{emptyActions}}" bind:action="onEmptyAction"/>
</block>
</view>
<view class="brand-actions" bindtap="onMessages">
<text class="message-label">消息</text>
<text wx:if="{{unreadCount}}" class="badge">{{unreadCount}}</text>
</view>
</view>
<!-- 善缘公益服务 -->
<view class="section"><ui-section-header title="善缘公益服务" subtitle="需要时,我们在这里陪伴你"/><view wx:for="{{serviceRecommendations}}" wx:key="type" class="service-card" bindtap="onServiceTap" data-type="{{item.type}}"><view><text class="service-title">{{item.title}}</text><text class="service-desc">{{item.description}}</text></view><view class="text-link">{{item.ctaText}} ›</view></view></view>
<view class="status-strip">
<view wx:if="{{isOnboarding}}" class="status-main">
<view class="status-copy">
<text class="status-title">完善资料,开启安心相遇</text>
<text class="status-desc">{{onboardingMessage}}</text>
</view>
<view class="status-action" bindtap="onEmptyAction" data-event="{{onboardingActions[0].event}}">{{onboardingActions[0].text}}</view>
</view>
<view wx:elif="{{isPaused}}" class="status-main">
<view class="status-copy"><text class="status-title">已暂停新的对象推荐</text><text class="status-desc">你的资料仍被安全保存</text></view>
<view class="status-action" bindtap="onResume">恢复寻找</view>
</view>
<view wx:elif="{{isSuccessful}}" class="status-main">
<view class="status-copy"><text class="status-title">祝福你们认真了解彼此</text><text class="status-desc">愿真诚与尊重陪伴你们继续前行</text></view>
</view>
<view wx:else class="status-checks">
<text wx:for="{{statusChecks}}" wx:key="label" class="status-check {{item.done ? 'done' : ''}}">{{item.done ? '已完成' : '待完成'}} · {{item.label}}</text>
</view>
</view>
<!-- 近期公益活动 -->
<view class="section"><ui-section-header title="近期公益活动" action="全部活动" bind:action="onSeeAllActivities"/><scroll-view scroll-x class="activity-scroll"><view wx:for="{{activities}}" wx:key="id" class="activity-card" bindtap="onActivityTap" data-id="{{item.id}}"><view class="activity-cover"><text class="activity-tag">{{item.typeLabel || '公益活动'}}</text><text class="activity-symbol">相遇</text></view><view class="activity-info"><text class="activity-title">{{item.title}}</text><text class="activity-meta">9月20日 14:00 · {{item.locationName}}</text></view></view></scroll-view></view>
<view class="banner-section">
<swiper wx:if="{{banners.length}}" class="banner-swiper" indicator-dots autoplay circular interval="4500" indicator-active-color="#FF4F91" bindchange="onBannerChange">
<swiper-item wx:for="{{banners}}" wx:key="id">
<view class="banner-card" bindtap="onBannerTap" data-item="{{item}}">
<image class="banner-img" src="{{item.imageFileId || '/assets/brand/home-hero-v2.jpg'}}" mode="aspectFill" />
<view class="banner-overlay"></view>
<view class="banner-copy"><text class="banner-kicker">公益 · 本地 · 真实 · 安全</text><text class="banner-title">{{item.title}}</text><text class="banner-subtitle">{{item.subtitle}}</text></view>
</view>
</swiper-item>
</swiper>
<view wx:else class="banner-card banner-fallback">
<image class="banner-img" src="/assets/brand/home-hero-v2.jpg" mode="aspectFill" />
<view class="banner-overlay"></view>
<view class="banner-copy"><text class="banner-kicker">公益 · 本地 · 真实 · 安全</text><text class="banner-title">在丹阳,遇见更好的你</text><text class="banner-subtitle">认真认识,温暖相伴</text></view>
</view>
</view>
</view>
<!-- 善缘服务站 -->
<view class="section"><ui-section-header title="善缘服务站" subtitle="就在身边的公益婚恋服务阵地"/><view class="station-card" bindtap="onStationTap"><view class="station-pin">站</view><view class="station-info"><text class="station-name">{{serviceLocation.name}}</text><text class="station-desc">{{serviceLocation.shortName}}</text><view><text wx:for="{{serviceLocation.services}}" wx:key="*this" class="mini-tag">{{item}}</text></view></view><text class="arrow">›</text></view></view>
<view class="entries-card">
<view class="entries-grid">
<view wx:for="{{quickEntries}}" wx:key="code" class="entry-item {{!item.enabled ? 'disabled' : ''}}" bindtap="onEntryTap" data-entry="{{item}}">
<view class="entry-icon tone-{{item.tone}}"><image class="entry-icon-image" src="{{item.iconAsset}}" mode="aspectFit" /></view>
<text class="entry-label">{{item.label}}</text>
</view>
</view>
</view>
<!-- 婚恋课堂 -->
<view class="section"><ui-section-header title="婚恋课堂" subtitle="学会认识自己,也学会理解关系" action="更多内容" bind:action="onEntryTap" data-entry="{{quickEntries[4]}}"/><view class="article-list"><view wx:for="{{articles}}" wx:key="id" class="article-row" bindtap="onArticleTap" data-id="{{item.id}}"><view class="article-icon">读</view><view class="article-info"><text class="article-title">{{item.title}}</text><text class="article-meta">{{item.categoryName}} · {{item.readTime}}</text></view><text class="arrow">›</text></view></view></view>
<view wx:if="{{previewMode}}" class="preview-note">当前为界面预览数据</view>
<view wx:if="{{loadError}}" class="section"><ui-error-state title="暂时无法加载首页" description="请检查网络后重试" bind:retry="onRetry" /></view>
<block wx:else>
<view class="section recommendation-section">
<ui-section-header title="今日推荐" subtitle="根据双方条件,为你认真筛选" action="查看更多" bind:action="onSeeMore" />
<ui-loading wx:if="{{loading}}" mode="skeleton" rows="2" />
<view wx:elif="{{recommendations.length}}" class="recommend-list">
<ui-recommend-card wx:for="{{recommendations}}" wx:key="userId" recommendation="{{item}}" action-loading="{{actionLoadingUserId === item.userId}}" bind:view="onViewDetail" bind:like="onLike" bind:pass="onPass" />
</view>
<view wx:else class="recommend-empty">
<image class="empty-logo" src="/assets/brand/logo-mark.png" mode="aspectFit" />
<view class="empty-copy"><text class="empty-title">暂时没有完全符合双方条件的人</text><text class="empty-desc">我们坚持 Required 条件,不会为了数量降低筛选标准</text></view>
<view class="empty-actions"><view class="empty-btn secondary" bindtap="onEmptyAction" data-event="preference">调整条件</view><view class="empty-btn primary" bindtap="onEmptyAction" data-event="matchmaker">公益红娘</view></view>
</view>
</view>
<!-- 安全提醒 -->
<view class="section safety-card" bindtap="onSafetyTap"><view class="shield">安</view><view class="safety-info"><text class="safety-title">善缘提醒</text><text class="safety-body">{{safetyNotice.text}}</text><text class="safety-link">{{safetyNotice.ctaText}} ›</text></view></view>
<view class="section"><ui-section-header title="善缘公益服务" subtitle="需要时,我们在这里陪伴你" /><view wx:if="{{serviceRecommendations.length}}"><view wx:for="{{serviceRecommendations}}" wx:key="type" class="service-card" bindtap="onServiceTap" data-type="{{item.type}}"><image class="service-cover" src="/assets/covers/counseling-default.jpg" mode="aspectFill" /><view class="service-copy"><text class="service-title">{{item.title}}</text><text class="service-desc">{{item.description}}</text><text class="text-link">{{item.ctaText}}</text></view></view></view><view wx:else class="section-empty">暂无待推荐的公益服务</view></view>
<view class="section"><ui-section-header title="近期公益活动" action="全部活动" bind:action="onSeeAllActivities" /><scroll-view wx:if="{{activities.length}}" scroll-x class="activity-scroll"><view wx:for="{{activities}}" wx:key="id" class="activity-card" bindtap="onActivityTap" data-id="{{item.id}}"><image class="activity-cover" src="{{item.coverFileId || '/assets/covers/activity-default.jpg'}}" mode="aspectFill" /><view class="activity-info"><text class="activity-tag">公益活动</text><text class="activity-title">{{item.title}}</text><text class="activity-meta">{{item.locationName}}</text></view></view></scroll-view><view wx:else class="section-empty">近期暂无可报名活动</view></view>
<view class="section"><ui-section-header title="善缘服务站" subtitle="就在身边的公益婚恋服务阵地" /><view wx:if="{{serviceLocation}}" class="station-card" bindtap="onStationTap"><image class="station-cover" src="/assets/covers/station-default.jpg" mode="aspectFill" /><view class="station-info"><text class="station-name">{{serviceLocation.name}}</text><text class="station-desc">{{serviceLocation.shortName || serviceLocation.address}}</text><view><text wx:for="{{serviceLocation.services}}" wx:key="*this" class="mini-tag">{{item}}</text></view></view><image class="arrow-icon" src="/assets/icons/ui/chevron-right.png" mode="aspectFit" /></view><view wx:else class="section-empty" bindtap="onStationTap">查看线下公益服务阵地</view></view>
<view class="section"><ui-section-header title="婚恋课堂" subtitle="学习成长,收获更好的自己" action="更多内容" bind:action="onEntryTap" data-entry="{{quickEntries[4]}}" /><view wx:if="{{articles.length}}" class="article-list"><view wx:for="{{articles}}" wx:key="id" class="article-row" bindtap="onArticleTap" data-id="{{item.id}}"><image class="article-cover" src="{{item.coverFileId || '/assets/covers/classroom-default.jpg'}}" mode="aspectFill" /><view class="article-info"><text class="article-title">{{item.title}}</text><text class="article-meta">善缘课堂 · 公益内容</text></view><image class="arrow-icon" src="/assets/icons/ui/chevron-right.png" mode="aspectFit" /></view></view><view wx:else class="section-empty">暂无新内容,稍后再来看看</view></view>
<view class="section safety-card" bindtap="onSafetyTap"><image class="safety-cover" src="/assets/covers/anti-fraud-default.jpg" mode="aspectFill" /><view class="safety-info"><text class="safety-title">善缘安全提醒</text><text class="safety-body">{{safetyNotice.text}}</text><text class="safety-link">{{safetyNotice.ctaText}}</text></view></view>
</block>
<view class="safe-area-bottom"></view>
</view>
+10 -14
View File
@@ -1,14 +1,10 @@
.page{min-height:100vh;background:var(--color-bg-page);padding-bottom:var(--spacing-8)}
.brand-bar{height:104rpx;padding:12rpx var(--spacing-page-x);display:flex;align-items:center;gap:16rpx;background:var(--color-bg-card);position:sticky;top:0;z-index:var(--z-sticky);border-bottom:1rpx solid var(--color-divider)}
.brand-bar>view:first-child{display:flex;flex-direction:column;flex:1}.brand-name{font-size:36rpx;font-weight:700;color:var(--color-primary);line-height:1.25}.brand-slogan{font-size:22rpx;color:var(--color-text-tertiary);margin-top:4rpx}.brand-mark{font-size:20rpx;color:var(--color-success);background:var(--color-success-light);padding:6rpx 12rpx;border-radius:var(--radius-full)}
.bell-btn{position:relative;min-width:88rpx;height:88rpx;display:flex;align-items:center;justify-content:center;font-size:24rpx;color:var(--color-text-secondary)}.badge{position:absolute;right:0;top:8rpx;min-width:28rpx;height:28rpx;padding:0 4rpx;line-height:28rpx;text-align:center;font-size:18rpx;color:var(--color-text-inverse);background:var(--color-primary);border-radius:var(--radius-full)}
.preview-note{text-align:center;background:var(--color-warning-light);color:var(--color-warning);font-size:20rpx;padding:6rpx}
.status-strip{margin:16rpx var(--spacing-page-x) 0;padding:20rpx 24rpx;background:var(--color-bg-card);border:1rpx solid var(--color-border);border-radius:var(--radius-md)}.status-main{display:flex;align-items:center;gap:12rpx;flex-wrap:wrap}.status-title{font-size:27rpx;font-weight:600}.status-desc{font-size:23rpx;color:var(--color-text-tertiary);flex:1}.status-action{min-height:64rpx;padding:0 22rpx;display:flex;align-items:center;color:var(--color-primary);font-size:24rpx;font-weight:600}.status-checks{display:flex;justify-content:space-between;gap:12rpx}.status-check{font-size:23rpx;color:var(--color-warning)}.status-check.done{color:var(--color-success)}
.banner-section{padding:16rpx var(--spacing-page-x) 0}.banner-swiper{height:292rpx;border-radius:var(--radius-lg);overflow:hidden}.banner-card{height:100%;position:relative;background:linear-gradient(135deg,var(--color-primary-lighter),#FBE8DE)}.banner-img{width:100%;height:100%}.banner-art{position:absolute;right:-24rpx;top:-28rpx;width:260rpx;height:260rpx;border-radius:50%;background:rgba(255,255,255,.55);display:flex;align-items:center;justify-content:center}.heart-line{font-size:128rpx;color:rgba(233,107,107,.34)}.banner-copy{position:absolute;left:28rpx;top:36rpx;width:72%}.banner-title{display:block;font-size:34rpx;font-weight:700;color:#6F3A35;line-height:1.35}.banner-subtitle{display:block;margin-top:12rpx;font-size:24rpx;color:#8B5E56}.banner-btn{display:inline-flex;align-items:center;gap:8rpx;margin-top:24rpx;min-height:64rpx;padding:0 24rpx;border-radius:var(--radius-full);background:var(--color-primary);color:var(--color-text-inverse);font-size:24rpx}
.entries-card{margin:20rpx var(--spacing-page-x) 0;padding:24rpx 12rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-card)}.entries-grid{display:grid;grid-template-columns:repeat(4,1fr);row-gap:24rpx}.entry-item{min-height:116rpx;display:flex;flex-direction:column;align-items:center;justify-content:center}.entry-item.disabled{opacity:.45}.entry-icon{width:76rpx;height:76rpx;display:flex;align-items:center;justify-content:center;border-radius:24rpx;font-size:28rpx;font-weight:700}.entry-label{margin-top:9rpx;font-size:23rpx;color:var(--color-text-primary)}.tone-rose{background:#FFF0EF;color:#D85D62}.tone-amber{background:#FFF7E6;color:#B77B19}.tone-green{background:#ECF8F2;color:#2E8A62}.tone-blue{background:#EEF5FF;color:#4A72B8}.tone-purple{background:#F5EFFB;color:#8865A7}.tone-orange{background:#FFF2E8;color:#B9723B}.tone-indigo{background:#EEF0FB;color:#5F6FAE}.tone-teal{background:#EAF7F5;color:#31867C}
.section{margin-top:var(--spacing-section);padding:0 var(--spacing-page-x)}.rec-wrap{margin-top:20rpx}.service-card{display:flex;align-items:center;gap:20rpx;padding:28rpx;background:linear-gradient(135deg,var(--color-primary-lighter),#FFF9F0);border:1rpx solid #F8E3DD;border-radius:var(--radius-lg)}.service-card>view:first-child{flex:1}.service-title{display:block;font-size:30rpx;font-weight:650}.service-desc{display:block;margin-top:10rpx;font-size:24rpx;color:var(--color-text-secondary);line-height:1.55}.text-link{font-size:23rpx;font-weight:600;color:var(--color-primary);white-space:nowrap}
.activity-scroll{white-space:nowrap;margin-top:18rpx;width:100%}.activity-card{display:inline-block;width:560rpx;margin-right:20rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-card);white-space:normal}.activity-cover{height:210rpx;padding:18rpx;background:linear-gradient(135deg,#F3D7CA,#F8ECE4);position:relative}.activity-tag{font-size:21rpx;color:#A45D50;background:rgba(255,255,255,.8);padding:7rpx 14rpx;border-radius:var(--radius-full)}.activity-symbol{position:absolute;right:28rpx;bottom:18rpx;font-size:52rpx;color:rgba(164,93,80,.32);font-weight:700}.activity-info{padding:22rpx}.activity-title{display:block;font-size:28rpx;font-weight:600}.activity-meta{display:block;margin-top:10rpx;font-size:23rpx;color:var(--color-text-tertiary)}
.station-card{display:flex;align-items:center;gap:20rpx;padding:28rpx;background:var(--color-bg-card);border:1rpx solid var(--color-border);border-radius:var(--radius-lg)}.station-pin,.shield,.article-icon{width:72rpx;height:72rpx;flex:none;display:flex;align-items:center;justify-content:center;border-radius:22rpx;background:var(--color-success-light);color:var(--color-success);font-size:27rpx;font-weight:700}.station-info,.article-info,.safety-info{flex:1;min-width:0}.station-name{display:block;font-size:29rpx;font-weight:650}.station-desc{display:block;margin:7rpx 0 12rpx;font-size:23rpx;color:var(--color-text-tertiary)}.mini-tag{font-size:20rpx;color:var(--color-text-secondary);margin-right:10rpx;padding:5rpx 10rpx;background:var(--color-bg-page);border-radius:var(--radius-full)}.arrow{font-size:40rpx;color:var(--color-text-disabled)}
.article-list{margin-top:18rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);overflow:hidden}.article-row{min-height:124rpx;padding:18rpx 24rpx;display:flex;align-items:center;gap:18rpx;border-bottom:1rpx solid var(--color-divider)}.article-row:last-child{border-bottom:0}.article-icon{background:#F5EFFB;color:#8865A7}.article-title{display:block;font-size:27rpx;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.article-meta{display:block;margin-top:8rpx;font-size:22rpx;color:var(--color-text-tertiary)}
.safety-card{display:flex;gap:20rpx;margin-left:var(--spacing-page-x);margin-right:var(--spacing-page-x);padding:28rpx;background:#F3F7FC;border-radius:var(--radius-lg)}.shield{background:#E7EEF9;color:var(--color-info)}.safety-title{display:block;font-size:28rpx;font-weight:650}.safety-body{display:block;margin-top:8rpx;font-size:23rpx;color:var(--color-text-secondary);line-height:1.6}.safety-link{display:block;margin-top:12rpx;font-size:23rpx;color:var(--color-info);font-weight:600}
@media (max-width:340px){.brand-mark{display:none}.entry-label{font-size:21rpx}.entry-icon{width:68rpx;height:68rpx}.banner-title{font-size:31rpx}.status-checks{flex-wrap:wrap;justify-content:flex-start}}
.page{min-height:100vh;background:#F4F5FA;padding-bottom:40rpx}.night-shell{padding:0 24rpx 28rpx;background:linear-gradient(160deg,var(--mini-night-start),var(--mini-night-end));border-radius:0 0 42rpx 42rpx;box-shadow:var(--mini-shadow)}
.brand-bar{height:112rpx;padding-top:env(safe-area-inset-top);display:flex;align-items:center;justify-content:space-between;color:var(--mini-text-on-night)}.brand-lockup{display:flex;align-items:center}.brand-logo{width:66rpx;height:66rpx}.brand-copy{display:flex;flex-direction:column;margin-left:12rpx}.brand-name{font-size:34rpx;font-weight:700;letter-spacing:2rpx}.brand-slogan{font-size:18rpx;letter-spacing:4rpx;color:var(--mini-text-on-night-muted)}.brand-actions{position:relative;min-width:76rpx;height:72rpx;display:flex;align-items:center;justify-content:center}.message-label{font-size:23rpx}.badge{position:absolute;right:0;top:4rpx;min-width:28rpx;height:28rpx;padding:0 4rpx;line-height:28rpx;text-align:center;font-size:18rpx;color:#fff;background:var(--mini-accent-pink);border-radius:20rpx}
.status-strip{margin:4rpx 0 16rpx;padding:18rpx 20rpx;border:1rpx solid rgba(255,255,255,.16);background:rgba(255,255,255,.09);border-radius:20rpx;color:#fff}.status-main{display:flex;align-items:center;gap:16rpx}.status-copy{flex:1;min-width:0;display:flex;flex-direction:column}.status-title{font-size:25rpx;font-weight:650}.status-desc{margin-top:5rpx;font-size:20rpx;color:var(--mini-text-on-night-muted);line-height:1.4}.status-action{flex:none;min-width:108rpx;min-height:56rpx;padding:0 18rpx;display:flex;align-items:center;justify-content:center;border-radius:28rpx;background:linear-gradient(135deg,var(--mini-accent-coral),var(--mini-accent-pink));font-size:22rpx;font-weight:650}.status-checks{display:flex;gap:10rpx;overflow:hidden}.status-check{padding:8rpx 12rpx;border-radius:20rpx;background:rgba(255,255,255,.08);font-size:19rpx;color:var(--mini-text-on-night-muted);white-space:nowrap}.status-check.done{background:rgba(90,226,184,.16);color:#A6F5D7}
.banner-section{width:100%}.banner-swiper,.banner-card{height:272rpx;border-radius:26rpx;overflow:hidden}.banner-card{position:relative}.banner-img{width:100%;height:100%;display:block}.banner-overlay{position:absolute;inset:0;background:linear-gradient(90deg,rgba(25,22,86,.78) 0%,rgba(43,22,101,.25) 58%,rgba(255,79,145,.08) 100%)}.banner-copy{position:absolute;left:26rpx;bottom:26rpx;width:68%;display:flex;flex-direction:column;color:#fff}.banner-kicker{font-size:18rpx;letter-spacing:3rpx;color:rgba(255,255,255,.78)}.banner-title{margin-top:9rpx;font-size:35rpx;line-height:1.2;font-weight:750;text-shadow:0 3rpx 10rpx rgba(0,0,0,.2)}.banner-subtitle{margin-top:8rpx;font-size:22rpx;color:rgba(255,255,255,.84)}
.entries-card{position:relative;z-index:2;margin:-14rpx 24rpx 0;padding:22rpx 10rpx 18rpx;background:var(--mini-card-bg);border-radius:30rpx;box-shadow:var(--mini-shadow)}.entries-grid{display:grid;grid-template-columns:repeat(4,1fr);row-gap:18rpx}.entry-item{min-height:104rpx;display:flex;flex-direction:column;align-items:center;justify-content:center}.entry-item.disabled{opacity:.4}.entry-icon{width:66rpx;height:66rpx;display:flex;align-items:center;justify-content:center;border-radius:20rpx;overflow:hidden}.entry-icon-image{width:45rpx;height:45rpx}.entry-label{margin-top:8rpx;font-size:21rpx;color:#283056;font-weight:550}.tone-rose{background:#FFF0F5}.tone-purple{background:#F0EAFE}.tone-blue{background:#EAF3FF}.tone-pink{background:#FFF0F7}.tone-amber{background:#FFF3E5}.tone-indigo{background:#ECEEFF}.tone-red{background:#FFEDEF}.tone-green{background:#EAF8F2}
.preview-note{margin:16rpx 24rpx 0;text-align:center;color:#8B63BC;font-size:20rpx}.section{margin-top:34rpx;padding:0 24rpx}.recommendation-section{margin-top:30rpx}.recommend-list{margin-top:16rpx;border-radius:26rpx;overflow:hidden;box-shadow:0 8rpx 28rpx rgba(35,35,95,.07)}.recommend-empty{margin-top:16rpx;padding:22rpx;display:flex;align-items:center;gap:18rpx;background:#fff;border-radius:26rpx;box-shadow:0 8rpx 28rpx rgba(35,35,95,.07)}.empty-logo{width:76rpx;height:76rpx;flex:none}.empty-copy{flex:1;min-width:0;display:flex;flex-direction:column}.empty-title{font-size:25rpx;font-weight:650;color:#222746}.empty-desc{margin-top:6rpx;font-size:20rpx;line-height:1.4;color:#7A7F98}.empty-actions{display:flex;flex-direction:column;gap:8rpx}.empty-btn{min-width:112rpx;height:48rpx;padding:0 12rpx;display:flex;align-items:center;justify-content:center;border-radius:24rpx;font-size:20rpx}.empty-btn.secondary{color:#7C60BC;background:#F1EEFB}.empty-btn.primary{color:#fff;background:linear-gradient(135deg,var(--mini-accent-coral),var(--mini-accent-pink))}
.service-card{display:flex;align-items:stretch;margin-top:16rpx;background:#fff;border-radius:26rpx;overflow:hidden;box-shadow:0 8rpx 28rpx rgba(35,35,95,.07)}.service-cover{width:210rpx;min-height:156rpx}.service-copy{flex:1;padding:20rpx;display:flex;flex-direction:column}.service-title{font-size:25rpx;font-weight:650;color:#222746}.service-desc{margin-top:7rpx;font-size:21rpx;color:#757B91;line-height:1.45}.text-link{margin-top:auto;padding-top:10rpx;font-size:21rpx;color:var(--mini-accent-pink);font-weight:650}
.activity-scroll{white-space:nowrap;margin-top:16rpx}.activity-card{display:inline-flex;width:550rpx;margin-right:18rpx;background:#fff;border-radius:24rpx;overflow:hidden;box-shadow:0 8rpx 28rpx rgba(35,35,95,.07);white-space:normal}.activity-cover{width:210rpx;height:168rpx}.activity-info{flex:1;padding:18rpx;display:flex;flex-direction:column}.activity-tag{align-self:flex-start;padding:5rpx 12rpx;border-radius:18rpx;background:#FFF0F5;color:var(--mini-accent-pink);font-size:18rpx}.activity-title{margin-top:10rpx;font-size:25rpx;font-weight:650;color:#222746;line-height:1.35}.activity-meta{margin-top:auto;font-size:20rpx;color:#81869B}
.station-card,.article-row,.safety-card{display:flex;align-items:center;background:#fff;border-radius:26rpx;overflow:hidden;box-shadow:0 8rpx 28rpx rgba(35,35,95,.07)}.station-cover{width:190rpx;height:170rpx}.station-info{flex:1;min-width:0;padding:18rpx}.station-name{display:block;font-size:26rpx;font-weight:650;color:#222746}.station-desc{display:block;margin:7rpx 0 12rpx;font-size:20rpx;color:#7A7F98}.mini-tag{display:inline-block;margin:4rpx 7rpx 0 0;padding:4rpx 9rpx;border-radius:14rpx;background:#F0EEFB;color:#6857A2;font-size:17rpx}.arrow-icon{width:28rpx;height:28rpx;margin:16rpx;flex:none}.article-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.article-row{min-height:112rpx}.article-cover{width:146rpx;height:112rpx}.article-info,.safety-info{flex:1;min-width:0;padding:16rpx}.article-title{display:block;font-size:24rpx;font-weight:600;color:#222746;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.article-meta{display:block;margin-top:6rpx;font-size:19rpx;color:#85899C}.section-empty{margin-top:16rpx;padding:30rpx 24rpx;text-align:center;color:#8A90A6;font-size:22rpx;background:#fff;border-radius:24rpx;box-shadow:0 8rpx 28rpx rgba(35,35,95,.05)}.safety-card{padding:0}.safety-cover{width:180rpx;align-self:stretch}.safety-title{display:block;font-size:25rpx;font-weight:650;color:#222746}.safety-body{display:block;margin-top:7rpx;font-size:20rpx;line-height:1.45;color:#74798E}.safety-link{display:block;margin-top:9rpx;font-size:20rpx;color:var(--mini-accent-pink);font-weight:650}
@media (max-width:340px){.entry-label{font-size:19rpx}.entry-icon{width:60rpx;height:60rpx}.banner-title{font-size:31rpx}.status-checks{overflow-x:auto}.empty-actions{display:none}}