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,183 @@
|
||||
/* ═════ 拍照页样式 ═════ */
|
||||
.cam {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #101823;
|
||||
}
|
||||
.cam-device {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ─── 步骤文案 ─── */
|
||||
.cam-step {
|
||||
position: absolute;
|
||||
top: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.cam-step-no {
|
||||
color: #FFFFFF;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cam-step-tip {
|
||||
color: #FFFFFF;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* ─── 取景蒙版(椭圆线框) ─── */
|
||||
.cam-mask {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cam-oval {
|
||||
width: 560rpx;
|
||||
height: 720rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 侧脸步轮廓偏移提示(骨架版:向左/右微移示意朝向) */
|
||||
.mask-1 { transform: translateX(-60rpx); } /* 左脸出镜:耳在画面左 */
|
||||
.mask-2 { transform: translateX(60rpx); } /* 右脸出镜:耳在画面右 */
|
||||
|
||||
/* ─── 底部引导要点 ─── */
|
||||
.cam-guide {
|
||||
position: absolute;
|
||||
bottom: 260rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.cam-guide-item {
|
||||
color: #FFFFFF;
|
||||
font-size: 26rpx;
|
||||
padding: 0 24rpx;
|
||||
border-right: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.cam-guide-item:last-child { border-right: none; }
|
||||
|
||||
/* ─── 控制条 ─── */
|
||||
.cam-controls {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.cam-side {
|
||||
width: 120rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.cam-shutter {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border: 6rpx solid #FFFFFF;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cam-shutter-dot {
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F078;
|
||||
}
|
||||
|
||||
/* ─── 停顿卡 ─── */
|
||||
.cam-done {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 60rpx;
|
||||
right: 60rpx;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
/* ─── 拍后预览 ─── */
|
||||
.cam-preview {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #000000;
|
||||
}
|
||||
.cam-preview-img {
|
||||
width: 100%;
|
||||
height: 78%;
|
||||
}
|
||||
.cam-preview-btns {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.cam-preview-btn {
|
||||
width: 280rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #FFFFFF;
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.cam-preview-btn.ok {
|
||||
background: #F5F078;
|
||||
border-color: #F5F078;
|
||||
color: #282828;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 分析中 ─── */
|
||||
.cam-analyzing {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cam-analyzing-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
/* ─── 隐藏画布(压缩降级用) ─── */
|
||||
.compress-canvas {
|
||||
position: fixed;
|
||||
left: -9999rpx;
|
||||
top: -9999rpx;
|
||||
width: 640px;
|
||||
height: 640px;
|
||||
}
|
||||
.cam-analyzing { flex-direction: column; }
|
||||
.cam-cancel { margin-top: 48rpx; color: #fff; font-size: 26rpx; padding: 24rpx; }
|
||||
.camera-permission { padding: 56rpx 40rpx; color: #fff; line-height: 1.8; }
|
||||
.camera-permission button { margin-top: 24rpx; }
|
||||
.debug-banner { padding: 24rpx; background: #F5F078; color: #282828; font-size: 24rpx; }
|
||||
.debug-results { padding: 32rpx; background: #101823; color: white; line-height: 1.8; }
|
||||
.debug-title { font-size: 32rpx; font-weight: 600; }
|
||||
.debug-result { margin: 24rpx 0; }
|
||||
.debug-result image { display: block; width: 100%; height: 320rpx; }
|
||||
.debug-results button { margin-top: 24rpx; }
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- ═════ 05-07 拍照页 ═════
|
||||
覆盖在 camera 原生组件上的一切必须用 cover-view / cover-image -->
|
||||
<view class="cam">
|
||||
<view a:if="{{debugMode && !cameraEnabled}}" class="debug-banner">相机验收 · 免登录 · 照片只在本页处理,不上传</view>
|
||||
<view a:if="{{debugComplete}}" class="debug-results">
|
||||
<view class="debug-title">三步拍摄与 JPEG 压缩已完成</view>
|
||||
<view>请核对实际照片方向是否为正脸、右脸、左脸;本页不判断姿态是否合格。</view>
|
||||
<view class="debug-result" a:for="{{debugPhotos}}" a:key="key">
|
||||
<image src="{{item.photo}}" mode="aspectFit" />
|
||||
<view>{{item.title}} · JPEG {{item.width}}×{{item.height}} · 约{{item.kilobytes}}KB</view>
|
||||
</view>
|
||||
<button class="btn-primary" onTap="onDebugRestart">重新验收</button>
|
||||
<button onTap="onCancel">退出并清空本页照片</button>
|
||||
</view>
|
||||
<view a:if="{{!cameraEnabled && !debugComplete}}" class="camera-permission">
|
||||
<view>{{stepText}} · {{stepTip}}</view>
|
||||
<view>{{permissionMessage}}</view>
|
||||
<button class="btn-primary" onTap="onEnableCamera">开启相机</button>
|
||||
<button onTap="onOpenSettings">授权设置</button>
|
||||
<button onTap="onSystemCamera">使用系统相机</button>
|
||||
<button onTap="onAlbum">从相册选择</button>
|
||||
<view a:if="{{previewUrl}}">
|
||||
<image src="{{previewUrl}}" mode="aspectFit" style="width:100%;height:400rpx;" />
|
||||
<button onTap="onRetake">重新选择</button><button class="btn-primary" onTap="onConfirm">使用照片</button>
|
||||
</view>
|
||||
<view a:if="{{analyzing}}">{{debugMode ? '正在验证本地图片压缩…' : '正在生成您的肌肤报告…'}}</view>
|
||||
</view>
|
||||
<camera a:if="{{cameraEnabled}}" id="skin-camera" class="cam-device" device-position="{{devicePosition}}"
|
||||
flash="off" onError="onCameraError" onStop="onCameraStop">
|
||||
<!-- 顶部步骤文案 -->
|
||||
<cover-view class="cam-step">
|
||||
<cover-view a:if="{{debugMode}}">开发调试 · 不上传照片</cover-view>
|
||||
<cover-view class="cam-step-no">{{stepText}}</cover-view>
|
||||
<cover-view class="cam-step-tip">{{stepTip}}</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 取景蒙版:人脸轮廓(骨架版用椭圆线框,朝向提示随步切换;精确轮廓 TODO:换 cover-image 轮廓图) -->
|
||||
<cover-view class="cam-mask">
|
||||
<cover-view class="cam-oval mask-{{maskStep}}"></cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 底部引导要点 -->
|
||||
<cover-view class="cam-guide">
|
||||
<cover-view class="cam-guide-item" a:for="{{guides}}" a:key="*this">{{item}}</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 控制条:相册 / 快门 / 翻转 -->
|
||||
<cover-view class="cam-controls">
|
||||
<cover-view class="cam-side" onTap="onAlbum">相册</cover-view>
|
||||
<cover-view class="cam-shutter" onTap="onShutter">
|
||||
<cover-view class="cam-shutter-dot"></cover-view>
|
||||
</cover-view>
|
||||
<cover-view class="cam-side" onTap="onFlip">翻转</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 本步完成停顿卡 -->
|
||||
<cover-view class="cam-done" a:if="{{doneTip}}">{{doneTip}}</cover-view>
|
||||
|
||||
<!-- 拍后预览:确认使用 / 重拍 -->
|
||||
<cover-view class="cam-preview" a:if="{{previewUrl}}" catchTap="noop">
|
||||
<cover-image class="cam-preview-img" src="{{previewUrl}}"></cover-image>
|
||||
<cover-view class="cam-preview-btns">
|
||||
<cover-view class="cam-preview-btn" onTap="onRetake">重拍</cover-view>
|
||||
<cover-view class="cam-preview-btn ok" onTap="onConfirm">使用照片</cover-view>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 分析中 -->
|
||||
<cover-view class="cam-analyzing" a:if="{{analyzing}}">
|
||||
<cover-view class="cam-analyzing-text">{{debugMode ? '正在验证本地图片压缩…' : '正在生成您的肌肤报告…'}}</cover-view>
|
||||
<cover-view class="cam-cancel" onTap="onCancel">{{debugMode ? '退出验收' : '返回,稍后查看历史报告'}}</cover-view>
|
||||
</cover-view>
|
||||
</camera>
|
||||
|
||||
<!-- 压缩降级用的隐藏画布(canvas 重绘压缩,不可见) -->
|
||||
<canvas id="compress-canvas" class="compress-canvas" width="{{compressWidth}}" height="{{compressHeight}}" style="width:{{compressWidth}}px;height:{{compressHeight}}px;" />
|
||||
</view>
|
||||
@@ -0,0 +1,186 @@
|
||||
var api = require('../../utils/api'), image = require('../../utils/image'), voice = require('../../utils/voice');
|
||||
var permissions = require('../../utils/permissions');
|
||||
var development = require('../../utils/development');
|
||||
var STEPS = [
|
||||
{ target: 'front', tip: '请单人正脸出镜', turn: '正脸居中', mask: 0 },
|
||||
{ target: 'right', tip: '请右脸出镜', turn: '请向左转头', mask: 2 },
|
||||
{ target: 'left', tip: '请左脸出镜', turn: '请向右转头', mask: 1 }
|
||||
];
|
||||
Page({
|
||||
data: { stepIndex: 0, stepText: '1/3', stepTip: STEPS[0].tip, guides: ['正脸居中', '取下眼镜', '光线充足'],
|
||||
maskStep: 0, devicePosition: 'front', camError: false, previewUrl: '', doneTip: '', analyzing: false,
|
||||
compressWidth: 1, compressHeight: 1, cameraEnabled: false, permissionMessage: '拍摄面部照片用于肌肤检测,请开启相机权限。',
|
||||
debugMode: false, debugComplete: false, debugPhotos: [] },
|
||||
onLoad: function (query) {
|
||||
this.alive = true; this.visible = true; this.photos = []; this.busy = false; this.seq = 0;
|
||||
this.debugMode = !!(query && query.debug === 'camera' && development.cameraDebugEnabled());
|
||||
this.setData({ debugMode: this.debugMode });
|
||||
if (this.debugMode) return;
|
||||
if (!api.user() || !getApp().globalData.profile) my.redirectTo({ url: '/pages/splash/splash' });
|
||||
},
|
||||
onReady: function () {
|
||||
this.pageReady = true;
|
||||
if (this.cameraAuthorized) this.mountCamera();
|
||||
},
|
||||
onShow: function () {
|
||||
this.visible = true;
|
||||
if (!this.data.previewUrl && !this.data.analyzing && !this.data.debugComplete) this.renderStep();
|
||||
var that = this;
|
||||
permissions.settings().then(function (current) {
|
||||
if (!that.alive || !that.visible) return;
|
||||
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
||||
else if (permissions.state(current, 'scope.camera') === false) that.setData({ cameraEnabled: false, permissionMessage: '相机权限未开启,可前往授权设置或使用相册。' });
|
||||
});
|
||||
},
|
||||
mountCamera: function () {
|
||||
if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled || this.data.debugComplete) return;
|
||||
var that = this;
|
||||
this.setData({ cameraEnabled: true, camError: false }, function () {
|
||||
if (!that.alive || !that.visible) return;
|
||||
try {
|
||||
that.cameraCtx = my.createCameraContext('skin-camera');
|
||||
if (!that.cameraCtx || !that.cameraCtx.takePhoto) that.onCameraError();
|
||||
} catch (_) { that.onCameraError(); }
|
||||
});
|
||||
},
|
||||
onEnableCamera: function () {
|
||||
if (this.authorizing) return;
|
||||
var that = this; this.authorizing = true;
|
||||
permissions.ensure('scope.camera').then(function () { that.cameraAuthorized = true; that.mountCamera(); }).catch(function (err) {
|
||||
if (that.alive && that.visible) that.setData({ permissionMessage: err.message });
|
||||
}).then(function () { that.authorizing = false; });
|
||||
},
|
||||
onOpenSettings: function () {
|
||||
var that = this;
|
||||
permissions.open().then(function (current) {
|
||||
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
|
||||
}).catch(function (err) { if (that.alive) my.showToast({ content: err.message, type: 'none' }); });
|
||||
},
|
||||
onHide: function () { this.pause(); },
|
||||
onUnload: function () { this.pause(); this.alive = false; this.photos = []; this.debugResults = []; },
|
||||
pause: function () {
|
||||
this.visible = false; this.seq++; this.busy = false;
|
||||
clearTimeout(this.nextTimer);
|
||||
if (this.signal) this.signal.canceled = true;
|
||||
voice.stop(true);
|
||||
this.cameraCtx = null;
|
||||
if (this.alive) this.setData({ cameraEnabled: false });
|
||||
if (this.alive && this.data.analyzing) this.setData({ analyzing: false, doneTip: '' });
|
||||
},
|
||||
renderStep: function () {
|
||||
var cfg = STEPS[this.data.stepIndex];
|
||||
this.setData({ stepText: (this.data.stepIndex + 1) + '/3', stepTip: cfg.tip,
|
||||
guides: [cfg.turn, '取下眼镜', '光线充足'], maskStep: cfg.mask, doneTip: '' });
|
||||
if (!this.debugMode) voice.play(voice.VOICE.step[this.data.stepIndex]);
|
||||
},
|
||||
onCameraError: function () {
|
||||
this.setData({ camError: true, cameraEnabled: false, permissionMessage: '相机暂不可用,请检查淘宝的系统相机权限,或使用系统相机/相册。' });
|
||||
my.showToast({ content: '相机不可用,请使用系统相机或相册', type: 'none' });
|
||||
},
|
||||
onCameraStop: function () { if (this.alive && this.visible) this.onCameraError(); },
|
||||
onShutter: function () {
|
||||
if (this.busy || this.data.previewUrl || this.data.analyzing) return;
|
||||
if (this.data.camError || !this.cameraCtx) return this.choose(['camera']);
|
||||
var that = this, seq = this.seq;
|
||||
this.busy = true;
|
||||
this.cameraCtx.takePhoto({ quality: 'high', success: function (res) {
|
||||
if (!that.alive || !that.visible || seq !== that.seq) return;
|
||||
var file = res.tempImagePath || res.tempFilePath || res.apFilePath;
|
||||
if (file) that.setData({ previewUrl: file });
|
||||
else my.showToast({ content: '拍摄失败,请重试', type: 'none' });
|
||||
}, fail: function () {
|
||||
if (that.alive && that.visible) my.showToast({ content: '拍摄失败,请重试', type: 'none' });
|
||||
}, complete: function () { if (seq === that.seq) that.busy = false; } });
|
||||
},
|
||||
choose: function (sourceType) {
|
||||
if (this.busy || this.data.previewUrl || this.data.analyzing) return;
|
||||
var that = this;
|
||||
this.busy = true;
|
||||
// chooseImage has its own album scope; native capture also needs camera.
|
||||
permissions.ensure('scope.album').then(function () {
|
||||
if (sourceType[0] === 'camera') return permissions.ensure('scope.camera');
|
||||
}).then(function () {
|
||||
if (!that.alive || !that.visible) { that.busy = false; return; }
|
||||
my.chooseImage({ count: 1, sourceType: sourceType,
|
||||
success: function (res) {
|
||||
var paths = res.apFilePaths || res.tempFilePaths || [];
|
||||
if (that.alive && paths[0]) that.setData({ previewUrl: paths[0] });
|
||||
}, fail: function () { if (that.alive && that.visible) my.showToast({ content: '未选取照片,可重试或检查系统权限', type: 'none' }); },
|
||||
complete: function () { that.busy = false; } });
|
||||
}).catch(function (err) {
|
||||
that.busy = false;
|
||||
if (that.alive && that.visible) { that.setData({ permissionMessage: err.message }); my.showToast({ content: err.message, type: 'none' }); }
|
||||
});
|
||||
},
|
||||
onSystemCamera: function () { this.choose(['camera']); },
|
||||
onAlbum: function () { this.choose(['album']); },
|
||||
onFlip: function () {
|
||||
if (this.busy || this.data.previewUrl || this.data.analyzing) return;
|
||||
this.setData({ devicePosition: this.data.devicePosition === 'front' ? 'back' : 'front' });
|
||||
},
|
||||
onRetake: function () { if (!this.busy) this.setData({ previewUrl: '' }); },
|
||||
onConfirm: function () {
|
||||
if (this.busy || !this.data.previewUrl || this.data.analyzing) return;
|
||||
var that = this, step = this.data.stepIndex;
|
||||
this.busy = true; this.photos[step] = this.data.previewUrl;
|
||||
if (!this.debugMode) voice.play(voice.VOICE.shot, { cut: true });
|
||||
if (step === 2) { this.setData({ previewUrl: '' }); this.submitPhotos(); return; }
|
||||
this.setData({ stepIndex: step + 1, previewUrl: '', doneTip: '拍摄完成。下一张:' + STEPS[step + 1].tip + ',' + STEPS[step + 1].turn });
|
||||
this.nextTimer = setTimeout(function () {
|
||||
if (!that.alive || !that.visible) return;
|
||||
that.busy = false; that.renderStep();
|
||||
}, 2000);
|
||||
},
|
||||
submitPhotos: function () {
|
||||
if (!this.photos[0] || !this.photos[1] || !this.photos[2]) return;
|
||||
var that = this, seq = ++this.seq, dataUrls = [];
|
||||
var debugResults = [];
|
||||
this.signal = { canceled: false }; var signal = this.signal;
|
||||
this.busy = true; this.setData({ analyzing: true, doneTip: '' });
|
||||
if (!this.debugMode) voice.play(voice.VOICE.analyzing);
|
||||
// One shared native canvas: draw the photos sequentially.
|
||||
this.photos.reduce(function (chain, photo) {
|
||||
return chain.then(function () {
|
||||
if (signal.canceled) throw { canceled: true };
|
||||
return image.compressToBase64(photo, that);
|
||||
}).then(function (value) {
|
||||
dataUrls.push(value);
|
||||
if (that.debugMode) debugResults.push({ key: STEPS[debugResults.length].target,
|
||||
title: STEPS[debugResults.length].tip.replace(/^请/, ''), photo: photo,
|
||||
width: that.data.compressWidth, height: that.data.compressHeight,
|
||||
kilobytes: Math.round(value.replace(/^data:[^,]+,/, '').length * 3 / 4 / 1024) });
|
||||
});
|
||||
}, Promise.resolve()).then(function () {
|
||||
if (signal.canceled) throw { canceled: true };
|
||||
if (that.debugMode) {
|
||||
if (!that.alive || !that.visible || seq !== that.seq) return;
|
||||
that.busy = false; that.cameraCtx = null;
|
||||
dataUrls = []; // Do not keep base64, upload, create an account or touch report state.
|
||||
that.setData({ analyzing: false, cameraEnabled: false, debugComplete: true, debugPhotos: debugResults });
|
||||
return;
|
||||
}
|
||||
var ordered = [dataUrls[0], dataUrls[2], dataUrls[1]];
|
||||
return api.analyze(ordered, getApp().globalData.profile, signal).then(function (payload) {
|
||||
if (!that.alive || !that.visible || seq !== that.seq) return;
|
||||
getApp().globalData.photos = ordered; getApp().globalData.payload = payload;
|
||||
voice.stop(true); my.redirectTo({ url: '/pages/report/report' });
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (err.canceled || !that.alive || !that.visible || seq !== that.seq) return;
|
||||
that.busy = false; that.setData({ analyzing: false });
|
||||
if (!that.debugMode) voice.play(voice.VOICE.fail, { cut: true });
|
||||
my.showToast({ content: err.message || '照片处理失败,请重新拍摄', type: 'none', duration: 3000 });
|
||||
});
|
||||
},
|
||||
onDebugRestart: function () {
|
||||
if (!this.debugMode || !development.cameraDebugEnabled()) return;
|
||||
this.photos = []; this.busy = false; this.seq++;
|
||||
this.setData({ debugComplete: false, debugPhotos: [], stepIndex: 0, previewUrl: '', analyzing: false });
|
||||
this.renderStep(); this.onEnableCamera();
|
||||
},
|
||||
onCancel: function () {
|
||||
if (this.debugMode) { this.photos = []; this.setData({ debugPhotos: [], previewUrl: '' }); }
|
||||
this.pause(); my.navigateBack();
|
||||
},
|
||||
noop: function () {}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
"titleBarColor": "#101823",
|
||||
"backgroundColor": "#101823",
|
||||
"disableScroll": true
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
page { background: #EFEFEF; }
|
||||
.history { padding-bottom: 48rpx; }
|
||||
.history-note { padding: 32rpx; color: #666; text-align: center; }
|
||||
.history-item { line-height: 1.9; }
|
||||
.history-open { color: #666; text-align: right; }
|
||||
@@ -0,0 +1,10 @@
|
||||
<view class="history">
|
||||
<view class="history-note">最近5份检测报告</view>
|
||||
<view a:if="{{loading}}" class="history-note">加载中…</view>
|
||||
<view a:if="{{error}}" class="history-note" onTap="load">{{error}},点击重试</view>
|
||||
<view a:if="{{!loading && !error && !items.length}}" class="history-note">暂无检测报告</view>
|
||||
<view class="card history-item" a:for="{{items}}" a:key="id" data-id="{{item.id}}" onTap="onOpen">
|
||||
<view>{{item.date}}</view><view>综合评分 {{item.total}} · 肌肤年龄 {{item.skinAge}} 岁</view>
|
||||
<view class="history-open">查看报告 ›</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,34 @@
|
||||
var api = require('../../utils/api');
|
||||
Page({
|
||||
data: { items: [], loading: false, error: '' },
|
||||
onShow: function () {
|
||||
if (!api.user()) { this.setData({ items: [] }); my.redirectTo({ url: '/pages/splash/splash' }); return; }
|
||||
this.load();
|
||||
},
|
||||
onUnload: function () { this.closed = true; },
|
||||
load: function () {
|
||||
var that = this;
|
||||
this.setData({ loading: true, error: '' });
|
||||
api.request('reports').then(function (data) {
|
||||
if (that.closed) return;
|
||||
that.setData({ items: data.items.map(function (item) {
|
||||
var date = new Date(item.created_at);
|
||||
return { id: item.id, total: item.total == null ? '--' : item.total, skinAge: item.skinAge == null ? '--' : item.skinAge,
|
||||
date: date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() };
|
||||
}) });
|
||||
}).catch(function (err) { if (!that.closed) that.setData({ error: err.message }); })
|
||||
.then(function () { if (!that.closed) that.setData({ loading: false }); });
|
||||
},
|
||||
onOpen: function (e) {
|
||||
if (this.data.loading) return;
|
||||
var that = this;
|
||||
this.setData({ loading: true });
|
||||
api.request('reports', { id: e.currentTarget.dataset.id }).then(function (data) {
|
||||
if (that.closed) return;
|
||||
getApp().globalData.photos = data.report.photos;
|
||||
getApp().globalData.payload = Object.assign({}, data.report.payload, { vm: data.report.vm, presentation: data.report.presentation });
|
||||
my.navigateTo({ url: '/pages/report/report' });
|
||||
}).catch(function (err) { my.showToast({ content: err.message, type: 'none' }); })
|
||||
.then(function () { if (!that.closed) that.setData({ loading: false }); });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "defaultTitle": "历史报告", "backgroundColor": "#EFEFEF" }
|
||||
@@ -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; }
|
||||
@@ -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>
|
||||
@@ -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 () {}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
"titleBarColor": "#101823",
|
||||
"backgroundColor": "#101823"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/* ═════ 启动页样式 ═════ */
|
||||
.splash {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.splash-bg {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.splash-slogan {
|
||||
position: absolute;
|
||||
top: 200rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.splash-slogan text {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.splash-bottom {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
padding: 0 64rpx 96rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.brand-logo {
|
||||
height: 48rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.brand-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 隐私授权弹窗 ─── */
|
||||
.privacy-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.checkbox {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid #E5E5E5;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.checkbox.checked {
|
||||
background: #F5F078;
|
||||
border-color: #F5F078;
|
||||
}
|
||||
.checkbox.checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 6rpx;
|
||||
width: 8rpx;
|
||||
height: 14rpx;
|
||||
border-right: 4rpx solid #282828;
|
||||
border-bottom: 4rpx solid #282828;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.privacy-text {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
}
|
||||
.privacy-link {
|
||||
color: #282828;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.popup-btn { width: 100%; }
|
||||
|
||||
/* ─── 隐私协议全文弹层 ─── */
|
||||
.overlay-policy { padding: 0; }
|
||||
.policy-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.policy-back {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
left: 24rpx;
|
||||
z-index: 2;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
line-height: 56rpx;
|
||||
font-size: 56rpx;
|
||||
color: #282828;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.policy-back::after { border: none; }
|
||||
.policy-scroll {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 96rpx 40rpx 64rpx;
|
||||
}
|
||||
.policy-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.policy-h2 {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin: 40rpx 0 16rpx;
|
||||
}
|
||||
.policy-p {
|
||||
font-size: 26rpx;
|
||||
color: #4A4A4A;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.policy-end {
|
||||
text-align: center;
|
||||
color: #868686;
|
||||
font-size: 24rpx;
|
||||
margin: 48rpx 0;
|
||||
}
|
||||
.account-links { display: flex; justify-content: center; gap: 40rpx; color: #fff; margin-top: 24rpx; font-size: 24rpx; }
|
||||
.splash-slogan { top: 10.7%; }
|
||||
.splash-slogan text { font-size: 44rpx; line-height: 1.4; }
|
||||
.brand-row { justify-content: center; }
|
||||
.brand-logo { width: 450rpx; height: 52rpx; margin: 0; }
|
||||
.camera-debug-entry { position: absolute; z-index: 2; top: 24rpx; right: 24rpx; padding: 16rpx 24rpx; border-radius: 16rpx; color: #fff; background: rgba(0,0,0,.65); font-size: 24rpx; }
|
||||
@@ -0,0 +1,82 @@
|
||||
<!-- ═════ 01 启动页 ═════ -->
|
||||
<view class="splash">
|
||||
<view class="camera-debug-entry" a:if="{{showCameraDebug}}" onTap="onCameraDebug">开发调试 · 相机验收</view>
|
||||
<image class="splash-bg" src="{{bgUrl}}" mode="aspectFill" />
|
||||
<view class="splash-slogan">
|
||||
<text>1'me,</text>
|
||||
<text>FROM THE INSIDE OUT</text>
|
||||
<text>以我为始 自有表现</text>
|
||||
</view>
|
||||
|
||||
<view class="splash-bottom">
|
||||
<view class="brand-row">
|
||||
<image src="{{logoUrl}}" class="brand-logo" mode="heightFix" />
|
||||
</view>
|
||||
<button class="btn-primary splash-btn" onTap="onOpenPrivacy">开始检测</button>
|
||||
<view a:if="{{signedIn}}" class="account-links">
|
||||
<text onTap="onHistory">历史报告</text><text onTap="onLogout">退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹窗:隐私授权 ═══ -->
|
||||
<view class="overlay" a:if="{{showPrivacy}}" onTap="onClosePrivacy">
|
||||
<view class="popup" catchTap="noop">
|
||||
<button class="popup-close" onTap="onClosePrivacy">✕</button>
|
||||
<view class="popup-title">
|
||||
<text>欢迎体验1'me</text>
|
||||
<text>肌肤状态评估服务!</text>
|
||||
</view>
|
||||
<view class="popup-body">授权淘宝账号后,我们将采集您的面部影像和基础档案,用于生成肌肤分析报告,并在服务器保存检测记录及照片,供您查看最近5份报告。照片将交由 Face⁺⁺ 提供检测分析。</view>
|
||||
<view class="privacy-row" onTap="onToggleAgree">
|
||||
<view class="checkbox {{agreed ? 'checked' : ''}}"></view>
|
||||
<view class="privacy-text">
|
||||
我已阅读并同意
|
||||
<text class="privacy-link" catchTap="onOpenPolicy">相关隐私协议</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn-primary popup-btn" loading="{{busy}}" disabled="{{busy}}" onTap="onAgreeContinue">授权登录并继续</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ═══ 弹层:隐私政策全文(Face++ 个人信息与隐私保护政策,与 H5 同文) ═══ -->
|
||||
<view class="overlay overlay-policy" a:if="{{showPolicy}}">
|
||||
<view class="policy-page">
|
||||
<button class="policy-back" onTap="onClosePolicy">‹</button>
|
||||
<scroll-view scroll-y class="policy-scroll">
|
||||
<view class="policy-title">Face⁺⁺个人信息与隐私保护政策</view>
|
||||
|
||||
<view class="policy-h2">前言</view>
|
||||
<view class="policy-p">北京旷视科技有限公司及其关联公司(以下简称"我们"或"旷视")尊重并致力于保护个人信息与隐私。Face⁺⁺人工智能开放平台(以下简称"本平台")和/或Face⁺⁺人工智能开放平台服务(以下简称"本服务")作为旷视面向开发者的视觉智能开放平台与服务,严格参照《旷视个人信息与隐私保护政策》中的统一要求和标准处理相关个人信息;为进一步明确在本平台和本服务中可能涉及的个人信息处理实践情况,我们特制定了Face⁺⁺个人信息与隐私保护政策(以下简称"Face⁺⁺政策"),以介绍本平台和本服务中收集、使用、留存等个人信息的具体处理。</view>
|
||||
<view class="policy-p">Face⁺⁺政策拟帮助客户和用户了解:我们如何收集使用个人信息、如何保留和披露、如何保护、个人信息在全球范围内的转移、您的个人信息权利、如何处理儿童的个人信息、如何联系我们、政策如何更新。</view>
|
||||
|
||||
<view class="policy-h2">一、我们如何收集使用个人信息</view>
|
||||
<view class="policy-p">旷视将与客户一道,在符合《个人信息保护法》第十三条规定的合法性基础的前提下,确保所收集和处理的个人信息已取得您的完整、有效授权同意。</view>
|
||||
<view class="policy-p">为提供人脸和人体图像识别相关技术的能力,我们将基于客户主动提供的原始图片、视频等数据进一步识别和提取面部识别特征、人体区域、人体特征和关键点、手势等生物识别信息,用于实现人脸比对、人脸检测、人脸搜索、人脸美颜、人脸融合等具体功能。请您理解,如您拒绝提供原始图片、视频等数据,前述功能应用将无法实现。</view>
|
||||
<view class="policy-p">提请您注意的是,该等功能项下处理的面部识别特征等生物识别信息属于个人敏感信息,只有在具有特定的目的和充分的必要性、并采取严格保护措施的情形下,我们才会进行处理。我们将依法向您单独告知处理该等敏感个人信息的必要性以及对您个人权益的影响,并在您通过弹窗、勾选等明示方式单独同意后,方进行该等处理;您拒绝单独同意的,我们将不处理您的上述生物识别信息,但不影响您使用本平台和本服务的其他功能。</view>
|
||||
|
||||
<view class="policy-h2">二、我们如何保留和披露您的个人信息</view>
|
||||
<view class="policy-p">除法律法规另有规定外,我们将在实现处理目的所必要的最短时间内保留您的个人信息,并通过本政策或相关服务规则向您明确告知各类个人信息的具体保存期限。在本平台和本服务下,我们主要提供技术能力的具体应用,因此在具体功能实现后,对于客户提供的原始图片、视频等数据以及基于该等原始数据形成的服务成果,我们将尽快删除或进行匿名化处理。此外,根据《网络安全法》等强制性法律法规要求,我们将切实保留网络日志不少于六个月。</view>
|
||||
|
||||
<view class="policy-h2">三、我们如何保护您的个人信息</view>
|
||||
<view class="policy-p">我们已采取了合理、可行的管理措施和技术措施,以保护所处理的个人信息并应对个人信息安全事件,包括:及时对个人信息进行去标识化处理;定期审查个人信息处理方式并不断强化 API 和 SDK 等技术工具的安全性;实施传输全程安全加密等保障手段。如发生或者可能发生个人信息泄露、篡改、丢失的,我们将立即采取补救措施,并按照法律法规要求及时通知您。</view>
|
||||
|
||||
<view class="policy-h2">四、个人信息在全球范围内的转移</view>
|
||||
<view class="policy-p">我们的服务器位于中国和加拿大。由于中国法律法规要求,原则上,在中国境内收集和产生的个人信息将统一在中国境内服务器进行处理。对于中国境外的业务,根据客户的选择,您的个人信息可能转移至位于中国或加拿大的服务器上进行处理。如确有必要向境外提供您的个人信息,我们将遵守《个人信息保护法》及数据出境相关法律法规的规定。</view>
|
||||
|
||||
<view class="policy-h2">五、您的个人信息权利</view>
|
||||
<view class="policy-p">您依法享有知情权、访问权、更正权、删除权、注销账户权、拒绝自动决策权、解释权、可携权、查询权、撤销权。您可以通过具体产品或服务中设置的权限管理或隐私设置功能直接撤回相应授权,也可以通过发送邮件至 business@megvii.com 提交相关请求,我们将在核实您的身份后及时处理。</view>
|
||||
|
||||
<view class="policy-h2">六、我们如何处理儿童的个人信息</view>
|
||||
<view class="policy-p">我们的所有产品、网站和服务主要面向企业客户,并不会自行主动收集、处理儿童个人信息。如我们发现任何客户或用户在未经父母或监护人同意的情况下提供包含儿童个人信息的原始数据,我们将立即删除该等原始数据并不会就该等数据提供任何服务。为免歧义,我们将不满14周岁的任何人均视为儿童。</view>
|
||||
|
||||
<view class="policy-h2">七、如何联系我们</view>
|
||||
<view class="policy-p">如您对本政策及您的个人信息存在任何相关问题、申/投诉,或您需要任何个人信息权利相关的协助,请及时联系我们。您可以通过向 business@megvii.com 发送邮件联系我们,并提供您的姓名(名称)和联系方式、详细请求及相应链接。一般而言,我们会在15个工作日内或法律法规规定的期限内尽快回复。</view>
|
||||
|
||||
<view class="policy-h2">八、Face⁺⁺政策如何更新</view>
|
||||
<view class="policy-p">我们保留不时更新或修改 Face⁺⁺政策的权利。但未经您明确同意,我们不会削减您按照 Face⁺⁺政策所应享有的权利。对于重大变更,我们会提供更为显著的通知。您可通过本页面查看最新版本的 Face⁺⁺政策。</view>
|
||||
|
||||
<view class="policy-end">—— 全文完 ——</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,70 @@
|
||||
/* ═════ 启动页:品牌底图 + Slogan + 隐私授权弹窗(对齐 H5 #s-splash + #p-privacy) ═════ */
|
||||
var api = require('../../utils/api.js');
|
||||
var development = require('../../utils/development');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bgUrl: api.assetUrl('img/bg-splash.jpg'), // 底图远程加载
|
||||
logoUrl: api.assetUrl('img/ui-20261012/brand-lockup.png'),
|
||||
showPrivacy: false, // 隐私授权弹窗
|
||||
agreed: false, // 隐私协议勾选
|
||||
showPolicy: false, busy: false, signedIn: false, showCameraDebug: development.cameraDebugEnabled()
|
||||
},
|
||||
onShow: function () {
|
||||
var that = this;
|
||||
api.restore().then(function (user) { that.setData({ signedIn: !!user }); }).catch(function () {});
|
||||
},
|
||||
|
||||
// 「开始检测」→ 隐私授权弹窗
|
||||
onOpenPrivacy: function () {
|
||||
if (api.user()) { my.navigateTo({ url: '/pages/profile/profile' }); return; }
|
||||
this.setData({ showPrivacy: true });
|
||||
},
|
||||
onClosePrivacy: function () {
|
||||
this.setData({ showPrivacy: false });
|
||||
},
|
||||
|
||||
// 勾选/取消隐私协议(点链接不触发,由 catchtap 分隔)
|
||||
onToggleAgree: function () {
|
||||
this.setData({ agreed: !this.data.agreed });
|
||||
},
|
||||
|
||||
onOpenPolicy: function () {
|
||||
this.setData({ showPolicy: true });
|
||||
},
|
||||
onClosePolicy: function () {
|
||||
this.setData({ showPolicy: false });
|
||||
},
|
||||
|
||||
// 「继续」:必须已勾选协议
|
||||
onAgreeContinue: function () {
|
||||
if (this.data.busy) return;
|
||||
if (!this.data.agreed) {
|
||||
my.showToast({ content: '请先阅读并同意隐私协议', type: 'none' });
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
this.setData({ busy: true });
|
||||
api.login(true).then(function () {
|
||||
that.setData({ showPrivacy: false, signedIn: true });
|
||||
my.navigateTo({ url: '/pages/profile/profile' });
|
||||
}).catch(function (err) {
|
||||
my.showToast({ content: err.message, type: 'none' });
|
||||
}).then(function () { that.setData({ busy: false }); });
|
||||
},
|
||||
onHistory: function () {
|
||||
if (!api.user()) { this.onOpenPrivacy(); return; }
|
||||
my.navigateTo({ url: '/pages/history/history' });
|
||||
},
|
||||
onLogout: function () {
|
||||
var that = this;
|
||||
api.logout().then(function () { that.setData({ signedIn: false, agreed: false }); })
|
||||
.catch(function (err) { my.showToast({ content: err.message, type: 'none' }); });
|
||||
},
|
||||
onCameraDebug: function () {
|
||||
if (!development.cameraDebugEnabled()) return;
|
||||
my.navigateTo({ url: '/pages/camera/camera?debug=camera' });
|
||||
},
|
||||
|
||||
noop: function () {} // 弹窗卡片内点击拦截冒泡用
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "1'me在线肌肤测试",
|
||||
"titleBarColor": "#101823",
|
||||
"backgroundColor": "#101823"
|
||||
}
|
||||
Reference in New Issue
Block a user