chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 基础资料页 U06(Phase 04 P04-T21)
|
||||
*
|
||||
* 重构要点(遵循 AGENTS.md 不变量):
|
||||
* - 数据:profileService.getProfile + userStore.updateProfile,不再直连 api(第 9 条)
|
||||
* - 性别/出生日期:由身份证自动生成,只读展示(第 8 条:本地不长期保存身份证/完整生日)
|
||||
* - 草稿恢复:onLoad 读 ONBOARDING_DRAFT.basic,保存前写回(验收条件"Draft退出后可恢复")
|
||||
* - 数量限制:本页无数值型限制字段,选项列表遵循 §4.1 配置
|
||||
* - 下一步:跳 profile-lifestyle
|
||||
*/
|
||||
const { userStore, appStore } = require('../../stores');
|
||||
const { profileService } = require('../../services');
|
||||
const { ROUTE, STORAGE_KEYS } = require('../../constants');
|
||||
|
||||
const MARITAL_OPTIONS = [
|
||||
{ value: 'never_married', label: '未婚' },
|
||||
{ value: 'divorced', label: '离异' },
|
||||
{ value: 'widowed', label: '丧偶' },
|
||||
];
|
||||
|
||||
const CHILDREN_OPTIONS = [
|
||||
{ value: 'none', label: '无子女' },
|
||||
{ value: 'has', label: '有子女' },
|
||||
];
|
||||
|
||||
const EDUCATION_OPTIONS = [
|
||||
{ value: 'high_school', label: '高中/中专' },
|
||||
{ value: 'college', label: '大专' },
|
||||
{ value: 'bachelor', label: '本科' },
|
||||
{ value: 'master', label: '硕士' },
|
||||
{ value: 'phd', label: '博士' },
|
||||
];
|
||||
|
||||
const OCCUPATION_OPTIONS = [
|
||||
{ value: 'government', label: '机关事业单位' },
|
||||
{ value: 'education', label: '教育' },
|
||||
{ value: 'medical', label: '医疗' },
|
||||
{ value: 'enterprise', label: '企业职员' },
|
||||
{ value: 'technical', label: '技术人员' },
|
||||
{ value: 'self_employed', label: '个体经营' },
|
||||
{ value: 'freelance', label: '自由职业' },
|
||||
{ value: 'service', label: '服务行业' },
|
||||
{ value: 'agriculture', label: '农业' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
const REGION_OPTIONS = ['丹阳', '镇江市区', '镇江其他辖区', '南京'];
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 20,
|
||||
navBarHeight: 44,
|
||||
loading: false,
|
||||
saving: false,
|
||||
// 身份证自动生成(只读)
|
||||
gender: '',
|
||||
genderLabel: '',
|
||||
ageText: '',
|
||||
// 用户填写
|
||||
regionName: '',
|
||||
heightCm: '',
|
||||
maritalStatus: '',
|
||||
hasChildren: '',
|
||||
educationCode: '',
|
||||
occupationCode: '',
|
||||
// 选项
|
||||
maritalOptions: MARITAL_OPTIONS,
|
||||
childrenOptions: CHILDREN_OPTIONS,
|
||||
educationOptions: EDUCATION_OPTIONS,
|
||||
occupationOptions: OCCUPATION_OPTIONS,
|
||||
regionOptions: REGION_OPTIONS,
|
||||
regionIndex: -1,
|
||||
maritalIndex: -1,
|
||||
childrenIndex: -1,
|
||||
educationIndex: -1,
|
||||
occupationIndex: -1,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this._initSystemInfo();
|
||||
this._applyDraft();
|
||||
this.loadProfile();
|
||||
},
|
||||
|
||||
_initSystemInfo() {
|
||||
try {
|
||||
const sysInfo = wx.getSystemInfoSync();
|
||||
this.setData({
|
||||
statusBarHeight: sysInfo.statusBarHeight || 20,
|
||||
navBarHeight: 44,
|
||||
});
|
||||
} catch (e) { /* 使用默认值 */ }
|
||||
},
|
||||
|
||||
/** 从服务端拉取已有资料(合并草稿) */
|
||||
async loadProfile() {
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const profile = await profileService.getProfile();
|
||||
this._mergeProfile(profile);
|
||||
} catch (err) {
|
||||
// 首次填写时无资料,使用草稿默认值
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 合并服务端资料到表单(草稿优先,草稿未填则用服务端值) */
|
||||
_mergeProfile(profile) {
|
||||
if (!profile) return;
|
||||
const draft = this._readDraft();
|
||||
const regionName = draft.regionName || profile.regionName || '';
|
||||
const maritalStatus = draft.maritalStatus || profile.maritalStatus || '';
|
||||
const hasChildren = draft.hasChildren || (profile.hasChildren !== undefined ? (profile.hasChildren ? 'has' : 'none') : '');
|
||||
const educationCode = draft.educationCode || profile.educationCode || '';
|
||||
const occupationCode = draft.occupationCode || profile.occupationCode || '';
|
||||
const heightCm = draft.heightCm || profile.heightCm || '';
|
||||
|
||||
this.setData({
|
||||
gender: profile.gender || draft.gender || '',
|
||||
genderLabel: profile.gender === 'male' ? '男' : (profile.gender === 'female' ? '女' : ''),
|
||||
ageText: profile.age ? `${profile.age}岁` : '',
|
||||
regionName,
|
||||
heightCm,
|
||||
maritalStatus,
|
||||
hasChildren,
|
||||
educationCode,
|
||||
occupationCode,
|
||||
regionIndex: REGION_OPTIONS.indexOf(regionName),
|
||||
maritalIndex: MARITAL_OPTIONS.findIndex((o) => o.value === maritalStatus),
|
||||
childrenIndex: CHILDREN_OPTIONS.findIndex((o) => o.value === hasChildren),
|
||||
educationIndex: EDUCATION_OPTIONS.findIndex((o) => o.value === educationCode),
|
||||
occupationIndex: OCCUPATION_OPTIONS.findIndex((o) => o.value === occupationCode),
|
||||
});
|
||||
},
|
||||
|
||||
// ============ 草稿 ============
|
||||
_readDraft() {
|
||||
try {
|
||||
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
|
||||
return draft.basic || {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
/** 把草稿值填入表单(loadProfile 失败时也能恢复) */
|
||||
_applyDraft() {
|
||||
const d = this._readDraft();
|
||||
if (!d || Object.keys(d).length === 0) return;
|
||||
this.setData({
|
||||
regionName: d.regionName || '',
|
||||
heightCm: d.heightCm || '',
|
||||
maritalStatus: d.maritalStatus || '',
|
||||
hasChildren: d.hasChildren || '',
|
||||
educationCode: d.educationCode || '',
|
||||
occupationCode: d.occupationCode || '',
|
||||
regionIndex: REGION_OPTIONS.indexOf(d.regionName || ''),
|
||||
maritalIndex: MARITAL_OPTIONS.findIndex((o) => o.value === (d.maritalStatus || '')),
|
||||
childrenIndex: CHILDREN_OPTIONS.findIndex((o) => o.value === (d.hasChildren || '')),
|
||||
educationIndex: EDUCATION_OPTIONS.findIndex((o) => o.value === (d.educationCode || '')),
|
||||
occupationIndex: OCCUPATION_OPTIONS.findIndex((o) => o.value === (d.occupationCode || '')),
|
||||
});
|
||||
},
|
||||
|
||||
_writeDraft() {
|
||||
try {
|
||||
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
|
||||
draft.basic = {
|
||||
regionName: this.data.regionName,
|
||||
heightCm: this.data.heightCm,
|
||||
maritalStatus: this.data.maritalStatus,
|
||||
hasChildren: this.data.hasChildren,
|
||||
educationCode: this.data.educationCode,
|
||||
occupationCode: this.data.occupationCode,
|
||||
};
|
||||
draft.currentStep = 'basic';
|
||||
wx.setStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT, draft);
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
|
||||
// ============ 表单交互 ============
|
||||
onRegionChange(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.setData({ regionIndex: idx, regionName: REGION_OPTIONS[idx] });
|
||||
},
|
||||
|
||||
onHeightInput(e) {
|
||||
this.setData({ heightCm: e.detail.value });
|
||||
},
|
||||
|
||||
onMaritalChange(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.setData({ maritalIndex: idx, maritalStatus: MARITAL_OPTIONS[idx].value });
|
||||
},
|
||||
|
||||
onChildrenChange(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.setData({ childrenIndex: idx, hasChildren: CHILDREN_OPTIONS[idx].value });
|
||||
},
|
||||
|
||||
onEducationChange(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.setData({ educationIndex: idx, educationCode: EDUCATION_OPTIONS[idx].value });
|
||||
},
|
||||
|
||||
onOccupationChange(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
this.setData({ occupationIndex: idx, occupationCode: OCCUPATION_OPTIONS[idx].value });
|
||||
},
|
||||
|
||||
// ============ 导航 ============
|
||||
onBack() {
|
||||
this._writeDraft();
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
/** 下一步:校验必填 → 保存草稿 → 提交服务端 → 跳 profile-lifestyle */
|
||||
async onNext() {
|
||||
if (this.data.saving) return;
|
||||
const { regionName, heightCm, maritalStatus, hasChildren, educationCode, occupationCode } = this.data;
|
||||
if (!regionName) return appStore.toast('请选择居住区域');
|
||||
if (!heightCm) return appStore.toast('请填写身高');
|
||||
if (!maritalStatus) return appStore.toast('请选择婚姻状况');
|
||||
if (!hasChildren) return appStore.toast('请选择子女情况');
|
||||
if (!educationCode) return appStore.toast('请选择学历');
|
||||
if (!occupationCode) return appStore.toast('请选择职业类型');
|
||||
|
||||
this.setData({ saving: true });
|
||||
this._writeDraft();
|
||||
try {
|
||||
await userStore.updateProfile({
|
||||
regionName,
|
||||
heightCm: Number(heightCm),
|
||||
maritalStatus,
|
||||
hasChildren: hasChildren === 'has',
|
||||
educationCode,
|
||||
occupationCode,
|
||||
});
|
||||
this.setData({ saving: false });
|
||||
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_LIFESTYLE });
|
||||
} catch (err) {
|
||||
this.setData({ saving: false });
|
||||
appStore.toastError(err, '保存失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "基础资料",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"ui-nav-bar": "/components/nav-bar/index",
|
||||
"ui-section-header": "/components/section-header/index",
|
||||
"ui-input": "/components/input/index",
|
||||
"ui-button": "/components/button/index",
|
||||
"ui-status-badge": "/components/status-badge/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<!-- 基础资料页 U06 -->
|
||||
<view class="pb-page">
|
||||
<ui-nav-bar
|
||||
title="基础资料"
|
||||
status-bar-height="{{statusBarHeight}}"
|
||||
nav-bar-height="{{navBarHeight}}"
|
||||
bind:back="onBack"
|
||||
/>
|
||||
|
||||
<view class="pb-body">
|
||||
<!-- 身份证自动生成(只读) -->
|
||||
<view class="pb-section card-base">
|
||||
<ui-section-header title="基本信息" subtitle="由实名认证自动生成" />
|
||||
<view class="pb-readonly">
|
||||
<view class="pb-row">
|
||||
<text class="pb-row__label">性别</text>
|
||||
<text class="pb-row__value">{{genderLabel || '未认证'}}</text>
|
||||
</view>
|
||||
<view class="pb-row">
|
||||
<text class="pb-row__label">年龄</text>
|
||||
<text class="pb-row__value">{{ageText || '未认证'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户填写 -->
|
||||
<view class="pb-section card-base">
|
||||
<ui-section-header title="个人资料" subtitle="请如实填写" />
|
||||
<view class="pb-form">
|
||||
<!-- 居住区域 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{regionOptions}}"
|
||||
value="{{regionIndex}}"
|
||||
bindchange="onRegionChange"
|
||||
>
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">居住区域</text>
|
||||
<view class="pb-field__value {{regionIndex >= 0 ? '' : 'pb-field__value--placeholder'}}">
|
||||
{{regionIndex >= 0 ? regionOptions[regionIndex] : '请选择'}}
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 身高 -->
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">身高(cm)</text>
|
||||
<input
|
||||
class="pb-field__input"
|
||||
type="number"
|
||||
value="{{heightCm}}"
|
||||
placeholder="请输入"
|
||||
bindinput="onHeightInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 婚姻状况 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{maritalOptions}}"
|
||||
range-key="label"
|
||||
value="{{maritalIndex < 0 ? 0 : maritalIndex}}"
|
||||
bindchange="onMaritalChange"
|
||||
>
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">婚姻状况</text>
|
||||
<view class="pb-field__value {{maritalIndex >= 0 ? '' : 'pb-field__value--placeholder'}}">
|
||||
{{maritalIndex >= 0 ? maritalOptions[maritalIndex].label : '请选择'}}
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 子女情况 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{childrenOptions}}"
|
||||
range-key="label"
|
||||
value="{{childrenIndex < 0 ? 0 : childrenIndex}}"
|
||||
bindchange="onChildrenChange"
|
||||
>
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">子女情况</text>
|
||||
<view class="pb-field__value {{childrenIndex >= 0 ? '' : 'pb-field__value--placeholder'}}">
|
||||
{{childrenIndex >= 0 ? childrenOptions[childrenIndex].label : '请选择'}}
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 学历 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{educationOptions}}"
|
||||
range-key="label"
|
||||
value="{{educationIndex < 0 ? 0 : educationIndex}}"
|
||||
bindchange="onEducationChange"
|
||||
>
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">学历</text>
|
||||
<view class="pb-field__value {{educationIndex >= 0 ? '' : 'pb-field__value--placeholder'}}">
|
||||
{{educationIndex >= 0 ? educationOptions[educationIndex].label : '请选择'}}
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 职业类型 -->
|
||||
<picker
|
||||
mode="selector"
|
||||
range="{{occupationOptions}}"
|
||||
range-key="label"
|
||||
value="{{occupationIndex < 0 ? 0 : occupationIndex}}"
|
||||
bindchange="onOccupationChange"
|
||||
>
|
||||
<view class="pb-field">
|
||||
<text class="pb-field__label">职业类型</text>
|
||||
<view class="pb-field__value {{occupationIndex >= 0 ? '' : 'pb-field__value--placeholder'}}">
|
||||
{{occupationIndex >= 0 ? occupationOptions[occupationIndex].label : '请选择'}}
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<view class="pb-actions">
|
||||
<ui-button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
block
|
||||
loading="{{saving}}"
|
||||
bind:tap="onNext"
|
||||
>下一步</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 基础资料页 U06
|
||||
*/
|
||||
|
||||
.pb-page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.pb-body {
|
||||
padding: var(--spacing-2) var(--spacing-page-x);
|
||||
}
|
||||
|
||||
.pb-section {
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
/* ============ 只读区 ============ */
|
||||
.pb-readonly {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.pb-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) 0;
|
||||
border-bottom: var(--border-width) solid var(--color-divider);
|
||||
}
|
||||
|
||||
.pb-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pb-row__label {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.pb-row__value {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* ============ 表单 ============ */
|
||||
.pb-form {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
|
||||
.pb-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;
|
||||
}
|
||||
|
||||
.pb-field:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pb-field__label {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
flex-shrink: 0;
|
||||
width: 180rpx;
|
||||
}
|
||||
|
||||
.pb-field__value {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.pb-field__value--placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.pb-field__input {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* ============ 底部 ============ */
|
||||
.pb-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);
|
||||
}
|
||||
Reference in New Issue
Block a user