feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
This commit is contained in:
+64
-22
@@ -49,6 +49,27 @@ function api(url, opts) {
|
||||
});
|
||||
});
|
||||
}
|
||||
function channelName(channel) { return ({ h5: 'H5', taobao: '淘宝小程序', rednote: '小红书' })[channel] || channel || 'H5'; }
|
||||
function selectedChannelQuery(url) {
|
||||
var channel = $('#f-channel').value;
|
||||
return channel ? url + (url.indexOf('?') < 0 ? '?' : '&') + 'channel=' + encodeURIComponent(channel) : url;
|
||||
}
|
||||
function sourceQuery(source) { return '?source=' + encodeURIComponent(source || 'local'); }
|
||||
function renderSources(data) {
|
||||
var sources = data.sources || [];
|
||||
$('#channel-summary').textContent = sources.length > 1
|
||||
? '已汇总 H5 与淘宝数据;外部渠道记录只读,重试请进入原服务后台。'
|
||||
: '当前读取本服务的数据;尚未接入其他服务的数据源。';
|
||||
}
|
||||
function renderChannelStats(data) {
|
||||
renderSources(data);
|
||||
var rows = data.by_channel || {}, html = '<table><thead><tr><th>渠道</th><th>累计检测</th><th>今日检测</th><th>成功</th><th>失败</th></tr></thead><tbody>';
|
||||
Object.keys(rows).forEach(function (key) {
|
||||
var row = rows[key];
|
||||
html += '<tr><td>' + esc(channelName(key)) + '</td><td>' + row.total + '</td><td>' + row.today_total + '</td><td>' + row.success_total + '</td><td>' + row.fail_total + '</td></tr>';
|
||||
});
|
||||
$('#channel-stats').innerHTML = html + '</tbody></table>';
|
||||
}
|
||||
|
||||
/* ─── 页签切换 ─── */
|
||||
var TABS = { records: 1, fails: 1, stats: 1 };
|
||||
@@ -116,6 +137,7 @@ function collectFilters() {
|
||||
add('from', $('#f-from').value);
|
||||
add('to', $('#f-to').value);
|
||||
add('detect_status', $('#f-status').value);
|
||||
add('channel', $('#f-channel').value);
|
||||
add('fail_code', $('#f-failcode').value);
|
||||
add('age_min', $('#f-agemin').value);
|
||||
add('age_max', $('#f-agemax').value);
|
||||
@@ -130,6 +152,7 @@ function collectFilters() {
|
||||
|
||||
function loadRecords() {
|
||||
api('/api/admin/records?' + collectFilters()).then(function (data) {
|
||||
renderSources(data);
|
||||
pageState.total = data.total;
|
||||
var tbody = $('#tbl-records tbody');
|
||||
tbody.innerHTML = '';
|
||||
@@ -145,7 +168,7 @@ function loadRecords() {
|
||||
: '<span class="sub">无</span>';
|
||||
tr.innerHTML =
|
||||
'<td>' + thumb + '</td>' +
|
||||
'<td class="mono">' + esc(it.record_id) + (it.retry_of ? '<div class="sub">重试自 ' + esc(it.retry_of) + '</div>' : '') + '</td>' +
|
||||
'<td class="mono">' + esc(it.record_id) + '<div class="sub">' + esc(channelName(it.channel)) + (it.readonly ? ' · 只读' : '') + '</div>' + (it.retry_of ? '<div class="sub">重试自 ' + esc(it.retry_of) + '</div>' : '') + '</td>' +
|
||||
'<td>' + profileHtml(it) + '</td>' +
|
||||
'<td>' + esc(it.shoot_status) + captureQualityHtml(it) + '</td>' +
|
||||
'<td>' + esc(it.upload_status) + '</td>' +
|
||||
@@ -159,7 +182,7 @@ function loadRecords() {
|
||||
'<td class="mono">' + fmtTime(it.created_at) + '</td>';
|
||||
tr.addEventListener('click', function (e) {
|
||||
if (e.target && e.target.className === 'thumb') { showLightbox(e.target.src); return; }
|
||||
openDetail(it.record_id);
|
||||
openDetail(it.record_id, it.source);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
})(data.items[i]);
|
||||
@@ -175,6 +198,7 @@ function loadRecords() {
|
||||
function renderCaptureFunnel(st) {
|
||||
var funnel = st.capture_funnel;
|
||||
var panel = $('#capture-funnel');
|
||||
if (panel) panel.parentNode.hidden = !funnel;
|
||||
if (funnel && panel) {
|
||||
var labels = [['started','开始拍照'],['front','正面拍完'],['right','右脸拍完'],['left','三张拍完'],['submitted','提交检测'],['response','收到成功结果'],['report','进入报告页']];
|
||||
var html = '<table><thead><tr><th>阶段</th><th>累计流程</th><th>今日流程</th></tr></thead><tbody>';
|
||||
@@ -190,7 +214,8 @@ function renderCaptureFunnel(st) {
|
||||
}
|
||||
}
|
||||
function loadFails() {
|
||||
api('/api/admin/stats').then(function (st) {
|
||||
api(selectedChannelQuery('/api/admin/stats')).then(function (st) {
|
||||
renderSources(st);
|
||||
var box = $('#fail-dist');
|
||||
box.innerHTML = '';
|
||||
if (!st.fail_dist.length) box.appendChild(el('div', 'pie-empty', '暂无失败记录'));
|
||||
@@ -205,7 +230,7 @@ function loadFails() {
|
||||
}
|
||||
}).catch(function (e) { toast('加载失败:' + e.message); });
|
||||
|
||||
api('/api/admin/records?detect_status=失败&page_size=100').then(function (data) {
|
||||
api(selectedChannelQuery('/api/admin/records?detect_status=失败&page_size=100')).then(function (data) {
|
||||
var tbody = $('#tbl-fails tbody');
|
||||
tbody.innerHTML = '';
|
||||
if (!data.items.length) {
|
||||
@@ -215,17 +240,17 @@ function loadFails() {
|
||||
(function (it) {
|
||||
var tr = el('tr');
|
||||
tr.innerHTML =
|
||||
'<td class="mono">' + esc(it.record_id) + '</td>' +
|
||||
'<td class="mono">' + esc(it.record_id) + '<div class="sub">' + esc(channelName(it.channel)) + '</div></td>' +
|
||||
'<td>' + esc(it.fail_stage || '-') + '</td>' +
|
||||
'<td><span class="st st-fail">' + esc(it.fail_code || '-') + '</span></td>' +
|
||||
'<td style="white-space:normal;max-width:340px">' + esc(it.fail_msg || '-') + '</td>' +
|
||||
'<td>' + (it.real_age != null ? it.real_age + '岁' : '-') + '</td>' +
|
||||
'<td>' + esc(it.shoot_status) + '</td>' +
|
||||
'<td class="mono">' + fmtTime(it.created_at) + '</td>' +
|
||||
'<td><button type="button" class="btn small primary">重试</button></td>';
|
||||
'<td>' + (it.readonly ? '<span class="sub">只读,请到原服务后台重试</span>' : '<button type="button" class="btn small primary">重试</button>') + '</td>';
|
||||
tr.addEventListener('click', function (e) {
|
||||
if (e.target && e.target.tagName === 'BUTTON') { doRetry(it.record_id); return; }
|
||||
openDetail(it.record_id);
|
||||
if (e.target && e.target.tagName === 'BUTTON') { doRetry(it.record_id, it.source); return; }
|
||||
openDetail(it.record_id, it.source);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
})(data.items[i]);
|
||||
@@ -233,10 +258,10 @@ function loadFails() {
|
||||
}).catch(function (e) { toast('加载失败:' + e.message); });
|
||||
}
|
||||
|
||||
function doRetry(id) {
|
||||
function doRetry(id, source) {
|
||||
if (!confirm('将使用原照片重新检测并生成新记录,确认重试 ' + id + ' ?')) return;
|
||||
toast('重试中,请稍候(最长约 1 分钟)…');
|
||||
api('/api/admin/records/' + encodeURIComponent(id) + '/retry', { method: 'POST' }).then(function (data) {
|
||||
api('/api/admin/records/' + encodeURIComponent(id) + '/retry' + sourceQuery(source), { method: 'POST' }).then(function (data) {
|
||||
toast('重试完成,新记录 ' + data.record.record_id + '(' + data.record.detect_status + ')');
|
||||
var active = $('.panel.on').id;
|
||||
if (active === 'tab-records') loadRecords();
|
||||
@@ -247,7 +272,8 @@ function doRetry(id) {
|
||||
/* ═══════════ 统计概览 ═══════════ */
|
||||
var PIE_COLORS = ['#2563eb', '#2fa84f', '#f0a020', '#8a5cf6', '#d93026'];
|
||||
function loadStats() {
|
||||
api('/api/admin/stats').then(function (st) {
|
||||
api(selectedChannelQuery('/api/admin/stats')).then(function (st) {
|
||||
renderChannelStats(st);
|
||||
renderCaptureFunnel(st);
|
||||
var cards = [
|
||||
['今日检测', st.today_total],
|
||||
@@ -332,18 +358,23 @@ function loadStats() {
|
||||
|
||||
/* ═══════════ 记录详情(抽屉) ═══════════ */
|
||||
var currentDetailId = null;
|
||||
var currentDetailSource = 'local';
|
||||
var detailSeq = 0;
|
||||
|
||||
function openDetail(id) {
|
||||
function openDetail(id, source) {
|
||||
var seq = ++detailSeq;
|
||||
currentDetailId = id;
|
||||
currentDetailSource = source || 'local';
|
||||
$('#d-title').textContent = '记录详情 ' + id;
|
||||
$('#btn-retry').disabled = true;
|
||||
$('#d-body').innerHTML = '<div class="sub">加载中…</div>';
|
||||
$('#drawer').classList.remove('hidden');
|
||||
$('#drawer-mask').classList.remove('hidden');
|
||||
api('/api/admin/records/' + encodeURIComponent(id)).then(renderDetail)
|
||||
.catch(function (e) { $('#d-body').innerHTML = '<div class="sub">加载失败:' + esc(e.message) + '</div>'; });
|
||||
api('/api/admin/records/' + encodeURIComponent(id) + sourceQuery(currentDetailSource)).then(function (data) { if (seq === detailSeq) renderDetail(data); })
|
||||
.catch(function (e) { if (seq === detailSeq) $('#d-body').innerHTML = '<div class="sub">加载失败:' + esc(e.message) + '</div>'; });
|
||||
}
|
||||
function closeDetail() {
|
||||
detailSeq++;
|
||||
$('#drawer').classList.add('hidden');
|
||||
$('#drawer-mask').classList.add('hidden');
|
||||
currentDetailId = null;
|
||||
@@ -355,7 +386,8 @@ function dSec(title, innerHtml) {
|
||||
|
||||
function renderDetail(data) {
|
||||
var r = data.record;
|
||||
$('#btn-retry').disabled = !(data.photos && data.photos.front);
|
||||
$('#btn-retry').disabled = data.readonly || !(data.photos && data.photos.front);
|
||||
$('#btn-retry').title = data.readonly ? '只读汇总记录,请在原服务后台重试' : '';
|
||||
var h = '';
|
||||
|
||||
/* 三张照片 */
|
||||
@@ -375,6 +407,7 @@ function renderDetail(data) {
|
||||
|
||||
/* 状态行 */
|
||||
h += dSec('状态', '<div class="d-kv">' +
|
||||
'<span>渠道 <b>' + esc(channelName(r.channel)) + (data.readonly ? '(只读汇总)' : '') + '</b></span>' +
|
||||
'<span>检测状态 <b>' + esc(r.detect_status) + '</b></span>' +
|
||||
'<span>拍摄状态 <b>' + esc(r.shoot_status) + '</b></span>' +
|
||||
'<span>上传状态 <b>' + esc(r.upload_status) + '</b></span>' +
|
||||
@@ -553,7 +586,7 @@ function renderDetail(data) {
|
||||
if (data.raw_json) {
|
||||
var rawFile = data.raw_file || (window.JuziEntry && window.JuziEntry.mode === 'rednote' ? r.raw_file : null);
|
||||
var rawDl = rawFile
|
||||
? ' <a href="' + adminURL('/api/admin/records/' + encodeURIComponent(r.record_id) + '/raw') + '" download>下载完整原始 JSON</a>'
|
||||
? ' <a href="' + adminURL('/api/admin/records/' + encodeURIComponent(r.record_id) + '/raw' + sourceQuery(data.source)) + '" download>下载完整原始 JSON</a>'
|
||||
: '';
|
||||
h += dSec('接口原始 JSON', '<details><summary>展开/收起(' + (rawFile || '') + ')' + rawDl + '</summary><pre>' +
|
||||
esc(JSON.stringify(data.raw_json, null, 1)) + '</pre></details>');
|
||||
@@ -572,7 +605,7 @@ function renderDetail(data) {
|
||||
/* 版本链跳转 */
|
||||
var links = body.querySelectorAll('.d-versions a');
|
||||
for (var v = 0; v < links.length; v++) {
|
||||
links[v].addEventListener('click', function () { openDetail(this.getAttribute('data-id')); });
|
||||
links[v].addEventListener('click', function () { openDetail(this.getAttribute('data-id'), data.source); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,6 +617,14 @@ function showLightbox(src) {
|
||||
|
||||
/* ═══════════ 事件绑定与初始化 ═══════════ */
|
||||
function bind() {
|
||||
if (window.JuziEntry && window.JuziEntry.mode === 'rednote') {
|
||||
$('#f-channel').innerHTML = '<option value="">全部渠道</option><option value="rednote">小红书</option>';
|
||||
}
|
||||
$('#f-channel').addEventListener('change', function () {
|
||||
pageState.page = 1;
|
||||
var active = $('.panel.on').id.replace('tab-', '');
|
||||
switchTab(active);
|
||||
});
|
||||
var tabs = document.querySelectorAll('.tab');
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
tabs[i].addEventListener('click', function () { switchTab(this.dataset.tab); });
|
||||
@@ -599,13 +640,14 @@ function bind() {
|
||||
$('#btn-next').addEventListener('click', function () { pageState.page++; loadRecords(); });
|
||||
$('#btn-close').addEventListener('click', closeDetail);
|
||||
$('#drawer-mask').addEventListener('click', closeDetail);
|
||||
$('#btn-retry').addEventListener('click', function () { if (currentDetailId) doRetry(currentDetailId); });
|
||||
$('#btn-retry').addEventListener('click', function () { if (currentDetailId) doRetry(currentDetailId, currentDetailSource); });
|
||||
$('#lightbox').addEventListener('click', function () { this.classList.add('hidden'); });
|
||||
$('#btn-export').addEventListener('click', function () {
|
||||
var p = [];
|
||||
if ($('#exp-from').value) p.push('from=' + $('#exp-from').value);
|
||||
if ($('#exp-to').value) p.push('to=' + $('#exp-to').value);
|
||||
var url = adminURL('/api/admin/export' + (p.length ? '?' + p.join('&') : ''));
|
||||
var p = new URLSearchParams(collectFilters());
|
||||
p.delete('page'); p.delete('page_size');
|
||||
if ($('#exp-from').value) p.set('from', $('#exp-from').value);
|
||||
if ($('#exp-to').value) p.set('to', $('#exp-to').value);
|
||||
var url = adminURL('/api/admin/export?' + p.toString());
|
||||
var a = el('a');
|
||||
a.href = url;
|
||||
a.download = '';
|
||||
|
||||
Reference in New Issue
Block a user