499 lines
22 KiB
JavaScript
499 lines
22 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════
|
|
巨子测肤 · 后台查看页脚本(纯原生 ES5,零依赖)
|
|
页签:记录列表 / 失败与异常 / 统计概览;行点击开详情抽屉。
|
|
═══════════════════════════════════════════════════════════ */
|
|
'use strict';
|
|
|
|
/* ─── 工具 ─── */
|
|
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
function el(tag, cls, text) {
|
|
var e = document.createElement(tag);
|
|
if (cls) e.className = cls;
|
|
if (text != null) e.textContent = text;
|
|
return e;
|
|
}
|
|
function esc(s) { // 拼 HTML 前转义(接口文案/失败信息不可信)
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
function fmtTime(iso) {
|
|
if (!iso) return '-';
|
|
return iso.replace('T', ' ').slice(0, 19);
|
|
}
|
|
function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
|
|
function fmtScore(v) { return v == null ? '-' : v; }
|
|
|
|
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
|
|
var DIM_NAMES = { firm: '紧塑力', vita: '活力感', stab: '稳定力' };
|
|
var CAT_ORDER = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
|
var ST_CLASS = { '成功': 'st-ok', '失败': 'st-fail', '检测中': 'st-ing', '待检测': 'st-wait' };
|
|
|
|
function toast(msg) {
|
|
var t = $('#toast');
|
|
t.textContent = msg;
|
|
t.classList.remove('hidden');
|
|
clearTimeout(toast._timer);
|
|
toast._timer = setTimeout(function () { t.classList.add('hidden'); }, 2600);
|
|
}
|
|
|
|
function api(url, opts) {
|
|
return fetch(url, opts || {}).then(function (res) {
|
|
return res.json().then(function (data) {
|
|
if (!res.ok || data.ok === false) throw new Error(data.message || ('HTTP ' + res.status));
|
|
return data;
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ─── 页签切换 ─── */
|
|
var TABS = { records: 1, fails: 1, stats: 1 };
|
|
function switchTab(name) {
|
|
var tabs = document.querySelectorAll('.tab');
|
|
for (var i = 0; i < tabs.length; i++) tabs[i].classList.toggle('on', tabs[i].dataset.tab === name);
|
|
var panels = document.querySelectorAll('.panel');
|
|
for (var j = 0; j < panels.length; j++) panels[j].classList.toggle('on', panels[j].id === 'tab-' + name);
|
|
if (name === 'records') loadRecords();
|
|
else if (name === 'fails') loadFails();
|
|
else if (name === 'stats') loadStats();
|
|
}
|
|
|
|
/* ─── 档位标签 ─── */
|
|
function lvHtml(level) {
|
|
if (!level) return '<span class="sub">-</span>';
|
|
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(level) + '</span>';
|
|
}
|
|
function dimsHtml(dims) {
|
|
if (!dims) return '<span class="sub">-</span>';
|
|
var h = '';
|
|
for (var k in DIM_NAMES) {
|
|
var d = dims[k];
|
|
h += '<span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
|
|
DIM_NAMES[k].slice(0, 1) + (d ? esc(d.level) : '-') + '</span>';
|
|
}
|
|
return h;
|
|
}
|
|
function profileHtml(it) {
|
|
var parts = [];
|
|
parts.push(it.real_age != null ? it.real_age + '岁' : '-');
|
|
parts.push(it.gender || '-');
|
|
parts.push(it.region || '-');
|
|
parts.push(it.daily_skin_type || '-');
|
|
return esc(parts.join(' / '));
|
|
}
|
|
|
|
/* ═══════════ 记录列表 ═══════════ */
|
|
var pageState = { page: 1, page_size: 20, total: 0 };
|
|
|
|
function collectFilters() {
|
|
var p = [];
|
|
function add(k, v) { if (v) p.push(k + '=' + encodeURIComponent(v)); }
|
|
add('from', $('#f-from').value);
|
|
add('to', $('#f-to').value);
|
|
add('detect_status', $('#f-status').value);
|
|
add('fail_code', $('#f-failcode').value);
|
|
add('age_min', $('#f-agemin').value);
|
|
add('age_max', $('#f-agemax').value);
|
|
add('group', $('#f-group').value);
|
|
add('dim', $('#f-dim').value);
|
|
add('dim_level', $('#f-dimlevel').value);
|
|
add('q', $('#f-q').value.trim());
|
|
p.push('page=' + pageState.page);
|
|
p.push('page_size=' + pageState.page_size);
|
|
return p.join('&');
|
|
}
|
|
|
|
function loadRecords() {
|
|
api('/api/admin/records?' + collectFilters()).then(function (data) {
|
|
pageState.total = data.total;
|
|
var tbody = $('#tbl-records tbody');
|
|
tbody.innerHTML = '';
|
|
if (!data.items.length) {
|
|
tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
|
|
}
|
|
for (var i = 0; i < data.items.length; i++) {
|
|
(function (it) {
|
|
var tr = el('tr');
|
|
var photos = it.photos || {};
|
|
var thumb = photos.front
|
|
? '<img class="thumb" src="' + photos.front + '" alt="正脸">'
|
|
: '<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>' + profileHtml(it) + '</td>' +
|
|
'<td>' + esc(it.shoot_status) + '</td>' +
|
|
'<td>' + esc(it.upload_status) + '</td>' +
|
|
'<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' +
|
|
(it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' +
|
|
'<td><b>' + fmtScore(it.total_score) + '</b></td>' +
|
|
'<td>' + dimsHtml(it.dims) + '</td>' +
|
|
'<td>' + esc(it.essence || '-') + '</td>' +
|
|
'<td class="mono">' + fmtMs(it.cost_ms) + '</td>' +
|
|
'<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);
|
|
});
|
|
tbody.appendChild(tr);
|
|
})(data.items[i]);
|
|
}
|
|
var pages = Math.max(1, Math.ceil(data.total / data.page_size));
|
|
$('#page-info').textContent = '第 ' + data.page + ' / ' + pages + ' 页 · 共 ' + data.total + ' 条';
|
|
$('#btn-prev').disabled = data.page <= 1;
|
|
$('#btn-next').disabled = data.page >= pages;
|
|
}).catch(function (e) { toast('加载失败:' + e.message); });
|
|
}
|
|
|
|
/* ═══════════ 失败与异常 ═══════════ */
|
|
function loadFails() {
|
|
api('/api/admin/stats').then(function (st) {
|
|
var box = $('#fail-dist');
|
|
box.innerHTML = '';
|
|
if (!st.fail_dist.length) box.appendChild(el('div', 'pie-empty', '暂无失败记录'));
|
|
for (var i = 0; i < st.fail_dist.length; i++) {
|
|
(function (f) {
|
|
var chip = el('div', 'fail-chip');
|
|
chip.innerHTML = '<span class="code">' + esc(f.code) + '</span>' +
|
|
'<span class="pct">' + f.count + ' 次 · 占比 ' + f.pct + '%</span>' +
|
|
'<span class="bar"><i style="width:' + Math.min(100, f.pct) + '%"></i></span>';
|
|
box.appendChild(chip);
|
|
})(st.fail_dist[i]);
|
|
}
|
|
}).catch(function (e) { toast('加载失败:' + e.message); });
|
|
|
|
api('/api/admin/records?detect_status=失败&page_size=100').then(function (data) {
|
|
var tbody = $('#tbl-fails tbody');
|
|
tbody.innerHTML = '';
|
|
if (!data.items.length) {
|
|
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:#8a94a6;padding:32px">暂无失败记录</td></tr>';
|
|
}
|
|
for (var i = 0; i < data.items.length; i++) {
|
|
(function (it) {
|
|
var tr = el('tr');
|
|
tr.innerHTML =
|
|
'<td class="mono">' + esc(it.record_id) + '</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>';
|
|
tr.addEventListener('click', function (e) {
|
|
if (e.target && e.target.tagName === 'BUTTON') { doRetry(it.record_id); return; }
|
|
openDetail(it.record_id);
|
|
});
|
|
tbody.appendChild(tr);
|
|
})(data.items[i]);
|
|
}
|
|
}).catch(function (e) { toast('加载失败:' + e.message); });
|
|
}
|
|
|
|
function doRetry(id) {
|
|
if (!confirm('将使用原照片重新检测并生成新记录,确认重试 ' + id + ' ?')) return;
|
|
toast('重试中,请稍候(最长约 1 分钟)…');
|
|
api('/api/admin/records/' + encodeURIComponent(id) + '/retry', { method: 'POST' }).then(function (data) {
|
|
toast('重试完成,新记录 ' + data.record.record_id + '(' + data.record.detect_status + ')');
|
|
var active = $('.panel.on').id;
|
|
if (active === 'tab-records') loadRecords();
|
|
else if (active === 'tab-fails') loadFails();
|
|
}).catch(function (e) { toast('重试失败:' + e.message); });
|
|
}
|
|
|
|
/* ═══════════ 统计概览 ═══════════ */
|
|
var PIE_COLORS = ['#2563eb', '#2fa84f', '#f0a020', '#8a5cf6', '#d93026'];
|
|
function loadStats() {
|
|
api('/api/admin/stats').then(function (st) {
|
|
var cards = [
|
|
['今日检测', st.today_total],
|
|
['累计检测', st.total],
|
|
['成功率', st.success_rate + '%'],
|
|
['平均耗时', st.avg_cost_ms + 'ms']
|
|
];
|
|
var box = $('#stat-cards');
|
|
box.innerHTML = '';
|
|
for (var i = 0; i < cards.length; i++) {
|
|
var c = el('div', 'card');
|
|
c.innerHTML = '<div class="lab">' + cards[i][0] + '</div><div class="num">' + cards[i][1] + '</div>';
|
|
box.appendChild(c);
|
|
}
|
|
|
|
/* 三维度档位分布:每组三根横条 */
|
|
var dd = $('#dim-dist');
|
|
dd.innerHTML = '';
|
|
for (var d in DIM_NAMES) {
|
|
var dist = st.dim_dist[d] || {};
|
|
var max = Math.max(1, dist['预警'] || 0, dist['改善'] || 0, dist['良好'] || 0);
|
|
var row = el('div', 'dim-row');
|
|
var bars = '';
|
|
var levels = [['预警', 'b-bad'], ['改善', 'b-warn'], ['良好', 'b-good']];
|
|
for (var l = 0; l < levels.length; l++) {
|
|
var cnt = dist[levels[l][0]] || 0;
|
|
bars += '<div style="display:flex;align-items:center;gap:8px;margin:3px 0">' +
|
|
'<span style="width:34px;color:#5a6578;font-size:12px">' + levels[l][0] + '</span>' +
|
|
'<span style="flex:1;height:14px;background:#f0f2f5;border-radius:3px;overflow:hidden">' +
|
|
'<i style="display:block;height:100%;width:' + (cnt * 100 / max) + '%" class="' + levels[l][1] + '"></i></span>' +
|
|
'<span style="width:24px;text-align:right;font-size:12px;color:#4a5568">' + cnt + '</span></div>';
|
|
}
|
|
row.innerHTML = '<div class="dim-name">' + DIM_NAMES[d] + '</div>' + bars;
|
|
dd.appendChild(row);
|
|
}
|
|
|
|
/* 产品组别分布:纯 CSS conic-gradient 饼图 */
|
|
var gd = $('#group-dist');
|
|
gd.innerHTML = '';
|
|
var keys = Object.keys(st.group_dist);
|
|
if (!keys.length) {
|
|
gd.appendChild(el('div', 'pie-empty', '暂无成功记录'));
|
|
} else {
|
|
var total = 0, k;
|
|
for (k in st.group_dist) total += st.group_dist[k];
|
|
var acc = 0, segs = [];
|
|
for (var g = 0; g < keys.length; g++) {
|
|
var from = acc * 100 / total;
|
|
acc += st.group_dist[keys[g]];
|
|
var to = acc * 100 / total;
|
|
segs.push(PIE_COLORS[g % PIE_COLORS.length] + ' ' + from + '% ' + to + '%');
|
|
}
|
|
var legend = '';
|
|
for (var g2 = 0; g2 < keys.length; g2++) {
|
|
legend += '<div><i style="background:' + PIE_COLORS[g2 % PIE_COLORS.length] + '"></i>' +
|
|
esc(keys[g2]) + ':' + st.group_dist[keys[g2]] + ' 次(' + Math.round(st.group_dist[keys[g2]] * 100 / total) + '%)</div>';
|
|
}
|
|
gd.innerHTML = '<div class="pie" style="background:conic-gradient(' + segs.join(',') + ')"></div>' +
|
|
'<div class="pie-legend">' + legend + '</div>';
|
|
}
|
|
|
|
/* 近 14 天每日检测量(蓝=总量,绿=成功数) */
|
|
var dc = $('#daily-chart');
|
|
dc.innerHTML = '';
|
|
var daily = st.daily || [];
|
|
var dmax = 1;
|
|
for (var n = 0; n < daily.length; n++) dmax = Math.max(dmax, daily[n].total);
|
|
for (var m = 0; m < daily.length; m++) {
|
|
(function (day) {
|
|
var col = el('div', 'daily-col');
|
|
var h = Math.max(2, day.total * 130 / dmax);
|
|
var okH = day.total ? Math.max(1, day.success * 130 / dmax) : 0;
|
|
col.innerHTML = '<span class="daily-val">' + day.total + '</span>' +
|
|
'<div class="daily-bar" style="height:' + h + 'px"><div class="daily-bar ok" style="width:70%;left:15%;height:' + okH + 'px"></div></div>' +
|
|
'<span class="daily-date">' + day.date.slice(5) + '</span>';
|
|
col.title = day.date + ':检测 ' + day.total + ' 次,成功 ' + day.success + ' 次';
|
|
dc.appendChild(col);
|
|
})(daily[m]);
|
|
}
|
|
}).catch(function (e) { toast('加载失败:' + e.message); });
|
|
}
|
|
|
|
/* ═══════════ 记录详情(抽屉) ═══════════ */
|
|
var currentDetailId = null;
|
|
|
|
function openDetail(id) {
|
|
currentDetailId = id;
|
|
$('#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>'; });
|
|
}
|
|
function closeDetail() {
|
|
$('#drawer').classList.add('hidden');
|
|
$('#drawer-mask').classList.add('hidden');
|
|
currentDetailId = null;
|
|
}
|
|
|
|
function dSec(title, innerHtml) {
|
|
return '<div class="d-sec"><h3>' + title + '</h3>' + innerHtml + '</div>';
|
|
}
|
|
|
|
function renderDetail(data) {
|
|
var r = data.record;
|
|
$('#btn-retry').disabled = !(data.photos && data.photos.front);
|
|
var h = '';
|
|
|
|
/* 三张照片 */
|
|
var photosHtml = '<div class="d-photos">';
|
|
var whichNames = { front: '正脸', left: '左脸', right: '右脸' };
|
|
var anyPhoto = false;
|
|
for (var w in whichNames) {
|
|
var u = data.photos[w];
|
|
if (!u) continue;
|
|
anyPhoto = true;
|
|
photosHtml += '<div class="d-photo"><img src="' + u + '" alt="' + whichNames[w] + '" data-zoom="' + u + '">' +
|
|
'<a href="' + u + '" download="' + r.record_id + '-' + w + '.jpg">下载' + whichNames[w] + '</a></div>';
|
|
}
|
|
photosHtml += anyPhoto ? '' : '<span class="sub">无照片</span>';
|
|
photosHtml += '</div>';
|
|
h += dSec('检测照片', photosHtml);
|
|
|
|
/* 状态行 */
|
|
h += dSec('状态', '<div class="d-kv">' +
|
|
'<span>检测状态 <b>' + esc(r.detect_status) + '</b></span>' +
|
|
'<span>拍摄状态 <b>' + esc(r.shoot_status) + '</b></span>' +
|
|
'<span>上传状态 <b>' + esc(r.upload_status) + '</b></span>' +
|
|
'<span>创建时间 <b>' + fmtTime(r.created_at) + '</b></span>' +
|
|
'<span>更新时间 <b>' + fmtTime(r.updated_at) + '</b></span>' +
|
|
'<span>耗时 <b>' + fmtMs(r.cost_ms) + '</b></span>' +
|
|
(r.retry_of ? '<span>重试来源 <b class="mono">' + esc(r.retry_of) + '</b></span>' : '') +
|
|
'<span>被重试次数 <b>' + (r.retry_count || 0) + '</b></span>' +
|
|
'</div>');
|
|
|
|
/* 档案 */
|
|
h += dSec('档案信息', '<div class="d-kv">' +
|
|
'<span>年龄 <b>' + (r.real_age != null ? r.real_age + '岁' : '-') + '</b></span>' +
|
|
'<span>性别 <b>' + esc(r.gender || '-') + '</b></span>' +
|
|
'<span>地区 <b>' + esc(r.region || '-') + '</b></span>' +
|
|
'<span>日常肤质 <b>' + esc(r.daily_skin_type || '-') + '</b></span>' +
|
|
'<span>产品组别 <b>' + esc(r.group || '-') + '</b></span>' +
|
|
'</div>');
|
|
|
|
/* 失败:错误 + 版本链 */
|
|
if (r.detect_status === '失败') {
|
|
h += dSec('失败信息', '<div class="d-copy"><b>' + esc(r.fail_code || '-') + '</b>(环节:' + esc(r.fail_stage || '-') + ')<br>' + esc(r.fail_msg || '-') + '</div>');
|
|
}
|
|
|
|
/* 版本链(重试关系) */
|
|
var vHtml = '';
|
|
if (data.versions && data.versions.parent) {
|
|
vHtml += '<div>上一版本(本次为重试):<a data-id="' + esc(data.versions.parent.record_id) + '">' +
|
|
esc(data.versions.parent.record_id) + '(' + esc(data.versions.parent.detect_status) + ')</a></div>';
|
|
}
|
|
if (data.versions && data.versions.children && data.versions.children.length) {
|
|
vHtml += '<div>重试产生的记录:';
|
|
for (var c = 0; c < data.versions.children.length; c++) {
|
|
vHtml += '<a data-id="' + esc(data.versions.children[c].record_id) + '">' +
|
|
esc(data.versions.children[c].record_id) + '(' + esc(data.versions.children[c].detect_status) + ')</a>';
|
|
}
|
|
vHtml += '</div>';
|
|
}
|
|
if (vHtml) h += dSec('版本链', '<div class="d-versions">' + vHtml + '</div>');
|
|
|
|
/* 成功:评分 + 文案 */
|
|
if (r.detect_status === '成功') {
|
|
h += dSec('综合', '<div class="d-kv">' +
|
|
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' +
|
|
'<span>肌肤年龄(接口原始) <b>' + fmtScore(r.skin_age) + '岁</b></span>' +
|
|
'<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' +
|
|
'<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' +
|
|
'<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' +
|
|
'</div>');
|
|
|
|
/* 6 分类表格 */
|
|
var catRows = '';
|
|
for (var i = 0; i < CAT_ORDER.length; i++) {
|
|
var ck = CAT_ORDER[i], cat = r.cats && r.cats[ck];
|
|
var catName = (cat && cat.name) || ({ contour: '轮廓', wrinkles: '皱纹', smooth: '光滑', tone: '肤色水润', sens: '敏感', acne: '痘痘' })[ck];
|
|
catRows += '<tr><td>' + esc(catName) + '</td><td>' + fmtScore(cat && cat.P) + '</td><td>' + fmtScore(cat && cat.S) + '</td><td>' + lvHtml(cat && cat.level) + '</td></tr>';
|
|
}
|
|
h += dSec('六分类评分', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
|
|
|
|
/* 3 维度表格 */
|
|
var dimRows = '';
|
|
for (var dk in DIM_NAMES) {
|
|
var dim = r.dims && r.dims[dk];
|
|
dimRows += '<tr><td>' + DIM_NAMES[dk] + '</td><td>' + fmtScore(dim && dim.P) + '</td><td>' + fmtScore(dim && dim.S) + '</td><td>' + lvHtml(dim && dim.level) + '</td></tr>';
|
|
}
|
|
h += dSec('三维度评分', '<table class="d-table"><thead><tr><th>维度</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + dimRows + '</tbody></table>');
|
|
|
|
/* 文案全文 */
|
|
var copyHtml = '';
|
|
if (r.copy_6) {
|
|
for (var c2 = 0; c2 < CAT_ORDER.length; c2++) {
|
|
var txt = r.copy_6[CAT_ORDER[c2]];
|
|
if (txt) copyHtml += '<div class="d-copy"><b>' + esc((r.cats[CAT_ORDER[c2]] || {}).name || '') + ':</b>' + esc(txt) + '</div>';
|
|
}
|
|
}
|
|
if (r.copy_summary) copyHtml += '<div class="d-copy"><b>综合文案:</b>' + esc(r.copy_summary) + '</div>';
|
|
if (r.lifestyle) copyHtml += '<div class="d-copy"><b>生活方式:</b>' + esc(r.lifestyle) + '</div>';
|
|
if (copyHtml) h += dSec('生成文案', copyHtml);
|
|
|
|
if (r.careAdvice && r.careAdvice.length) {
|
|
var advice = '';
|
|
for (var a = 0; a < r.careAdvice.length; a++) {
|
|
advice += '<div class="d-copy"><b>[' + esc(r.careAdvice[a].tag) + ']</b>' + esc(r.careAdvice[a].text) + '</div>';
|
|
}
|
|
h += dSec('护理建议', advice);
|
|
}
|
|
|
|
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc((r.essence && r.essence.name) || '-') + '</b><br>' +
|
|
esc((r.essence && r.essence.text) || '') + '</div>' +
|
|
'<div class="d-kv"><span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
|
|
|
|
if (r.notes && r.notes.length) {
|
|
var notes = '<ul class="d-notes">';
|
|
for (var n2 = 0; n2 < r.notes.length; n2++) notes += '<li>' + esc(r.notes[n2]) + '</li>';
|
|
notes += '</ul>';
|
|
h += dSec('计算备注', notes);
|
|
}
|
|
}
|
|
|
|
/* 接口原始 JSON:details 折叠,pre 展示 */
|
|
if (data.raw_json) {
|
|
h += dSec('接口原始 JSON', '<details><summary>展开/收起(raw_json,' + (data.raw_file || '') + ')</summary><pre>' +
|
|
esc(JSON.stringify(data.raw_json, null, 1)) + '</pre></details>');
|
|
} else {
|
|
h += dSec('接口原始 JSON', '<div class="sub">无(失败记录或未落盘)</div>');
|
|
}
|
|
|
|
var body = $('#d-body');
|
|
body.innerHTML = h;
|
|
|
|
/* 图片点击看大图 */
|
|
var imgs = body.querySelectorAll('img[data-zoom]');
|
|
for (var z = 0; z < imgs.length; z++) {
|
|
imgs[z].addEventListener('click', function () { showLightbox(this.getAttribute('data-zoom')); });
|
|
}
|
|
/* 版本链跳转 */
|
|
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')); });
|
|
}
|
|
}
|
|
|
|
/* ═══════════ 图片查看 ═══════════ */
|
|
function showLightbox(src) {
|
|
$('#lightbox-img').src = src;
|
|
$('#lightbox').classList.remove('hidden');
|
|
}
|
|
|
|
/* ═══════════ 事件绑定与初始化 ═══════════ */
|
|
function bind() {
|
|
var tabs = document.querySelectorAll('.tab');
|
|
for (var i = 0; i < tabs.length; i++) {
|
|
tabs[i].addEventListener('click', function () { switchTab(this.dataset.tab); });
|
|
}
|
|
$('#btn-search').addEventListener('click', function () { pageState.page = 1; loadRecords(); });
|
|
$('#btn-reset').addEventListener('click', function () {
|
|
var ids = ['f-from', 'f-to', 'f-status', 'f-failcode', 'f-agemin', 'f-agemax', 'f-group', 'f-dim', 'f-dimlevel', 'f-q'];
|
|
for (var i2 = 0; i2 < ids.length; i2++) $('#' + ids[i2]).value = '';
|
|
pageState.page = 1;
|
|
loadRecords();
|
|
});
|
|
$('#btn-prev').addEventListener('click', function () { if (pageState.page > 1) { pageState.page--; loadRecords(); } });
|
|
$('#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); });
|
|
$('#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 = '/api/admin/export' + (p.length ? '?' + p.join('&') : '');
|
|
var a = el('a');
|
|
a.href = url;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
});
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') { closeDetail(); $('#lightbox').classList.add('hidden'); }
|
|
});
|
|
}
|
|
|
|
bind();
|
|
loadRecords();
|