feat: 完善淘宝小程序拍照、报告与登录流程

接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
xsl
2026-11-04 15:07:51 +08:00
parent 381da60ae4
commit 774376fa16
147 changed files with 17420 additions and 597 deletions
+40 -20
View File
@@ -1,20 +1,40 @@
.wrap {
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
background: #000;
}
.cam {
position: absolute;
left: 0; top: 0;
width: 100%;
height: 100%;
}
.tip {
position: absolute;
left: 0; right: 0;
bottom: 80rpx;
text-align: center;
color: #fff;
font-size: 13px;
z-index: 2;
}
.wrap { position: absolute; left: 0; top: 0; width:100vw; height:100vh; padding-top:calc(112px + 240rpx); box-sizing:border-box; background: #000; overflow-y: auto; overflow-x:hidden; }
.cam { position: fixed; left: 0; top: 0; width: 100%; height: 100%; }
.diag-toolbar { position: fixed; z-index: 20; left: 0; top: 112px; width: 100%; box-sizing: border-box; padding: 12rpx 24rpx; background: #ffe56c; color: #211800; box-shadow: 0 6rpx 18rpx rgba(0,0,0,.4); }
.toolbar-version { font-size: 11px; line-height: 1.5; margin-bottom: 8rpx; }
.native-probe-entry { margin:0 0 12rpx; padding:0 12rpx; height:72rpx; line-height:68rpx; background:#126597; color:#fff; font-size:14px; font-weight:700; }
.toolbar-actions { display:flex; }
.copy-diagnostics, .export-photo { flex:1; width:0; height:72rpx; line-height:68rpx; box-sizing:border-box; margin:0; padding:0 8rpx; background:#ffda00; color:#211800; border:2rpx solid #705500; font-size:14px; font-weight:700; }
.export-photo { margin-left:12rpx; background:#fff7d3; }
.export-photo[disabled] { background:#ece3ac; color:#817c61; border-color:#b6ac78; }
.copy-status { margin-top: 8rpx; font-size: 11px; line-height: 1.5; }
.copy-fallback { margin-top: 8rpx; padding: 12rpx; background: #fffbe9; border-radius: 8rpx; }
.copy-help { margin-bottom: 8rpx; font-size: 10px; line-height: 1.5; }
.copy-text-scroll { height: 240rpx; }
.copy-text { font-size: 11px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; user-select: text; }
.probe { position: relative; z-index: 2; width:702rpx; max-width:calc(100vw - 48rpx); box-sizing:border-box; margin: 24rpx; padding: 24rpx; border-radius: 12rpx; background: rgba(0, 0, 0, .76); color: #fff; }
.title { font-size: 18px; font-weight: 600; margin-bottom: 16rpx; }
.version { font-size: 12px; color: #f5f078; margin-top: 8rpx; }
.diagnostics { font-size: 10px; line-height: 1.6; margin-top: 12rpx; color: #c7e0f5; word-break: break-all; }
.diagnostics text { white-space: pre-wrap; }
.note { font-size: 11px; line-height: 1.6; color: #ddd; margin-top: 12rpx; word-break: break-all; }
.facing { margin-top: 16rpx; color: #f5f078; font-size: 13px; line-height: 1.6; }
.actions { display: flex; flex-wrap: wrap; margin: 20rpx 0; }
.actions button { font-size: 12px; margin: 0 12rpx 12rpx 0; padding: 0 16rpx; max-width:100%; }
.calibration { margin: 16rpx 0; padding: 12rpx; border: 1rpx solid #777; border-radius: 8rpx; }
.calibration-optional { margin-top: 12rpx; padding: 0 12rpx; font-size: 11px; }
.calibration-confirmed { margin-top: 12rpx; font-size: 12px; line-height: 1.6; color: #bceaa7; }
.calibration-grid { display: flex; flex-wrap: wrap; margin-top: 16rpx; }
.calibration-card { width: calc(50% - 12rpx); margin: 0 12rpx 16rpx 0; box-sizing: border-box; background: #161616; }
.calibration-format { padding: 8rpx; font-size: 12px; text-align: center; color: #f5f078; }
.calibration-image { width: 100%; height: 320rpx; background: #222; }
.calibration-card button { padding: 0 6rpx; margin: 8rpx 0 0; font-size: 11px; }
.targets { display: flex; flex-wrap: wrap; }
.target { margin: 0 12rpx 12rpx 0; padding: 12rpx; font-size: 11px; border: 1rpx solid #777; border-radius: 8rpx; }
.target.active { border-color: #f5f078; color: #f5f078; }
.status { font-size: 14px; line-height: 1.6; }
.error { margin-top: 16rpx; padding: 12rpx; border: 1rpx solid #e79384; color: #ffd1c7; font-size: 11px; line-height: 1.6; word-break: break-all; }
.fields { max-height: 240rpx; margin-top: 16rpx; }
.fields text { font-size: 10px; line-height: 1.6; white-space: pre-wrap; color: #c7e0f5; }
.snapshot { margin-top: 20rpx; width: 100%; height: 440rpx; background: #111; }
.compress-canvas { position: fixed; left: -10000px; top: -10000px; pointer-events: none; }
+63 -5
View File
@@ -1,6 +1,64 @@
<!-- 相机组件冒烟测试:页面里只有一个 <camera>,不放任何嵌套覆盖组件。
用来判断淘宝真机到底支不支持相机组件(之前整页崩,怀疑是嵌了微信专有的 <cover-view>)。 -->
<view class="wrap">
<camera id="test-camera" class="cam" device-position="front" flash="off" onError="onError" />
<view class="tip">如果能看到实时画面 → 相机组件可用</view>
<view class="wrap" style="padding-top:calc({{toolbarTop}}px + 240rpx);">
<!-- Uses the documented Taobao component, only after camera permission. -->
<ar-camera a:if="{{running}}" id="test-ar-camera-{{cameraSession}}" data-session="{{cameraSession}}" class="cam" mode="faceDetection2D" useCapturedImage="{{true}}" devicePosition="{{devicePosition}}"
onInit="onARInit" onARFrame="onARFrame" onStop="onARStop" onError="onError" />
<view class="diag-toolbar" style="top:{{toolbarTop}}px;">
<view class="toolbar-version">AR 相机诊断 · {{probeVersion}}</view>
<button class="native-probe-entry" onTap="onOpenNativeProbe" disabled="{{busy || exporting}}">进入普通前置相机测试</button>
<view class="toolbar-actions">
<button class="copy-diagnostics" onTap="onCopyDiagnostics">复制诊断信息</button>
<button class="export-photo" onTap="onSnapshot" disabled="{{!running || busy || exporting}}">导出照片</button>
</view>
<view class="copy-status" a:if="{{copyStatus}}">{{copyStatus}}</view>
<view class="copy-fallback" a:if="{{diagnosticText}}">
<view class="copy-help">长按文字可选择复制;再次点击上方按钮可更新。仅含安全诊断,不含照片或账号信息。</view>
<scroll-view class="copy-text-scroll" scroll-y><text class="copy-text" selectable="{{true}}">{{diagnosticText}}</text></scroll-view>
</view>
</view>
<view class="probe">
<view class="title">AR 相机真机验收</view>
<view class="version">诊断版本 {{probeVersion}}</view>
<view class="note">只检测本机实时帧与照片导出;不登录账号、不上传、不生成报告。</view>
<view class="diagnostics"><text>{{environmentText}}</text></view>
<view class="note">{{capabilityText}}</view>
<view class="facing">请求{{devicePosition === 'front' ? '前置' : '后置'}}镜头 · 请核对实际画面</view>
<view class="note">切换将重新挂载相机;方向属性是否生效需在手机核验。原始人脸坐标未作镜像,请分别向左右转头并核对提示。</view>
<view class="actions">
<button onTap="onStart" disabled="{{running || busy}}">开启 AR 相机</button>
<button onTap="onFlip" disabled="{{!running}}">切换镜头</button>
<button onTap="onStop" disabled="{{!running}}">停止</button>
</view>
<view class="calibration">
<view class="note">高级可选:校验照片颜色。可直接使用顶部“导出照片”。</view>
<button class="calibration-optional" onTap="onCalibrate" disabled="{{!running || busy || exporting}}">高级:校验照片颜色</button>
<view class="note">此功能只在本机生成同一帧的候选预览,不上传、不保存,可根据实际肤色、衣物和背景选择。</view>
<view class="calibration-confirmed" a:if="{{calibrationConfirmed}}">本次运行已确认格式:{{calibrationConfirmed}}</view>
<view class="note" a:if="{{calibrationText}}">{{calibrationText}}</view>
<view class="calibration-grid" a:if="{{calibrationItems.length}}">
<view class="calibration-card" a:for="{{calibrationItems}}" a:key="format">
<view class="calibration-format">{{item.format}}</view>
<image class="calibration-image" src="{{item.url}}" mode="aspectFit" />
<button data-format="{{item.format}}" data-batch="{{item.batchId}}" onTap="onConfirmCalibration" disabled="{{!calibrationReady}}">这张颜色正常</button>
</view>
</view>
</view>
<view class="note">只读取 context 公开方法的属性描述符,不执行 getter 或未知方法。复制内容不包含照片、人脸坐标或账号信息。</view>
<view class="diagnostics"><text>Context 方法:{{contextText}}</text></view>
<view class="diagnostics"><text>{{eventText}}</text></view>
<view class="targets">
<view class="target {{target === 'front' ? 'active' : ''}}" data-target="front" onTap="onTarget">正脸</view>
<view class="target {{target === 'right' ? 'active' : ''}}" data-target="right" onTap="onTarget">右脸(向左转头)</view>
<view class="target {{target === 'left' ? 'active' : ''}}" data-target="left" onTap="onTarget">左脸(向右转头)</view>
</view>
<view class="status">{{status}}</view>
<view class="note">{{frameText}} · 回调 {{frameCount}} 次 · 标准帧 {{standardFrameCount}} 次</view>
<view class="note" a:if="{{poseText}}">{{poseText}}</view>
<view class="error" a:if="{{errorText}}">{{errorText}}</view>
<scroll-view class="fields" scroll-y a:if="{{fieldText}}"><text>{{fieldText}}</text></scroll-view>
<view class="note">收到人脸 anchor 不代表能够导出照片。未知帧格式将明确报错;请核对预览是否为本次本人、左右方向和肤色是否正常。</view>
<image class="snapshot" a:if="{{snapshotUrl}}" src="{{snapshotUrl}}" mode="aspectFit" />
<view class="note" a:if="{{snapshotText}}">{{snapshotText}}</view>
</view>
<!-- Keep this canvas mounted and laid out for actual device exports. -->
<canvas id="compress-canvas" canvas-id="compress-canvas" width="{{compressWidth}}" height="{{compressHeight}}" class="compress-canvas" style="width:{{compressWidth}}px;height:{{compressHeight}}px" />
</view>
+313 -6
View File
@@ -1,10 +1,317 @@
// Standalone device probe. No account API, upload, server call or saved photo.
var AR = require('../../utils/ar-camera');
var image = require('../../utils/image');
var permissions = require('../../utils/permissions');
var config = require('../../config');
var PROBE_VERSION = 'AR-DIAG-20261102-05';
var CALIBRATION_FORMATS = ['RGBA', 'BGRA', 'ARGB', 'ABGR'];
function confirmedPixelFormat() {
try {
var format = image.confirmedFramePixelFormat();
return CALIBRATION_FORMATS.indexOf(format) >= 0 ? format : '';
} catch (_) { return ''; }
}
Page({
data: {},
data: { running: false, busy: false, exporting: false, devicePosition: config.arFrontPosition === 'back' ? 'back' : 'front', cameraSession: 0, status: '请点击开启,验证淘宝真实 AR 帧与照片导出。',
probeVersion: PROBE_VERSION, toolbarTop: 112, environmentText: '', capabilityText: '', contextText: '等待 onInit 后读取公开方法名', eventText: '尚未收到原生事件', copyStatus: '', diagnosticText: '',
target: 'front', frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件',
calibrationItems: [], calibrationReady: false, calibrationText: '', calibrationConfirmed: '',
poseText: '', fieldText: '', errorText: '', snapshotUrl: '', snapshotText: '', compressWidth: 1, compressHeight: 1 },
onLoad: function () {
this.alive = true;
this.alive = true; this.visible = true; this.sequence = 0;
var capability = AR.capability(my);
this.diagnosticSystem = AR.diagnostics.system(my); this.contextMethodNames = []; this.nativeEventStats = {};
this.diagnosticCapability = 'ar-camera=' + capability.componentSignal + '; createArCameraContext=' + capability.contextFactory;
this.setData({ toolbarTop: AR.diagnostics.toolbarTop(my), calibrationConfirmed: confirmedPixelFormat(), environmentText: this.diagnosticSystem.join('\n') || '系统未返回可读取的设备/宿主版本;请手动补充淘宝版本。',
capabilityText: 'ar-camera 声明:' + capability.componentSignal + ';上下文工厂:' + (capability.contextFactory ? '存在' : '不存在') + '。声明结果不代表真机已支持。' });
},
onUnload: function () { this.alive = false; },
onError: function (err) {
my.showToast({ content: '相机组件报错: ' + JSON.stringify(err || {}), type: 'none', duration: 6000 });
}
onShow: function () { this.visible = true; },
flushFrameDiagnostic: function () {
var pending = this.pendingFrameDiagnostic;
if (!pending) return;
this.pendingFrameDiagnostic = null;
var record = this.nativeEventStats[pending.key], summary = AR.diagnostics.event(pending.event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
},
nativeEventView: function () {
var that = this;
return { eventText: Object.keys(this.nativeEventStats).map(function (item) {
var value = that.nativeEventStats[item];
return item + ' · type=' + value.type + ' · 次数 ' + value.count;
}).join('\n'), fieldText: (this.lastDiagnosticMetadata || []).join('\n') || '本事件未返回白名单元信息;不会输出未知字段值或人脸坐标。' };
},
recordNativeEvent: function (name, event, deferFrame) {
var key = name + ':request=' + this.data.devicePosition;
var record = this.nativeEventStats[key] || { count: 0, type: '(未提供 type)', metadata: [] };
record.count++;
this.nativeEventStats[key] = record;
// Count every frame in JS, but do not scan descriptors or cross the UI
// bridge until the 400ms refresh. Only the latest frame is retained.
if (deferFrame) { this.pendingFrameDiagnostic = { key: key, event: event }; return; }
this.flushFrameDiagnostic();
var summary = AR.diagnostics.event(event);
record.type = summary.type || '(未提供 type)'; record.metadata = summary.metadata;
this.lastDiagnosticMetadata = summary.metadata;
this.setData(this.nativeEventView());
return summary;
},
diagnosticReport: function () {
// A user-requested copy gets up-to-date counters and last-frame metadata
// without introducing another UI refresh on the high-rate native stream.
this.flushFrameDiagnostic();
var that = this, lines = [PROBE_VERSION].concat(this.diagnosticSystem || []);
lines.push(this.diagnosticCapability || 'capability=unavailable');
lines.push('requestedPosition=' + this.data.devicePosition + '; actualPosition=需核对实际画面');
lines.push('context.methods=' + ((this.contextMethodNames || []).join(', ') || '(none observed)'));
lines.push('pixelFormat.default=' + (CALIBRATION_FORMATS.indexOf(config.arPixelFormat) >= 0 ? config.arPixelFormat : '(none)') + '; defaultIsUserConfirmed=false');
lines.push('pixelFormat.confirmed=' + (confirmedPixelFormat() || '(unconfirmed)'));
lines.push('currentSession.frameEvents=' + (this.frames || 0) + '; parsableFrames=' + (this.standardFrames || 0));
Object.keys(this.nativeEventStats || {}).forEach(function (key) {
var record = that.nativeEventStats[key];
lines.push(key + '; type=' + record.type + '; count=' + record.count);
record.metadata.forEach(function (item) { lines.push(' ' + item); });
});
return lines.join('\n');
},
onCopyDiagnostics: function () {
if (!this.alive) return;
// Always reveal the same privacy-filtered text first, so clipboard failure
// or an absent API still leaves a native selectable-text fallback.
var report = this.diagnosticReport();
this.setData({ diagnosticText: report, copyStatus: '安全诊断已生成;下方文字可长按复制。' });
if (typeof my.setClipboard !== 'function') { this.setData({ copyStatus: '当前淘宝未提供剪贴板 API,请长按下方诊断文字复制。' }); return; }
var that = this;
try {
my.setClipboard({ text: report, success: function () { if (that.alive) that.setData({ copyStatus: '诊断信息已复制。也可长按下方文字复制。' }); },
fail: function () { if (that.alive) that.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); } });
} catch (_) { this.setData({ copyStatus: '自动复制失败,请长按下方诊断文字复制。' }); }
},
onOpenNativeProbe: function () {
if (this.data.busy || this.data.exporting) return;
my.redirectTo({ url: '/pages/camnative/camnative' });
},
currentCalibration: function (batch) {
return !!(batch && this.calibrationBatch === batch && batch.sequence === this.sequence && this.alive && this.visible && this.data.running);
},
invalidateCalibration: function () {
if (this.calibrationBatch) this.calibrationBatch.frame = null;
this.calibrationBatch = null;
if (this.alive) this.setData({ calibrationItems: [], calibrationReady: false, calibrationText: '' });
},
onCalibrate: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
if (this.exportInFlight || this.data.busy) {
this.setData({ errorText: '上一批照片仍在导出,请稍候再校验颜色。' }); return Promise.resolve();
}
if (!this.lastFrame) { this.setData({ errorText: '尚未收到真实相机帧,无法校验照片颜色。' }); return Promise.resolve(); }
var frozen, formats;
try {
// Freeze once: every preview and the eventual explicit confirmation refer
// to the exact same image, not a later native/reused camera buffer.
frozen = image.freezeCalibrationFrame(this.lastFrame);
formats = image.frameCalibrationFormats(frozen).filter(function (format) { return CALIBRATION_FORMATS.indexOf(format) >= 0; });
} catch (error) { this.setData({ errorText: error.message || '当前帧无法进行本地颜色校验。' }); return Promise.resolve(); }
if (!formats.length) {
this.setData({ errorText: '当前帧不是可校验的无格式四通道图像,请等待新帧或直接导出照片。' }); return Promise.resolve();
}
this.invalidateCalibration();
var that = this, operation = {}, batch = { sequence: this.sequence, id: this.sequence + '-' + ((this.calibrationSerial || 0) + 1),
frame: frozen, formats: formats, items: [], settled: false };
this.calibrationSerial = (this.calibrationSerial || 0) + 1;
this.calibrationBatch = batch; this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '',
calibrationText: '正在按顺序生成本地颜色预览;全部完成后,请选择颜色正常的一张。' });
return formats.reduce(function (chain, format) {
return chain.then(function () {
if (!that.currentCalibration(batch)) return;
return image.captureFrameToBase64(batch.frame, that, { pixelFormat: format, maxSide: 480 }).then(function (url) {
if (!that.currentCalibration(batch)) return;
batch.items.push({ format: format, url: url, batchId: batch.id });
that.setData({ calibrationItems: batch.items.slice(), calibrationText: '已生成 ' + batch.items.length + '/' + batch.formats.length + ' 张,完成后可确认。' });
});
});
}, Promise.resolve()).then(function () {
batch.settled = true;
if (that.currentCalibration(batch)) that.setData({ calibrationReady: true, calibrationText: '请比较肤色和背景,点击颜色正常照片下方的按钮;尚未确认任何新格式。' });
}).catch(function (error) {
batch.settled = true;
if (that.currentCalibration(batch)) {
that.invalidateCalibration();
that.setData({ errorText: error.message || '颜色预览导出失败,请重试。', calibrationText: '本批颜色预览导出失败,未确认新格式,请重试。' });
}
}).then(function () {
if (!that.currentCalibration(batch)) batch.frame = null;
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (batch.sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onConfirmCalibration: function (event) {
var batch = this.calibrationBatch, dataset = event && event.currentTarget && event.currentTarget.dataset || {}, format = dataset.format;
if (!this.currentCalibration(batch) || !batch.settled || String(dataset.batch) !== batch.id || batch.formats.indexOf(format) < 0
|| !batch.items.some(function (item) { return item.format === format; })) return;
try {
var confirmed = image.confirmFramePixelFormat(batch.frame, format);
if (confirmed !== format) throw new Error('未能确认相机格式,请重新校验。');
this.invalidateCalibration();
this.setData({ calibrationConfirmed: confirmed, calibrationText: '本次相机格式已确认:' + confirmed + ',已记录本次颜色选择。', errorText: '' });
} catch (error) { this.setData({ errorText: error.message || '格式确认失败,请重新校验。' }); }
},
acceptsCameraEvent: function (event) {
if (!this.alive || !this.visible || !this.data.running) return false;
var target = event && (event.currentTarget || event.target), dataset = target && target.dataset;
// Native callbacks from an unmounted camera must not enter a new session.
// Events without a marker remain compatible with older host callbacks.
if (dataset && dataset.session !== undefined && String(dataset.session) !== String(this.data.cameraSession)) return false;
var id = target && target.id;
return typeof id !== 'string' || id.indexOf('test-ar-camera-') !== 0 || id === 'test-ar-camera-' + this.data.cameraSession;
},
mountCamera: function (sequence) {
if (!this.alive || !this.visible || sequence !== this.sequence) return;
var that = this;
clearTimeout(this.frameTimer);
this.frames = 0; this.standardFrames = 0; this.lastFrame = null; this.context = null; this.lastUpdate = 0; this.frameUiShown = false; this.pendingFrameDiagnostic = null; this.contextMethodNames = [];
this.setData({ running: true, busy: false, cameraSession: sequence, frameCount: 0, standardFrameCount: 0, frameText: '尚未收到帧事件', contextText: '等待 onInit 后读取公开方法名',
status: '已请求' + (this.data.devicePosition === 'front' ? '前置' : '后置') + '镜头,等待真实帧;请核对实际画面…' });
this.frameTimer = setTimeout(function () {
if (that.alive && that.visible && sequence === that.sequence && !that.frames) that.setData({ status: '10秒内未收到帧事件(onARFrame)。请复制诊断,不能视为能力已通过。' });
}, 10000);
},
onStart: function () {
if (this.data.running || this.data.busy) return Promise.resolve();
var that = this, sequence = ++this.sequence;
this.setData({ busy: true, errorText: '', snapshotUrl: '', snapshotText: '', frameCount: 0, poseText: '', fieldText: '' });
// Camera permission only. Neither Taobao userInfo nor album is needed.
return permissions.ensure('scope.camera').then(function () {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.mountCamera(sequence);
}).catch(function (error) {
if (that.alive && sequence === that.sequence) that.setData({ errorText: error.message || '相机授权失败' });
}).then(function () { if (that.alive && sequence === that.sequence) that.setData({ busy: false }); });
},
onARInit: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onInit', event);
try {
this.context = AR.context(my, 'test-ar-camera-' + this.data.cameraSession);
// Enumerate descriptors only. This diagnostic never probes unknown methods
// or executes property getters/automatic context actions.
this.contextMethodNames = AR.diagnostics.methods(this.context);
this.setData({ contextText: this.contextMethodNames.join(', ') || '未发现可读取的公开方法;不代表原生能力不存在。', status: 'AR 初始化完成,等待实时人脸数据…' });
} catch (error) { this.setData({ errorText: error.message || 'AR 上下文初始化失败' }); }
},
onARFrame: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.frames = (this.frames || 0) + 1; this.lastFrame = event && event.detail || event;
this.recordNativeEvent('onARFrame', event, true);
clearTimeout(this.frameTimer);
// Facing selects hardware, not the native landmark coordinate convention.
// Do not reverse yaw solely because a front camera was requested.
var verdict;
try { verdict = AR.evaluate(event, this.data.target, false); } catch (_) { verdict = { frameReceived: false }; }
if (verdict.frameReceived) this.standardFrames = (this.standardFrames || 0) + 1;
var now = Date.now();
if (this.frameUiShown && now - this.lastUpdate < 400) return;
this.lastUpdate = now; this.frameUiShown = true;
this.flushFrameDiagnostic();
var patch = this.nativeEventView();
patch.frameCount = this.frames; patch.standardFrameCount = this.standardFrames || 0;
if (!verdict.frameReceived) {
patch.status = '已收到 onARFrame 回调,但格式不符合 width/height/anchors;请复制诊断。';
patch.frameText = '已收到原生回调,尚无可解析的标准帧'; patch.poseText = '';
this.setData(patch);
return;
}
var observation = verdict.observation;
patch.status = verdict.ok ? '实时人脸判断通过;仍需点击导出照片验证原始图像。' : verdict.hint;
patch.frameText = verdict.width + '×' + verdict.height + ' · 人脸 ' + verdict.faceCount + ' · 已收到真实帧';
patch.poseText = observation ? '几何姿态 yaw ' + observation.yaw.toFixed(1) + '° / pitch ' + observation.pitch.toFixed(1) + '° / roll ' + observation.roll.toFixed(1) + '°' : '';
this.setData(patch);
},
onTarget: function (event) {
var target = event.currentTarget.dataset.target;
if (['front', 'left', 'right'].indexOf(target) >= 0) this.setData({ target: target });
},
onFlip: function () {
if (!this.alive || !this.visible || !this.data.running) return Promise.resolve();
var that = this, sequence = ++this.sequence;
clearTimeout(this.frameTimer);
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.lastFrame = null; this.context = null; this.frames = 0; this.lastUpdate = 0;
// devicePosition is shared-component compatibility evidence. Its actual
// effect in the Taobao host must be checked on a phone; no context method
// or successful callback is treated as proof that hardware changed.
return new Promise(function (resolve) {
that.setData({ running: false, busy: true, devicePosition: that.data.devicePosition === 'front' ? 'back' : 'front',
cameraSession: sequence, frameCount: 0, frameText: '尚未收到真实帧', poseText: '', fieldText: '', errorText: '',
snapshotUrl: '', snapshotText: '', status: '正在重新挂载相机;切换效果以实际画面为准…' }, function () {
that.mountCamera(sequence);
resolve();
});
});
},
onSnapshot: function () {
// Image decoding/draw callbacks may still use the shared canvas after a
// camera session changes. Release this lock only when that export settles.
if (this.exportInFlight) {
this.setData({ errorText: '上一帧照片仍在导出,请稍候再试。' });
return Promise.resolve();
}
if (!this.lastFrame || this.data.busy) {
if (!this.lastFrame) this.setData({ errorText: '尚未收到真实相机帧,无法验证照片导出。' });
return Promise.resolve();
}
var that = this, sequence = this.sequence, operation = {};
this.exportInFlight = operation;
this.setData({ busy: true, exporting: true, errorText: '', snapshotUrl: '', snapshotText: '' });
var work;
try { work = image.captureFrameToBase64(this.lastFrame, this, { defaultPixelFormat: config.arPixelFormat }); } catch (error) { work = Promise.reject(error); }
return Promise.resolve(work).then(function (value) {
if (!that.alive || !that.visible || sequence !== that.sequence) return;
that.setData({ snapshotUrl: value, snapshotText: 'JPEG ' + that.data.compressWidth + '×' + that.data.compressHeight + ',仅本页预览,未上传、未保存到相册。', status: '已导出真实帧照片,请核对方向和颜色。' });
}).catch(function (error) {
if (that.alive && that.visible && sequence === that.sequence) that.setData({ errorText: error.message || '照片导出失败,请记录帧字段与实际错误。' });
}).then(function () {
if (that.exportInFlight !== operation) return;
that.exportInFlight = null;
if (that.alive) {
var patch = { exporting: false };
if (sequence === that.sequence) patch.busy = false;
that.setData(patch);
}
});
},
onARStop: function (event) {
if (!this.acceptsCameraEvent(event)) return;
this.recordNativeEvent('onStop', event);
this.invalidateCalibration();
this.sequence++;
this.setData({ status: 'AR相机已停止。请重新开启后验证。', running: false, busy: false });
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
},
onError: function (event) {
if (!this.acceptsCameraEvent(event)) return;
var summary = this.recordNativeEvent('onError', event);
this.invalidateCalibration();
this.sequence++;
this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
this.setData({ running: false, busy: false, status: 'AR 相机未通过真机验证。', errorText: '收到 AR 错误事件。' + (summary.metadata.join(';') || '未返回可安全展示的错误码,请复制诊断。') });
},
onStop: function () {
this.flushFrameDiagnostic();
this.invalidateCalibration();
this.sequence++; this.lastFrame = null; this.context = null; clearTimeout(this.frameTimer);
if (this.alive) this.setData({ running: false, busy: false, status: '已停止,帧缓存已清空。' });
},
onHide: function () {
this.visible = false; this.onStop();
if (this.alive) this.setData({ snapshotUrl: '', snapshotText: '', poseText: '', fieldText: '' });
},
onUnload: function () { this.onHide(); this.alive = false; }
});
+3 -2
View File
@@ -1,5 +1,6 @@
{
"defaultTitle": "相机组件测试",
"defaultTitle": "AR 相机真机验收",
"titleBarColor": "#101823",
"backgroundColor": "#000000"
"backgroundColor": "#000000",
"gestureBack": "NO"
}