/** * 择偶条件页 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年', '顺其自然'], }; // ============ 展示形态 ↔ 服务端规范形态 适配 ============ // 页面内部用「展示形态」(ageMin / ageImportance / 中文标签…)便于渲染与草稿恢复; // PUT /me/preferences 用的是规范形态(minAge / agePriority / 英文枚举码…)。 // 此前直接把展示形态发给后端,除个别同名字段外**全部被静默丢弃** // (UAT 走查发现:择偶条件实际没有存进去,推荐评分也就没有条件可用)。 // // 暂未映射(需产品/数据确认,见 docs/UAT-WALKTHROUGH-2026-09-23.md): // - region:regionScope 需 GB/T 2260 区划码,而地区选项含「镇江其他辖区」这类 // 非单值项 → 不臆造区划码; // - occupationPreference:后端 user_preferences 无对应列; // - settlementPreference:后端无既定的取值字典。 const MARITAL_LABEL_TO_CODE = { 未婚: 'never_married', 离异: 'divorced', 丧偶: 'widowed' }; const MARITAL_CODE_TO_LABEL = { never_married: '未婚', divorced: '离异', widowed: '丧偶' }; const EDUCATION_LABEL_TO_CODE = { 不限: null, '高中/中专以上': 'senior_high', '大专以上': 'college', '本科以上': 'bachelor', '研究生以上': 'master', }; const EDUCATION_CODE_TO_LABEL = { junior_high: '高中/中专以上', senior_high: '高中/中专以上', college: '大专以上', bachelor: '本科以上', master: '研究生以上', doctor: '研究生以上', }; const SMOKING_LABEL_TO_CODE = { 不限: null, 不吸烟: 'never', 偶尔: 'sometimes', 经常: 'often' }; const SMOKING_CODE_TO_LABEL = { never: '不吸烟', sometimes: '偶尔', often: '经常' }; const DRINKING_LABEL_TO_CODE = { 不限: null, 不饮酒: 'never', 偶尔: 'sometimes', 经常: 'often' }; const DRINKING_CODE_TO_LABEL = { never: '不饮酒', sometimes: '偶尔', often: '经常' }; const MARRIAGE_PLAN_LABEL_TO_CODE = { 不限: null, 半年内: 'half_year', '1年内': 'one_year', '1~2年': 'one_to_two_years', 顺其自然: 'let_it_be', }; const MARRIAGE_PLAN_CODE_TO_LABEL = { half_year: '半年内', one_year: '1年内', one_to_two_years: '1~2年', two_years_plus: '1~2年', let_it_be: '顺其自然', }; /** 展示形态 → PUT /me/preferences 规范请求体 */ function toApiPayload(p) { const firstEducation = (p.educationAccepted || [])[0]; return { minAge: p.ageMin, maxAge: p.ageMax, agePriority: p.ageImportance, minHeightCm: p.heightMin, maxHeightCm: p.heightMax, heightPriority: p.heightImportance, maritalStatuses: (p.maritalStatusAccepted || []).map((x) => MARITAL_LABEL_TO_CODE[x]).filter(Boolean), maritalPriority: p.maritalImportance, minEducationCode: EDUCATION_LABEL_TO_CODE[firstEducation] || null, educationPriority: p.educationImportance, childrenAcceptance: p.acceptChildren, smokingPreference: SMOKING_LABEL_TO_CODE[p.smokingPreference] || null, drinkingPreference: DRINKING_LABEL_TO_CODE[p.drinkingPreference] || null, marriagePlanPreference: MARRIAGE_PLAN_LABEL_TO_CODE[p.marriagePlanPreference] || null, }; } /** GET /me/preferences 规范响应 → 展示形态(用于回填表单) */ function fromApiPayload(prefs) { if (!prefs) return null; const num = (v) => (v === undefined || v === null ? null : v); return { ageMin: num(prefs.minAge), ageMax: num(prefs.maxAge), ageImportance: prefs.agePriority || 'preferred', heightMin: num(prefs.minHeightCm), heightMax: num(prefs.maxHeightCm), heightImportance: prefs.heightPriority || 'preferred', maritalStatusAccepted: (prefs.maritalStatuses || []).map((c) => MARITAL_CODE_TO_LABEL[c]).filter(Boolean), maritalImportance: prefs.maritalPriority || 'preferred', educationAccepted: prefs.minEducationCode ? [EDUCATION_CODE_TO_LABEL[prefs.minEducationCode]].filter(Boolean) : [], educationImportance: prefs.educationPriority || 'preferred', acceptChildren: prefs.childrenAcceptance || 'any', smokingPreference: SMOKING_CODE_TO_LABEL[prefs.smokingPreference] || '', drinkingPreference: DRINKING_CODE_TO_LABEL[prefs.drinkingPreference] || '', marriagePlanPreference: MARRIAGE_PLAN_CODE_TO_LABEL[prefs.marriagePlanPreference] || '', }; } 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(), ...(fromApiPayload(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(toApiPayload(preference)); this.setData({ saving: false }); wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_CONFIRM }); } catch (err) { this.setData({ saving: false }); appStore.toastError(err, '保存失败'); } }, });