// ==UserScript==
// @name BLY行人
// @namespace https://label.bilinyun.net
// @version 1.2
// @description 比邻云行人(2DPedestrian)质检助手:① 进题包全量预加载全部帧,消除首次切帧的灰屏转圈等待 ② 切帧去黑屏遮罩 ③ 帧级标注缓存 + 缺帧自动补请求 ④ 面板实时显示本帧框数与整包统计 ⑤ 整包框尺寸检查(宽≥20 高≥40,忽略框跳过,错误帧点击跳转)⑥ 一键复制「包号 + 有效帧数 + 框数」(有效帧 = 有框的帧;包号与帧数之间空一列)
// @author CC
// @match https://label.bilinyun.net/*
// @grant none
// @run-at document-start
// @license CC
// ==/UserScript==
(function () {
'use strict';
/* ==================== 常量 ==================== */
const API_MARK = '/api/mark/image';
const API_STATIC = '/api/mark/contents/static';
const WARMUP_KEY = '_blyped_warmup';
// ---- 框尺寸规则(来自 BB 平台「行人验收框大小」脚本,单位:图像像素)----
// 不分主/非主行人,统一按此标准;标了「忽略」的框跳过检查
const MIN_W = 20;
const MIN_H = 40;
let WARMUP_ON = true;
try { WARMUP_ON = localStorage.getItem(WARMUP_KEY) !== '0'; } catch (e) { }
/* ==================== 全局状态 ==================== */
let CMAP = null; // 类目映射(从 projectClass 动态构建)
let IMG_SIZE = null; // 图像宽高缓存
let AUTH_HDRS = null; // 页面真实请求里的鉴权头(用于兜底补请求)
let FRAME_IDS = []; // 帧序号(1-based) → frameId
const FRAME_CACHE = {}; // frameId → shapeList
let PROJECT_INFO = {}; // taskNo / projectName 等
let lastFrameNo = null;
let lastRenderKey = null;
let PACK_STAT = null; // 整包统计结果
let WARMUP_RUNNING = false;
let WARMUP_DONE = false;
let warmupTried = false;
let staticTried = false;
/* ==================== 工具 ==================== */
function num(v) {
const n = parseFloat(v);
return isNaN(n) ? null : n;
}
function parseJson(s) {
try { return JSON.parse(s) || {}; } catch (e) { return {}; }
}
function esc(s) {
return String(s === null || s === undefined ? '' : s)
.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
function fmt1(n) {
if (n === null || n === undefined || isNaN(n)) return '?';
return (Math.round(n * 10) / 10).toFixed(1);
}
function taskIdFromUrl() {
const m = /[?&]recordId=([^&]+)/.exec(location.search || '');
return m ? m[1] : null;
}
function frameIdFromUrl(u) {
const m = /frame_id=([^&]+)/.exec(u || '');
return m ? m[1] : null;
}
function currentFrameNo() {
const el = document.querySelector('.frame-counter-clickable');
if (!el) return null;
const m = /(\d+)\s*\/\s*(\d+)/.exec(el.innerText || '');
return m ? parseInt(m[1], 10) : null;
}
/* ==================== 类目映射(自适应 projectClass) ==================== */
// 行人项目实测两个类目:
// full_bbox → 行人主框(含 是否忽略 / 遮挡 / 截断)
// Other pedestrian boxes → 其他行人框(含 忽略 / 遮挡 / 截断)
function buildClassMap(pcStr) {
let pc;
if (!pcStr) return null;
try { pc = (typeof pcStr === 'string') ? JSON.parse(pcStr) : pcStr; } catch (e) { return null; }
if (!pc || !pc.id || !pc.id.length) return null;
const ids = pc.id || [];
const exps = pc.exportName || [];
const names = pc.name || [];
const attrs = pc.attributes || [];
const m = {
mainId: null, // 行人主框类目 id
otherId: null, // 其他行人框类目 id
idName: {}, // 类目 id → 名称
ignoreAttr: {}, // 类目 id → { attrId, values:Set(忽略值id) }
occAttr: {}, // 类目 id → { attrId, values:{} }
truncAttr: {}, // 类目 id → { attrId, values:{} }
allIds: ids.slice()
};
ids.forEach(function (id, i) {
const ex = String(exps[i] || '');
const nm = String(names[i] || '');
m.idName[id] = nm || ex || id;
if (/other/i.test(ex) || nm.indexOf('其他') !== -1) m.otherId = id;
else if (!m.mainId) m.mainId = id;
const collect = function (a, target) {
const vals = {};
(a.attributes || []).forEach(function (v) { vals[v.id] = String(v.name || v.exportName || ''); });
target[id] = { attrId: a.id, values: vals };
};
(attrs[i] || []).forEach(function (a) {
const ax = String(a.exportName || '');
const an = String(a.name || '');
// 是否忽略:主框是「Ignore / 0=是 1=否」,其他框是「ignore / 0=有效 1=忽略」
if (/ignore/i.test(ax) || an.indexOf('忽略') !== -1) {
const set = {};
(a.attributes || []).forEach(function (v) {
const vn = String(v.name || '').trim();
const ve = String(v.exportName || '').trim();
if (vn === '忽略' || ve === '忽略' || vn === '是' || ve === '是') set[v.id] = true;
});
m.ignoreAttr[id] = { attrId: a.id, values: set };
}
if (/occlusion/i.test(ax) || an.indexOf('遮挡') !== -1) collect(a, m.occAttr);
if (/truncation/i.test(ax) || an.indexOf('截断') !== -1) collect(a, m.truncAttr);
});
});
if (!m.mainId) m.mainId = m.otherId;
return m;
}
function getMap() { return CMAP; }
function isIgnored(box) {
const M = getMap();
if (!M || !M.ignoreAttr) return false;
const conf = M.ignoreAttr[box.typeId];
if (!conf) return false;
const v = box.attrs ? box.attrs[conf.attrId] : null;
return !!(v && conf.values[v]);
}
/* ==================== 标注归一化 ==================== */
// 比邻云 shapeList 元素:{ labelId, imageId, shapeJson, attrJson }(后两者是字符串化 JSON)
function boxOf(s) {
if (!s || s.labelId === undefined) return null;
const sh = parseJson(s.shapeJson);
const at = parseJson(s.attrJson);
const g = sh.shape || {};
const lx = num(g.lx), ly = num(g.ly), rx = num(g.rx), ry = num(g.ry);
return {
labelId: s.labelId,
typeId: at.type_id,
attrs: at.attributes || {},
isRect: String(sh.shapeType) === '0',
lx: lx, ly: ly, rx: rx, ry: ry,
w: (lx !== null && rx !== null) ? (rx - lx) : null,
h: (ly !== null && ry !== null) ? (ry - ly) : null,
clsName: (getMap() && getMap().idName[s.attrJson ? at.type_id : '']) || ''
};
}
/* ==================== 图像真实尺寸 ==================== */
// 页面 里有 logo,不能直接用;优先从 Konva 舞台的 Image 节点取
function getImageSize() {
let w = 0, h = 0;
try {
if (typeof Konva !== 'undefined' && Konva.stages && Konva.stages.length) {
const nodes = Konva.stages[0].find('Image');
for (let i = 0; i < nodes.length; i++) {
const im = nodes[i].image && nodes[i].image();
if (im && im.naturalWidth > 200 && im.naturalHeight > 200) {
w = im.naturalWidth; h = im.naturalHeight; break;
}
}
}
} catch (e) { }
if (!w) {
const imgs = document.querySelectorAll('img');
for (let i = 0; i < imgs.length; i++) {
const iw = imgs[i].naturalWidth, ih = imgs[i].naturalHeight;
if (iw > 1000 && ih > 1000) { w = iw; h = ih; break; }
}
}
if (!w) { IMG_SIZE = null; return null; }
if (!IMG_SIZE || IMG_SIZE.w !== w || IMG_SIZE.h !== h) IMG_SIZE = { w: w, h: h };
return IMG_SIZE;
}
/* ==================== 本帧分析 ==================== */
let lastBoxes = null;
function analyzeShapes(shapeList, size) {
const boxes = [];
(shapeList || []).forEach(function (s) {
const b = boxOf(s);
if (b && b.isRect) boxes.push(b);
});
const ignored = boxes.filter(function (b) { return isIgnored(b); });
lastBoxes = { boxes: boxes, valid: boxes.length - ignored.length, ignored: ignored.length };
const rk = boxes.map(function (b) {
return b.labelId + '|' + b.lx + ',' + b.ly + ',' + b.rx + ',' + b.ry + '|' + JSON.stringify(b.attrs);
}).join(';') + '|' + currentFrameNo() + '|' + (size ? size.w + 'x' + size.h : '-');
if (rk === lastRenderKey) return; // 数据未变不重渲染,避免面板闪烁
lastRenderKey = rk;
renderPanel();
}
function showWaiting(no) {
if (WARMUP_RUNNING) return;
lastBoxes = null;
lastRenderKey = null;
renderPanel(no);
}
/* ==================== 面板 UI ==================== */
let panelContainer = null, panelBody = null, panelContent = null;
let isMinimized = false, isDragging = false, dragOffsetX = 0, dragOffsetY = 0;
let panelX = null, panelY = 60;
function createPanel() {
if (panelContainer && document.body && document.body.contains(panelContainer)) return;
const exist = document.getElementById('_blyped_panel');
if (exist) { panelContainer = exist; panelBody = exist.querySelector('#_blyped_body'); panelContent = exist.querySelector('#_blyped_content'); return; }
panelContainer = document.createElement('div');
panelContainer.id = '_blyped_panel';
if (panelX === null) panelX = Math.max(20, window.innerWidth - 372);
panelContainer.style.cssText = 'position:fixed;z-index:999999;width:352px;background:#ffffff;'
+ 'border:1px solid #d1d5db;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.15);'
+ 'font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:13px;color:#1f2937;'
+ 'overflow:hidden;left:' + panelX + 'px;top:' + panelY + 'px;user-select:none;';
const titleBar = document.createElement('div');
titleBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;'
+ 'background:#f3f4f6;cursor:move;border-bottom:1px solid #d1d5db;';
titleBar.innerHTML = '🚶 比邻云行人助手 ';
titleBar.addEventListener('mousedown', startDrag);
const btns = document.createElement('div');
btns.style.cssText = 'display:flex;gap:6px;';
btns.appendChild(createBtn('—', '#e5e7eb', '#d1d5db', function (e) { e.stopPropagation(); toggleMinimize(); }));
btns.appendChild(createBtn('✕', '#fca5a5', '#f87171', function (e) { e.stopPropagation(); closePanel(); }));
titleBar.appendChild(btns);
panelContainer.appendChild(titleBar);
panelBody = document.createElement('div');
panelBody.id = '_blyped_body';
panelBody.style.cssText = 'max-height:520px;overflow-y:auto;';
panelContent = document.createElement('div');
panelContent.id = '_blyped_content';
panelContent.style.cssText = 'padding:11px;background:#ffffff;';
panelBody.appendChild(panelContent);
panelContainer.appendChild(panelBody);
document.body.appendChild(panelContainer);
}
function createBtn(text, color, hover, onClick) {
const b = document.createElement('span');
b.textContent = text;
b.style.cssText = 'width:26px;height:26px;display:flex;align-items:center;justify-content:center;'
+ 'border-radius:6px;cursor:pointer;font-size:14px;font-weight:bold;color:#374151;background:' + color + ';';
b.addEventListener('mouseenter', function () { b.style.background = hover; });
b.addEventListener('mouseleave', function () { b.style.background = color; });
b.addEventListener('click', onClick);
return b;
}
function toggleMinimize() {
isMinimized = !isMinimized;
if (panelBody) panelBody.style.maxHeight = isMinimized ? '0' : '520px';
}
function closePanel() {
if (panelContainer) { panelContainer.remove(); panelContainer = null; }
}
function startDrag(e) {
if (e.button !== 0 || !panelContainer) return;
isDragging = true;
const r = panelContainer.getBoundingClientRect();
dragOffsetX = e.clientX - r.left;
dragOffsetY = e.clientY - r.top;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
e.preventDefault();
}
function onDrag(e) {
if (!isDragging || !panelContainer) return;
panelX = Math.max(0, e.clientX - dragOffsetX);
panelY = Math.max(0, e.clientY - dragOffsetY);
panelContainer.style.left = panelX + 'px';
panelContainer.style.top = panelY + 'px';
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
}
function row(label, value) {
return '
'
+ '' + label + ' '
+ '' + value + '
';
}
function renderPanel(waitingNo) {
createPanel();
if (!panelContent) return;
const no = currentFrameNo();
const total = FRAME_IDS.length || (function () {
const el = document.querySelector('.frame-counter-clickable');
const m = el ? /(\d+)\s*\/\s*(\d+)/.exec(el.innerText || '') : null;
return m ? parseInt(m[2], 10) : 0;
})();
let h = '';
// 顶部信息
h += '';
if (PROJECT_INFO.projectName) h += row('项目', esc(PROJECT_INFO.projectName));
if (PROJECT_INFO.taskNo) h += row('包号', esc(PROJECT_INFO.taskNo));
h += row('当前帧', (no || '-') + ' / ' + (total || '-'));
h += '
';
// 本帧状态
if (waitingNo) {
h += ''
+ '
⏳ '
+ '
'
+ '
第 ' + waitingNo + ' 帧加载中…
'
+ '
正在获取该帧标注数据 ';
} else if (lastBoxes) {
const ok = lastBoxes.ignored === 0;
h += ''
+ '
' + (ok ? '✅' : 'ℹ️') + ' '
+ '
'
+ '
本帧 ' + lastBoxes.boxes.length + ' 个框'
+ (lastBoxes.ignored ? '(忽略 ' + lastBoxes.ignored + ')' : '') + '
'
+ '
参与统计 ' + lastBoxes.valid + ' 个 ';
} else {
h += '本帧暂无标注数据
';
}
// 整包统计
h += '';
h += '
';
const st = PACK_STAT;
h += cell('总帧', st ? st.frameTotal : (total || '-'));
h += cell('有框帧数', st ? st.frameWithBox : '-');
h += cell('总框数', st ? st.boxTotal : '-');
h += '
';
// 尺寸异常列表(点击跳帧)
if (st && st.sizeErrors && st.sizeErrors.length) {
h += '';
h += '
⚠ 尺寸不达标(点击跳帧):
';
st.sizeErrors.forEach(function (ef) {
h += '
';
h += '
'
+ '第 ' + ef.frameNo + ' 帧 ' + ef.items.length + ' 个 →
';
ef.items.forEach(function (it) {
h += '
• [' + esc(it.cls) + ' #' + it.labelId + '] ' + esc(it.msg) + '
';
});
h += '
';
});
h += '
';
} else if (st) {
h += '✓ 未发现尺寸异常(宽≥' + MIN_W + ' 高≥' + MIN_H + ',忽略框已跳过)
';
}
// 功能区
h += '';
h += '
';
h += ' ';
h += '进题包预加载全部帧(消除首次切帧等待) ';
h += '
' + COPY_LABEL + '
';
h += '
🔍 整包检查(框尺寸)
';
h += '
';
h += '
';
panelContent.innerHTML = h;
const cb = document.getElementById('_blyped_warm');
if (cb) cb.addEventListener('change', function () {
WARMUP_ON = cb.checked;
try { localStorage.setItem(WARMUP_KEY, WARMUP_ON ? '1' : '0'); } catch (e) { }
if (WARMUP_ON && !WARMUP_DONE) warmupAllFrames();
});
const cpb = document.getElementById('_blyped_copy');
if (cpb) cpb.addEventListener('click', function () { copyPackInfo(); });
const sb = document.getElementById('_blyped_stat');
if (sb) sb.addEventListener('click', function () { runPackCheck(); });
panelContent.querySelectorAll('[data-blyped-frame]').forEach(function (el) {
el.addEventListener('click', function () { goToFrame(el.getAttribute('data-blyped-frame')); });
});
// 只在帧号变化时回到顶部,避免统计结果被滚走
const key = 'f' + no;
if (panelBody && key !== lastPanelFrame) { panelBody.scrollTop = 0; lastPanelFrame = key; }
}
let lastPanelFrame = null;
function cell(label, value) {
return ''
+ '
' + label + '
'
+ '
' + esc(value) + '
';
}
function setMsg(s) {
const el = document.getElementById('_blyped_msg');
if (el) el.innerHTML = s;
}
/* ==================== 帧缓存与补请求 ==================== */
function requestFrame(no) {
const fid = FRAME_IDS[no - 1];
if (!fid || !AUTH_HDRS) { showWaiting(no); return; }
fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS })
.then(function (r) { return r.json(); })
.then(function (j) {
if (j && (j.code === 200 || j.code === 0) && j.data && j.data.shapeList) {
FRAME_CACHE[fid] = j.data.shapeList;
analyzeShapes(j.data.shapeList, getImageSize());
} else { showWaiting(no); }
})
.catch(function () { showWaiting(no); });
}
let staticFetching = false;
function fetchStatic() {
const tid = taskIdFromUrl();
if (!tid || !AUTH_HDRS || staticFetching) return;
staticFetching = true;
fetch(API_STATIC + '?task_id=' + tid, { headers: AUTH_HDRS })
.then(function (r) { return r.json(); })
.then(function (j) { handleStaticResponse(j); })
.catch(function () { })
.then(function () { staticFetching = false; });
}
function handleMarkResponse(url, json) {
if (!json || (json.code !== 200 && json.code !== 0)) return;
const d = json.data;
if (!d || !d.shapeList) return;
const fid = frameIdFromUrl(url);
if (fid) FRAME_CACHE[fid] = d.shapeList;
analyzeShapes(d.shapeList, getImageSize());
}
function handleStaticResponse(json) {
if (!json || (json.code !== 200 && json.code !== 0)) return;
const d = json.data || {};
const list = d.image2dList || [];
if (list.length) FRAME_IDS = list.map(function (f) { return f.frameId; });
const pi = d.projectInfo || {};
PROJECT_INFO = {
taskNo: pi.taskNo ? String(pi.taskNo) : '',
projectName: pi.projectName ? String(pi.projectName) : ''
};
const m = buildClassMap(pi.projectClass);
if (m) {
CMAP = m;
lastRenderKey = null;
if (lastBoxes) renderPanel();
if (!warmupTried && WARMUP_ON && FRAME_IDS.length) {
warmupTried = true;
setTimeout(function () { warmupAllFrames(); }, 2500);
}
}
}
function routeResponse(url, json) {
if (!url) return;
if (url.indexOf(API_MARK) !== -1) handleMarkResponse(url, json);
else if (url.indexOf(API_STATIC) !== -1) handleStaticResponse(json);
}
/* ==================== 整包检查(统计 + 框尺寸) ==================== */
function clsLabel(b) {
const M = getMap();
return (M && M.idName && M.idName[b.typeId]) || '行人框';
}
function sizeProblem(b) {
if (b.w === null || b.h === null) return '无有效尺寸数据';
if (b.w < MIN_W || b.h < MIN_H) {
return '宽 ' + fmt1(b.w) + 'px(需≥' + MIN_W + ') 高 ' + fmt1(b.h) + 'px(需≥' + MIN_H + ')';
}
return null;
}
function frameStat(shapeList) {
let total = 0, valid = 0;
const errs = [];
(shapeList || []).forEach(function (s) {
const b = boxOf(s);
if (!b || !b.isRect) return;
total++;
if (isIgnored(b)) return; // 忽略框计入总数,但不参与尺寸检查
valid++;
const msg = sizeProblem(b);
if (msg) errs.push({ labelId: b.labelId, cls: clsLabel(b), w: b.w, h: b.h, msg: msg });
});
return { total: total, valid: valid, errs: errs };
}
async function getFrameList(fid) {
let list = FRAME_CACHE[fid];
if (list) return list;
if (!AUTH_HDRS) return [];
try {
const j = await (await fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS })).json();
list = (j && j.data && j.data.shapeList) || [];
} catch (e) { list = []; }
FRAME_CACHE[fid] = list;
return list;
}
let CHECK_RUNNING = false;
async function runPackCheck() {
if (CHECK_RUNNING) return;
if (!FRAME_IDS.length) { setMsg('⚠ 帧列表未就绪,请刷新页面'); return; }
if (!AUTH_HDRS) { setMsg('⚠ 请先在工作台切一次帧'); return; }
CHECK_RUNNING = true;
setMsg('⏳ 整包检查中…');
let frameWithBox = 0, boxTotal = 0, boxValid = 0;
const sizeErrors = [];
for (let i = 0; i < FRAME_IDS.length; i++) {
const list = await getFrameList(FRAME_IDS[i]);
const s = frameStat(list);
boxTotal += s.total;
boxValid += s.valid;
if (s.total > 0) frameWithBox++;
if (s.errs.length) sizeErrors.push({ frameNo: i + 1, items: s.errs });
}
PACK_STAT = {
frameTotal: FRAME_IDS.length, frameWithBox: frameWithBox,
boxTotal: boxTotal, boxValid: boxValid, boxIgnored: boxTotal - boxValid,
sizeErrors: sizeErrors,
badFrameCount: sizeErrors.length,
badBoxCount: sizeErrors.reduce(function (a, f) { return a + f.items.length; }, 0)
};
lastRenderKey = null;
renderPanel();
setMsg(PACK_STAT.badBoxCount
? '⚠ ' + PACK_STAT.badBoxCount + ' 个框尺寸不达标(分布 ' + PACK_STAT.badFrameCount + ' 帧),见上方列表'
: '✓ 框尺寸全部达标(宽≥' + MIN_W + ' 高≥' + MIN_H + ',忽略框已跳过)');
CHECK_RUNNING = false;
}
/* ==================== 复制「包号 + 帧数 + 框数」 ==================== */
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
return new Promise(function (resolve, reject) {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;left:-9999px;top:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
resolve();
} catch (e) { reject(e); }
});
}
const COPY_LABEL = '📋 复制「包号 + 有效帧数 + 框数」';
function setCopyBtn(t) {
const b = document.getElementById('_blyped_copy');
if (b) b.innerText = t;
}
async function copyPackInfo() {
if (!FRAME_IDS.length) { setMsg('⚠ 帧列表未就绪,请刷新页面'); return; }
if (!AUTH_HDRS) { setMsg('⚠ 请先在工作台切一次帧'); return; }
setCopyBtn('⏳ 统计中…');
if (!PACK_STAT) await runPackCheck();
const stat = PACK_STAT || { frameWithBox: 0, boxTotal: 0 };
const no = PROJECT_INFO.taskNo || '未知包号';
// 包号 → 空一列 → 有效帧数(有框的帧)→ 框数
try {
await copyText(no + '\t\t' + stat.frameWithBox + '\t' + stat.boxTotal);
setCopyBtn('✓ 已复制:' + no + ' · ' + stat.frameWithBox + ' 有效帧 · ' + stat.boxTotal + ' 框');
} catch (e) {
setCopyBtn('⚠ 复制失败:' + (e && e.message ? e.message : e));
}
setTimeout(function () { setCopyBtn(COPY_LABEL); }, 2500);
}
/* ==================== 体验优化:去黑屏 ==================== */
// 平台切帧会弹全屏遮罩(.block-mask) + 转圈(.pc-loading),实测约 400ms
function installMaskCss() {
if (document.getElementById('_blyped_mask_style')) return;
const st = document.createElement('style');
st.id = '_blyped_mask_style';
st.textContent = '.block-mask{background-color:transparent !important;}'
+ '.pc-loading{display:none !important;}'
+ '._blyped_row:hover{background:#eef2ff !important;}'
+ '._blyped_row:active{background:#e0e7ff !important;}';
(document.head || document.documentElement).appendChild(st);
}
/* ==================== 进题包全量预热 ==================== */
// 平台切帧流程 loadAnnotatesDate() 第一步就 clearResource() 清空画布(灰屏来源),
// 随后若 dataManager.getFrameObject(frameId) 命中则直接 return(秒开)。
// 所以这里进题包后主动把每一帧过一遍,把 dataManager 填满 —— 之后切帧就顺滑了。
function goToFrame(n) {
const items = document.querySelectorAll('.frame-navigation .page-item');
for (let i = 0; i < items.length; i++) {
if ((items[i].innerText || '').trim() === String(n)) { items[i].click(); return true; }
}
return false;
}
function frameDataReady(fid) {
try {
const dm = window.editor && window.editor.dataManager;
if (!dm || typeof dm.getFrameObject !== 'function') return false;
return !!dm.getFrameObject(fid);
} catch (e) { return false; }
}
function waitFrameReady(fid, timeout) {
return new Promise(function (resolve) {
const t0 = Date.now();
const timer = setInterval(function () {
if (frameDataReady(fid)) { clearInterval(timer); resolve(true); return; }
if (Date.now() - t0 > (timeout || 4000)) { clearInterval(timer); resolve(false); }
}, 80);
});
}
function warmupOverlay(show, cur, total) {
let el = document.getElementById('_blyped_warm');
if (!show) { if (el) el.remove(); return; }
if (!el) {
el = document.createElement('div');
el.id = '_blyped_warm';
el.style.cssText = 'position:fixed;z-index:999997;left:50%;top:50%;transform:translate(-50%,-50%);'
+ 'background:rgba(17,24,39,0.92);color:#fff;padding:14px 22px;border-radius:9px;text-align:center;'
+ 'font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:13px;box-shadow:0 8px 28px rgba(0,0,0,.35);';
document.body.appendChild(el);
}
el.innerHTML = '📦 正在预加载全部帧…
'
+ '' + (cur || 0) + ' / ' + (total || 0) + ' 完成后切帧不再等待
';
}
async function warmupAllFrames() {
if (WARMUP_RUNNING || WARMUP_DONE) return;
if (!WARMUP_ON || !FRAME_IDS.length || !AUTH_HDRS) return;
WARMUP_RUNNING = true;
const startNo = currentFrameNo() || 1;
const total = FRAME_IDS.length;
warmupOverlay(true, 0, total);
let done = 0;
for (let n = 1; n <= total; n++) {
const fid = FRAME_IDS[n - 1];
if (!frameDataReady(fid)) {
goToFrame(n);
await waitFrameReady(fid, 4000);
}
const cached = FRAME_CACHE[fid];
if (!cached && AUTH_HDRS) {
try {
const j = await (await fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS })).json();
if (j && j.data && j.data.shapeList) FRAME_CACHE[fid] = j.data.shapeList;
} catch (e) { }
}
done++;
warmupOverlay(true, done, total);
}
goToFrame(startNo);
await waitFrameReady(FRAME_IDS[startNo - 1], 3000);
warmupOverlay(false);
WARMUP_DONE = true;
WARMUP_RUNNING = false;
lastRenderKey = null;
renderPanel();
// 全部帧已在缓存里,顺手把整包检查(统计 + 尺寸)跑出来
runPackCheck();
}
/* ==================== 鉴权头捕获 ==================== */
try {
const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
if (!AUTH_HDRS && /^blade-auth$/i.test(k)) {
AUTH_HDRS = { 'Blade-Auth': v, 'Tenant-Id': localStorage.getItem('tenantId') || '' };
}
return origSetHeader.apply(this, arguments);
};
} catch (e) { }
/* ==================== XHR / fetch 拦截 ==================== */
try {
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
this._blypedUrl = url;
return origOpen.apply(this, arguments);
};
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
const u = this._blypedUrl || '';
if (u.indexOf(API_MARK) !== -1 || u.indexOf(API_STATIC) !== -1) {
this.addEventListener('load', function () {
let j = null;
try { j = JSON.parse(this.responseText); } catch (e) { return; }
routeResponse(u, j);
});
}
return origSend.apply(this, arguments);
};
} catch (e) { }
try {
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function (input, init) {
const url = (typeof input === 'string') ? input : ((input instanceof Request) ? input.url : '');
const hit = (url.indexOf(API_MARK) !== -1 || url.indexOf(API_STATIC) !== -1);
if (!hit) return origFetch.apply(this, arguments);
return origFetch.apply(this, arguments).then(function (response) {
try {
response.clone().json().then(function (data) { routeResponse(url, data); }).catch(function () { });
} catch (e) { }
return response;
});
};
}
} catch (e) { }
/* ==================== 帧切换监听 ==================== */
function handleFrameSwitch(no) {
lastFrameNo = no;
const fid = FRAME_IDS[no - 1];
const cached = (fid && FRAME_CACHE[fid]) ? FRAME_CACHE[fid] : null;
if (cached) {
analyzeShapes(cached, getImageSize());
} else {
showWaiting(no);
requestFrame(no);
}
}
// 用 MutationObserver 即时感知帧切换,定时器只做兜底
function setupFrameWatcher() {
const nav = document.querySelector('.frame-navigation');
if (!nav || nav.__blypedMo) return;
nav.__blypedMo = new MutationObserver(function () {
const n = currentFrameNo();
if (n !== null && n !== lastFrameNo) handleFrameSwitch(n);
});
nav.__blypedMo.observe(nav, { childList: true, subtree: true, attributes: true, characterData: true });
}
setInterval(function () {
if (!staticTried && AUTH_HDRS && FRAME_IDS.length === 0) { staticTried = true; fetchStatic(); }
setupFrameWatcher();
const no = currentFrameNo();
if (no !== null && no !== lastFrameNo) { handleFrameSwitch(no); return; }
// 兜底:数据未变时 analyzeShapes 内部会因指纹相同直接 return,不会重建面板
if (lastBoxes) analyzeShapesFromCache();
}, 900);
function analyzeShapesFromCache() {
const no = currentFrameNo();
const fid = no ? FRAME_IDS[no - 1] : null;
const list = fid ? FRAME_CACHE[fid] : null;
if (list) analyzeShapes(list, getImageSize());
}
/* ==================== 启动 ==================== */
function boot() {
if (location.pathname.indexOf('/tool/image') === -1) return;
installMaskCss();
createPanel();
renderPanel();
setTimeout(function () { if (AUTH_HDRS && !FRAME_IDS.length) fetchStatic(); }, 3000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
/* ==================== 调试导出 ==================== */
window.__BLYPED__ = {
getState: function () {
return {
frameIds: FRAME_IDS.length,
cached: Object.keys(FRAME_CACHE).length,
auth: Boolean(AUTH_HDRS),
curFrame: currentFrameNo(),
warmupDone: WARMUP_DONE,
cmap: CMAP ? { main: CMAP.idName[CMAP.mainId], other: CMAP.otherId ? CMAP.idName[CMAP.otherId] : null } : null,
imgSize: getImageSize(),
packStat: PACK_STAT
};
},
analyze: analyzeShapes,
frameStat: frameStat,
boxOf: boxOf,
isIgnored: isIgnored,
sizeProblem: sizeProblem,
minSize: { w: MIN_W, h: MIN_H },
runPackCheck: runPackCheck,
copyPackInfo: copyPackInfo,
setPackStat: function (s) { PACK_STAT = s; lastRenderKey = null; renderPanel(); },
setFrameIds: function (ids) { FRAME_IDS = ids || []; },
setCmap: function (m) { CMAP = m; },
closePanel: closePanel
};
console.log('[比邻云行人助手] 脚本加载成功');
})();