feat(taobao): add native mini program with shared backend

Preserve H5 flows while adding Taobao identity, native capture, reports and isolated local development. Fix canceled province selection and expired-session logout. Record passing local regressions, two pre-existing H5 assertion failures and pending platform validation.
This commit is contained in:
xsl
2026-10-19 20:14:31 +08:00
parent d07f8cc1bb
commit 9eec4369da
61 changed files with 4827 additions and 17 deletions
+141
View File
@@ -0,0 +1,141 @@
/* ═════ 档案填写页样式 ═════ */
.profile {
position: relative;
min-height: 100vh;
}
.profile-bg {
position: fixed;
left: 0; top: 0;
width: 100%;
height: 100%;
/* 上半部分露出背景图,表单区由白色卡片承载 */
}
.profile-body {
position: relative;
padding: 48rpx 32rpx 64rpx;
box-sizing: border-box;
}
.page-title {
color: #FFFFFF;
font-size: 44rpx;
font-weight: 600;
text-align: center;
margin-bottom: 48rpx;
}
.form-row {
display: flex;
justify-content: space-between;
}
.field { margin-bottom: 32rpx; }
.field-half { width: 48%; }
.field-label {
color: #FFFFFF;
font-size: 28rpx;
margin-bottom: 16rpx;
text-align: center;
}
.pick {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
background: #FFFFFF;
border-radius: 16rpx;
padding: 0 24rpx;
box-sizing: border-box;
}
.pick-value {
color: #282828;
font-size: 28rpx;
}
.chev {
color: #868686;
font-size: 28rpx;
}
/* ─── 肤质卡 ─── */
.skin-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx 24rpx;
box-sizing: border-box;
}
.skin-axis {
display: flex;
justify-content: center;
margin-bottom: 24rpx;
}
.axis-item {
display: flex;
align-items: center;
font-size: 24rpx;
color: #868686;
margin: 0 16rpx;
}
.chip {
display: inline-block;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.chip-dry { background: #EAE6E1; }
.chip-oily { background: #F5F078; }
.chip-balance { background: #DBF6D6; }
.skin-stage {
display: flex;
align-items: center;
}
.skin-arrow {
width: 64rpx;
font-size: 56rpx;
color: #868686;
text-align: center;
flex-shrink: 0;
}
.skin-swiper {
flex: 1;
height: 460rpx;
}
.skin-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 16rpx;
box-sizing: border-box;
}
.skin-img {
width: 280rpx;
height: 280rpx;
}
.skin-name {
font-size: 32rpx;
font-weight: 600;
margin-top: 16rpx;
}
.skin-desc {
font-size: 24rpx;
color: #868686;
text-align: center;
margin-top: 8rpx;
}
.skin-dots {
display: flex;
justify-content: center;
margin-top: 16rpx;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #E5E5E5;
margin: 0 6rpx;
}
.dot.on { background: #282828; }
.btn-submit { margin-top: 24rpx; }
.picker-sheet { background: white; width: 100%; align-self: flex-end; border-radius: 24rpx 24rpx 0 0; }
.picker-actions { display: flex; justify-content: space-between; padding: 32rpx; }
.native-picker { width: 100%; height: 440rpx; text-align: center; line-height: 80rpx; }
+91
View File
@@ -0,0 +1,91 @@
<!-- ═════ 03 档案填写 ═════ -->
<view class="profile">
<image class="profile-bg" src="{{bgUrl}}" mode="aspectFill" />
<view class="profile-body">
<view class="page-title">您的肌肤状态</view>
<!-- 出生年月 + 性别(原生 picker 底部滚轮) -->
<view class="form-row">
<view class="field field-half">
<view class="field-label">您的出生年月</view>
<view onTap="onOpenBirth">
<view class="pick">
<text class="pick-value">{{birthText}}</text>
<text class="chev">⌄</text>
</view>
</view>
</view>
<view class="field field-half">
<view class="field-label">您的性别</view>
<picker mode="selector" range="{{genderRange}}" onChange="onGenderChange">
<view class="pick">
<text class="pick-value">{{genderText}}</text>
<text class="chev">⌄</text>
</view>
</picker>
</view>
</view>
<!-- 所在地区(省+市联动) -->
<view class="field">
<view class="field-label">您的所在地区</view>
<view onTap="onOpenRegion">
<view class="pick">
<text class="pick-value">{{regionText}}</text>
<text class="chev">⌄</text>
</view>
</view>
</view>
<!-- 日常肤质(轮播卡) -->
<view class="field">
<view class="field-label">您的日常肤质</view>
<view class="skin-card">
<view class="skin-axis">
<text class="axis-item"><text class="chip chip-dry"></text>偏干</text>
<text class="axis-item"><text class="chip chip-oily"></text>偏油</text>
<text class="axis-item"><text class="chip chip-balance"></text>水油平衡</text>
</view>
<view class="skin-stage">
<view class="skin-arrow" onTap="onSkinPrev">‹</view>
<swiper class="skin-swiper" current="{{skinIndex}}" onChange="onSkinChange" circular="{{false}}">
<swiper-item a:for="{{skins}}" a:key="name">
<view class="skin-item">
<image src="{{item.img}}" class="skin-img" mode="aspectFit" lazy-load />
<view class="skin-name">{{item.name}}</view>
<view class="skin-desc">{{item.desc}}</view>
</view>
</swiper-item>
</swiper>
<view class="skin-arrow" onTap="onSkinNext">›</view>
</view>
<view class="skin-dots">
<text a:for="{{skins}}" a:key="name" class="dot {{index === skinIndex ? 'on' : ''}}"></text>
</view>
</view>
</view>
<button class="btn-primary btn-submit" onTap="onSubmit">提交</button>
</view>
<!-- ═══ 弹窗:拍照引导 ═══ -->
<view class="overlay" a:if="{{showGuide}}" onTap="onCloseGuide">
<view class="popup" catchTap="noop">
<button class="popup-close" onTap="onCloseGuide">✕</button>
<view class="popup-title">
<text>请拍摄或上传</text>
<text>正面及左右侧面照片</text>
</view>
<button class="btn-primary popup-btn" onTap="onStartCamera">开始</button>
</view>
</view>
<view class="overlay" a:if="{{picker}}" onTap="onPickerClose">
<view class="picker-sheet" catchTap="noop">
<view class="picker-actions"><text onTap="onPickerClose">取消</text><text onTap="onPickerConfirm">确定</text></view>
<picker-view value="{{pickerValue}}" onChange="onPickerChange" class="native-picker">
<picker-view-column><view a:for="{{picker === 'birth' ? birthRange[0] : regionRange[0]}}" a:key="*this">{{item}}</view></picker-view-column>
<picker-view-column><view a:for="{{picker === 'birth' ? birthRange[1] : regionRange[1]}}" a:key="*this">{{item}}</view></picker-view-column>
</picker-view>
</view>
</view>
</view>
+176
View File
@@ -0,0 +1,176 @@
/* ═════ 档案填写页:出生年月/性别/地区(原生 picker)+ 日常肤质选择(对齐 H5 #s-profile) ═════
* 档案字段与 H5 一致:{ real_age, gender, region, daily_skin_type } */
var api = require('../../utils/api.js');
var REGION_DATA = require('../../utils/region-data.js');
// 肤质选项(与 H5 SKINS 一致,图片远程加载)
var SKINS = [
{ img: 'img/ui-20261009/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、缺乏光泽' },
{ img: 'img/ui-20261009/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' },
{ img: 'img/ui-20261009/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' },
{ img: 'img/ui-20261009/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' },
{ img: 'img/ui-20261009/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' }
];
var NOW = new Date(), NOW_Y = NOW.getFullYear(), NOW_M = NOW.getMonth() + 1;
var YEARS = [], MONTHS = [];
for (var y = NOW_Y - 99; y <= NOW_Y - 1; y++) YEARS.push(y + '年');
for (var m = 1; m <= 12; m++) MONTHS.push(m + '月');
function ageFromBirth(y, m) { return NOW_Y - y - (NOW_M < m ? 1 : 0); } // 未过生日月份按少一岁(与 H5 一致)
Page({
data: {
bgUrl: api.assetUrl('img/bg-splash.jpg'),
// 出生年月(两列滚轮:年+月,默认 30 岁 1 月)
birthRange: [YEARS, MONTHS],
birthIndex: [YEARS.indexOf((NOW_Y - 30) + '年'), 0],
birthText: '',
birth: '', // 'YYYY-MM'
age: 0, // real_age
// 性别
genderRange: ['女', '男'],
genderText: '',
// 所在地区(省 + 市两列)
regionRange: [
REGION_DATA.map(function (p) { return p[0]; }),
REGION_DATA[0][1]
],
regionIndex: [0, 0],
regionText: '',
// 肤质轮播
skins: SKINS.map(function (s) {
return { img: api.assetUrl(s.img), name: s.name, desc: s.desc };
}),
skinIndex: 0,
showGuide: false, picker: '', pickerValue: [0, 0]
},
onLoad: function () {
if (!api.user()) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
this.ownerId = api.user().id;
try {
var saved = my.getStorageSync({ key: 'juzi.taobao.profile.' + api.user().id }).data;
if (saved) {
saved.age = ageFromBirth(Number(saved.birth.split('-')[0]), Number(saved.birth.split('-')[1]));
this.setData(saved);
}
} catch (_) {}
},
onShow: function () {
if (!api.user() || this.ownerId !== api.user().id) {
this.setData({ birthText: '', genderText: '', regionText: '', showGuide: false });
my.redirectTo({ url: '/pages/splash/splash' });
}
},
onOpenBirth: function () { this.setData({ picker: 'birth', pickerValue: this.data.birthIndex.slice() }); },
onOpenRegion: function () {
var index = this.data.regionIndex.slice();
// A canceled draft may have changed the cities while the saved province stayed put.
this.setData({ picker: 'region', pickerValue: index,
regionRange: [REGION_DATA.map(function (p) { return p[0]; }), REGION_DATA[index[0]][1]] });
},
onPickerChange: function (e) {
var value = e.detail.value;
if (this.data.picker === 'region' && value[0] !== this.data.pickerValue[0]) value[1] = 0;
var patch = { pickerValue: value };
if (this.data.picker === 'region') patch.regionRange = [REGION_DATA.map(function (p) { return p[0]; }), REGION_DATA[value[0]][1]];
this.setData(patch);
},
onPickerConfirm: function () {
var e = { detail: { value: this.data.pickerValue } };
if (this.data.picker === 'birth') this.onBirthChange(e); else this.onRegionChange(e);
this.onPickerClose();
},
onPickerClose: function () { this.setData({ picker: '' }); },
/* ─── 出生年月 ─── */
onBirthChange: function (e) {
var idx = e.detail.value;
var y = parseInt(YEARS[idx[0]], 10);
var m = idx[1] + 1;
this.setData({
birthIndex: idx,
birthText: y + '年' + m + '月',
birth: y + '-' + (m < 10 ? '0' : '') + m,
age: ageFromBirth(y, m)
});
},
/* ─── 性别 ─── */
onGenderChange: function (e) {
this.setData({ genderText: this.data.genderRange[e.detail.value] });
},
/* ─── 地区:省份列滚动时联动城市列 ─── */
onRegionColumnChange: function (e) {
if (e.detail.column !== 0) return;
var cities = REGION_DATA[e.detail.value][1];
this.setData({ 'regionRange[1]': cities, regionIndex: [e.detail.value, 0] });
},
onRegionChange: function (e) {
var idx = e.detail.value;
var prov = REGION_DATA[idx[0]][0];
var city = REGION_DATA[idx[0]][1][idx[1]];
// 直辖市/港澳台只显示一级(与 H5 一致)
var text = !city || city === prov ? prov : prov + ' ' + city;
this.setData({ regionIndex: idx, regionText: text });
},
/* ─── 肤质轮播 ─── */
onSkinChange: function (e) {
this.setData({ skinIndex: e.detail.current });
},
onSkinPrev: function () {
if (this.data.skinIndex > 0) this.setData({ skinIndex: this.data.skinIndex - 1 });
},
onSkinNext: function () {
if (this.data.skinIndex < SKINS.length - 1) this.setData({ skinIndex: this.data.skinIndex + 1 });
},
/* ─── 提交 → 拍照引导弹窗 ─── */
onSubmit: function () {
var d = this.data;
if (!d.birth || !(d.age >= 1 && d.age <= 99)) {
my.showToast({ content: '请选择出生年月', type: 'none' });
return;
}
if (!d.genderText) {
my.showToast({ content: '请选择性别', type: 'none' });
return;
}
if (!d.regionText) {
my.showToast({ content: '请选择所在地区', type: 'none' });
return;
}
this.setData({ showGuide: true });
},
onCloseGuide: function () {
this.setData({ showGuide: false });
},
// 「开始」:档案入全局态,进入拍照页
onStartCamera: function () {
if (!api.user()) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
var d = this.data;
getApp().globalData.profile = {
real_age: d.age,
gender: d.genderText,
region: d.regionText,
daily_skin_type: SKINS[d.skinIndex].name
};
my.setStorageSync({ key: 'juzi.taobao.profile.' + api.user().id, data: {
birth: d.birth, birthIndex: d.birthIndex, birthText: d.birthText, genderText: d.genderText,
regionIndex: d.regionIndex, regionText: d.regionText, regionRange: d.regionRange, skinIndex: d.skinIndex
} });
this.setData({ showGuide: false });
my.navigateTo({ url: '/pages/camera/camera' });
},
noop: function () {}
});
+5
View File
@@ -0,0 +1,5 @@
{
"defaultTitle": "1'me在线肌肤测试",
"titleBarColor": "#101823",
"backgroundColor": "#101823"
}