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
@@ -0,0 +1,53 @@
/**
* RecommendCard 推荐卡片(规范 §22-25, §107-108)
*
* 推荐首页核心卡片,包含:头像 + 基本信息 + 推荐理由 + Like/Pass 操作。
* 不变量(AGENTS.md):
* - 第 2 条:Required 不可自动放宽——推荐理由仅作弱文字提示,
* 不暴露"放宽了 X 条件才得到这个推荐"的字样,前端只显示后端返回的 reasons。
* - 第 5 条:红娘不可替用户操作——本卡片仅由当前用户操作,不接受 matchmakerId 参数。
*/
const { STATUS_BADGE } = require('../../constants');
Component({
options: { addGlobalClass: true },
properties: {
recommendation: { type: Object, value: null },
// 是否显示操作按钮(详情页用,可能用底部固定按钮替代)
showActions: { type: Boolean, value: true },
// 是否禁用按钮(操作进行中)
actionLoading: { type: Boolean, value: false },
},
data: {
levelTextMap: {
high: '很合适',
medium: '比较合适',
normal: '值得了解',
},
levelKindMap: {
high: 'success',
medium: 'info',
normal: 'warning',
},
},
methods: {
onViewDetail() {
if (!this.data.recommendation) return;
this.triggerEvent('view', { userId: this.data.recommendation.userId });
},
onLike() {
if (this.data.actionLoading) return;
if (!this.data.recommendation) return;
this.triggerEvent('like', { userId: this.data.recommendation.userId });
},
onPass() {
if (this.data.actionLoading) return;
if (!this.data.recommendation) return;
this.triggerEvent('pass', { userId: this.data.recommendation.userId });
},
onBlock(e) {
// 阻止冒泡避免触发 view
this.triggerEvent('block', { userId: this.data.recommendation.userId });
},
},
});
@@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"ui-avatar": "../avatar/index",
"ui-status-badge": "../status-badge/index",
"ui-button": "../button/index"
}
}
@@ -0,0 +1,75 @@
<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__info">
<view class="ui-rec__name-row">
<text class="ui-rec__name">{{recommendation.displayName}}</text>
<text class="ui-rec__age">{{recommendation.age}}岁</text>
</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>
</view>
@@ -0,0 +1,113 @@
.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);
}