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,192 @@
/**
* 兴趣与生活方式页 U09(Phase 04 P04-T24)
*
* 重构要点(遵循 AGENTS.md 不变量):
* - 数据:profileService.getProfile + userStore.updateProfile(第 9 条)
* - 数量限制:BUSINESS_LIMITS.MAX_INTEREST_TAGS(第 12 条,不硬编码)
* - 草稿恢复:onLoad 读 ONBOARDING_DRAFT.interests,保存前写回
* - 下一步:跳 profile-intent
*/
const { userStore, appStore } = require('../../stores');
const { profileService } = require('../../services');
const { ROUTE, STORAGE_KEYS, BUSINESS_LIMITS } = require('../../constants');
const INTEREST_OPTIONS = [
{ code: 'travel', name: '旅行' },
{ code: 'reading', name: '阅读' },
{ code: 'sports', name: '运动' },
{ code: 'fitness', name: '健身' },
{ code: 'photography', name: '摄影' },
{ code: 'movie', name: '电影' },
{ code: 'music', name: '音乐' },
{ code: 'food', name: '美食' },
{ code: 'cooking', name: '烹饪' },
{ code: 'outdoor', name: '户外' },
{ code: 'pets', name: '宠物' },
{ code: 'games', name: '游戏' },
{ code: 'culture', name: '传统文化' },
{ code: 'charity', name: '公益活动' },
{ code: 'other', name: '其他' },
];
const LIFESTYLE_OPTIONS = [
{ code: 'quiet', name: '喜欢安静' },
{ code: 'social', name: '喜欢社交' },
{ code: 'outdoor', name: '喜欢户外' },
{ code: 'homebody', name: '居家型' },
{ code: 'regular_work', name: '工作规律' },
{ code: 'frequent_travel', name: '经常出差' },
{ code: 'family_oriented', name: '注重家庭' },
{ code: 'other', name: '其他' },
];
Page({
data: {
statusBarHeight: 20,
navBarHeight: 44,
loading: false,
saving: false,
interests: INTEREST_OPTIONS.map((o) => ({ ...o, selected: false })),
lifestyles: LIFESTYLE_OPTIONS.map((o) => ({ ...o, selected: false })),
maxTags: BUSINESS_LIMITS.MAX_INTEREST_TAGS,
selectedInterestsCount: 0,
},
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 d = this._readDraft();
const savedInterests = d.interests || profile.interests || [];
const savedLifestyles = d.lifestyles || profile.lifestyles || [];
const interests = this.data.interests.map((o) => ({
...o,
selected: savedInterests.includes(o.code),
}));
const lifestyles = this.data.lifestyles.map((o) => ({
...o,
selected: savedLifestyles.includes(o.code),
}));
this.setData({
interests,
lifestyles,
selectedInterestsCount: interests.filter((o) => o.selected).length,
});
},
// ============ 草稿 ============
_readDraft() {
try {
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
return draft.interests || {};
} catch (e) {
return {};
}
},
_applyDraft() {
const d = this._readDraft();
if (!d || Object.keys(d).length === 0) return;
const savedInterests = d.interests || [];
const savedLifestyles = d.lifestyles || [];
const interests = this.data.interests.map((o) => ({
...o,
selected: savedInterests.includes(o.code),
}));
const lifestyles = this.data.lifestyles.map((o) => ({
...o,
selected: savedLifestyles.includes(o.code),
}));
this.setData({
interests,
lifestyles,
selectedInterestsCount: interests.filter((o) => o.selected).length,
});
},
_writeDraft() {
try {
const draft = wx.getStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT) || {};
draft.interests = {
interests: this.data.interests.filter((o) => o.selected).map((o) => o.code),
lifestyles: this.data.lifestyles.filter((o) => o.selected).map((o) => o.code),
};
draft.currentStep = 'interests';
wx.setStorageSync(STORAGE_KEYS.ONBOARDING_DRAFT, draft);
} catch (e) { /* ignore */ }
},
// ============ 交互 ============
onInterestTap(e) {
const code = e.currentTarget.dataset.code;
const interests = this.data.interests.map((o) => {
if (o.code !== code) return o;
// 取消选中直接切换;选中需检查上限
if (!o.selected && this.data.selectedInterestsCount >= this.data.maxTags) {
appStore.toast(`最多选择 ${this.data.maxTags} 个兴趣`);
return o;
}
return { ...o, selected: !o.selected };
});
this.setData({
interests,
selectedInterestsCount: interests.filter((o) => o.selected).length,
});
},
onLifestyleTap(e) {
const code = e.currentTarget.dataset.code;
const lifestyles = this.data.lifestyles.map((o) => {
if (o.code !== code) return o;
return { ...o, selected: !o.selected };
});
this.setData({ lifestyles });
},
// ============ 导航 ============
onBack() {
this._writeDraft();
wx.navigateBack();
},
async onNext() {
if (this.data.saving) return;
this.setData({ saving: true });
this._writeDraft();
try {
await userStore.updateProfile({
interests: this.data.interests.filter((o) => o.selected).map((o) => o.code),
lifestyles: this.data.lifestyles.filter((o) => o.selected).map((o) => o.code),
});
this.setData({ saving: false });
wx.navigateTo({ url: ROUTE.ONBOARDING.PROFILE_INTENT });
} catch (err) {
this.setData({ saving: false });
appStore.toastError(err, '保存失败');
}
},
});
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "兴趣与生活方式",
"navigationStyle": "custom",
"usingComponents": {
"ui-nav-bar": "/components/nav-bar/index",
"ui-section-header": "/components/section-header/index",
"ui-button": "/components/button/index"
}
}
@@ -0,0 +1,43 @@
<!-- 兴趣与生活方式页 U09 -->
<view class="pi-page">
<ui-nav-bar
title="兴趣与生活方式"
status-bar-height="{{statusBarHeight}}"
nav-bar-height="{{navBarHeight}}"
bind:back="onBack"
/>
<view class="pi-body">
<!-- 兴趣标签 -->
<view class="pi-section card-base">
<ui-section-header title="兴趣" subtitle="最多选择 {{maxTags}} 项({{selectedInterestsCount}}/{{maxTags}})" />
<view class="pi-tags">
<view
wx:for="{{interests}}"
wx:key="code"
class="pi-tag {{item.selected ? 'pi-tag--selected' : ''}}"
data-code="{{item.code}}"
bindtap="onInterestTap"
>{{item.name}}</view>
</view>
</view>
<!-- 生活方式 -->
<view class="pi-section card-base">
<ui-section-header title="生活方式" subtitle="可选,多选" />
<view class="pi-tags">
<view
wx:for="{{lifestyles}}"
wx:key="code"
class="pi-tag {{item.selected ? 'pi-tag--selected' : ''}}"
data-code="{{item.code}}"
bindtap="onLifestyleTap"
>{{item.name}}</view>
</view>
</view>
</view>
<view class="pi-actions">
<ui-button variant="primary" size="lg" block loading="{{saving}}" bind:tap="onNext">下一步</ui-button>
</view>
</view>
@@ -0,0 +1,53 @@
/**
* 兴趣与生活方式页 U09
*/
.pi-page {
min-height: 100vh;
background: var(--color-bg-page);
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
}
.pi-body {
padding: var(--spacing-2) var(--spacing-page-x);
}
.pi-section {
margin-bottom: var(--spacing-3);
}
/* ============ 标签 ============ */
.pi-tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-2);
margin-top: var(--spacing-3);
}
.pi-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);
transition: all var(--button-press-feedback) ease;
}
.pi-tag--selected {
background: var(--color-primary);
color: var(--color-text-inverse);
font-weight: var(--font-weight-medium);
}
/* ============ 底部 ============ */
.pi-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);
}