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
+221
View File
@@ -0,0 +1,221 @@
/**
* 我的页 U30-U33(重构:用 userStore 派生状态 + ui-section-header)
*
* 不变量(AGENTS.md):
* - 第 8 条:敏感数据默认拒绝——Profile 中不展示身份证号/手机号/完整生日,
* 仅展示前台脱敏字段(昵称/年龄/地区/职业大类)。
* - 第 2 条:Required 不可自动放宽——择偶条件入口仅跳转编辑页,不在本页软化。
* - 第 9 条:PostgreSQL 是 Source of Truth——本页状态全部来自 userStore 拉取的 /me/status。
*/
const { userStore, authStore, matchStore, appStore } = require('../../stores');
const { ROUTE, OPERATOR, DANGER_ACTION, RELATIONSHIP_STATUS, PROFILE_REVIEW } = require('../../constants');
const RELATIONSHIP_TEXT = {
[RELATIONSHIP_STATUS.SEARCHING]: '正在寻找',
[RELATIONSHIP_STATUS.GETTING_TO_KNOW]: '正在了解',
[RELATIONSHIP_STATUS.PAUSED]: '已暂停寻找',
[RELATIONSHIP_STATUS.MATCHED_SUCCESSFULLY]: '已成功牵手',
[RELATIONSHIP_STATUS.SERVICE_EXITED]: '已退出服务',
};
Page({
data: {
isLoggedIn: false,
profile: null,
userState: null,
verificationStatus: 'not_submitted',
loadStatus: 'idle',
isRealNameVerified: false,
isProfileApproved: false,
reviewStatusText: '',
relationshipText: '',
canPause: false,
canResume: false,
likersCount: 0,
showLogoutDialog: false,
logoutAction: DANGER_ACTION.LOGOUT,
loginActions: [{ text: '微信登录', event: 'login', variant: 'primary' }],
hasOperatorToken: false,
},
onLoad() {
// 订阅 userStore,派生展示状态
this._unsubUser = userStore.subscribe((state) => this._syncFromUserStore(state));
this._syncFromUserStore(userStore.state);
// 绑定 matchStore.likersCount(驱动"喜欢我"徽标)
this._unsubMatch = matchStore.bindToPage(this, { likersCount: 'likersCount' });
},
onShow() {
// 检测红娘工作台登录状态
this.setData({ hasOperatorToken: !!wx.getStorageSync('operatorToken') });
if (authStore.state.isLoggedIn) {
userStore.fetchStatus().catch((err) => appStore.toastError(err));
userStore.fetchProfile().catch(() => {});
userStore.fetchVerificationStatus().catch(() => {});
matchStore.fetchLikersCount().catch(() => {});
}
},
onUnload() {
this._unsubUser && this._unsubUser();
this._unsubMatch && this._unsubMatch();
},
/** 把 userStore 状态派生为页面展示字段 */
_syncFromUserStore(state) {
const us = state.userState;
const isLoggedIn = !!us || authStore.state.isLoggedIn;
const isRealNameVerified = !!us && !!us.isRealNameVerified;
const reviewStatus = us ? us.profileReviewStatus : null;
const isProfileApproved = reviewStatus === PROFILE_REVIEW.APPROVED;
let reviewStatusText = '未提交';
if (reviewStatus === PROFILE_REVIEW.PENDING) reviewStatusText = '审核中';
else if (reviewStatus === PROFILE_REVIEW.APPROVED) reviewStatusText = '已审核';
else if (reviewStatus === PROFILE_REVIEW.NEED_EDIT) reviewStatusText = '需修改';
else if (reviewStatus === PROFILE_REVIEW.REJECTED) reviewStatusText = '未通过';
const rel = us ? us.relationshipStatus : null;
const relationshipText = rel ? RELATIONSHIP_TEXT[rel] || '' : '';
const canPause =
rel === RELATIONSHIP_STATUS.SEARCHING || rel === RELATIONSHIP_STATUS.GETTING_TO_KNOW;
const canResume = rel === RELATIONSHIP_STATUS.PAUSED;
this.setData({
isLoggedIn,
profile: state.profile,
userState: us,
verificationStatus: state.verificationStatus,
loadStatus: state.loadStatus,
isRealNameVerified,
isProfileApproved,
reviewStatusText,
relationshipText,
canPause,
canResume,
});
},
// ============ 登录 ============
onLoginAction(e) {
if ((e.detail || {}).event === 'login') {
wx.navigateTo({ url: ROUTE.AUTH.LOGIN });
}
},
// ============ 资料编辑 ============
onEditProfile() {
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTRO });
},
onEditPreference() {
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_PREFERENCE });
},
onEditPhotos() {
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_PHOTOS });
},
// ============ 匹配 ============
onGoMessages() {
wx.switchTab({ url: ROUTE.TAB.MESSAGES });
},
onGoLikers() {
wx.navigateTo({ url: ROUTE.DETAIL.LIKERS });
},
// ============ 账户与安全 ============
onGoVerification() {
wx.navigateTo({ url: ROUTE.AUTH.VERIFICATION });
},
onGoPrivacy() {
wx.navigateTo({ url: ROUTE.SETTINGS.PRIVACY });
},
onGoBlacklist() {
wx.navigateTo({ url: ROUTE.SETTINGS.BLACKLIST });
},
onGoAccountStatus() {
wx.navigateTo({ url: ROUTE.SETTINGS.ACCOUNT_STATUS_PAGE });
},
// ============ 关系状态切换 ============
async onPause() {
try {
appStore.showLoading('处理中…');
await userStore.pauseSearching();
appStore.hideLoading();
appStore.toastSuccess('已暂停寻找');
} catch (err) {
appStore.hideLoading();
appStore.toastError(err);
}
},
async onResume() {
try {
appStore.showLoading('处理中…');
await userStore.resumeSearching();
appStore.hideLoading();
appStore.toastSuccess('已恢复寻找');
} catch (err) {
appStore.hideLoading();
appStore.toastError(err);
}
},
// ============ 其他 ============
onGoSettings() {
wx.navigateTo({ url: ROUTE.SETTINGS.SETTINGS });
},
onGoHelp() {
wx.navigateTo({ url: ROUTE.SETTINGS.HELP_CENTER });
},
onGoAbout() {
wx.navigateTo({ url: ROUTE.SETTINGS.ABOUT });
},
// ============ 身份切换 ============
onSwitchRole() {
const token = wx.getStorageSync('operatorToken');
if (token) {
wx.navigateTo({ url: ROUTE.OPERATOR.WORKBENCH });
} else {
wx.navigateTo({ url: ROUTE.OPERATOR.LOGIN });
}
},
onLogout() {
this.setData({ showLogoutDialog: true });
},
async onConfirmLogout() {
this.setData({ showLogoutDialog: false });
try {
appStore.showLoading('退出中…');
await authStore.logout();
appStore.hideLoading();
wx.reLaunch({ url: ROUTE.AUTH.LOGIN });
} catch (err) {
appStore.hideLoading();
appStore.toastError(err);
}
},
onCancelLogout() {
this.setData({ showLogoutDialog: false });
},
});
+12
View File
@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "我的",
"usingComponents": {
"ui-section-header": "/components/section-header/index",
"ui-avatar": "/components/avatar/index",
"ui-status-badge": "/components/status-badge/index",
"ui-button": "/components/button/index",
"ui-loading": "/components/loading/index",
"ui-empty": "/components/empty/index",
"ui-danger-action-dialog": "/components/danger-action-dialog/index"
}
}
+170
View File
@@ -0,0 +1,170 @@
<!-- 我的页 U30-U33(重构:userStore 派生状态 + ui-section-header) -->
<view class="page">
<!-- 未登录 -->
<ui-empty
wx:if="{{!isLoggedIn}}"
illustration="default"
title="未登录"
description="登录后可管理资料、查看匹配与消息。"
actions="{{loginActions}}"
bind:action="onLoginAction"
/>
<!-- 加载中(首次且无缓存资料) -->
<ui-loading
wx:elif="{{loadStatus === 'loading' && !profile}}"
mode="skeleton"
rows="6"
/>
<!-- 已登录:个人中心 -->
<block wx:else>
<!-- 个人资料卡 -->
<view class="profile-card">
<ui-avatar
src="{{profile.avatarUrl || ''}}"
size="xl"
default-text="{{profile.displayName || ''}}"
verified="{{isRealNameVerified}}"
reviewed="{{isProfileApproved}}"
/>
<view class="profile-info">
<view class="profile-name">{{profile.displayName || '未填写昵称'}}</view>
<view class="profile-meta">{{profile.regionName || '丹阳'}}</view>
<view class="profile-badges">
<ui-status-badge
wx:if="{{isRealNameVerified}}"
kind="success"
label="已实名"
icon
/>
<ui-status-badge
wx:elif="{{verificationStatus === 'pending'}}"
kind="warning"
label="认证中"
icon
/>
<ui-status-badge
wx:else
kind="disabled"
label="未实名"
icon
/>
</view>
</view>
</view>
<!-- 婚恋状态 -->
<ui-section-header title="婚恋状态" subtitle="{{relationshipText}}" />
<view class="action-row">
<ui-button
wx:if="{{canPause}}"
variant="secondary"
size="md"
block
bind:tap="onPause"
>暂停寻找</ui-button>
<ui-button
wx:if="{{canResume}}"
variant="primary"
size="md"
block
bind:tap="onResume"
>恢复寻找</ui-button>
<view wx:if="{{!canPause && !canResume}}" class="action-hint">
<text>当前状态不支持切换</text>
</view>
</view>
<!-- 我的资料 -->
<ui-section-header title="我的资料" subtitle="{{reviewStatusText}}" />
<view class="list-group">
<view class="list-row" bindtap="onEditProfile">
<text class="list-label">编辑基本资料</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onEditPreference">
<text class="list-label">择偶条件</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onEditPhotos">
<text class="list-label">生活照管理</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoAccountStatus">
<text class="list-label">审核与账号状态</text>
<text class="list-arrow">›</text>
</view>
</view>
<!-- 我的匹配 -->
<ui-section-header title="我的匹配" />
<view class="list-group">
<view class="list-row" bindtap="onGoMessages">
<text class="list-label">互相喜欢</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoLikers">
<text class="list-label">喜欢我的人</text>
<text class="list-value">{{likersCount > 0 ? likersCount + ' 人' : ''}}</text>
<text class="list-arrow">›</text>
</view>
</view>
<!-- 账户与安全 -->
<ui-section-header title="账户与安全" />
<view class="list-group">
<view class="list-row" bindtap="onGoVerification">
<text class="list-label">实名认证</text>
<text class="list-value">{{isRealNameVerified ? '已认证' : (verificationStatus === 'pending' ? '认证中' : '未认证')}}</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoPrivacy">
<text class="list-label">隐私设置</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoBlacklist">
<text class="list-label">黑名单</text>
<text class="list-arrow">›</text>
</view>
</view>
<!-- 其他 -->
<ui-section-header title="其他" />
<view class="list-group">
<view class="list-row" bindtap="onGoSettings">
<text class="list-label">设置</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoHelp">
<text class="list-label">帮助中心</text>
<text class="list-arrow">›</text>
</view>
<view class="list-row" bindtap="onGoAbout">
<text class="list-label">关于我们</text>
<text class="list-arrow">›</text>
</view>
</view>
<!-- 红娘工作台独立入口按钮 -->
<view class="workbench-btn" bindtap="onSwitchRole">
<text class="workbench-icon">👩</text>
<text class="workbench-text">{{hasOperatorToken ? '进入红娘工作台' : '红娘工作台登录'}}</text>
</view>
<!-- 退出登录 -->
<view class="list-group" style="margin-top: 24rpx;">
<view class="list-row danger" bindtap="onLogout">
<text class="list-label">退出登录</text>
</view>
</view>
<!-- 退出登录确认 -->
<ui-danger-action-dialog
visible="{{showLogoutDialog}}"
action="{{logoutAction}}"
bind:confirm="onConfirmLogout"
bind:cancel="onCancelLogout"
/>
</block>
</view>
+128
View File
@@ -0,0 +1,128 @@
/**
* 我的页样式(Design Token,中低密度留白)
*/
.page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: var(--spacing-8);
}
/* ============ 个人资料卡 ============ */
.profile-card {
display: flex;
align-items: center;
gap: var(--spacing-4);
background: var(--color-bg-card);
padding: var(--spacing-5) var(--spacing-page-x);
border-bottom: var(--border-width) solid var(--color-divider);
}
.profile-info {
flex: 1;
min-width: 0;
}
.profile-name {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-1);
}
.profile-meta {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
margin-bottom: var(--spacing-2);
}
.profile-badges {
display: flex;
gap: var(--spacing-2);
flex-wrap: wrap;
}
/* ============ 操作行 ============ */
.action-row {
background: var(--color-bg-card);
padding: var(--spacing-3) var(--spacing-page-x);
border-bottom: var(--border-width) solid var(--color-divider);
}
.action-hint {
text-align: center;
color: var(--color-text-tertiary);
font-size: var(--font-size-sm);
padding: var(--spacing-2) 0;
}
/* ============ 列表组 ============ */
.list-group {
background: var(--color-bg-card);
margin-bottom: var(--spacing-section);
}
.list-row {
display: flex;
align-items: center;
padding: var(--spacing-4) var(--spacing-page-x);
border-bottom: var(--border-width) solid var(--color-divider);
background: var(--color-bg-card);
}
.list-row:last-child {
border-bottom: none;
}
.list-label {
flex: 1;
font-size: var(--font-size-base);
color: var(--color-text-primary);
}
.list-value {
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
margin-right: var(--spacing-2);
}
.list-arrow {
font-size: var(--font-size-lg);
color: var(--color-text-disabled);
line-height: 1;
}
.list-row.danger .list-label {
color: var(--color-danger);
text-align: center;
flex: none;
width: 100%;
}
/* ============ 红娘工作台入口按钮 ============ */
.workbench-btn {
margin: 32rpx var(--spacing-page-x);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
background: linear-gradient(135deg, #E96B6B 0%, #ff9a9a 100%);
color: #fff;
height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: 600;
box-shadow: 0 4rpx 16rpx rgba(233, 107, 107, 0.3);
}
.workbench-btn:active {
opacity: 0.85;
transform: scale(0.98);
}
.workbench-icon {
font-size: 36rpx;
}
.workbench-text {
color: #fff;
}