chore: establish V1.1 recovery baseline

This commit is contained in:
2026-09-09 22:05:00 +08:00
commit b46b48eb33
947 changed files with 155471 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
/**
* 首页 - 每日推荐(重构:用 matchStore + recommend-card + appStore 统一编排)
*
* 关键不变量(AGENTS.md):
* - 第 2 条:Required 不可自动放宽——推荐列表完全由后端按 PartnerPreference 过滤,
* 前端只渲染后端返回的 recommendations,不做任何"放宽条件再查"的逻辑。
* - 第 5 条:红娘不可替用户操作——所有 Like/Pass 由当前用户触发。
* - 第 6 条:单向意向不泄露——likersCount 仅展示计数,不暴露具体身份。
*/
const { matchStore, appStore, userStore, authStore, blockUser, likeUser } = require('../../stores');
const { ROUTE, to, RECOMMENDATION_STATE } = require('../../constants');
Page({
data: {
greeting: '',
userName: '',
recommendations: [],
recState: RECOMMENDATION_STATE.IDLE,
likersCount: 0,
actionLoadingUserId: '', // 正在操作的用户 id(Like/Pass 进行中)
emptyActions: [
{ text: '去「找对象」', event: 'discover', variant: 'primary' },
{ text: '申请公益红娘', event: 'matchmaker', variant: 'secondary' },
],
},
onLoad() {
this.setGreeting();
// 绑定 matchStore 关键字段到 page data
this._unsubMatch = matchStore.bindToPage(this, {
recommendations: 'recommendations',
recState: 'recState',
});
// 绑定 likersCount
this._unsubMatchSub = matchStore.subscribe((state) => {
if (state.likersCount !== this.data.likersCount) {
this.setData({ likersCount: state.likersCount });
}
});
// 绑定 userStore 用户名(用于问候语)
this._unsubUser = userStore.subscribe((state) => {
const name = state.profile?.displayName || '';
if (name !== this.data.userName) this.setData({ userName: name });
});
this.setData({ userName: userStore.state.profile?.displayName || '' });
},
onShow() {
// 未登录则不请求推荐(bootstrap 已跳转登录页,此处为保险兜底)
if (!authStore.state.isLoggedIn) return;
// 每次显示刷新推荐(用户从聊天页返回后可能已 Like/Pass,需要新批次)
this.loadRecommendations();
// 异步刷新 likersCount(Tab 徽标也会更新)
matchStore.fetchLikersCount().catch(() => {});
},
onUnload() {
this._unsubMatch && this._unsubMatch();
this._unsubMatchSub && this._unsubMatchSub();
this._unsubUser && this._unsubUser();
},
onPullDownRefresh() {
this.loadRecommendations().finally(() => wx.stopPullDownRefresh());
},
onReachBottom() {
// 加载更多推荐
matchStore.loadMoreRecommendations().catch(() => {});
},
setGreeting() {
const hour = new Date().getHours();
let greeting = '早上好';
if (hour >= 12 && hour < 17) greeting = '下午好';
else if (hour >= 17) greeting = '晚上好';
this.setData({ greeting });
},
async loadRecommendations() {
try {
await matchStore.fetchRecommendations();
} catch (err) {
appStore.toastError(err);
}
},
onViewDetail(e) {
const { userId } = e.detail;
wx.navigateTo({ url: to.detail(userId) });
},
async onLike(e) {
const { userId } = e.detail;
if (this.data.actionLoadingUserId) return;
this.setData({ actionLoadingUserId: userId });
try {
const result = await likeUser(userId);
if (result && result.mutualMatch) {
// 双向匹配达成:弹祝福 + 引导去聊天
appStore.toastSuccess('双向喜欢!可以开始聊天了');
setTimeout(() => {
wx.showModal({
title: '互相喜欢',
content: '你们已经互相喜欢,是否现在开始聊天?',
confirmText: '开始聊天',
cancelText: '稍后',
confirmColor: '#E96B6B',
success: (res) => {
if (res.confirm && result.matchedConversationId) {
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
}
},
});
}, 800);
} else {
appStore.toastSuccess('已表达喜欢');
}
} catch (err) {
appStore.toastError(err);
} finally {
this.setData({ actionLoadingUserId: '' });
}
},
async onPass(e) {
const { userId } = e.detail;
if (this.data.actionLoadingUserId) return;
this.setData({ actionLoadingUserId: userId });
try {
await matchStore.pass(userId);
// Pass 是单向意向,不打扰用户(AGENTS 第 6 条隐式)
} catch (err) {
// Pass 失败不提示,避免打扰
} finally {
this.setData({ actionLoadingUserId: '' });
}
},
async onBlock(e) {
const { userId } = e.detail;
const target = this.data.recommendations.find((r) => r.userId === userId);
const name = target?.displayName || '该用户';
// 危险动作二次确认(AGENTS 第 4 条)
const confirmed = await appStore.confirm({
title: '拉黑该用户',
content: `拉黑后 ${name} 将无法与你互动,你也不会再收到对方的推荐与消息。此操作可由你随时解除。`,
confirmText: '确认拉黑',
danger: true,
});
if (!confirmed) return;
try {
await blockUser(userId, '用户主动拉黑');
appStore.toastSuccess('已拉黑');
} catch (err) {
appStore.toastError(err);
}
},
onViewLikers() {
wx.navigateTo({ url: ROUTE.DETAIL.LIKERS });
},
onEmptyAction(e) {
const { event } = e.detail;
if (event === 'discover') {
wx.switchTab({ url: ROUTE.TAB.DISCOVER });
} else if (event === 'matchmaker') {
wx.navigateTo({ url: ROUTE.DETAIL.MATCHMAKER_APPLY });
}
},
});
+14
View File
@@ -0,0 +1,14 @@
{
"navigationBarTitleText": "善缘丹阳",
"navigationStyle": "custom",
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#FAFAF8",
"usingComponents": {
"ui-section-header": "/components/section-header/index",
"ui-recommend-card": "/components/recommend-card/index",
"ui-empty": "/components/empty/index",
"ui-loading": "/components/loading/index",
"ui-status-badge": "/components/status-badge/index"
}
}
+239
View File
@@ -0,0 +1,239 @@
/**
* 首页 - V1.1 新版(U18)
* 结构:品牌区 → 用户状态条 → Banner → 8入口 → 推荐 → 服务建议 → 活动 → 服务站 → 安全提醒
*/
const authStore = require('../../stores/authStore')
const api = require('../../utils/api')
Page({
data: {
unreadCount: 0,
userState: null,
isOnboarding: false,
isVerified: false,
isPaused: false,
isSuccessful: false,
onboardingMessage: '',
onboardingActions: [],
statusChecks: [],
banners: [],
currentBannerIndex: 0,
quickEntries: [],
recommendations: [],
recState: 'idle',
actionLoadingUserId: '',
serviceRecommendations: [],
activities: [],
serviceLocation: null,
articles: [],
safetyNotice: {
text: '实名认证和人工审核可以提高信息真实性,交往过程中仍请谨慎涉及借款、投资和个人隐私。',
ctaText: '查看婚恋反诈指南',
ctaTarget: ''
},
emptyActions: [
{ text: '查看择偶条件', event: 'preference', variant: 'secondary' },
{ text: '申请公益红娘', event: 'matchmaker', variant: 'primary' },
],
},
onLoad() {
this.checkAuthAndLoad()
},
onShow() {
if (authStore.state.isLoggedIn) {
this.loadHomeData()
}
},
onPullDownRefresh() {
this.loadHomeData().finally(() => wx.stopPullDownRefresh())
},
// ==================== 核心加载 ====================
async checkAuthAndLoad() {
if (!authStore.state.isLoggedIn) {
this.setData({
isOnboarding: true,
onboardingMessage: '登录后即可开始寻找合适的人',
onboardingActions: [{ text: '去登录', event: 'login', variant: 'primary' }],
})
return
}
await this.loadHomeData()
},
async loadHomeData() {
try {
const res = await api.get('/home')
this.processHomeData(res)
} catch (err) {
console.error('首页数据加载失败', err)
this.setData({ recState: 'error' })
}
},
processHomeData(data) {
const { userState, banners, quickEntries, recommendations,
serviceRecommendations, activities, serviceLocation, safetyNotice } = data
const relStatus = userState?.relationshipStatus || ''
const profileStatus = userState?.profileStatus || ''
const eligStatus = userState?.localEligibilityStatus || 'not_declared'
const isOnboarding = !userState || profileStatus !== 'approved'
const isPaused = relStatus === 'paused'
const isSuccessful = relStatus === 'matched_successfully'
const isVerified = eligStatus === 'verified'
const checks = [
{ label: '已实名', done: userState?.verified || false },
{ label: '本地资格', done: eligStatus === 'verified' || eligStatus === 'pending_review' },
{ label: '资料已审核', done: profileStatus === 'approved' || profileStatus === 'completed' },
]
this.setData({
userState,
isOnboarding, isVerified, isPaused, isSuccessful,
onboardingMessage: this.buildOnboardingMessage(userState, profileStatus, eligStatus),
onboardingActions: this.buildOnboardingActions(userState, profileStatus, eligStatus),
statusChecks: checks,
banners, quickEntries,
recommendations: recommendations || [],
recState: (recommendations && recommendations.length > 0) ? 'loaded' : 'empty',
serviceRecommendations, activities, serviceLocation,
safetyNotice: safetyNotice || this.data.safetyNotice,
})
},
buildOnboardingMessage(state, profileStatus, eligStatus) {
if (!state) return '登录后即可开始寻找合适的人'
if (eligStatus === 'not_declared') return '完成本地资格声明后即可开始寻找合适的人'
if (eligStatus === 'needs_evidence') return '补充本地资格佐证后即可开始寻找合适的人'
if (profileStatus !== 'approved' && profileStatus !== 'completed') return '资料审核通过后即可开始寻找合适的人'
return '完成资料后开始寻找合适的人'
},
buildOnboardingActions(state, profileStatus, eligStatus) {
if (!state) return [{ text: '去登录', event: 'login', variant: 'primary' }]
const actions = []
if (eligStatus === 'not_declared' || eligStatus === 'needs_evidence') {
actions.push({ text: '声明本地资格', event: 'local-eligibility', variant: 'primary' })
}
if (profileStatus !== 'approved' && profileStatus !== 'completed') {
actions.push({ text: '完善资料', event: 'profile', variant: 'secondary' })
}
if (actions.length === 0) {
actions.push({ text: '开始寻找', event: 'discover', variant: 'primary' })
}
return actions
},
// ==================== 事件处理 ====================
onBannerChange(e) {
this.setData({ currentBannerIndex: e.detail.current })
},
onBannerTap(e) {
const { targetType, targetValue } = e.currentTarget.dataset.item
if (targetType === 'activity' && targetValue) {
wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${targetValue}` })
} else if (targetType === 'matchmaker') {
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
} else if (targetType === 'service_location' && targetValue) {
wx.navigateTo({ url: `/pages/local-eligibility/local-eligibility?from=service_station&id=${targetValue}` })
}
},
onEntryTap(e) {
const { code, enabled, targetPage } = e.currentTarget.dataset.entry
if (!enabled) {
wx.showToast({ title: '完成资料后可使用此功能', icon: 'none' })
return
}
if (targetPage.startsWith('pages/')) {
wx.navigateTo({ url: targetPage })
}
},
onViewDetail(e) {
const userId = e.detail?.userId || e.currentTarget.dataset?.userId
if (userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${userId}` })
},
onLike(e) {
const userId = e.detail?.userId
if (!userId) return
this.setData({ actionLoadingUserId: userId })
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
},
onPass(e) {
const userId = e.detail?.userId
if (!userId) return
this.setData({ actionLoadingUserId: userId })
setTimeout(() => this.setData({ actionLoadingUserId: '' }), 500)
},
onEmptyAction(e) {
const event = e.currentTarget.dataset.event
switch (event) {
case 'login':
wx.reLaunch({ url: '/pages/login/login' })
break
case 'discover':
wx.switchTab({ url: '/pages/discover/discover' })
break
case 'matchmaker':
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
break
case 'preference':
wx.navigateTo({ url: '/pages/profile-preference/profile-preference' })
break
case 'local-eligibility':
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility' })
break
case 'profile':
wx.navigateTo({ url: '/pages/profile-basic/profile-basic' })
break
}
},
onResume() {
wx.showToast({ title: '功能开发中', icon: 'none' })
},
onSeeMore() { wx.switchTab({ url: '/pages/discover/discover' }) },
onSeeAllActivities() { wx.switchTab({ url: '/pages/activity/activity' }) },
onActivityTap(e) {
const id = e.currentTarget.dataset.id
if (id) wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${id}` })
},
onStationTap() {
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility?from=service_station' })
},
onServiceTap(e) {
const type = e.currentTarget.dataset.type
switch (type) {
case 'matchmaker': wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
break
case 'counseling':
case 'premarital':
wx.navigateTo({ url: `/pages/local-eligibility/local-eligibility?from=${type}` })
break
}
},
onSafetyTap() {
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility?from=anti_fraud' })
},
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
formatDate(timestamp) {
if (!timestamp) return ''
const d = new Date(timestamp)
return `${d.getMonth() + 1}月${d.getDate()}日 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
},
})
+214
View File
@@ -0,0 +1,214 @@
<!-- U18 首页 - V1.1 新版结构 -->
<view class="page">
<!-- ========== 品牌区 ========== -->
<view class="brand-bar">
<view class="brand-info">
<text class="brand-name">善缘丹阳</text>
<text class="brand-slogan">丹阳公益婚恋服务平台</text>
</view>
<view class="brand-actions">
<view class="bell-btn" bindtap="onMessages">
<text class="bell-icon">🔔</text>
<block wx:if="{{unreadCount > 0}}">
<text class="badge">{{unreadCount > 9 ? '9+' : unreadCount}}</text>
</block>
</view>
</view>
</view>
<!-- ========== 用户状态条 ========== -->
<view class="status-strip" wx:if="{{userState}}">
<!-- 未完成准入 -->
<block wx:if="{{isOnboarding}}">
<view class="status-prompt">
<text class="prompt-text">{{onboardingMessage}}</text>
<view class="prompt-actions" wx:if="{{onboardingActions.length > 0}}">
<view
wx:for="{{onboardingActions}}"
wx:key="event"
class="prompt-btn {{item.variant}}"
bindtap="onEmptyAction"
data-event="{{item.event}}"
>{{item.text}}</view>
</view>
</view>
</block>
<!-- 已验证 -->
<block wx:elif="{{isVerified}}">
<view class="status-checks">
<text class="status-check {{item.done ? 'done' : 'pending'}}"
wx:for="{{statusChecks}}" wx:key="label">
{{item.done ? '✓' : '○'}} {{item.label}}
</text>
</view>
</block>
<!-- 已暂停 -->
<block wx:elif="{{isPaused}}">
<view class="paused-hint">
<text>你已暂停新的对象推荐</text>
<view class="resume-btn" bindtap="onResume">恢复寻找</view>
</view>
</block>
<!-- 已成功牵手 -->
<block wx:elif="{{isSuccessful}}">
<view class="successful-hint">
<text>祝福你们正在认真了解彼此</text>
</view>
</block>
</view>
<!-- ========== Banner 轮播 ========== -->
<view class="banner-section" wx:if="{{banners.length > 0}}">
<swiper
class="banner-swiper"
indicator-dots
autoplay
circular
interval="4500"
indicator-active-color="#E96B6B"
bindchange="onBannerChange"
>
<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-overlay">
<text class="banner-title">{{item.title}}</text>
<text class="banner-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
<view class="banner-btn" wx:if="{{item.buttonText}}">{{item.buttonText}}</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
<!-- ========== 8个核心入口 ========== -->
<view class="entries-section">
<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-bg {{item.iconClass}}">
<text class="entry-icon">{{item.icon}}</text>
</view>
<text class="entry-label">{{item.label}}</text>
</view>
</view>
</view>
<!-- ========== 今日推荐 ========== -->
<view class="section" wx:if="{{recommendations.length > 0 || recState === 'loading'}}">
<ui-section-header title="今日推荐"
subtitle="根据双方条件,为你筛选更合适的人"
more="{{true}}"
bind:more="onSeeMore" />
<block wx:if="{{recState === 'loading' && recommendations.length === 0}}">
<view class="skeleton-card" wx:for="{{3}}" wx:key="index">
<view class="skeleton-avatar"></view>
<view class="skeleton-info">
<view class="skeleton-line w60"></view>
<view class="skeleton-line w40"></view>
<view class="skeleton-line w80"></view>
</view>
</view>
</block>
<block wx:elif="{{recommendations.length > 0}}">
<view wx:for="{{recommendations}}" wx:key="userId" class="rec-card">
<ui-recommend-card
recommendation="{{item}}"
action-loading="{{actionLoadingUserId === item.userId}}"
bind:view="onViewDetail"
bind:like="onLike"
bind:pass="onPass"
/>
</view>
</block>
<view wx:if="{{recState === 'finished' && recommendations.length > 0}}" class="end-tip">
已展示今日全部推荐 · 明天再来看看
</view>
</view>
<!-- ========== 无推荐空状态 ========== -->
<view class="section" wx:if="{{!isOnboarding && recommendations.length === 0 && recState !== 'loading'}}">
<ui-empty
illustration="heart"
title="暂时没有找到完全符合双方条件的人"
description="你可以调整择偶条件,或申请公益红娘帮你推荐"
actions="{{emptyActions}}"
bind:action="onEmptyAction"
/>
</view>
<!-- ========== 善缘公益服务 ========== -->
<view class="section service-reco-section" wx:if="{{serviceRecommendations.length > 0}}">
<ui-section-header title="善缘公益服务" />
<view wx:for="{{serviceRecommendations}}" wx:key="type"
class="service-card" bindtap="onServiceTap" data-type="{{item.type}}">
<view class="service-card-inner">
<text class="service-title">{{item.title}}</text>
<text class="service-desc">{{item.description}}</text>
<view class="service-cta">{{item.ctaText}}</view>
</view>
</view>
</view>
<!-- ========== 近期公益活动 ========== -->
<view class="section activity-section" wx:if="{{activities.length > 0}}">
<ui-section-header title="近期公益活动" more="{{true}}"
bind:more="onSeeAllActivities" />
<scroll-view scroll-x class="activity-scroll">
<view wx:for="{{activities}}" wx:key="id"
class="activity-h-card" bindtap="onActivityTap" data-id="{{item.id}}">
<image wx:if="{{item.coverFileId}}" src="{{item.coverFileId}}"
mode="aspectFill" class="activity-cover" />
<view class="activity-info">
<text class="activity-title">{{item.title}}</text>
<text class="activity-meta" wx:if="{{item.startAt}}">{{formatDate(item.startAt)}}</text>
<text class="activity-meta" wx:if="{{item.locationName}}">{{item.locationName}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- ========== 善缘服务站 ========== -->
<view class="section" wx:if="{{serviceLocation}}">
<view class="station-card" bindtap="onStationTap">
<view class="station-card-inner">
<view class="station-icon-area">
<text class="station-icon">📍</text>
</view>
<view class="station-info">
<text class="station-name">{{serviceLocation.name}}</text>
<text class="station-short">{{serviceLocation.shortName}}</text>
<view class="station-services">
<text wx:for="{{serviceLocation.services}}" wx:key="*this"
class="station-tag">{{item}}</text>
</view>
</view>
<text class="station-arrow">›</text>
</view>
</view>
</view>
<!-- ========== 安全提醒 ========== -->
<view class="safety-notice">
<view class="safety-notice-inner">
<text class="safety-icon">🛡</text>
<view class="safety-text-area">
<text class="safety-title">善缘提醒</text>
<text class="safety-body">{{safetyNotice.text}}</text>
</view>
<text class="safety-cta" bindtap="onSafetyTap">{{safetyNotice.ctaText}}</text>
</view>
</view>
<!-- 底部安全区 -->
<view class="safe-area-bottom"></view>
</view>
+246
View File
@@ -0,0 +1,246 @@
/* U18 首页 - V1.1 新版样式 */
.page {
min-height: 100vh;
background: #FAFAF8;
padding-bottom: 0;
}
/* ========== 品牌区 ========== */
.brand-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx 16rpx;
background: #fff;
position: sticky;
top: 0;
z-index: 100;
}
.brand-info { display: flex; flex-direction: column; }
.brand-name {
font-size: 36rpx;
font-weight: 700;
color: #E96B6B;
line-height: 1.2;
}
.brand-slogan {
font-size: 22rpx;
color: #888;
margin-top: 4rpx;
}
.bell-btn {
position: relative;
width: 64rpx; height: 64rpx;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
background: #f5f5f5;
}
.bell-icon { font-size: 32rpx; }
.badge {
position: absolute; top: -4rpx; right: -4rpx;
min-width: 28rpx; height: 28rpx; line-height: 28rpx;
padding: 0 6rpx;
background: #E96B6B; color: #fff;
font-size: 20rpx; border-radius: 14rpx;
text-align: center;
}
/* ========== 用户状态条 ========== */
.status-strip {
background: #fff;
padding: 16rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.status-checks { display: flex; gap: 24rpx; flex-wrap: wrap; }
.status-check {
font-size: 24rpx; color: #666;
}
.status-check.done { color: #2E7D32; }
.status-check.pending { color: #F57C00; }
.status-prompt { padding: 16rpx 0; }
.prompt-text {
font-size: 26rpx; color: #555;
display: block; margin-bottom: 16rpx;
}
.prompt-actions { display: flex; gap: 16rpx; }
.prompt-btn {
padding: 12rpx 28rpx; border-radius: 8rpx;
font-size: 26rpx; font-weight: 500;
}
.prompt-btn.primary { background: #E96B6B; color: #fff; }
.prompt-btn.secondary { background: #fff; color: #E96B6B; border: 1rpx solid #E96B6B; }
.paused-hint, .successful-hint { padding: 16rpx 0; text-align: center; }
.paused-hint text { font-size: 26rpx; color: #888; display: block; margin-bottom: 12rpx; }
.resume-btn {
display: inline-block; padding: 10rpx 32rpx;
background: #E96B6B; color: #fff;
border-radius: 8rpx; font-size: 26rpx;
}
/* ========== Banner ========== */
.banner-section { padding: 16rpx 24rpx; }
.banner-swiper {
width: 100%; height: 340rpx;
border-radius: 16rpx; overflow: hidden;
}
.banner-card { width: 100%; height: 100%; position: relative; }
.banner-img { width: 100%; height: 100%; object-fit: cover; }
.banner-overlay {
position: absolute; bottom: 0; left: 0; right: 0;
padding: 24rpx;
background: linear-gradient(transparent, rgba(0,0,0,0.6));
color: #fff;
}
.banner-title { font-size: 30rpx; font-weight: 600; display: block; margin-bottom: 4rpx; }
.banner-subtitle { font-size: 22rpx; opacity: 0.9; display: block; margin-bottom: 8rpx; }
.banner-btn {
display: inline-block; padding: 8rpx 20rpx;
background: rgba(255,255,255,0.25); border: 1rpx solid rgba(255,255,255,0.6);
border-radius: 20rpx; font-size: 22rpx; color: #fff;
}
/* ========== 8个核心入口 ========== */
.entries-section {
background: #fff;
margin: 0 24rpx; border-radius: 16rpx;
padding: 24rpx 16rpx; margin-top: -8rpx;
position: relative; z-index: 10;
}
.entries-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.entry-item {
display: flex; flex-direction: column; align-items: center;
padding: 12rpx 0;
}
.entry-item.disabled { opacity: 0.5; }
.entry-icon-bg {
width: 72rpx; height: 72rpx;
border-radius: 16rpx;
display: flex; align-items: center; justify-content: center;
margin-bottom: 8rpx;
}
.entry-icon-bg.icon-dating { background: #FFF0F0; }
.entry-icon-bg.icon-matchmaker { background: #FFF8E6; }
.entry-icon-bg.icon-activity { background: #E8F5E9; }
.entry-icon-bg.icon-counseling { background: #E3F2FD; }
.entry-icon-bg.icon-class { background: #F3E5F5; }
.entry-icon-bg.icon-premarital { background: #FFF3E0; }
.entry-icon-bg.icon-shield { background: #E8EAF6; }
.entry-icon-bg.icon-location { background: #E0F2F1; }
.entry-icon { font-size: 32rpx; }
.entry-label { font-size: 22rpx; color: #333; text-align: center; line-height: 1.3; }
/* ========== 通用 Section ========== */
.section { margin-top: 24rpx; padding: 0 24rpx; }
/* ========== 推荐卡片 ========== */
.rec-card { margin-bottom: 16rpx; }
.end-tip { text-align: center; font-size: 24rpx; color: #999; padding: 16rpx 0; }
/* ========== 骨架屏 ========== */
.skeleton-card {
display: flex; align-items: center;
padding: 16rpx 0; margin-bottom: 12rpx;
}
.skeleton-avatar {
width: 88rpx; height: 112rpx;
background: #eee; border-radius: 8rpx; margin-right: 16rpx;
}
.skeleton-info { flex: 1; }
.skeleton-line {
height: 24rpx; background: #eee;
border-radius: 4rpx; margin-bottom: 8rpx;
}
.skeleton-line.w60 { width: 60%; }
.skeleton-line.w40 { width: 40%; }
.skeleton-line.w80 { width: 80%; }
/* ========== 善缘公益服务 ========== */
.service-card {
background: linear-gradient(135deg, #FFF8F0 0%, #FFF0F0 100%);
border-radius: 16rpx; padding: 24rpx; margin-bottom: 12rpx;
}
.service-card-inner { display: flex; flex-direction: column; gap: 8rpx; }
.service-title { font-size: 28rpx; font-weight: 600; color: #333; }
.service-desc { font-size: 24rpx; color: #666; line-height: 1.5; }
.service-cta {
display: inline-block; margin-top: 8rpx;
padding: 8rpx 20rpx; background: #E96B6B;
color: #fff; border-radius: 20rpx;
font-size: 24rpx; width: fit-content;
}
/* ========== 近期活动 ========== */
.activity-scroll { white-space: nowrap; margin: 0 -4rpx; }
.activity-h-card {
display: inline-block; width: 480rpx; margin: 0 4rpx 12rpx;
background: #fff; border-radius: 12rpx; overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.activity-cover { width: 100%; height: 270rpx; background: #f0f0f0; }
.activity-info { padding: 12rpx 16rpx; }
.activity-title { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 4rpx; }
.activity-meta { font-size: 22rpx; color: #888; display: block; }
/* ========== 善缘服务站 ========== */
.station-card {
background: #fff; border-radius: 16rpx; overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); margin-top: 24rpx;
}
.station-card-inner {
display: flex; align-items: center; padding: 24rpx; gap: 16rpx;
}
.station-icon-area {
width: 80rpx; height: 80rpx; background: #E8F5E9;
border-radius: 16rpx; display: flex; align-items: center;
justify-content: center; flex-shrink: 0;
}
.station-icon { font-size: 40rpx; }
.station-info { flex: 1; min-width: 0; }
.station-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
.station-short { font-size: 22rpx; color: #888; display: block; margin-top: 2rpx; }
.station-services { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 8rpx; }
.station-tag {
font-size: 20rpx; padding: 2rpx 12rpx;
background: #F5F5F5; border-radius: 12rpx; color: #666;
}
.station-arrow { font-size: 36rpx; color: #ccc; flex-shrink: 0; }
/* ========== 安全提醒 ========== */
.safety-notice {
margin: 24rpx 24rpx 0;
background: #FFF9E6; border-radius: 12rpx;
padding: 20rpx 24rpx;
}
.safety-notice-inner { display: flex; align-items: flex-start; gap: 12rpx; }
.safety-icon { font-size: 32rpx; flex-shrink: 0; margin-top: 2rpx; }
.safety-text-area { flex: 1; }
.safety-title { font-size: 24rpx; font-weight: 600; color: #8B6914; display: block; margin-bottom: 4rpx; }
.safety-body { font-size: 22rpx; color: #8B6914; line-height: 1.5; display: block; }
.safety-cta { font-size: 24rpx; color: #E96B6B; flex-shrink: 0; padding-left: 12rpx; }
/* ========== 底部安全区 ========== */
.safe-area-bottom { height: env(safe-area-inset-bottom); }