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,242 @@
/**
* 用户资料详情页 U21
*
* 重构要点(遵循 AGENTS.md 不变量):
* - 数据获取:recommendationService.getUserDetail(API_INVENTORY 行 43),不再直连 api 模块
* - 喜欢:matchStore.like(mutualMatch=true 时跳转会话页,AGENTS 第 3/6 条)
* - 拉黑:blockUser 跨 Store 编排(MatchStore + MessageStore 同步停用,AGENTS 第 4 条)
* - 举报:跳转 report 页,不在本页处理
* - 敏感数据:profile.birthday/身份证/手机号 仅服务端持有,前端不缓存(AGENTS 第 8 条)
* - Required 不可放宽:本页只读展示,无任何放宽择偶条件的入口(AGENTS 第 2 条)
*/
const { matchStore, appStore, blockUser } = require('../../stores');
const { recommendationService } = require('../../services');
const { to, DANGER_ACTION, RELATION_STATUS, BUSINESS_ERROR_CODE } = require('../../constants');
Page({
data: {
userId: '',
profile: null,
loading: true,
loadError: '',
/** @type {import('../../types').RelationStatus} */
relationStatus: RELATION_STATUS.NEUTRAL,
isLiked: false,
isMutual: false,
canChat: false,
actionLoading: false,
statusBarHeight: 20,
navBarHeight: 44,
showBlockDialog: false,
blockAction: DANGER_ACTION.BLOCK,
/** 派生的展示字段 */
displayName: '',
ageText: '',
metaLine: '',
badges: [],
basicInfo: [],
attitudeInfo: [],
},
onLoad(options) {
const userId = options.id || '';
if (!userId) {
this.setData({ loading: false, loadError: '用户不存在' });
return;
}
// 状态栏高度由 app.js 注入到全局,这里读一次
try {
const sysInfo = wx.getSystemInfoSync ? wx.getSystemInfoSync() : null;
if (sysInfo) {
this.setData({
statusBarHeight: sysInfo.statusBarHeight || 20,
navBarHeight: 44,
});
}
} catch (e) { /* 使用默认值 */ }
this.setData({ userId });
this.loadProfile();
},
async loadProfile() {
this.setData({ loading: true, loadError: '' });
try {
const user = await recommendationService.getUserDetail(this.data.userId);
this.setData({ loading: false, profile: user });
this._deriveDisplay(user);
} catch (err) {
this.setData({
loading: false,
loadError: (err && err.message) || '加载失败',
});
appStore.toastError(err, '加载用户资料失败');
}
},
/**
* 把后端 User 字段派生为展示用结构,避免 wxml 内做条件拼接
*/
_deriveDisplay(user) {
if (!user) return;
const relationStatus = user.relationStatus || RELATION_STATUS.NEUTRAL;
const isLiked = relationStatus === RELATION_STATUS.LIKED || relationStatus === RELATION_STATUS.MUTUAL_MATCH;
const isMutual = relationStatus === RELATION_STATUS.MUTUAL_MATCH;
// AGENTS 第 3 条:只有 Mutual 后才能聊天
const canChat = isMutual && relationStatus !== RELATION_STATUS.BLOCKED;
const badges = [];
if (user.verified || user.isRealNameVerified) {
badges.push({ kind: 'success', label: '已实名' });
}
if (user.reviewed || user.profileReviewStatus === 'approved') {
badges.push({ kind: 'info', label: '资料已审核' });
}
const metaParts = [];
if (user.heightCm) metaParts.push(`${user.heightCm}cm`);
if (user.regionName) metaParts.push(user.regionName);
if (user.educationCode) metaParts.push(user.educationCode);
if (user.occupationCode) metaParts.push(user.occupationCode);
const basicInfo = [];
if (user.maritalStatus) basicInfo.push({ label: '婚姻状况', value: user.maritalStatus });
if (user.childrenStatus || user.hasChildren !== undefined) {
basicInfo.push({ label: '子女情况', value: user.childrenStatus || (user.hasChildren ? '有孩' : '无孩') });
}
if (user.educationCode) basicInfo.push({ label: '学历', value: user.educationCode });
if (user.occupationCode) basicInfo.push({ label: '职业', value: user.occupationCode });
if (user.smokingStatus) basicInfo.push({ label: '吸烟', value: user.smokingStatus });
if (user.drinkingStatus) basicInfo.push({ label: '饮酒', value: user.drinkingStatus });
if (user.longTermDanyang) basicInfo.push({ label: '定居意愿', value: user.longTermDanyang, full: true });
if (user.futureDanyang) basicInfo.push({ label: '未来规划', value: user.futureDanyang, full: true });
const attitudeInfo = [];
if (user.relationshipGoal) attitudeInfo.push({ label: '寻找', value: user.relationshipGoal, full: true });
if (user.marriagePlan) attitudeInfo.push({ label: '结婚计划', value: user.marriagePlan, full: true });
this.setData({
relationStatus,
isLiked,
isMutual,
canChat,
displayName: user.displayName || '用户',
ageText: user.age ? `${user.age}岁` : '',
metaLine: metaParts.join(' · '),
badges,
basicInfo,
attitudeInfo,
});
},
onBack() {
wx.navigateBack();
},
onRetry() {
this.loadProfile();
},
/**
* 喜欢(AGENTS 第 3/5/6 条:用户本人操作,mutualMatch 时跳聊天)
*/
async onLike() {
const { userId, actionLoading, isLiked } = this.data;
if (!userId || actionLoading || isLiked) return;
this.setData({ actionLoading: true });
try {
const result = await matchStore.like(userId);
// 本地派生状态更新
if (result && result.mutualMatch) {
this.setData({
isLiked: true,
isMutual: true,
canChat: true,
relationStatus: RELATION_STATUS.MUTUAL_MATCH,
actionLoading: false,
});
appStore.toastSuccess('双向喜欢!可以开始聊天了');
if (result.matchedConversationId) {
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
}
} else {
this.setData({
isLiked: true,
relationStatus: RELATION_STATUS.LIKED,
actionLoading: false,
});
appStore.toastSuccess('已表达喜欢');
}
} catch (err) {
this.setData({ actionLoading: false });
if (err && err.code === BUSINESS_ERROR_CODE.DUPLICATE_ACTION) {
appStore.toast('已表达过喜欢');
} else {
appStore.toastError(err, '操作失败');
}
}
},
/**
* 开始聊天(仅 Mutual 后可见,AGENTS 第 3 条)
*/
onGoChat() {
const { userId, canChat, profile } = this.data;
if (!canChat) {
appStore.toast('双方互相喜欢后才能开始聊天');
return;
}
// matchedConversationId 优先来自最近 like 结果;无则用 userId 占位由 chat 页拉取
const conversationId = (profile && profile.matchedConversationId) || userId;
wx.navigateTo({ url: to.chat(conversationId) });
},
/**
* 举报:跳转 report 页,不在本页处理(AGENTS 第 7 条:举报内容不在此暴露)
*/
onReport() {
const { userId } = this.data;
if (!userId) return;
wx.navigateTo({ url: to.report(userId) });
},
/**
* 拉黑入口:弹出 DangerActionDialog 二次确认(AGENTS 第 4 条)
*/
onBlock() {
if (!this.data.userId) return;
this.setData({ showBlockDialog: true });
},
onConfirmBlock() {
this.setData({ showBlockDialog: false });
this._doBlock();
},
onCancelBlock() {
this.setData({ showBlockDialog: false });
},
/**
* 执行拉黑:调用跨 Store 编排 blockUser
* - MatchStore:从推荐/Mutual 列表移除
* - MessageStore:会话立即标记不可发消息
*/
async _doBlock() {
const { userId, actionLoading } = this.data;
if (!userId || actionLoading) return;
this.setData({ actionLoading: true });
try {
appStore.showLoading('处理中…');
await blockUser(userId, '用户主动拉黑');
appStore.hideLoading();
appStore.toastSuccess('已拉黑');
// 拉黑后离开当前页(AGENTS 第 4 条:Block 即时禁止互动)
setTimeout(() => wx.navigateBack(), 800);
} catch (err) {
appStore.hideLoading();
this.setData({ actionLoading: false });
appStore.toastError(err, '拉黑失败');
}
},
});
@@ -0,0 +1,15 @@
{
"navigationBarTitleText": "用户资料",
"navigationStyle": "custom",
"usingComponents": {
"ui-nav-bar": "/components/nav-bar/index",
"ui-avatar": "/components/avatar/index",
"ui-status-badge": "/components/status-badge/index",
"ui-section-header": "/components/section-header/index",
"ui-button": "/components/button/index",
"ui-tag": "/components/tag/index",
"ui-loading": "/components/loading/index",
"ui-empty": "/components/empty/index",
"ui-danger-action-dialog": "/components/danger-action-dialog/index"
}
}
@@ -0,0 +1,149 @@
<!-- 用户资料详情页 U21 -->
<view class="profile-page">
<ui-nav-bar
title="用户资料"
status-bar-height="{{statusBarHeight}}"
nav-bar-height="{{navBarHeight}}"
bind:back="onBack"
/>
<!-- 首次加载 -->
<view wx:if="{{loading}}" class="profile-loading">
<ui-loading mode="spinner" text="正在加载资料…" />
</view>
<!-- 加载失败 -->
<view wx:elif="{{loadError}}" class="profile-empty">
<ui-empty
title="加载失败"
description="{{loadError}}"
illustration="default"
actions="{{[{text:'重试', event:'retry', variant:'primary'}]}}"
bind:action="onRetry"
/>
</view>
<!-- 用户不存在 -->
<view wx:elif="{{!profile}}" class="profile-empty">
<ui-empty
title="用户不存在"
description="该用户可能已注销或不可见"
illustration="default"
/>
</view>
<!-- 资料主体 -->
<view wx:else class="profile-body">
<!-- 顶部信息 -->
<view class="profile-header">
<ui-avatar
src="{{profile.avatarUrl}}"
size="xl"
verified="{{profile.verified || profile.isRealNameVerified}}"
reviewed="{{profile.reviewed || profile.profileReviewStatus === 'approved'}}"
default-text="{{displayName}}"
/>
<view class="profile-header__info">
<view class="profile-header__name-row">
<text class="profile-header__name">{{displayName}}</text>
<text wx:if="{{ageText}}" class="profile-header__age">{{ageText}}</text>
</view>
<view wx:if="{{metaLine}}" class="profile-header__meta">{{metaLine}}</view>
<view wx:if="{{badges.length}}" class="profile-header__badges">
<ui-status-badge
wx:for="{{badges}}"
wx:key="label"
kind="{{item.kind}}"
label="{{item.label}}"
/>
</view>
</view>
</view>
<!-- 关于我 -->
<view wx:if="{{basicInfo.length}}" class="profile-section">
<ui-section-header title="关于我" />
<view class="profile-info-grid">
<view
wx:for="{{basicInfo}}"
wx:key="label"
class="profile-info-item {{item.full ? 'profile-info-item--full' : ''}}"
>
<text class="profile-info-item__label">{{item.label}}</text>
<text class="profile-info-item__value">{{item.value}}</text>
</view>
</view>
</view>
<!-- 自我介绍 -->
<view wx:if="{{profile.selfIntro}}" class="profile-section">
<ui-section-header title="自我介绍" />
<text class="profile-intro-text">{{profile.selfIntro}}</text>
</view>
<!-- 婚恋态度 -->
<view wx:if="{{attitudeInfo.length}}" class="profile-section">
<ui-section-header title="婚恋态度" />
<view class="profile-info-grid">
<view
wx:for="{{attitudeInfo}}"
wx:key="label"
class="profile-info-item profile-info-item--full"
>
<text class="profile-info-item__label">{{item.label}}</text>
<text class="profile-info-item__value">{{item.value}}</text>
</view>
</view>
</view>
</view>
<!-- 底部操作(仅 profile 加载成功时展示) -->
<view wx:if="{{profile && !loadError}}" class="profile-actions">
<ui-button
variant="ghost"
size="md"
custom-class="profile-actions__btn profile-actions__btn--block"
bind:tap="onBlock"
disabled="{{actionLoading}}"
>拉黑</ui-button>
<ui-button
variant="ghost"
size="md"
custom-class="profile-actions__btn profile-actions__btn--report"
bind:tap="onReport"
disabled="{{actionLoading}}"
>举报</ui-button>
<ui-button
wx:if="{{canChat}}"
variant="secondary"
size="md"
custom-class="profile-actions__btn"
bind:tap="onGoChat"
disabled="{{actionLoading}}"
>开始聊天</ui-button>
<ui-button
wx:if="{{!isLiked}}"
variant="primary"
size="md"
custom-class="profile-actions__btn profile-actions__btn--like"
loading="{{actionLoading}}"
bind:tap="onLike"
>喜欢</ui-button>
<ui-button
wx:else
variant="primary"
size="md"
custom-class="profile-actions__btn"
disabled
>已喜欢</ui-button>
</view>
<!-- 拉黑确认弹窗 -->
<ui-danger-action-dialog
visible="{{showBlockDialog}}"
action="{{blockAction}}"
target-name="{{displayName}}"
bind:confirm="onConfirmBlock"
bind:cancel="onCancelBlock"
/>
</view>
@@ -0,0 +1,139 @@
/**
* 用户资料详情页 U21
* 全部使用 Design Token,禁止硬编码颜色/间距/圆角
*/
.profile-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
/* ============ 加载/空状态 ============ */
.profile-loading,
.profile-empty {
padding-top: 200rpx;
display: flex;
justify-content: center;
}
/* ============ 顶部信息 ============ */
.profile-header {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-5) var(--spacing-page-x);
background: var(--color-bg-card);
}
.profile-header__info {
flex: 1;
min-width: 0;
}
.profile-header__name-row {
display: flex;
align-items: baseline;
gap: var(--spacing-2);
margin-bottom: var(--spacing-1);
}
.profile-header__name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.profile-header__age {
font-size: var(--font-size-md);
color: var(--color-text-tertiary);
}
.profile-header__meta {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
line-height: var(--line-height-normal);
}
.profile-header__badges {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-1);
margin-top: var(--spacing-2);
}
/* ============ 内容区块 ============ */
.profile-body {
padding-top: var(--spacing-2);
}
.profile-section {
background: var(--color-bg-card);
margin: var(--spacing-2) var(--spacing-page-x);
border-radius: var(--radius-lg);
padding: var(--spacing-card-inner);
}
.profile-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-3);
}
.profile-info-item {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
min-width: 0;
}
.profile-info-item--full {
grid-column: span 2;
}
.profile-info-item__label {
font-size: var(--font-size-xs);
color: var(--color-text-placeholder);
}
.profile-info-item__value {
font-size: var(--font-size-md);
color: var(--color-text-secondary);
word-break: break-all;
}
.profile-intro-text {
font-size: var(--font-size-md);
color: var(--color-text-secondary);
line-height: var(--line-height-relaxed);
}
/* ============ 底部操作栏 ============ */
.profile-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: var(--spacing-2);
align-items: center;
padding: var(--spacing-2) var(--spacing-page-x);
padding-bottom: calc(var(--spacing-2) + env(safe-area-inset-bottom));
background: var(--color-bg-card);
border-top: var(--border-width) solid var(--color-divider);
z-index: var(--z-sticky);
}
.profile-actions__btn {
flex: 1;
}
.profile-actions__btn--block,
.profile-actions__btn--report {
flex: 0 0 auto;
min-width: 140rpx;
}
.profile-actions__btn--report {
color: var(--color-warning);
}