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:
@@ -0,0 +1,245 @@
|
||||
/* ═════ 报告页样式(灰底白卡,对齐设计稿 08) ═════ */
|
||||
page { background: #EFEFEF; }
|
||||
.report { padding-bottom: 64rpx; }
|
||||
.report-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 32rpx 0 8rpx;
|
||||
}
|
||||
|
||||
/* ─── 总分卡 ─── */
|
||||
.card-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.ring-box {
|
||||
position: relative;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
.ring-canvas {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
.ring-center {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sum-age {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.stat-value { display: flex; align-items: baseline; }
|
||||
.stat-num {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
color: #282828;
|
||||
}
|
||||
.stat-unit {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
.age-pill {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #282828;
|
||||
background: #F5F078;
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 20rpx;
|
||||
}
|
||||
|
||||
/* ─── 三维度 ─── */
|
||||
.dims-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.dim {
|
||||
width: 31%;
|
||||
position: relative;
|
||||
}
|
||||
.dim-name {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
}
|
||||
.dim-score { margin: 8rpx 0; }
|
||||
.dim-int {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dim-dec {
|
||||
font-size: 28rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.dim.worst .dim-int, .dim.worst .dim-dec { color: #D30C0C; }
|
||||
.dim-bar {
|
||||
height: 8rpx;
|
||||
background: #EFEFEF;
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dim-bar-fill {
|
||||
height: 100%;
|
||||
background: #E5E5E5;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.dim.best .dim-bar-fill { background: #DBF6D6; }
|
||||
.dim.worst .dim-bar-fill { background: #F5C6C6; }
|
||||
.dim-flag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 20rpx;
|
||||
color: #D30C0C;
|
||||
}
|
||||
.dims-note { margin-top: 24rpx; }
|
||||
.dims-note-l1 {
|
||||
font-size: 26rpx;
|
||||
color: #868686;
|
||||
}
|
||||
.dims-focus {
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dims-note-l2 {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* ─── 6 分类 Tab ─── */
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2rpx solid #E5E5E5;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.dtab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #868686;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
.dtab.active {
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
border-bottom: 4rpx solid #F5F078;
|
||||
}
|
||||
.ditem { margin-bottom: 32rpx; }
|
||||
.ditem-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.ditem-img {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: #EFEFEF;
|
||||
}
|
||||
.ditem-para {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ─── 降级标注 + 免责 ─── */
|
||||
.vm-notes {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
padding: 16rpx 32rpx 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.report-disclaimer {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ─── 护理建议 / 精华 ─── */
|
||||
.care-item {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.care-summary {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.essence-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.essence-triggers {
|
||||
font-size: 24rpx;
|
||||
color: #868686;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ─── 产品推荐 ─── */
|
||||
.prod-title {
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
padding: 40rpx 0 8rpx;
|
||||
}
|
||||
.prod-title text { display: block; line-height: 1.5; }
|
||||
.prod-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.prod-info { flex: 1; padding-right: 24rpx; }
|
||||
.prod-badge { height: 40rpx; margin-bottom: 16rpx; }
|
||||
.prod-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.prod-sub {
|
||||
font-size: 22rpx;
|
||||
color: #868686;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.prod-img {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-more {
|
||||
margin: 40rpx 32rpx 0;
|
||||
}
|
||||
.detail-tabs { display: block; white-space: nowrap; }
|
||||
.detail-tabs-inner { display: flex; width: max-content; }
|
||||
.dtab { flex-shrink: 0; padding: 16rpx 20rpx; }
|
||||
.analysis-image { position: relative; width: 100%; overflow: hidden; border-radius: 16rpx; background: #202020; }
|
||||
.analysis-photo { display: block; width: 100%; }
|
||||
.dark-photo { opacity: .45; }
|
||||
.analysis-layer { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
|
||||
.mark-box { position: absolute; box-sizing: border-box; border: 2rpx solid; border-radius: 4rpx; }
|
||||
.side-views { display: flex; gap: 16rpx; margin: 24rpx 0; }
|
||||
.side-view { flex: 1; min-width: 0; font-size: 24rpx; line-height: 1.8; }
|
||||
.side-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.4); color: white; }
|
||||
.side-note { color: #777; }
|
||||
.detail-grade { color: #666; font-size: 24rpx; }
|
||||
.care-name { font-weight: 600; margin-bottom: 8rpx; }
|
||||
.dim-level { font-size: 22rpx; margin-top: 8rpx; color: #777; }
|
||||
.ring-box { width: 120px; height: 120px; }
|
||||
@@ -0,0 +1,53 @@
|
||||
<view class="report">
|
||||
<view class="report-title">您的肌肤状态表现</view>
|
||||
<view class="card card-summary">
|
||||
<view class="sum-score"><view class="ring-box">
|
||||
<canvas id="score-ring" width="120" height="120" class="ring-canvas" style="width:120px;height:120px;" />
|
||||
<view class="ring-center"><view class="stat-value"><text class="stat-num">{{total != null ? total : '--'}}</text><text class="stat-unit">分</text></view><view class="stat-label">综合评分</view></view>
|
||||
</view></view>
|
||||
<view class="sum-age"><view class="stat-label">肌肤年龄</view><view class="stat-value"><text class="stat-num">{{skinAge != null ? skinAge : '--'}}</text><text class="stat-unit">岁</text></view><view class="age-pill" a:if="{{ageNote}}">{{ageNote}}</view></view>
|
||||
</view>
|
||||
<view class="card card-dims">
|
||||
<view class="dims-row"><view class="dim {{item.rank}}" a:for="{{dims}}" a:key="key">
|
||||
<view class="dim-name">{{item.name}}</view><view class="dim-score"><text class="dim-int">{{item.int}}</text><text class="dim-dec" a:if="{{item.dec}}">.{{item.dec}}</text></view>
|
||||
<view class="dim-bar"><view class="dim-bar-fill" style="width:{{item.barWidth}}%;"></view></view><view class="dim-level">{{item.level}}</view>
|
||||
<view class="dim-flag" a:if="{{item.rank === 'worst'}}">优先关注</view>
|
||||
</view></view>
|
||||
<view class="dims-note" a:if="{{focusName}}"><view>近期优先关注:{{focusName}}</view><view>{{focusSlogan}}</view></view>
|
||||
</view>
|
||||
<view class="card card-detail" a:if="{{views.length}}">
|
||||
<scroll-view class="detail-tabs" scroll-x="{{true}}"><view class="detail-tabs-inner">
|
||||
<view class="dtab {{index === viewIndex ? 'active' : ''}}" a:for="{{views}}" a:key="key" data-tab="{{index}}" onTap="onTabChange">{{item.name}}</view>
|
||||
</view></scroll-view>
|
||||
<block a:for="{{views}}" a:key="key">
|
||||
<view class="ditem" a:if="{{index === viewIndex}}">
|
||||
<view class="ditem-title">{{item.name}} <text class="detail-grade">{{item.score}} · {{item.level}}</text></view>
|
||||
<view class="analysis-image" a:if="{{frontPhoto}}">
|
||||
<image class="analysis-photo {{item.dim ? 'dark-photo' : ''}}" src="{{frontPhoto}}" mode="widthFix" data-index="{{index}}" onLoad="onPhotoLoad" />
|
||||
<image class="analysis-layer" a:for="{{activeLayers}}" a:for-item="layer" a:key="*this" src="{{layer}}" mode="scaleToFill" style="opacity:{{item.alpha}};" />
|
||||
<view class="mark-box" a:for="{{item.marks}}" a:for-item="mark" a:key="id" style="left:{{mark.left}}%;top:{{mark.top}}%;width:{{mark.width}}%;height:{{mark.height}}%;border-color:{{mark.color}};"></view>
|
||||
</view>
|
||||
<view class="side-views" a:if="{{item.key === 'wrinkles'}}">
|
||||
<view class="side-view" a:for="{{sideViews}}" a:for-item="side" a:key="key">
|
||||
<view>{{side.title}}</view><view class="analysis-image" a:if="{{side.photo}}">
|
||||
<image class="analysis-photo dark-photo" src="{{side.photo}}" mode="widthFix" />
|
||||
<image a:if="{{side.layer}}" class="analysis-layer" src="{{side.layer}}" mode="scaleToFill" />
|
||||
<view class="side-loading" a:if="{{side.loading}}">标注生成中…</view>
|
||||
</view><view class="side-note" a:if="{{side.note}}">{{side.note}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ditem-para">{{item.text}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="report-disclaimer">您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。本次检测仅供当前肌肤状态参考。</view>
|
||||
<view class="card card-care"><view class="card-heading">护理建议</view>
|
||||
<view class="care-item" a:for="{{careAdvice}}" a:key="key"><view class="care-name">{{item.name}}</view>{{item.text}}</view>
|
||||
<view class="care-summary">好状态,是内外同调的长期节奏。</view>
|
||||
</view>
|
||||
<view class="card card-product" a:if="{{product}}"><view class="prod-card">
|
||||
<view class="prod-info"><view class="prod-name">{{product.title}}</view><view class="prod-sub">{{product.name}}</view></view>
|
||||
<image class="prod-img" src="{{product.image}}" mode="aspectFit" />
|
||||
</view></view>
|
||||
<button class="btn-primary btn-more" onTap="onHistory">查看历史报告</button>
|
||||
</view>
|
||||
@@ -0,0 +1,71 @@
|
||||
var api = require('../../utils/api'), report = require('../../utils/report-model'), config = require('../../config');
|
||||
Page({
|
||||
data: { total: null, skinAge: null, ageNote: '', dims: [], focusName: '', focusSlogan: '',
|
||||
views: [], viewIndex: 0, frontPhoto: '', activeLayers: [], careAdvice: [], product: null, sideViews: [] },
|
||||
onLoad: function () {
|
||||
this.alive = true; this.visible = true;
|
||||
this.ownerId = api.user() && api.user().id;
|
||||
var gd = getApp().globalData;
|
||||
if (!gd.payload || !gd.payload.result) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
this.payload = gd.payload; this.photos = gd.photos || [];
|
||||
try { this.setData(report.model(this.payload, this.photos, gd.profile || {})); }
|
||||
catch (_) { my.showToast({ content: '报告生成失败,请重新检测', type: 'none' }); }
|
||||
this.renderSides(this.payload.wrinkle_views);
|
||||
},
|
||||
onReady: function () { this.ready = true; this.drawRing(this.data.total); this.loadSides(); },
|
||||
onShow: function () {
|
||||
if (!api.user() || this.ownerId !== api.user().id) {
|
||||
this.onHide(); this.setData({ frontPhoto: '', views: [], sideViews: [], careAdvice: [], product: null });
|
||||
my.redirectTo({ url: '/pages/splash/splash' }); return;
|
||||
}
|
||||
this.visible = true; if (this.ready) this.loadSides();
|
||||
},
|
||||
onHide: function () { this.visible = false; if (this.signal) this.signal.canceled = true; },
|
||||
onUnload: function () { this.onHide(); this.alive = false; this.photos = []; this.payload = null; },
|
||||
onTabChange: function (e) {
|
||||
var i = Number(e.currentTarget.dataset.tab), view = this.data.views[i];
|
||||
if (!view) return;
|
||||
this.setData({ viewIndex: i, activeLayers: report.layers(this.payload.result, view.key) });
|
||||
},
|
||||
onPhotoLoad: function (e) {
|
||||
var i = Number(e.currentTarget.dataset.index), view = this.data.views[i];
|
||||
if (!view || !this.payload) return;
|
||||
var points = report.marks(this.payload.result, view.key, e.detail.width, e.detail.height);
|
||||
var patch = {}; patch['views[' + i + '].marks'] = points; this.setData(patch);
|
||||
},
|
||||
renderSides: function (views) {
|
||||
var that = this;
|
||||
this.setData({ sideViews: ['left', 'right'].map(function (key, i) {
|
||||
var item = views && views[key] || {}, layer = report.mapURL(item.result && item.result.face_maps && item.result.face_maps.texture_enhanced_lines);
|
||||
return { key: key, title: i ? '右脸皱纹' : '左脸皱纹', photo: that.photos[i + 1] || '', layer: layer,
|
||||
loading: item.status === 'pending', note: item.status === 'unavailable' ? '侧脸标注暂不可用' : '' };
|
||||
}) });
|
||||
},
|
||||
loadSides: function () {
|
||||
var that = this, job = this.payload && this.payload.wrinkle_job;
|
||||
if (!job || job.status === 'complete' || !this.visible || this.loadingSides) return;
|
||||
this.loadingSides = true; this.signal = { canceled: false }; var signal = this.signal;
|
||||
api.wrinkleViews(job.token, signal).then(function (data) {
|
||||
if (!that.alive || !that.visible || signal.canceled) return;
|
||||
that.payload.wrinkle_views = data.wrinkle_views; job.status = 'complete'; that.renderSides(data.wrinkle_views);
|
||||
}).catch(function () {
|
||||
if (that.alive && that.visible && !signal.canceled) that.renderSides({ left: { status: 'unavailable' }, right: { status: 'unavailable' } });
|
||||
}).then(function () { that.loadingSides = false; if (signal.canceled && that.alive && that.visible) that.loadSides(); });
|
||||
},
|
||||
drawRing: function (total) {
|
||||
var ctx = my.createCanvasContext('score-ring'), ratio = total == null ? 0 : Math.max(0, Math.min(100, total)) / 100;
|
||||
ctx.clearRect(0, 0, 120, 120); ctx.setLineWidth(8);
|
||||
ctx.beginPath(); ctx.arc(60, 60, 52, 0, 2 * Math.PI); ctx.setStrokeStyle('#E5E5E5'); ctx.stroke();
|
||||
if (total != null) {
|
||||
ctx.beginPath(); ctx.arc(60, 60, 52, -Math.PI / 2, -Math.PI / 2 + ratio * 2 * Math.PI);
|
||||
ctx.setStrokeStyle('#F5F078'); ctx.setLineCap('round'); ctx.stroke();
|
||||
}
|
||||
ctx.draw();
|
||||
},
|
||||
onMore: function () {
|
||||
var id = this.data.product && config.productItemIds[this.data.product.tier];
|
||||
if (!id) return;
|
||||
if (my.tb && my.tb.openDetail) my.tb.openDetail({ itemId: String(id) });
|
||||
},
|
||||
onHistory: function () { my.navigateTo({ url: '/pages/history/history' }); }
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
"titleBarColor": "#FFFFFF",
|
||||
"backgroundColor": "#EFEFEF"
|
||||
}
|
||||
Reference in New Issue
Block a user