Files
juzi/taobao/pages/camera/camera.js
T
xsl fed52930cc feat(miniapp): align UI with H5 and harden device debugging
五个页面按 h5/css 的 style.css → ui-20261009 → ui-20261012 覆盖顺序逐条重排,
换算规则固定为 calc(N * var(--u)) → 2N rpx,固定 px 的字号保持 px。

- app.acss:换回 H5 设计 token(方角小倒角按钮 10rpx、卡片 14rpx、.chev 折线箭头)
- splash/profile/history:底图与按钮文字图改用 H5 资源,隐私/政策弹窗按 H5 重排
- report:环形分用 conic-gradient、人脸裁切窗口叠加 face_maps、标记点百分比盒、
  侧脸皱纹 pending/unavailable 文案;用 Playwright 抓 H5 94 个选择器几何比对,
  整页高 2438.7 vs 2438.67
- camera:淘宝 <camera> 组件在真机整页崩溃(嵌了微信专有的 <cover-view>),
  改为默认系统相机 + `config.nativeCamera` 开关 + pages/camtest 冒烟页验证组件
- permissions:淘宝用带前缀的 scope.camera(裸名返回 5022 Scope:camera信息不存在),
  先试带前缀再退回裸名,拒绝后不再重复弹窗
- voice:非 HTTPS 资源导致的异常不再冒泡成整页崩溃;app.js 加开发版错误 toast
- 开发版入口:权限诊断(启动页/相机页)、相机组件测试
- 测试 20/20(新增权限与相机能力用例)
2026-11-01 23:30:38 +08:00

216 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var api = require('../../utils/api'), image = require('../../utils/image'), voice = require('../../utils/voice');
var permissions = require('../../utils/permissions');
var development = require('../../utils/development');
var config = require('../../config');
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: [],
showDiagnose: config.environment !== 'production', diagText: '', cameraSupported: false },
onLoad: function (query) {
this.alive = true; this.visible = true; this.photos = []; this.busy = false; this.seq = 0;
var supported = config.nativeCamera === true &&
(typeof permissions.cameraSupported === 'function' ? permissions.cameraSupported() : true);
this.cameraSupported = supported;
this.debugMode = !!(query && query.debug === 'camera' && development.cameraDebugEnabled());
this.setData({ debugMode: this.debugMode, cameraSupported: supported });
if (this.debugMode) return;
var app = typeof getApp === 'function' ? getApp() : null;
if (!api.user() || !app || !app.globalData.profile) { my.redirectTo({ url: '/pages/splash/splash' }); return; }
if (!supported) {
// 淘宝 <camera> 组件不可用:直接用系统相机/相册,页面不再挂相机组件(否则整页崩溃)
this.setData({ cameraEnabled: false, camError: true,
permissionMessage: '请用下方按钮拍摄,三张依次为正脸、右脸、左脸。' });
}
},
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();
if (!this.cameraSupported) return; // 平台没有内置相机:直接用系统相机/相册
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: '相机权限未开启,可前往授权设置或使用相册。' });
}).catch(function () {});
},
mountCamera: function () {
if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled || this.data.debugComplete) return;
if (!this.cameraSupported) 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' }); });
},
onCloseDiagnose: function () { this.setData({ diagText: '' }); },
// 开发版:把真机上的相机能力/授权返回值直接打在页面上,供截图定位
onDiagnose: function () {
var that = this;
this.setData({ diagText: '检测中…' });
permissions.diagnose().then(function (info) {
if (that.alive) that.setData({ diagText: info.join('\n') });
}).catch(function (err) {
if (that.alive) that.setData({ diagText: 'diagnose error: ' + (err && err.message ? err.message : String(err)) });
});
},
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 || seq !== that.seq) return;
// 真机上内置相机 takePhoto 失败(机型/组件差异、权限被拒等):
// 不打断流程,直接退回系统相机走 my.chooseImage 那条已验证过的路。
that.busy = false;
my.showToast({ content: '内置相机不可用,改用系统相机', type: 'none' });
that.choose(['camera']);
}, 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 () {}
});