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,257 @@
/**
* 择偶条件页 U12-U14(Phase 04 P04-T27)
*
* 重构要点(遵循 AGENTS.md 不变量):
* - 数据:userStore.fetchPreferences + updatePreferences(第 9 条)
* - Required/Preferred:preference-editor 的 needConfirmRelax 触发二次确认(第 2 条:
* "必须符合"不可自动放宽为"最好符合",UI 必须显式提示)
* - 草稿恢复:onLoad 读 ONBOARDING_DRAFT.preference,保存前写回
* - 下一步:跳 profile-confirm
*/
const { userStore, appStore } = require('../../stores');
const { ROUTE, STORAGE_KEYS } = require('../../constants');
const REGION_OPTIONS = ['丹阳', '镇江市区', '镇江其他辖区', '不限'];
const MARITAL_ACCEPT_OPTIONS = ['未婚', '离异', '丧偶'];
const EDUCATION_OPTIONS = ['不限', '高中/中专以上', '大专以上', '本科以上', '研究生以上'];
const CHILDREN_OPTIONS = [
{ value: 'any', label: '不限' },
{ value: 'accept', label: '可接受' },
{ value: 'reject', label: '不接受' },
];
const MORE_PREF_OPTIONS = {
occupation: ['不限', '机关事业单位', '教育', '医疗', '企业职员', '技术人员', '其他'],
smoking: ['不限', '不吸烟', '偶尔', '经常'],
drinking: ['不限', '不饮酒', '偶尔', '经常'],
settlement: ['不限', '长期定居丹阳', '愿意来丹阳', '不限地区'],
marriagePlan: ['不限', '半年内', '1年内', '1~2年', '顺其自然'],
};
function buildDefaultPreference() {
return {
ageMin: null,
ageMax: null,
ageImportance: 'preferred',
heightMin: null,
heightMax: null,
heightImportance: 'preferred',
region: [],
regionImportance: 'preferred',
maritalStatusAccepted: [],
maritalImportance: 'preferred',
educationAccepted: [],
educationImportance: 'preferred',
acceptChildren: 'any',
// 更多条件(U14,均默认不限)
occupationPreference: '',
smokingPreference: '',
drinkingPreference: '',
settlementPreference: '',
marriagePlanPreference: '',
};
}
Page({
data: {
statusBarHeight: 20,
navBarHeight: 44,
loading: false,
saving: false,
preference: buildDefaultPreference(),
// 选项
regionOptions: REGION_OPTIONS,
maritalAcceptOptions: MARITAL_ACCEPT_OPTIONS,
educationOptions: EDUCATION_OPTIONS,
childrenOptions: CHILDREN_OPTIONS,
morePrefOptions: MORE_PREF_OPTIONS,
educationIndex: 0,
occupationIndex: 0,
smokingIndex: 0,
drinkingIndex: 0,
settlementIndex: 0,
marriagePlanIndex: 0,
},
onLoad() {
this._initSystemInfo();
this._applyDraft();
this.loadPreferences();
},
_initSystemInfo() {
try {
const sysInfo = wx.getSystemInfoSync();
this.setData({
statusBarHeight: sysInfo.statusBarHeight || 20,
navBarHeight: 44,
});
} catch (e) { /* 使用默认值 */ }
},
async loadPreferences() {
this.setData({ loading: true });
try {
const preferences = await userStore.fetchPreferences();
this._mergePreferences(preferences);
} catch (err) { /* 首次填写 */ } finally {
this.setData({ loading: false });
}
},
_mergePreferences(prefs) {
if (!prefs) return;
const d = this._readDraft();
const preference = { ...buildDefaultPreference(), ...prefs, ...(d.preference || {}) };
// 同步更多条件的 picker index
this.setData({
preference,
educationIndex: Math.max(0, EDUCATION_OPTIONS.indexOf((preference.educationAccepted && preference.educationAccepted[0]) || '不限')),
occupationIndex: Math.max(0, MORE_PREF_OPTIONS.occupation.indexOf(preference.occupationPreference || '不限')),
smokingIndex: Math.max(0, MORE_PREF_OPTIONS.smoking.indexOf(preference.smokingPreference || '不限')),
drinkingIndex: Math.max(0, MORE_PREF_OPTIONS.drinking.indexOf(preference.drinkingPreference || '不限')),
settlementIndex: Math.max(0, MORE_PREF_OPTIONS.settlement.indexOf(preference.settlementPreference || '不限')),
marriagePlanIndex: Math.max(0, MORE_PREF_OPTIONS.marriagePlan.indexOf(preference.marriagePlanPreference || '不限')),
});
},
// ============ 草稿 ============
_readDraft() {
try {
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
return draft.preference || {};
} catch (e) {
return {};
}
},
_applyDraft() {
const d = this._readDraft();
if (!d || !d.preference) return;
this._mergePreferences(d.preference);
},
_writeDraft() {
try {
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
draft.preference = { preference: this.data.preference };
draft.currentStep = 'preference';
wx.setStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT, draft);
} catch (e) { /* ignore */ }
},
// ============ preference-editor 变更 ============
/**
* 处理 preference-editor 的 change 事件
* AGENTS 第 2 条:Required → Preferred 需二次确认(needConfirmRelax=true)
*/
async onPrefChange(e) {
const { field, importance, value, needConfirmRelax } = e.detail;
if (needConfirmRelax) {
// 必须符合 → 最好符合,需用户二次确认
const ok = await appStore.confirm({
title: '确认放宽此条件?',
content: '"必须符合"的条件不会被系统放宽。改为"最好符合"后,可能匹配到略不满足此条件的对象。',
});
if (!ok) return; // 用户取消,不修改
}
const preference = { ...this.data.preference };
if (importance !== undefined) {
preference[field] = importance;
} else if (value !== undefined) {
preference[field] = value;
}
this.setData({ preference });
},
// ============ 地区/婚姻多选 ============
onRegionTap(e) {
const region = e.currentTarget.dataset.region;
const preference = { ...this.data.preference };
const list = [...(preference.region || [])];
const idx = list.indexOf(region);
if (idx >= 0) list.splice(idx, 1);
else list.push(region);
preference.region = list;
this.setData({ preference });
},
onMaritalTap(e) {
const status = e.currentTarget.dataset.status;
const preference = { ...this.data.preference };
const list = [...(preference.maritalStatusAccepted || [])];
const idx = list.indexOf(status);
if (idx >= 0) list.splice(idx, 1);
else list.push(status);
preference.maritalStatusAccepted = list;
this.setData({ preference });
},
// ============ 学历单选 ============
onEducationChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.educationAccepted = [EDUCATION_OPTIONS[idx]];
this.setData({ preference, educationIndex: idx });
},
// ============ 更多条件(U14) ============
onOccupationChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.occupationPreference = MORE_PREF_OPTIONS.occupation[idx];
this.setData({ preference, occupationIndex: idx });
},
onSmokingPrefChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.smokingPreference = MORE_PREF_OPTIONS.smoking[idx];
this.setData({ preference, smokingIndex: idx });
},
onDrinkingPrefChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.drinkingPreference = MORE_PREF_OPTIONS.drinking[idx];
this.setData({ preference, drinkingIndex: idx });
},
onSettlementChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.settlementPreference = MORE_PREF_OPTIONS.settlement[idx];
this.setData({ preference, settlementIndex: idx });
},
onMarriagePlanPrefChange(e) {
const idx = Number(e.detail.value);
const preference = { ...this.data.preference };
preference.marriagePlanPreference = MORE_PREF_OPTIONS.marriagePlan[idx];
this.setData({ preference, marriagePlanIndex: idx });
},
// ============ 导航 ============
onBack() {
this._writeDraft();
wx.navigateBack();
},
async onNext() {
if (this.data.saving) return;
const { preference } = this.data;
if (!preference.ageMin || !preference.ageMax) {
return appStore.toast('请填写年龄范围');
}
this.setData({ saving: true });
this._writeDraft();
try {
await userStore.updatePreferences(preference);
this.setData({ saving: false });
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_CONFIRM });
} catch (err) {
this.setData({ saving: false });
appStore.toastError(err, '保存失败');
}
},
});
@@ -0,0 +1,10 @@
{
"navigationBarTitleText": "择偶条件",
"navigationStyle": "custom",
"usingComponents": {
"ui-nav-bar": "/components/nav-bar/index",
"ui-section-header": "/components/section-header/index",
"ui-preference-editor": "/components/preference-editor/index",
"ui-button": "/components/button/index"
}
}
@@ -0,0 +1,103 @@
<!-- 择偶条件页 U12-U14 -->
<view class="ppref-page">
<ui-nav-bar
title="择偶条件"
status-bar-height="{{statusBarHeight}}"
nav-bar-height="{{navBarHeight}}"
bind:back="onBack"
/>
<view class="ppref-body">
<!-- 基础择偶条件(U12/U13) -->
<view class="ppref-section card-base">
<ui-section-header title="基础择偶条件" subtitle="可设置必须符合或最好符合" />
<ui-preference-editor
preference="{{preference}}"
bind:change="onPrefChange"
/>
</view>
<!-- 地区(多选,补充 preference-editor 的值编辑) -->
<view class="ppref-section card-base">
<ui-section-header title="期望地区" subtitle="多选" />
<view class="ppref-tags">
<view
wx:for="{{regionOptions}}"
wx:key="*this"
class="ppref-tag {{preference.region && preference.region.indexOf(item) >= 0 ? 'ppref-tag--selected' : ''}}"
data-region="{{item}}"
bindtap="onRegionTap"
>{{item}}</view>
</view>
</view>
<!-- 婚姻状况(多选) -->
<view class="ppref-section card-base">
<ui-section-header title="期望婚姻状况" subtitle="多选" />
<view class="ppref-tags">
<view
wx:for="{{maritalAcceptOptions}}"
wx:key="*this"
class="ppref-tag {{preference.maritalStatusAccepted && preference.maritalStatusAccepted.indexOf(item) >= 0 ? 'ppref-tag--selected' : ''}}"
data-status="{{item}}"
bindtap="onMaritalTap"
>{{item}}</view>
</view>
</view>
<!-- 学历(单选) -->
<view class="ppref-section card-base">
<ui-section-header title="期望学历" />
<picker mode="selector" range="{{educationOptions}}" value="{{educationIndex}}" bindchange="onEducationChange">
<view class="ppref-picker">
<text class="ppref-picker__value">{{educationOptions[educationIndex]}}</text>
</view>
</picker>
</view>
<!-- 更多择偶条件(U14,可选) -->
<view class="ppref-section card-base">
<ui-section-header title="更多择偶条件" subtitle="可选,均允许不限" />
<view class="ppref-form">
<picker mode="selector" range="{{morePrefOptions.occupation}}" value="{{occupationIndex}}" bindchange="onOccupationChange">
<view class="ppref-field">
<text class="ppref-field__label">职业倾向</text>
<text class="ppref-field__value">{{morePrefOptions.occupation[occupationIndex]}}</text>
</view>
</picker>
<picker mode="selector" range="{{morePrefOptions.smoking}}" value="{{smokingIndex}}" bindchange="onSmokingPrefChange">
<view class="ppref-field">
<text class="ppref-field__label">是否吸烟</text>
<text class="ppref-field__value">{{morePrefOptions.smoking[smokingIndex]}}</text>
</view>
</picker>
<picker mode="selector" range="{{morePrefOptions.drinking}}" value="{{drinkingIndex}}" bindchange="onDrinkingPrefChange">
<view class="ppref-field">
<text class="ppref-field__label">是否饮酒</text>
<text class="ppref-field__value">{{morePrefOptions.drinking[drinkingIndex]}}</text>
</view>
</picker>
<picker mode="selector" range="{{morePrefOptions.settlement}}" value="{{settlementIndex}}" bindchange="onSettlementChange">
<view class="ppref-field">
<text class="ppref-field__label">定居意愿</text>
<text class="ppref-field__value">{{morePrefOptions.settlement[settlementIndex]}}</text>
</view>
</picker>
<picker mode="selector" range="{{morePrefOptions.marriagePlan}}" value="{{marriagePlanIndex}}" bindchange="onMarriagePlanPrefChange">
<view class="ppref-field">
<text class="ppref-field__label">结婚时间计划</text>
<text class="ppref-field__value">{{morePrefOptions.marriagePlan[marriagePlanIndex]}}</text>
</view>
</picker>
</view>
</view>
</view>
<view class="ppref-actions">
<ui-button variant="primary" size="lg" block loading="{{saving}}" bind:tap="onNext">下一步</ui-button>
</view>
</view>
@@ -0,0 +1,91 @@
/**
* 择偶条件页 U12-U14
*/
.ppref-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
.ppref-body {
padding: var(--spacing-2) var(--spacing-page-x);
}
.ppref-section {
margin-bottom: var(--spacing-3);
}
/* ============ 标签 ============ */
.ppref-tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-2);
margin-top: var(--spacing-3);
}
.ppref-tag {
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-lg);
background: var(--color-bg-disabled);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.ppref-tag--selected {
background: var(--color-primary);
color: var(--color-text-inverse);
font-weight: var(--font-weight-medium);
}
/* ============ picker ============ */
.ppref-picker {
padding: var(--spacing-3) 0;
margin-top: var(--spacing-2);
}
.ppref-picker__value {
font-size: var(--font-size-md);
color: var(--color-text-primary);
}
/* ============ 表单 ============ */
.ppref-form {
margin-top: var(--spacing-2);
}
.ppref-field {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-3) 0;
border-bottom: var(--border-width) solid var(--color-divider);
min-height: 88rpx;
}
.ppref-field:last-child {
border-bottom: none;
}
.ppref-field__label {
font-size: var(--font-size-md);
color: var(--color-text-primary);
}
.ppref-field__value {
font-size: var(--font-size-md);
color: var(--color-text-tertiary);
}
/* ============ 底部 ============ */
.ppref-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
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);
}