feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
(<image> 会额外解码约 32MB,并把页面滚动高度撑到 2608px)。
|
||||
.report-body { padding: calc(28u) calc(17u) calc(42u) } */
|
||||
.report { position: relative; }
|
||||
/* 原生不透明页眉已预留系统顶部区域,正文恢复 H5 原有的 28u 上间距。 */
|
||||
.report-body { position: relative; z-index: 1; padding: 56rpx 34rpx 84rpx; }
|
||||
|
||||
/* .report-title { font-size:23px; font-weight:600(覆盖 700); letter-spacing:calc(.5u);
|
||||
@@ -318,14 +319,23 @@
|
||||
background: #F5F078;
|
||||
}
|
||||
|
||||
/* .detail-viewport { display:block; overflow:hidden } / .detail-panels { display:flex } →
|
||||
acss 只渲染当前 Tab 的面板,视觉效果等价(H5 用 transform 横滑切换) */
|
||||
.detail-viewport { overflow: hidden; }
|
||||
.detail-panels { display: block; }
|
||||
/* H5: follow the finger horizontally, leave vertical gestures to the report. */
|
||||
.detail-viewport { overflow: hidden; touch-action: pan-y; transition: height .32s ease; }
|
||||
.detail-viewport.dragging { transition: none; }
|
||||
.detail-panels { display: flex; align-items: flex-start; transition: transform .32s cubic-bezier(.22,.61,.36,1); }
|
||||
.detail-panels.dragging { transition: none; }
|
||||
/* .detail-panels .dpanel { display:block; flex:0 0 100%; padding-top:calc(10u) } */
|
||||
.dpanel { display: block; padding-top: 20rpx; }
|
||||
.dpanel { display: block; flex: 0 0 100%; min-width: 0; padding-top: 20rpx; box-sizing: border-box; pointer-events: none; }
|
||||
.dpanel.active { pointer-events: auto; }
|
||||
/* .ditem { display:block(覆盖 flex) } */
|
||||
.ditem { display: block; }
|
||||
.report-zoom { position: fixed; inset: 0; z-index: 80; background: rgba(0,0,0,.92); display: flex; align-items: center; justify-content: center; }
|
||||
.report-zoom-enter { animation: report-zoom-in .22s ease both; }
|
||||
.report-zoom-frame { position: relative; overflow: hidden; background: #F6F6F6; transform-origin: center; }
|
||||
.report-zoom-side { position: absolute; }
|
||||
.report-zoom-bitmap { left:0; top:0; width:100%; height:100%; }
|
||||
.report-zoom-canvas { position:fixed; left:-10000px; top:0; width:835px; height:738px; pointer-events:none; }
|
||||
@keyframes report-zoom-in { from { opacity: 0; transform: scale(.88); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* ─── 检测图框(.viz-frame + .contour-front-frame 的 90% 高裁切)
|
||||
.viz-frame { position:relative; width:calc(215u); height:calc(278u); margin:0 auto calc(12u);
|
||||
@@ -429,11 +439,14 @@
|
||||
}
|
||||
/* H5 grid: figure[data-view="right"] { grid-column:2 } → 只剩右脸时仍占第二列 */
|
||||
.contour-side.right { margin-left: auto; }
|
||||
/* 轮廓和皱纹统一交换两张侧脸的位置,照片、标注及点击预览仍绑定原侧。 */
|
||||
.contour-sides-reversed { flex-direction: row-reverse; }
|
||||
.contour-sides-reversed .contour-side.right { margin-left: 0; margin-right: auto; }
|
||||
.side-img {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%; height: 100%;
|
||||
transform: translateY(-25%) scale(2.8);
|
||||
transform: translateY(-25%) scale(2.52);
|
||||
transform-origin: center;
|
||||
}
|
||||
.side-dim {
|
||||
@@ -628,6 +641,5 @@
|
||||
}
|
||||
.report-disclaimer-line { display: block; }
|
||||
|
||||
/* ─── 小程序独有:历史报告入口(H5 报告页无此按钮)
|
||||
沿用 H5 style.css 里已有的 .btn-more { margin: calc(26u) auto 0 },保持既有跳转不丢失 ─── */
|
||||
.btn-more { margin: 52rpx auto 0; }
|
||||
.session-guard { padding: 120rpx 40rpx; text-align: center; font-size: 13px; line-height: 1.8; color: #707070; }
|
||||
.session-guard .btn-primary { margin: 32rpx auto 0; }
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<import src="../../components/brand-header.axml" />
|
||||
<template is="brand-header" data="{{brandHeader: brandHeader}}" />
|
||||
<!-- ═════ 08 报告页 —— 对照 H5 index.html #s-report L174-291 ═════
|
||||
结构逐块对应:.report-title → .card.card-summary(环形评分 + 生物可视年龄 + 三维度)
|
||||
→ 三组 .dimension-title + .card.card-detail(紧塑力 2 视图 / 透亮感 3 视图 / 稳韧力 2 视图)
|
||||
→ .prod-title → .card.report-care-products(日常护理 + 推荐产品)→ .report-disclaimer。
|
||||
样式见 report.acss;数据仍由 utils/report-model.js 的 model()/layers()/marks() 产出。 -->
|
||||
<view class="report">
|
||||
<view class="report" style="padding-top:{{brandHeader.height}}px;">
|
||||
<view class="session-guard" a:if="{{identityChecking || identityError}}">
|
||||
<view>{{identityChecking ? '正在确认当前淘宝账号…' : identityError}}</view>
|
||||
<button a:if="{{identityError}}" class="btn-primary" onTap="onRetryIdentity">重新验证</button>
|
||||
</view>
|
||||
<!-- H5 #s-report: background:#efefef url(bg-report-h5.webp) center top/100% auto。
|
||||
该 webp 为 1080×7512 的纯 #EFEFEF 单色图(全图仅一种颜色 239,239,239),
|
||||
与 app.acss 里 page 的底色完全等价;acss 侧不再引 32MB 解码量的空图,
|
||||
直接用 page 底色(否则 <image> 还会把页面滚动高度撑到 2608px)。 -->
|
||||
|
||||
<view class="report-body">
|
||||
<view class="report-body" hidden="{{identityChecking || identityError}}">
|
||||
<!-- H5 h1.report-title -->
|
||||
<view class="report-title">您的肌肤状态表现</view>
|
||||
|
||||
@@ -71,13 +77,14 @@
|
||||
<text class="dtab-tx">{{tab.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-viewport">
|
||||
<view class="detail-panels">
|
||||
<view class="detail-viewport {{card.dragging ? 'dragging' : ''}}" style="{{card.height ? 'height:' + card.height + 'px;' : ''}}">
|
||||
<view class="detail-panels {{card.dragging ? 'dragging' : ''}}" style="transform:translateX(-{{card.index * 100}}%) translateX({{card.dragOffset || 0}}px);">
|
||||
<block a:for="{{card.views}}" a:for-item="view" a:for-index="vi" a:key="key">
|
||||
<!-- H5 用 transform 横滑切换;acss 只渲染当前面板,视觉等价 -->
|
||||
<view class="dpanel" a:if="{{card.index === vi}}">
|
||||
<!-- Keep all panels mounted for the H5 finger-following track. -->
|
||||
<view class="dpanel {{card.index === vi ? 'active' : ''}}" aria-hidden="{{card.index !== vi}}">
|
||||
<view class="ditem">
|
||||
<view class="viz-frame {{view.frameCls}}">
|
||||
<view class="viz-frame {{view.frameCls}}" data-card="{{card.key}}" data-view="{{view.key}}"
|
||||
onTouchStart="onPanelTouchStart" onTouchMove="onPanelTouchMove" onTouchEnd="onPanelTouchEnd" onTouchCancel="onPanelTouchCancel" onTap="onZoomPhoto">
|
||||
<!-- hasVisual=false 时整块画框只留占位文案(H5 renderViz 的降级:轮廓/肤色/平滑度/痘痘
|
||||
必须有人脸照片,皱纹/水分/泛红可只挂 face_maps 图层) -->
|
||||
<view class="viz-stage" a:if="{{view.hasVisual}}">
|
||||
@@ -94,13 +101,15 @@
|
||||
</view>
|
||||
|
||||
<!-- H5 .contour-sides(轮廓=原图;皱纹=原图压暗 + 侧脸皱纹图层 / 加载态) -->
|
||||
<view class="contour-sides {{view.dim ? 'wrinkle-sides' : ''}}" a:if="{{sidesShown && (view.dim || view.key === 'contour')}}">
|
||||
<view class="contour-sides contour-sides-reversed {{view.dim ? 'wrinkle-sides' : ''}}" a:if="{{sidesShown && (view.dim || view.key === 'contour')}}">
|
||||
<block a:for="{{sides}}" a:for-item="side" a:key="key">
|
||||
<view class="contour-side {{side.key === 'right' ? 'right' : ''}}" a:if="{{side.photo}}">
|
||||
<image class="side-img" src="{{side.photo}}" mode="aspectFill" />
|
||||
<view class="contour-side {{side.key === 'right' ? 'right' : ''}}" a:if="{{side.photo}}"
|
||||
data-card="{{card.key}}" data-view="{{view.key}}" data-side="{{side.key}}" onTap="onZoomPhoto">
|
||||
<image class="side-img" src="{{side.photo}}" mode="{{side.photoStyle ? 'scaleToFill' : 'aspectFill'}}" style="{{side.photoStyle}}"
|
||||
data-side="{{side.key}}" data-photo="{{side.photo}}" onLoad="onSidePhotoLoad" />
|
||||
<block a:if="{{view.dim}}">
|
||||
<view class="side-dim"></view>
|
||||
<image class="side-img" a:if="{{side.layer}}" src="{{side.layer}}" mode="aspectFill" />
|
||||
<image class="side-img" a:if="{{side.layer}}" src="{{side.layer}}" mode="{{side.photoStyle ? 'scaleToFill' : 'aspectFill'}}" style="{{side.photoStyle}}" />
|
||||
<view class="side-loading" a:if="{{side.loading}}"><view>皱纹标注</view><view>加载中…</view></view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -157,7 +166,23 @@
|
||||
<view class="report-disclaimer-line">*仅根据您最近的测肤记录推荐。</view>
|
||||
</view>
|
||||
|
||||
<!-- 小程序独有入口(H5 报告页无此按钮):保留既有的历史报告跳转 -->
|
||||
<button class="btn-primary btn-more" onTap="onHistory">查看历史报告</button>
|
||||
</view>
|
||||
<!-- H5: tap an inspection image to enlarge; tap again to return. -->
|
||||
<view class="report-zoom" a:if="{{zoom && !identityChecking && !identityError}}" onTap="onCloseZoom" catchTouchMove="noop">
|
||||
<view class="report-zoom-enter">
|
||||
<view class="report-zoom-frame" style="{{zoom.frameStyle}}">
|
||||
<view class="viz-stage" a:if="{{!zoom.side}}">
|
||||
<image class="viz-photo" a:if="{{zoom.photo}}" src="{{zoom.photo}}" mode="{{zoom.fit}}" style="{{zoom.photoStyle}}" />
|
||||
<view class="viz-dim" a:if="{{zoom.dark}}"></view>
|
||||
<image class="viz-layer {{zoom.blend ? 'viz-blend' : ''}}" a:for="{{zoom.layers}}" a:for-item="layer" a:key="*this" src="{{layer}}" mode="scaleToFill" style="opacity:{{zoom.alpha}};{{zoom.photoStyle}}" />
|
||||
<view class="viz-mark" a:for="{{zoom.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>
|
||||
<block a:else>
|
||||
<image class="report-zoom-side report-zoom-bitmap" src="{{zoom.bakedPhoto || zoom.photo}}" mode="aspectFit"
|
||||
data-photo="{{zoom.photo}}" data-generation="{{zoom.generation}}" onLoad="onZoomSideLoad" />
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<canvas id="report-side-zoom-canvas" canvas-id="report-side-zoom-canvas" width="835" height="738" class="report-zoom-canvas" />
|
||||
</view>
|
||||
|
||||
+279
-27
@@ -8,6 +8,8 @@
|
||||
4) 痘/斑标记坐标:接口像素坐标 → 画框百分比(含 H5 的最小框放大规则);
|
||||
5) 侧脸皱纹标注:payload.wrinkle_job + api.wrinkleViews,pending/unavailable 文案同 H5。 */
|
||||
var api = require('../../utils/api'), report = require('../../utils/report-model'), config = require('../../config');
|
||||
var swipe = require('../../utils/swipe-gesture');
|
||||
var sideZoom = require('../../utils/report-side-zoom');
|
||||
|
||||
/* H5 检测图框 .viz-frame:calc(215 * var(--u)) × calc(278 * var(--u)) → 430×556rpx(report.acss 同值) */
|
||||
var FRAME_W = 430, FRAME_H = 556, FRAME_RATIO = 215 / 278;
|
||||
@@ -34,6 +36,25 @@ var MELANIN_COLOR = '#9F641F';
|
||||
|
||||
function numeric(value) { return typeof value === 'number' && isFinite(value); }
|
||||
function px(value) { return (Math.round(value * 100) / 100) + 'rpx'; }
|
||||
// H5 thumbnail framing with fill reduced by 10%: 2.8 * .9 = 2.52.
|
||||
// Keep the existing vertical anchor while showing more of the photograph.
|
||||
// Resolve the object-fit and transform together so native image components do
|
||||
// not reinterpret the percentage translation or scale their internal crop.
|
||||
function sideThumbnailStyle(width, height) {
|
||||
if (!numeric(width) || !numeric(height) || width <= 0 || height <= 0) return '';
|
||||
var w = 208.7, h = 184.5, scale = Math.max(w / width, h / height) * 2.52;
|
||||
var imageW = width * scale, imageH = height * scale;
|
||||
return 'left:' + px((w - imageW) / 2) + ';top:' + px(h / 4 - imageH / 2) +
|
||||
';width:' + px(imageW) + ';height:' + px(imageH) + ';transform:none;';
|
||||
}
|
||||
function sideZoomStyle(width, height) {
|
||||
var rect = sideZoom.rectangle(width, height);
|
||||
if (!rect) return '';
|
||||
// Express the side-photo crop relative to the final viewport, rather than magnifying
|
||||
// a tiny native image/overflow container with a parent CSS transform.
|
||||
return 'left:' + pct(rect.left / 208.7) + '%;top:' + pct(rect.top / 184.5) +
|
||||
'%;width:' + pct(rect.width / 208.7) + '%;height:' + pct(rect.height / 184.5) + '%;';
|
||||
}
|
||||
function pct(ratio) { return Math.round(ratio * 10000) / 100; }
|
||||
|
||||
/* H5 SVG 环形评分:#ring-fg 的 stroke-dashoffset = 326.73 × (1 - 总分/100),顺时针、从正上方开始。
|
||||
@@ -131,20 +152,23 @@ function smoothCaption(result) {
|
||||
: '本次未获取毛孔、黑头增强图;此处仅展示拍摄照片,不绘制色斑、痣或痤疮框。';
|
||||
}
|
||||
|
||||
Page({
|
||||
Page(require('../../utils/page-header').page({
|
||||
onShareAppMessage: function () { return require('../../utils/share').message(); },
|
||||
data: {
|
||||
// 图片统一走 api.assetUrl(与 H5 h5/assets/ 同相对路径)
|
||||
productBadgeUrl: api.assetUrl('img/ui-20261015/product-badge.png'),
|
||||
// report-model.model() 产出(接口不变)
|
||||
total: null, skinAge: null, ageNote: '', dims: [], focusName: '', focusSlogan: '',
|
||||
views: [], viewIndex: 0, frontPhoto: '', activeLayers: [], careAdvice: [], product: null,
|
||||
views: [], viewIndex: 0, frontPhoto: '', activeLayers: [], careAdvice: [], product: null, guest: false,
|
||||
identityChecking: true, identityError: '',
|
||||
// 版面对齐 H5 所需的派生数据
|
||||
cards: [], sides: [], sidesShown: false, wrinkleNote: '',
|
||||
ringDeg: 0, ringCap: false, ringCapEnd: ''
|
||||
ringDeg: 0, ringCap: false, ringCapEnd: '', zoom: null
|
||||
},
|
||||
onLoad: function () {
|
||||
this.alive = true; this.visible = true;
|
||||
this.ownerId = api.user() && api.user().id;
|
||||
this.setData({ guest: !!(api.user() && api.user().kind === 'taobao_guest') });
|
||||
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 || [];
|
||||
@@ -159,17 +183,63 @@ Page({
|
||||
}));
|
||||
this.renderSides(this.payload.wrinkle_views);
|
||||
},
|
||||
onReady: function () { this.ready = true; this.measurePhoto(); this.loadSides(); },
|
||||
onShow: function () {
|
||||
if (!api.user() || this.ownerId !== api.user().id) {
|
||||
this.onHide();
|
||||
this.setData({ frontPhoto: '', views: [], cards: [], sides: [], careAdvice: [], product: null });
|
||||
my.redirectTo({ url: '/pages/splash/splash' }); return;
|
||||
}
|
||||
this.visible = true; if (this.ready) this.loadSides();
|
||||
disableSwipeBack: function () {
|
||||
// Taobao's container gesture is separate from page gestureBack config.
|
||||
// Official contract: enable is numeric 0/1, not a boolean.
|
||||
try {
|
||||
if (my.tb && typeof my.tb.enableSwipeBack === 'function') {
|
||||
var pending = my.tb.enableSwipeBack({ enable: 0 });
|
||||
if (pending && typeof pending.catch === 'function') pending.catch(function () {});
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
onHide: function () { this.visible = false; if (this.signal) this.signal.canceled = true; },
|
||||
onUnload: function () { this.onHide(); this.alive = false; this.photos = []; this.payload = null; },
|
||||
onReady: function () { this.disableSwipeBack(); this.ready = true; this.measurePhoto(); this.loadSides(); this.measurePanels(); },
|
||||
onShow: function () {
|
||||
this.disableSwipeBack();
|
||||
this.openingProduct = false; this.productGeneration = (this.productGeneration || 0) + 1; clearTimeout(this.productTimer);
|
||||
if (!api.user() || this.ownerId !== api.user().id) {
|
||||
this.clearPrivateReport(); return Promise.resolve(false);
|
||||
}
|
||||
this.visible = true;
|
||||
if (api.user().kind === 'taobao_guest') {
|
||||
this.identityPending = false;
|
||||
var guestPage = this;
|
||||
this.setData({ identityChecking: false, identityError: '' }, function () { guestPage.measurePanels(); });
|
||||
if (this.ready) this.loadSides(); return Promise.resolve(true);
|
||||
}
|
||||
var that = this, seq = this.identitySeq = (this.identitySeq || 0) + 1;
|
||||
this.identityPending = true; this.setData({ identityChecking: true, identityError: '' });
|
||||
return (typeof api.restore === 'function' ? api.restore() : Promise.resolve(api.user())).then(function (user) {
|
||||
if (!that.alive || !that.visible || seq !== that.identitySeq) return false;
|
||||
if (!user || user.id !== that.ownerId) { that.clearPrivateReport(); return false; }
|
||||
that.identityPending = false; that.setData({ identityChecking: false }, function () { that.measurePanels(); });
|
||||
if (that.ready) that.loadSides(); return true;
|
||||
}).catch(function (err) {
|
||||
if (!that.alive || !that.visible || seq !== that.identitySeq) return false;
|
||||
if (err.status === 401 || !api.user() || api.user().id !== that.ownerId) { that.clearPrivateReport(); return false; }
|
||||
that.identityPending = true;
|
||||
that.setData({ identityChecking: false, identityError: '网络异常,暂时无法确认当前账号,请重试。' });
|
||||
return false;
|
||||
});
|
||||
},
|
||||
onRetryIdentity: function () { return this.onShow(); },
|
||||
clearPrivateReport: function () {
|
||||
this.onHide(); this.identityPending = true;
|
||||
this.setData({ total: null, skinAge: null, ageNote: '', dims: [], focusName: '', focusSlogan: '',
|
||||
frontPhoto: '', activeLayers: [], views: [], cards: [], sides: [], careAdvice: [], product: null,
|
||||
identityChecking: true, identityError: '' });
|
||||
this.photos = []; this.payload = null;
|
||||
my.redirectTo({ url: '/pages/login/login' });
|
||||
},
|
||||
onHide: function () {
|
||||
this.visible = false; this.identitySeq = (this.identitySeq || 0) + 1;
|
||||
this.cancelGestures(); this.onCloseZoom(); this.measureGeneration = (this.measureGeneration || 0) + 1;
|
||||
if (this.signal) this.signal.canceled = true;
|
||||
if (api.user() && api.user().kind !== 'taobao_guest') {
|
||||
this.identityPending = true; this.setData({ identityChecking: true, identityError: '' });
|
||||
}
|
||||
},
|
||||
onUnload: function () { this.onHide(); clearTimeout(this.productTimer); this.alive = false; this.photos = []; this.payload = null; },
|
||||
|
||||
/* ─── H5 三张维度卡的视图分组(七个检测视角各带图层/标记/框型) ─── */
|
||||
buildCards: function (model, result) {
|
||||
@@ -194,7 +264,8 @@ Page({
|
||||
caption: key === 'smooth' ? smoothCaption(result) : ''
|
||||
});
|
||||
});
|
||||
return { key: group.key, title: group.title, titleCls: group.titleCls, cardCls: group.cardCls, index: 0, views: views };
|
||||
return { key: group.key, title: group.title, titleCls: group.titleCls, cardCls: group.cardCls,
|
||||
index: 0, dragOffset: 0, dragging: false, height: 0, views: views };
|
||||
});
|
||||
},
|
||||
|
||||
@@ -210,6 +281,7 @@ Page({
|
||||
onPhotoLoad: function (event) {
|
||||
var detail = (event && event.detail) || {};
|
||||
this.applyGeometry(detail.width, detail.height);
|
||||
this.measurePanels();
|
||||
},
|
||||
applyGeometry: function (width, height) {
|
||||
if (!(width > 0 && height > 0) || this.geoDone) return;
|
||||
@@ -231,16 +303,24 @@ Page({
|
||||
patch[key + '.marks'] = placeMarks(marks, width, height, box);
|
||||
});
|
||||
});
|
||||
if (that.alive !== false) this.setData(patch);
|
||||
if (that.alive !== false) this.setData(patch, function () { that.measurePanels(); });
|
||||
},
|
||||
|
||||
/* ─── 视角切换:保持既有 viewIndex / activeLayers 语义 ─── */
|
||||
onTabTap: function (event) {
|
||||
var dataset = (event && event.currentTarget && event.currentTarget.dataset) || {};
|
||||
var key = dataset.card, index = Number(dataset.index), patch = {}, selected = null;
|
||||
this.selectTab(dataset.card, Number(dataset.index));
|
||||
},
|
||||
selectTab: function (key, index) {
|
||||
if (this.identityPending || !isFinite(index) || Math.floor(index) !== index) return;
|
||||
var patch = {}, selected = null, that = this;
|
||||
this.data.cards.forEach(function (card, ci) {
|
||||
if (card.key !== key) return;
|
||||
if (card.key !== key || !card.views[index]) return;
|
||||
patch['cards[' + ci + '].index'] = index;
|
||||
patch['cards[' + ci + '].dragOffset'] = 0;
|
||||
patch['cards[' + ci + '].dragging'] = false;
|
||||
var size = that.panelSizes && that.panelSizes[key];
|
||||
if (size && size.heights[index]) patch['cards[' + ci + '].height'] = size.heights[index];
|
||||
selected = card.views[index] || null;
|
||||
});
|
||||
if (!selected) return;
|
||||
@@ -248,14 +328,146 @@ Page({
|
||||
this.data.views.forEach(function (view, i) { if (view.key === selected.key) flat = i; });
|
||||
patch.viewIndex = flat;
|
||||
patch.activeLayers = this.payload && this.payload.result ? report.layers(this.payload.result, selected.key) : [];
|
||||
this.setData(patch);
|
||||
this.setData(patch, function () { that.measurePanels(); });
|
||||
},
|
||||
measurePanels: function () {
|
||||
if (!this.ready || !this.visible || this.identityPending || typeof my.createSelectorQuery !== 'function') return;
|
||||
var that = this, generation = this.measureGeneration = (this.measureGeneration || 0) + 1;
|
||||
try { my.createSelectorQuery().selectAll('.detail-viewport').boundingClientRect()
|
||||
.selectAll('.dpanel').boundingClientRect().exec(function (rows) {
|
||||
if (!that.alive || !that.visible || that.identityPending || generation !== that.measureGeneration || !rows || !rows[0] || !rows[1]) return;
|
||||
var patch = {}, cursor = 0;
|
||||
that.panelSizes = that.panelSizes || {};
|
||||
that.data.cards.forEach(function (card, ci) {
|
||||
var heights = card.views.map(function () { var rect = rows[1][cursor++]; return rect && rect.height > 0 ? rect.height : 0; });
|
||||
var viewport = rows[0][ci];
|
||||
if (!viewport || !(viewport.width > 0)) return;
|
||||
that.panelSizes[card.key] = { width: viewport.width, heights: heights };
|
||||
if (!card.dragging && heights[card.index]) patch['cards[' + ci + '].height'] = heights[card.index];
|
||||
});
|
||||
if (Object.keys(patch).length) that.setData(patch);
|
||||
}); } catch (_) {}
|
||||
},
|
||||
reportCard: function (event) {
|
||||
var key = event && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.card;
|
||||
for (var i = 0; i < this.data.cards.length; i++) if (this.data.cards[i].key === key) return { card: this.data.cards[i], index: i };
|
||||
return null;
|
||||
},
|
||||
onPanelTouchStart: function (event) {
|
||||
if (this.identityPending) return;
|
||||
var row = this.reportCard(event); if (!row) return;
|
||||
this.cancelGestures(); this.suppressImageTapUntil = 0;
|
||||
var width = this.panelSizes && this.panelSizes[row.card.key] && this.panelSizes[row.card.key].width;
|
||||
if (!width) { var info = typeof my.getSystemInfoSync === 'function' ? my.getSystemInfoSync() : {}; width = (info.windowWidth || 375) * 263 / 375; }
|
||||
this.panelGesture = { key: row.card.key, index: row.index, state: swipe.start(event, row.card.index, width, row.card.views.length,
|
||||
{ startThreshold: 12, directionRatio: 2 / 3, maxOverscroll: 16 }) };
|
||||
},
|
||||
onPanelTouchMove: function (event) {
|
||||
var gesture = this.panelGesture; if (!gesture || this.identityPending) return;
|
||||
var value = swipe.move(gesture.state, event);
|
||||
if (!value) { this.onPanelTouchCancel(); return; }
|
||||
if (!value.dragging) return;
|
||||
var patch = {}; patch['cards[' + gesture.index + '].dragOffset'] = value.offset;
|
||||
patch['cards[' + gesture.index + '].dragging'] = true; this.setData(patch);
|
||||
},
|
||||
onPanelTouchEnd: function (event) { this.finishPanelGesture(event, false); },
|
||||
onPanelTouchCancel: function (event) { this.finishPanelGesture(event, true); },
|
||||
finishPanelGesture: function (event, canceled) {
|
||||
var gesture = this.panelGesture; this.panelGesture = null; if (!gesture) return;
|
||||
var value = swipe.end(gesture.state, event, canceled);
|
||||
if (value && value.dragged) this.suppressImageTapUntil = Date.now() + 350;
|
||||
if (value) this.selectTab(gesture.key, value.index);
|
||||
},
|
||||
cancelGestures: function () {
|
||||
this.panelGesture = null;
|
||||
var patch = {};
|
||||
this.data.cards.forEach(function (card, ci) { if (card.dragging || card.dragOffset) {
|
||||
patch['cards[' + ci + '].dragging'] = false; patch['cards[' + ci + '].dragOffset'] = 0;
|
||||
} });
|
||||
if (Object.keys(patch).length) this.setData(patch);
|
||||
},
|
||||
onResize: function () { this.cancelGestures(); this.onCloseZoom(); this.measurePanels(); },
|
||||
onZoomPhoto: function (event) {
|
||||
if (this.identityPending || !this.visible || Date.now() < (this.suppressImageTapUntil || 0)) return;
|
||||
var row = this.reportCard(event), dataset = event.currentTarget.dataset || {};
|
||||
if (!row) return;
|
||||
var view = row.card.views[row.card.index];
|
||||
if (!view || dataset.view !== view.key) return;
|
||||
this.onCloseZoom();
|
||||
var zoom = { side: false, photo: this.data.frontPhoto, fit: view.fit, photoStyle: view.photoStyle,
|
||||
dark: view.dark, blend: view.blend, alpha: view.alpha, layers: view.layers, marks: view.marks,
|
||||
width: FRAME_W, height: view.frameCls ? 500.4 : FRAME_H };
|
||||
var that = this, generation = this.zoomGeneration, owner = this.ownerId;
|
||||
function show() {
|
||||
if (!that.alive || !that.visible || that.identityPending || generation !== that.zoomGeneration || owner !== (api.user() && api.user().id)) return;
|
||||
var info = typeof my.getSystemInfoSync === 'function' ? my.getSystemInfoSync() : {};
|
||||
var w = info.windowWidth || 375, h = info.windowHeight || 726;
|
||||
var scale = Math.min(w / (zoom.width * w / 750), h / (zoom.height * w / 750));
|
||||
zoom.frameStyle = zoom.side
|
||||
? 'width:' + (zoom.width * w / 750 * scale) + 'px;height:' + (zoom.height * w / 750 * scale) + 'px;'
|
||||
: 'width:' + zoom.width + 'rpx;height:' + zoom.height + 'rpx;transform:scale(' + scale + ');';
|
||||
that.setData({ zoom: zoom });
|
||||
}
|
||||
if (!dataset.side) { if (view.hasVisual) show(); return; }
|
||||
var side = null;
|
||||
this.data.sides.forEach(function (item) { if (item.key === dataset.side) side = item; });
|
||||
if (!side || !side.photo) return;
|
||||
zoom.side = true; zoom.photo = side.photo; zoom.width = 208.7; zoom.height = 184.5;
|
||||
zoom.sideKey = side.key; zoom.generation = generation; zoom.fit = 'aspectFit';
|
||||
zoom.dark = view.dim ? .55 : 0; zoom.layers = view.dim && side.layer ? [side.layer] : [];
|
||||
function sideSize(info) {
|
||||
if (!that.alive || !that.visible || generation !== that.zoomGeneration || owner !== (api.user() && api.user().id)) return;
|
||||
zoom.photoStyle = sideZoomStyle(info && info.width, info && info.height) || 'left:0;top:0;width:100%;height:100%;';
|
||||
show();
|
||||
that.bakeSideZoom(info);
|
||||
}
|
||||
function fallbackSize() {
|
||||
sideSize({ width: side.naturalWidth, height: side.naturalHeight });
|
||||
}
|
||||
// Read the source file, not dimensions reported by an already enlarged
|
||||
// native image. Only fall back to thumbnail metadata when this API fails.
|
||||
if (typeof my.getImageInfo !== 'function') { fallbackSize(); return; }
|
||||
try { my.getImageInfo({ src: side.photo, success: sideSize, fail: fallbackSize }); }
|
||||
catch (_) { fallbackSize(); }
|
||||
},
|
||||
onZoomSideLoad: function (event) {
|
||||
var zoom = this.data.zoom, data = event && event.currentTarget && event.currentTarget.dataset || {};
|
||||
if (!zoom || !zoom.side || !this.alive || !this.visible || this.identityPending ||
|
||||
this.ownerId !== (api.user() && api.user().id) || Number(data.generation) !== this.zoomGeneration ||
|
||||
zoom.generation !== this.zoomGeneration || data.photo !== zoom.photo) return;
|
||||
if (zoom.bakedPhoto || this.zoomCanvasJob) return;
|
||||
var info = event && event.detail, style = sideZoomStyle(info && info.width, info && info.height);
|
||||
if (!style) return;
|
||||
this.applySidePhotoSize(zoom.sideKey, zoom.photo, info);
|
||||
this.setData({ zoom: Object.assign({}, zoom, { photoStyle: style }) });
|
||||
this.bakeSideZoom(info);
|
||||
},
|
||||
bakeSideZoom: function (info) {
|
||||
var zoom = this.data.zoom, that = this;
|
||||
if (!zoom || !zoom.side || zoom.bakedPhoto || this.zoomCanvasJob || !sideZoom.rectangle(info && info.width, info && info.height) ||
|
||||
typeof my.createCanvasContext !== 'function') return;
|
||||
var generation = zoom.generation, owner = this.ownerId;
|
||||
var job = {}; this.zoomCanvasJob = job;
|
||||
// A full, uncropped photo is the safe preview while baking or on export
|
||||
// failure. Never fall back to the problematic native overflow crop.
|
||||
sideZoom.bake(my, { photo: zoom.photo, width: info.width, height: info.height, dark: zoom.dark, layers: zoom.layers }).then(function (src) {
|
||||
if (!that.alive || !that.visible || that.identityPending || that.zoomGeneration !== generation ||
|
||||
owner !== (api.user() && api.user().id) || !that.data.zoom || that.data.zoom.photo !== zoom.photo) return;
|
||||
that.setData({ zoom: Object.assign({}, that.data.zoom, { bakedPhoto: src }) });
|
||||
}, function () {}).then(function () { if (that.zoomCanvasJob === job) that.zoomCanvasJob = null; });
|
||||
},
|
||||
onCloseZoom: function () {
|
||||
this.zoomGeneration = (this.zoomGeneration || 0) + 1;
|
||||
if (this.data.zoom) this.setData({ zoom: null });
|
||||
},
|
||||
noop: function () {},
|
||||
|
||||
/* ─── 侧脸照片 + 皱纹标注(H5 renderWrinkleSides / renderContourSides) ───
|
||||
pending:未标注一侧显示「皱纹标注加载中…」
|
||||
unavailable:显示「部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。」 */
|
||||
renderSides: function (views) {
|
||||
var payload = this.payload || {}, photos = this.photos || [];
|
||||
var previous = this.data.sides || [];
|
||||
views = views || payload.wrinkle_views || {};
|
||||
var pending = !!(payload.wrinkle_job && payload.wrinkle_job.status === 'pending');
|
||||
var sides = ['left', 'right'].map(function (key, i) {
|
||||
@@ -264,16 +476,44 @@ Page({
|
||||
if (!item || !item.result) item = (payload.side || {})[key];
|
||||
var source = (item && (item.result || item)) || {};
|
||||
var layer = report.mapURL(source.face_maps && source.face_maps.texture_enhanced_lines);
|
||||
return { key: key, photo: photos[i + 1] || '', layer: layer, loading: pending && !layer };
|
||||
var photo = photos[i + 1] || '', photoStyle = '', naturalWidth = 0, naturalHeight = 0;
|
||||
previous.forEach(function (old) { if (old.key === key && old.photo === photo) {
|
||||
photoStyle = old.photoStyle || ''; naturalWidth = old.naturalWidth || 0; naturalHeight = old.naturalHeight || 0;
|
||||
} });
|
||||
return { key: key, photo: photo, photoStyle: photoStyle, naturalWidth: naturalWidth, naturalHeight: naturalHeight, layer: layer, loading: pending && !layer };
|
||||
});
|
||||
var shown = sides.filter(function (side) { return !!side.photo; });
|
||||
var plain = pending ? false : shown.some(function (side) { return !side.layer; });
|
||||
var that = this;
|
||||
this.setData({ sides: sides, sidesShown: shown.length > 0,
|
||||
wrinkleNote: plain ? '部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。' : '' });
|
||||
wrinkleNote: plain ? '部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。' : '' }, function () {
|
||||
that.measurePanels();
|
||||
sides.forEach(function (side) { if (side.photo && !side.photoStyle) that.readSidePhotoSize(side.key, side.photo); });
|
||||
});
|
||||
},
|
||||
applySidePhotoSize: function (key, photo, info) {
|
||||
if (!this.alive || !this.visible || this.ownerId !== (api.user() && api.user().id)) return;
|
||||
var style = sideThumbnailStyle(info && info.width, info && info.height);
|
||||
if (!style) return;
|
||||
var changed = false;
|
||||
var sides = this.data.sides.map(function (side) {
|
||||
if (side.key !== key || side.photo !== photo || side.photoStyle === style && side.naturalWidth === info.width && side.naturalHeight === info.height) return side;
|
||||
changed = true; return Object.assign({}, side, { photoStyle: style, naturalWidth: info.width, naturalHeight: info.height });
|
||||
});
|
||||
if (changed) this.setData({ sides: sides });
|
||||
},
|
||||
readSidePhotoSize: function (key, photo) {
|
||||
if (typeof my.getImageInfo !== 'function') return;
|
||||
var that = this;
|
||||
try { my.getImageInfo({ src: photo, success: function (info) { that.applySidePhotoSize(key, photo, info); }, fail: function () {} }); } catch (_) {}
|
||||
},
|
||||
onSidePhotoLoad: function (event) {
|
||||
var data = event && event.currentTarget && event.currentTarget.dataset || {};
|
||||
this.applySidePhotoSize(data.side, data.photo, event && event.detail);
|
||||
},
|
||||
loadSides: function () {
|
||||
var that = this, job = this.payload && this.payload.wrinkle_job;
|
||||
if (!job || job.status !== 'pending' || !this.visible || this.loadingSides) return;
|
||||
if (!job || job.status !== 'pending' || !this.visible || this.loadingSides || this.identityPending) return;
|
||||
this.loadingSides = true; this.signal = { canceled: false };
|
||||
var signal = this.signal;
|
||||
api.wrinkleViews(job.token, signal).then(function (data) {
|
||||
@@ -286,11 +526,23 @@ Page({
|
||||
}).then(function () { that.loadingSides = false; if (signal.canceled && that.alive && that.visible) that.loadSides(); });
|
||||
},
|
||||
|
||||
/* 推荐产品 → 淘宝商品详情(config.productItemIds 未配置时为 no-op,不伪造链接) */
|
||||
/* 推荐文案保持 H5;三个档位统一进入用户确认的天猫商品。 */
|
||||
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) });
|
||||
if (this.openingProduct || this.identityPending) return;
|
||||
var id = this.data.product && (config.productItemId || (config.productItemIds || {})[this.data.product.tier]);
|
||||
if (!id || !/^\d+$/.test(String(id))) { my.showToast({ content: '商品信息暂未就绪,请稍后重试', type: 'none' }); return; }
|
||||
if (!my.tb || typeof my.tb.openDetail !== 'function') { my.showToast({ content: '当前环境暂不支持打开商品,请使用最新版手机淘宝', type: 'none' }); return; }
|
||||
var that = this;
|
||||
var generation = this.productGeneration = (this.productGeneration || 0) + 1;
|
||||
this.openingProduct = true;
|
||||
this.productTimer = setTimeout(function () { if (generation === that.productGeneration) that.openingProduct = false; }, 10000);
|
||||
function failed() {
|
||||
if (generation !== that.productGeneration) return;
|
||||
clearTimeout(that.productTimer); that.openingProduct = false;
|
||||
if (that.alive !== false) my.showToast({ content: '暂时无法打开商品,请稍后重试', type: 'none' });
|
||||
}
|
||||
try { my.tb.openDetail({ itemId: String(id), fail: failed }); } catch (_) { failed(); }
|
||||
},
|
||||
onHistory: function () { my.navigateTo({ url: '/pages/history/history' }); }
|
||||
});
|
||||
onHistory: function () { my.navigateTo({ url: '/pages/history/history' }); },
|
||||
onEndExperience: function () { api.endGuest().then(function () { my.redirectTo({ url: '/pages/login/login' }); }); }
|
||||
}, typeof my === 'undefined' ? {} : my));
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
"defaultTitle": "",
|
||||
"titleBarColor": "#FFFFFF",
|
||||
"backgroundColor": "#EFEFEF"
|
||||
"backgroundColor": "#EFEFEF",
|
||||
"navigationBarForceEnable": true,
|
||||
"transparentTitle": "always",
|
||||
"gestureBack": "NO",
|
||||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user