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
+8 -70
View File
@@ -1,75 +1,13 @@
<view class="ui-rec" wx:if="{{recommendation}}">
<view class="ui-rec__header" bindtap="onViewDetail">
<ui-avatar
src="{{recommendation.avatarUrl}}"
size="xl"
verified="{{recommendation.verified}}"
reviewed="{{recommendation.reviewed}}"
default-text="{{recommendation.displayName}}"
/>
<view class="ui-rec" wx:if="{{recommendation}}" bindtap="onViewDetail">
<ui-avatar src="{{recommendation.avatarUrl || '/assets/brand/default-avatar.png'}}" size="lg" verified="{{recommendation.verified}}" reviewed="{{recommendation.reviewed}}" default-text="{{recommendation.displayName}}" />
<view class="ui-rec__info">
<view class="ui-rec__name-row">
<text class="ui-rec__name">{{recommendation.displayName}}</text>
<text class="ui-rec__age">{{recommendation.age}}岁</text>
<view class="ui-rec__name-row"><text class="ui-rec__name">{{recommendation.displayName || '善缘用户'}}</text><text wx:if="{{recommendation.age}}" class="ui-rec__age">{{recommendation.age}}岁</text><text wx:if="{{recommendation.online}}" class="ui-rec__online">在线</text></view>
<view class="ui-rec__meta"><text wx:if="{{recommendation.regionName}}">{{recommendation.regionName}}</text><text wx:if="{{recommendation.heightCm}}"> · {{recommendation.heightCm}}cm</text><text wx:if="{{recommendation.educationCode}}"> · {{recommendation.educationCode}}</text></view>
<view wx:if="{{recommendation.occupationCode}}" class="ui-rec__meta">{{recommendation.occupationCode}}</view>
<view wx:if="{{recommendation.recommendationReasons && recommendation.recommendationReasons.length}}" class="ui-rec__reasons"><text wx:for="{{recommendation.recommendationReasons}}" wx:key="*this" class="ui-rec__reason">{{item}}</text></view>
</view>
<view class="ui-rec__meta">
<text wx:if="{{recommendation.heightCm}}">{{recommendation.heightCm}}cm · </text>
<text>{{recommendation.regionName}}</text>
<text wx:if="{{recommendation.educationCode}}"> · {{recommendation.educationCode}}</text>
</view>
<view wx:if="{{recommendation.occupationCode}}" class="ui-rec__meta">
{{recommendation.occupationCode}}
</view>
<view wx:if="{{recommendation.maritalStatus || recommendation.hasChildren}}" class="ui-rec__meta">
<text wx:if="{{recommendation.maritalStatus}}">{{recommendation.maritalStatus}}</text>
<text wx:if="{{recommendation.hasChildren}}"> · 有孩</text>
</view>
<view wx:if="{{recommendation.photoUrls}}" class="ui-rec__photo-count">
{{recommendation.photoUrls.length}} 张生活照
</view>
</view>
<view
wx:if="{{recommendation.recommendationLevel}}"
class="ui-rec__level ui-rec__level--{{recommendation.recommendationLevel}}"
>
{{levelTextMap[recommendation.recommendationLevel]}}
</view>
</view>
<view
wx:if="{{recommendation.recommendationReasons && recommendation.recommendationReasons.length}}"
class="ui-rec__reasons"
>
<view class="ui-rec__reasons-title">为什么推荐给你</view>
<view
wx:for="{{recommendation.recommendationReasons}}"
wx:key="*this"
class="ui-rec__reason"
>✓ {{item}}</view>
</view>
<view wx:if="{{recommendation.intro}}" class="ui-rec__intro">
{{recommendation.intro}}
</view>
<view wx:if="{{showActions}}" class="ui-rec__actions">
<ui-button
variant="ghost"
size="md"
disabled="{{actionLoading}}"
class="ui-rec__btn-pass"
bindtap="onPass"
>不合适</ui-button>
<ui-button
variant="primary"
size="md"
loading="{{actionLoading}}"
class="ui-rec__btn-like"
bindtap="onLike"
>喜欢</ui-button>
</view>
<view class="ui-rec__more">
<text class="ui-rec__more-text" catchtap="onBlock">拉黑该用户</text>
<view class="compact-like" catchtap="onLike"><image src="/assets/brand/logo-mark.png" mode="aspectFit" /></view>
<text class="compact-pass" catchtap="onPass">暂不考虑</text>
</view>
</view>
+1 -113
View File
@@ -1,113 +1 @@
.ui-rec {
background: var(--color-bg-card);
border-radius: var(--radius-lg);
padding: var(--spacing-4);
margin-bottom: var(--spacing-3);
box-shadow: var(--shadow-card);
}
.ui-rec__header {
display: flex;
align-items: flex-start;
position: relative;
}
.ui-rec__info {
flex: 1;
min-width: 0;
margin-left: var(--spacing-3);
}
.ui-rec__name-row {
display: flex;
align-items: baseline;
margin-bottom: var(--spacing-1);
}
.ui-rec__name {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-right: var(--spacing-2);
}
.ui-rec__age {
font-size: var(--font-size-md);
color: var(--color-text-tertiary);
}
.ui-rec__meta {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: var(--line-height-normal);
margin-top: 4rpx;
}
.ui-rec__photo-count {
margin-top: var(--spacing-2);
font-size: var(--font-size-xs);
color: var(--color-primary);
}
.ui-rec__level {
position: absolute;
top: 0;
right: 0;
padding: 6rpx 14rpx;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
.ui-rec__level--high {
background: var(--color-success-light);
color: var(--color-success);
}
.ui-rec__level--medium {
background: var(--color-info-light);
color: var(--color-info);
}
.ui-rec__level--normal {
background: var(--color-warning-light);
color: var(--color-warning);
}
.ui-rec__reasons {
margin-top: var(--spacing-3);
padding: var(--spacing-3);
background: var(--color-primary-light);
border-radius: var(--radius-md);
}
.ui-rec__reasons-title {
font-size: var(--font-size-sm);
color: var(--color-primary);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-2);
}
.ui-rec__reason {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: var(--line-height-relaxed);
}
.ui-rec__intro {
margin-top: var(--spacing-3);
padding-top: var(--spacing-3);
border-top: 1rpx solid var(--color-divider);
font-size: var(--font-size-md);
color: var(--color-text-secondary);
line-height: var(--line-height-relaxed);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.ui-rec__actions {
display: flex;
gap: var(--spacing-3);
margin-top: var(--spacing-4);
}
.ui-rec__btn-pass {
flex: 1;
}
.ui-rec__btn-like {
flex: 2;
}
.ui-rec__more {
margin-top: var(--spacing-3);
text-align: center;
}
.ui-rec__more .ui-rec__more-text {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
padding: var(--spacing-1) var(--spacing-2);
}
.ui-rec{display:flex;align-items:center;gap:16rpx;min-height:132rpx;padding:14rpx 16rpx;background:#fff;border-bottom:1rpx solid #F0F1F6}.ui-rec:last-child{border-bottom:0}.ui-rec__info{flex:1;min-width:0}.ui-rec__name-row{display:flex;align-items:center;gap:10rpx}.ui-rec__name{font-size:26rpx;font-weight:700;color:#222746}.ui-rec__age{font-size:21rpx;color:#545B76}.ui-rec__online{margin-left:auto;font-size:18rpx;color:#39A987}.ui-rec__online::before{content:'·';margin-right:4rpx}.ui-rec__meta{margin-top:5rpx;font-size:20rpx;color:#767C94;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ui-rec__reasons{display:flex;gap:7rpx;margin-top:8rpx;overflow:hidden}.ui-rec__reason{flex:none;padding:4rpx 9rpx;border-radius:12rpx;background:#F3EEFF;color:#7860AE;font-size:17rpx;white-space:nowrap}.ui-rec__actions{width:82rpx;display:flex;flex-direction:column;align-items:center;gap:7rpx}.compact-like{width:64rpx;height:64rpx;display:flex;align-items:center;justify-content:center;border-radius:32rpx;background:#FFF0F5;box-shadow:0 7rpx 18rpx rgba(255,79,145,.18)}.compact-like image{width:44rpx;height:44rpx}.compact-pass{font-size:17rpx;color:#A0A4B4;white-space:nowrap}
+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) {
+71 -38
View File
@@ -1,58 +1,91 @@
<view class="page app-page reference-page">
<!-- 品牌区 -->
<view class="night-shell">
<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 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>
</view>
<view class="brand-actions" bindtap="onMessages">
<text class="message-label">消息</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 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>
<!-- 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>
<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>
<!-- 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 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 wx:if="{{loadError}}" class="section"><ui-error-state title="暂时无法加载首页" description="请检查网络后重试" bind:retry="onRetry"/></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">
<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="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>
</block>
<block wx:else>
<ui-empty illustration="heart" title="暂时没有完全符合双方条件的人" description="可以调整择偶条件,或申请公益红娘协助推荐" actions="{{emptyActions}}" bind:action="onEmptyAction"/>
</block>
</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="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 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="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 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="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 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 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"><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 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}}
+53
View File
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
const read = (path) => fs.readFileSync(path, 'utf8')
const home = read('backend/src/main/java/com/danyang/match/platform/home/service/HomeApplicationService.java')
const miniapp = read('miniapp/pages/index/index.ts')
const wxml = read('miniapp/pages/index/index.wxml')
const project = JSON.parse(read('project.config.json'))
const labels = ['找对象', '公益红娘', '公益活动', '婚恋咨询', '婚恋课堂', '婚前辅导', '婚恋反诈', '善缘服务站']
let previous = -1
for (const label of labels) {
const position = home.indexOf(`"${label}"`)
assert.ok(position > previous, `首页入口顺序错误: ${label}`)
previous = position
}
assert.match(home, /userIdentityMapper\.findByUserId/, '首页实名认证状态必须来自数据库')
assert.match(home, /recommendationEngineService\.getDailyRecommendations/, '首页推荐必须来自正式推荐引擎')
assert.match(home, /contentArticleMapper\.findPublishedAll/, '首页课堂内容必须来自内容模块')
assert.match(home, /serviceLocationMapper\.selectOne/, '首页服务站必须来自服务站模块')
assert.match(home, /setDisabledReason/, '不可用入口必须返回服务端原因')
assert.match(home, /isIdentityVerified/, 'Capability 必须校验真实实名状态')
assert.match(home, /ACCOUNT_STATUS_NORMAL/, 'Capability 必须拒绝非正常账户')
assert.match(home, /\/pages\/counseling-intro\/counseling-intro/, '咨询服务建议必须跳转真实页面')
assert.match(home, /\/pages\/premarital-intro\/premarital-intro/, '婚前辅导建议必须跳转真实页面')
assert.doesNotMatch(home, /"pages\//, '服务端返回的小程序路由必须以斜杠开头')
assert.doesNotMatch(home, /setVerified\(true\)/, '禁止硬编码实名通过')
assert.doesNotMatch(home, /card\.setId\("default"\)/, '禁止硬编码服务站')
assert.deepEqual(project.setting.useCompilerPlugins, ['typescript'])
assert.ok(fs.existsSync('miniapp/pages/index/index.ts'))
assert.ok(!fs.existsSync('miniapp/pages/index/index.js'), '首页只能保留 TypeScript 运行源')
assert.ok(fs.existsSync('miniapp/pages/discover/discover.ts'))
assert.ok(!fs.existsSync('miniapp/pages/discover/discover.js'), '找对象页只能保留 TypeScript 运行源')
assert.match(miniapp, /disabledReason/, '小程序必须显示服务端 Disabled 原因')
assert.match(miniapp, /loadError: true/, '首页必须有网络错误状态')
assert.match(miniapp, /matchStore\.like\(userId\)/, '首页喜欢操作必须调用正式关系服务')
assert.match(miniapp, /matchStore\.pass\(userId\)/, '首页略过操作必须调用正式关系服务')
assert.doesNotMatch(miniapp, /setTimeout\(/, '首页不得保留演示用延时反馈')
assert.match(miniapp, /QUICK_ENTRIES\.map/, '服务端入口必须按固定业务顺序合并')
assert.match(wxml, /home-hero-v2\.jpg/, '首页默认 Banner 必须使用参考图夜景主视觉')
assert.doesNotMatch(wxml, />›</, '首页不得继续使用字符箭头')
const sections = ['brand-bar', 'status-strip', 'banner-section', 'entries-card', '今日推荐', '善缘公益服务', '近期公益活动', '善缘服务站', '婚恋课堂', '安全提醒']
previous = -1
for (const section of sections) {
const position = wxml.indexOf(section)
assert.ok(position > previous, `首页区块顺序错误: ${section}`)
previous = position
}
console.log('Phase 07 home contracts passed')