539 lines
24 KiB
JavaScript
539 lines
24 KiB
JavaScript
const state = {
|
||
data: null,
|
||
filter: "UNREVIEWED",
|
||
visibleItems: [],
|
||
currentIndex: 0,
|
||
selectedStatus: null,
|
||
selectedLayers: new Set(),
|
||
selectedStdKey: null,
|
||
selectedComponentKind: null
|
||
};
|
||
|
||
const $ = function(id){ return document.getElementById(id); };
|
||
|
||
function esc(v){
|
||
return String(v == null ? "" : v).replace(/[&<>"']/g, function(ch){
|
||
return {"&":"&","<":"<",">":">","\"":""","'":"'"}[ch];
|
||
});
|
||
}
|
||
|
||
async function api(path, options){
|
||
const res = await fetch(path, options || {});
|
||
const data = await res.json();
|
||
if(!res.ok) throw new Error(data.error || ("HTTP " + res.status));
|
||
return data;
|
||
}
|
||
|
||
async function load(){
|
||
state.data = await api("/api/bootstrap");
|
||
populateLayerFilter();
|
||
applyFilter(true);
|
||
updateProgress(state.data.progress);
|
||
}
|
||
|
||
function decision(item){ return item.decision || null; }
|
||
|
||
function applyFilter(resetIndex){
|
||
const f = state.filter;
|
||
state.visibleItems = state.data.items.filter(function(item){
|
||
const d = decision(item);
|
||
if(f === "ALL") return true;
|
||
if(f === "UNREVIEWED") return !d;
|
||
return d && d.status === f;
|
||
});
|
||
if(resetIndex) state.currentIndex = 0;
|
||
if(state.currentIndex >= state.visibleItems.length) state.currentIndex = Math.max(0, state.visibleItems.length - 1);
|
||
renderQueue();
|
||
renderCurrent();
|
||
}
|
||
|
||
function renderQueue(){
|
||
$("queueCount").textContent = state.visibleItems.length + " 项";
|
||
$("queue").innerHTML = state.visibleItems.map(function(item, idx){
|
||
const d = decision(item);
|
||
const st = d ? d.status : "未确认";
|
||
return '<button class="queue-item ' + (idx===state.currentIndex?'active':'') + '" data-index="' + idx + '">' +
|
||
'<span class="queue-thumb"><img loading="lazy" src="/sprite/cm95/' + encodeURIComponent(item.anonymous_id) + '.png"></span>' +
|
||
'<span class="queue-info"><strong>' + esc(item.display_name_zh || item.anonymous_id) + '</strong>' +
|
||
'<small>' + esc(item.anonymous_id) + '</small>' +
|
||
'<small>' + esc(item.component_roles.join(", ") || "—") + ' · ' + item.occurrence_count + ' 次</small>' +
|
||
'<span class="queue-status">' + esc(st) + '</span></span></button>';
|
||
}).join("");
|
||
document.querySelectorAll(".queue-item").forEach(function(btn){
|
||
btn.addEventListener("click", function(){
|
||
state.currentIndex = Number(btn.dataset.index);
|
||
renderQueue();
|
||
renderCurrent();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderCurrent(){
|
||
const item = state.visibleItems[state.currentIndex];
|
||
const empty = !item;
|
||
$("emptyState").classList.toggle("hidden", !empty);
|
||
$("reviewContent").classList.toggle("hidden", empty);
|
||
if(empty) return;
|
||
|
||
$("cmNameZh").textContent = item.display_name_zh || "未命名视觉对象";
|
||
$("anonId").textContent = item.anonymous_id;
|
||
$("cmOfficialDesc").textContent = item.official_description_en ? ("S-52: " + item.official_description_en) : (item.name_source || "");
|
||
$("cmImage").src = "/sprite/cm95/" + encodeURIComponent(item.anonymous_id) + ".png";
|
||
$("componentRoles").textContent = item.component_roles.join(", ") || "—";
|
||
$("geometryRoles").textContent = item.geometry_roles.join(", ") || "—";
|
||
$("occurrenceCount").textContent = item.occurrence_count;
|
||
|
||
const sem = item.semantic || {};
|
||
$("semanticCategory").textContent = sem.category_zh || "尚未建立语义分类";
|
||
$("semanticReason").textContent = sem.reason_zh || "当前没有可靠的 Provider 语义规则。";
|
||
const componentHint = $("semanticComponentHint");
|
||
if(sem.component_kind){
|
||
const kind = (state.data.component_kinds || []).find(function(x){ return x.id === sem.component_kind; });
|
||
componentHint.classList.remove("hidden");
|
||
componentHint.textContent = "建议操作:COMPONENT · " + (kind ? kind.name_zh : sem.component_kind);
|
||
} else {
|
||
componentHint.classList.add("hidden");
|
||
componentHint.textContent = "";
|
||
}
|
||
$("decisionConflict").classList.toggle("hidden", !item.decision_conflict);
|
||
|
||
const s = item.suggestion;
|
||
if(s){
|
||
const layers = s.standard_layer ? [s.standard_layer] : (s.standard_layers || []);
|
||
$("suggestionBox").classList.remove("hidden");
|
||
const layerText = layers.map(function(layer){
|
||
return (state.data.standard_layer_zh[layer] || layer) + "(" + layer + ")";
|
||
}).join(" / ");
|
||
const nameText = s.standard_name_zh || "未指定标准对象名";
|
||
$("suggestionBox").innerHTML = "<b>旧视觉匹配建议(辅助证据)</b><br>" +
|
||
'<span class="candidate-name">' + esc(nameText) + '</span><br>' +
|
||
esc(s.status || "—") + " → " + esc(layerText || "—") +
|
||
(s.review_stage ? '<br><span class="muted">' + esc(s.review_stage) + "</span>" : "");
|
||
} else {
|
||
$("suggestionBox").classList.add("hidden");
|
||
$("suggestionBox").innerHTML = "";
|
||
}
|
||
|
||
const d = decision(item);
|
||
state.selectedStatus = d ? d.status : null;
|
||
state.selectedLayers = new Set(d ? (d.standard_layers || []) : []);
|
||
state.selectedStdKey = d ? d.standard_resource_key : null;
|
||
state.selectedComponentKind = d ? (d.component_kind || null) : null;
|
||
$("note").value = d ? (d.note || "") : "";
|
||
|
||
renderDecisionBadge(d);
|
||
renderStatusButtons();
|
||
renderModeFields();
|
||
renderComponentKindSelection();
|
||
renderLayerSelection();
|
||
renderLayerSuggestions(item);
|
||
renderCandidates(item);
|
||
renderSelectedStd();
|
||
$("auditDetails").open = false;
|
||
$("auditText").textContent = "尚未读取。";
|
||
$("message").textContent = "";
|
||
$("message").className = "message";
|
||
}
|
||
|
||
function renderDecisionBadge(d){
|
||
const badge = $("decisionBadge");
|
||
const st = d ? d.status : null;
|
||
badge.className = "badge " + (st ? st.toLowerCase() : "neutral");
|
||
badge.textContent = st || "未确认";
|
||
}
|
||
|
||
function renderStatusButtons(){
|
||
document.querySelectorAll(".status-btn").forEach(function(btn){
|
||
btn.classList.toggle("active", btn.dataset.status === state.selectedStatus);
|
||
});
|
||
}
|
||
|
||
function renderModeFields(){
|
||
const isComponent = state.selectedStatus === "COMPONENT";
|
||
$("componentKindField").classList.toggle("hidden", !isComponent);
|
||
$("standardLayerField").classList.toggle("hidden", isComponent);
|
||
$("standardObjectField").classList.toggle("hidden", isComponent);
|
||
}
|
||
|
||
function renderComponentKindSelection(){
|
||
const kinds = state.data.component_kinds || [];
|
||
$("componentKindSelection").innerHTML = kinds.map(function(k){
|
||
return '<button class="component-kind-chip ' + (state.selectedComponentKind===k.id?'selected':'') +
|
||
'" data-kind="' + esc(k.id) + '">' + esc(k.name_zh) + '</button>';
|
||
}).join("");
|
||
document.querySelectorAll(".component-kind-chip").forEach(function(btn){
|
||
btn.addEventListener("click", function(){
|
||
const kind = btn.dataset.kind;
|
||
state.selectedComponentKind = state.selectedComponentKind === kind ? null : kind;
|
||
renderComponentKindSelection();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderLayerSelection(){
|
||
$("layerSelection").innerHTML = state.data.standard_layers.map(function(layer){
|
||
const zh = state.data.standard_layer_zh[layer] || layer;
|
||
return '<button class="layer-chip ' + (state.selectedLayers.has(layer)?'selected':'') +
|
||
'" data-layer="' + esc(layer) + '">' + esc(zh) + '<span class="layer-code"> · ' + esc(layer) + '</span></button>';
|
||
}).join("");
|
||
|
||
document.querySelectorAll(".layer-chip").forEach(function(btn){
|
||
btn.addEventListener("click", function(){
|
||
const layer = btn.dataset.layer;
|
||
if(state.selectedStatus === "UNKNOWN" || state.selectedStatus === "COMPONENT") return;
|
||
if(state.selectedStatus === "MATCH" || state.selectedStatus === "PARTIAL"){
|
||
state.selectedLayers = new Set([layer]);
|
||
if(state.selectedStdKey){
|
||
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
|
||
if(sr && sr.standard_layer !== layer) state.selectedStdKey = null;
|
||
}
|
||
} else {
|
||
if(state.selectedLayers.has(layer)) state.selectedLayers.delete(layer);
|
||
else state.selectedLayers.add(layer);
|
||
}
|
||
renderLayerSelection();
|
||
renderSelectedStd();
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderLayerSuggestions(item){
|
||
const rows = item.layer_suggestions || [];
|
||
const box = $("layerSuggestionList");
|
||
if(item.semantic && item.semantic.component_kind){
|
||
const kind = (state.data.component_kinds || []).find(function(x){ return x.id === item.semantic.component_kind; });
|
||
const kindName = kind ? kind.name_zh : item.semantic.component_kind;
|
||
box.innerHTML = '<div class="component-recommendation">' +
|
||
'<div class="layer-rank-title">建议先作为组件处理</div>' +
|
||
'<div class="layer-rank-name">COMPONENT · ' + esc(kindName) + '</div>' +
|
||
'<div class="layer-rank-reason">' + esc(item.semantic.reason_zh || '') + '</div>' +
|
||
'<button id="applyComponentSuggestion" class="apply-suggestion-btn">采用 COMPONENT 建议</button>' +
|
||
'</div>' + renderLayerRows(rows);
|
||
const btn = $("applyComponentSuggestion");
|
||
if(btn){ btn.addEventListener("click", function(){
|
||
chooseStatus("COMPONENT");
|
||
state.selectedComponentKind = item.semantic.component_kind;
|
||
renderComponentKindSelection();
|
||
}); }
|
||
bindLayerSuggestionClicks();
|
||
return;
|
||
}
|
||
if(!rows.length){
|
||
box.innerHTML = '<div class="no-layer-suggestion">当前 S-52 语义没有可靠的 NavSea Layer 建议。请人工选择,或保留 UNKNOWN。</div>';
|
||
return;
|
||
}
|
||
box.innerHTML = renderLayerRows(rows);
|
||
bindLayerSuggestionClicks();
|
||
}
|
||
|
||
function renderLayerRows(rows){
|
||
return rows.map(function(r, idx){
|
||
const top = idx === 0;
|
||
const score = (Number(r.combined_score || 0) * 100).toFixed(1);
|
||
const sem = (Number(r.semantic_score || 0) * 100).toFixed(0);
|
||
const vis = (Number(r.visual_score || 0) * 100).toFixed(0);
|
||
return '<button class="layer-suggestion-card ' + (top?'top':'') + '" data-layer="' + esc(r.standard_layer) + '">' +
|
||
'<div class="layer-rank-head"><span class="layer-rank-index">' + (idx+1) + '</span>' +
|
||
'<span class="layer-rank-name">' + esc(r.standard_layer_zh || r.standard_layer) + '</span>' +
|
||
(top?'<span class="layer-rank-top">最可能</span>':'') + '</div>' +
|
||
'<div class="layer-rank-code">' + esc(r.standard_layer) + '</div>' +
|
||
'<div class="layer-rank-score">综合 ' + score + '% · 语义 ' + sem + '% · 视觉 ' + vis + '%</div>' +
|
||
'<div class="layer-rank-reason">' + esc(r.reason_zh || '') + '</div>' +
|
||
'</button>';
|
||
}).join('');
|
||
}
|
||
|
||
function bindLayerSuggestionClicks(){
|
||
document.querySelectorAll(".layer-suggestion-card").forEach(function(card){
|
||
card.addEventListener("click", function(){
|
||
const layer = card.dataset.layer;
|
||
state.selectedLayers = new Set([layer]);
|
||
if(!state.selectedStatus || state.selectedStatus === "UNKNOWN") state.selectedStatus = "PARTIAL";
|
||
if(state.selectedStdKey){
|
||
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
|
||
if(sr && sr.standard_layer !== layer) state.selectedStdKey = null;
|
||
}
|
||
renderStatusButtons();
|
||
renderModeFields();
|
||
renderLayerSelection();
|
||
renderSelectedStd();
|
||
showMessage("已选择 Layer:" + (state.data.standard_layer_zh[layer] || layer) + "。请确认后保存。", true);
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderCandidates(item){
|
||
const seen = new Set();
|
||
const candidates = [];
|
||
if(item.suggestion && item.suggestion.standard_resource_key){
|
||
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; });
|
||
const raw = (item.candidates || []).find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; });
|
||
if(sr){
|
||
candidates.push({
|
||
resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh,
|
||
score:raw ? raw.score : null,text_score:raw ? raw.text_score : null,recommendation_score:raw ? raw.recommendation_score : null,
|
||
source:"已有建议"
|
||
});
|
||
seen.add(sr.resource_key);
|
||
}
|
||
}
|
||
(item.candidates || []).forEach(function(c){
|
||
if(!seen.has(c.resource_key)){
|
||
candidates.push({resource_key:c.resource_key,standard_layer:c.standard_layer,standard_layer_zh:c.standard_layer_zh,display_name_zh:c.display_name_zh,score:c.score,text_score:c.text_score,recommendation_score:c.recommendation_score,source:"图标参考"});
|
||
seen.add(c.resource_key);
|
||
}
|
||
});
|
||
if(state.selectedStdKey && !seen.has(state.selectedStdKey)){
|
||
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
|
||
if(sr) candidates.unshift({resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh,source:"当前选择"});
|
||
}
|
||
|
||
if(!candidates.length){
|
||
$("candidateGrid").innerHTML = '<div class="muted">没有 Standard Visual Object 图标参考。Layer 仍可单独人工确认。</div>';
|
||
return;
|
||
}
|
||
|
||
$("candidateGrid").innerHTML = candidates.map(function(c){
|
||
const selected = c.resource_key === state.selectedStdKey;
|
||
const recommended = c.resource_key === item.recommended_resource_key;
|
||
const score = c.score != null ? (" · 视觉 " + (Number(c.score)*100).toFixed(1) + "%") : "";
|
||
const recScore = c.recommendation_score != null ? (" · 综合 " + (Number(c.recommendation_score)*100).toFixed(1) + "%") : "";
|
||
return '<div class="candidate ' + (selected?'selected ':'') + (recommended?'recommended':'') + '" data-key="' + esc(c.resource_key) +
|
||
'" data-layer="' + esc(c.standard_layer) + '">' +
|
||
'<div class="candidate-img"><img src="/sprite/std/' + encodeURIComponent(c.resource_key) + '.png"></div>' +
|
||
'<div class="candidate-name ' + (recommended?'recommended-name':'') + '">' +
|
||
esc(c.display_name_zh || c.standard_layer_zh || c.resource_key) +
|
||
(recommended ? '<span class="recommended-tag">最可能</span>' : '') +
|
||
'</div>' +
|
||
'<div class="candidate-code">' + esc(c.resource_key) + '</div>' +
|
||
'<small>' + esc(c.standard_layer_zh || c.standard_layer) + ' · ' + esc(c.standard_layer) + '</small>' +
|
||
'<small>' + esc(c.source) + score + recScore + '</small></div>';
|
||
}).join("");
|
||
|
||
document.querySelectorAll("#candidateGrid .candidate").forEach(function(card){
|
||
card.addEventListener("click", function(){ selectStandard(card.dataset.key, card.dataset.layer); });
|
||
});
|
||
}
|
||
|
||
function selectStandard(key, layer){
|
||
if(state.selectedStatus === "COMPONENT" || state.selectedStatus === "UNKNOWN") return;
|
||
state.selectedStdKey = key;
|
||
if(state.selectedStatus !== "AMBIGUOUS" && state.selectedStatus !== "UNKNOWN"){
|
||
state.selectedLayers = new Set([layer]);
|
||
} else if(state.selectedStatus === "AMBIGUOUS"){
|
||
state.selectedLayers.add(layer);
|
||
}
|
||
renderCandidates(state.visibleItems[state.currentIndex]);
|
||
renderLayerSelection();
|
||
renderSelectedStd();
|
||
}
|
||
|
||
function renderSelectedStd(){
|
||
const el = $("selectedStd");
|
||
if(!state.selectedStdKey){
|
||
el.textContent = "未选择";
|
||
el.classList.add("muted");
|
||
return;
|
||
}
|
||
const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; });
|
||
if(!sr) return;
|
||
el.classList.remove("muted");
|
||
el.innerHTML = "<b>" + esc(sr.display_name_zh || sr.resource_key) + "</b> · " +
|
||
esc(sr.standard_layer_zh || sr.standard_layer) +
|
||
'<div class="candidate-code">' + esc(sr.resource_key) + ' · ' + esc(sr.standard_layer) + '</div>';
|
||
}
|
||
|
||
function useLayerOnly(){
|
||
if(state.selectedLayers.size !== 1){
|
||
showMessage("请先选择一个标准 Layer。", false);
|
||
return;
|
||
}
|
||
state.selectedStdKey = null;
|
||
if(!state.selectedStatus || state.selectedStatus === "MATCH") state.selectedStatus = "PARTIAL";
|
||
renderStatusButtons();
|
||
renderCandidates(state.visibleItems[state.currentIndex]);
|
||
renderSelectedStd();
|
||
showMessage("已设为:仅确认 Layer,不绑定标准图标。", true);
|
||
}
|
||
|
||
function chooseStatus(status){
|
||
state.selectedStatus = status;
|
||
if(status === "UNKNOWN" || status === "COMPONENT"){
|
||
state.selectedLayers.clear();
|
||
state.selectedStdKey = null;
|
||
} else {
|
||
state.selectedComponentKind = null;
|
||
if((status === "MATCH" || status === "PARTIAL") && state.selectedLayers.size > 1){
|
||
state.selectedLayers = new Set([Array.from(state.selectedLayers)[0]]);
|
||
}
|
||
}
|
||
renderStatusButtons();
|
||
renderModeFields();
|
||
renderComponentKindSelection();
|
||
renderLayerSelection();
|
||
renderCandidates(state.visibleItems[state.currentIndex]);
|
||
renderSelectedStd();
|
||
}
|
||
|
||
async function saveDecision(goNext){
|
||
const item = state.visibleItems[state.currentIndex];
|
||
if(!item) return;
|
||
if(!state.selectedStatus){ showMessage("请选择 MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN。", false); return; }
|
||
const payload = {
|
||
anonymous_id:item.anonymous_id,
|
||
status:state.selectedStatus,
|
||
standard_layers:Array.from(state.selectedLayers),
|
||
standard_resource_key:state.selectedStdKey,
|
||
component_kind:state.selectedComponentKind,
|
||
note:$("note").value || ""
|
||
};
|
||
try{
|
||
const res = await api("/api/review", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
|
||
item.decision = res.decision;
|
||
updateProgress(res.progress);
|
||
showMessage("已保存。", true);
|
||
if(goNext){
|
||
if(state.filter === "UNREVIEWED") applyFilter(false);
|
||
else {
|
||
if(state.currentIndex < state.visibleItems.length-1) state.currentIndex++;
|
||
renderQueue(); renderCurrent();
|
||
}
|
||
} else {
|
||
renderDecisionBadge(item.decision); renderQueue();
|
||
}
|
||
}catch(e){ showMessage(e.message, false); }
|
||
}
|
||
|
||
async function clearDecision(){
|
||
const item = state.visibleItems[state.currentIndex];
|
||
if(!item || !item.decision) return;
|
||
try{
|
||
const res = await api("/api/unreview", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({anonymous_id:item.anonymous_id})});
|
||
item.decision = null;
|
||
updateProgress(res.progress);
|
||
applyFilter(false);
|
||
}catch(e){ showMessage(e.message, false); }
|
||
}
|
||
|
||
function showMessage(text, ok){
|
||
$("message").textContent = text;
|
||
$("message").className = "message " + (ok?"ok":"err");
|
||
}
|
||
|
||
function updateProgress(p){
|
||
const pct = p.total ? (p.reviewed/p.total*100) : 0;
|
||
$("progressText").textContent = p.reviewed + "/" + p.total + " 已确认 · MATCH " + p.MATCH +
|
||
" · PARTIAL " + p.PARTIAL + " · AMBIGUOUS " + p.AMBIGUOUS + " · COMPONENT " + p.COMPONENT + " · UNKNOWN " + p.UNKNOWN;
|
||
$("progressBar").style.width = pct + "%";
|
||
}
|
||
|
||
function move(delta){
|
||
if(!state.visibleItems.length) return;
|
||
state.currentIndex = Math.max(0, Math.min(state.visibleItems.length-1, state.currentIndex+delta));
|
||
renderQueue(); renderCurrent();
|
||
const active = document.querySelector(".queue-item.active");
|
||
if(active) active.scrollIntoView({block:"nearest"});
|
||
}
|
||
|
||
function populateLayerFilter(){
|
||
$("layerFilter").innerHTML = '<option value="">全部标准 Layer</option>' +
|
||
state.data.standard_layers.map(function(x){ return '<option value="' + esc(x) + '">' + esc(state.data.standard_layer_zh[x] || x) + ' · ' + esc(x) + "</option>"; }).join("");
|
||
}
|
||
|
||
function openLibrary(){ renderLibrary(); $("libraryDialog").showModal(); }
|
||
|
||
function renderLibrary(){
|
||
const layer = $("layerFilter").value;
|
||
const q = $("librarySearch").value.trim().toLowerCase();
|
||
const rows = state.data.standard_resources.filter(function(x){
|
||
return (!layer || x.standard_layer === layer) &&
|
||
(!q || x.resource_key.toLowerCase().includes(q) || x.standard_layer.toLowerCase().includes(q) ||
|
||
(x.display_name_zh || "").toLowerCase().includes(q) || (x.standard_layer_zh || "").toLowerCase().includes(q));
|
||
});
|
||
$("libraryGrid").innerHTML = rows.map(function(x){
|
||
return '<div class="library-item" data-key="' + esc(x.resource_key) + '" data-layer="' + esc(x.standard_layer) + '">' +
|
||
'<img loading="lazy" src="/sprite/std/' + encodeURIComponent(x.resource_key) + '.png">' +
|
||
'<div class="candidate-name">' + esc(x.display_name_zh || x.resource_key) + '</div>' +
|
||
'<div class="candidate-code">' + esc(x.resource_key) + '</div>' +
|
||
'<small>' + esc(x.standard_layer_zh || x.standard_layer) + ' · ' + esc(x.standard_layer) + '</small></div>';
|
||
}).join("");
|
||
document.querySelectorAll(".library-item").forEach(function(el){
|
||
el.addEventListener("click", function(){
|
||
selectStandard(el.dataset.key, el.dataset.layer);
|
||
$("libraryDialog").close();
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadAudit(){
|
||
const item = state.visibleItems[state.currentIndex];
|
||
if(!item) return;
|
||
try{
|
||
const data = await api("/api/audit?id=" + encodeURIComponent(item.anonymous_id));
|
||
$("auditText").textContent = JSON.stringify(data, null, 2);
|
||
}catch(e){ $("auditText").textContent = e.message; }
|
||
}
|
||
|
||
|
||
async function abandonAllUnreviewed(){
|
||
const p = state.data && state.data.progress ? state.data.progress : null;
|
||
const count = p ? p.unreviewed : 0;
|
||
if(!count){ alert("当前没有未确认项。 "); return; }
|
||
if(!confirm("确定放弃剩余 " + count + " 个未确认项吗?\n\n它们会全部设为 UNKNOWN;已经人工确认的项目不会改变。")) return;
|
||
try{
|
||
const res = await api("/api/abandon-unreviewed", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dry_run:false})});
|
||
state.data = await api("/api/bootstrap");
|
||
updateProgress(state.data.progress);
|
||
applyFilter(true);
|
||
alert("已放弃 " + res.affected + " 个未确认项,全部标记为 UNKNOWN。 ");
|
||
}catch(e){ alert("批量放弃失败:" + e.message); }
|
||
}
|
||
async function finalize(){
|
||
if(!confirm("生成当前人工确认结果?未确认项目会保持 UNKNOWN,不会自动继承机器建议。")) return;
|
||
try{
|
||
const res = await api("/api/finalize", {method:"POST",headers:{"Content-Type":"application/json"},body:"{}"});
|
||
alert("已生成。\n\n" + res.assignment_dir);
|
||
}catch(e){ alert("生成失败:" + e.message); }
|
||
}
|
||
|
||
document.addEventListener("click", function(e){
|
||
const f = e.target.closest(".filter");
|
||
if(f){
|
||
document.querySelectorAll(".filter").forEach(function(x){ x.classList.remove("active"); });
|
||
f.classList.add("active"); state.filter = f.dataset.filter; applyFilter(true);
|
||
}
|
||
const s = e.target.closest(".status-btn");
|
||
if(s) chooseStatus(s.dataset.status);
|
||
});
|
||
|
||
$("prevBtn").addEventListener("click", function(){ move(-1); });
|
||
$("nextBtn").addEventListener("click", function(){ move(1); });
|
||
$("saveBtn").addEventListener("click", function(){ saveDecision(true); });
|
||
$("saveStayBtn").addEventListener("click", function(){ saveDecision(false); });
|
||
$("clearBtn").addEventListener("click", clearDecision);
|
||
$("layerOnlyBtn").addEventListener("click", useLayerOnly);
|
||
$("openLibraryBtn").addEventListener("click", openLibrary);
|
||
$("closeLibraryBtn").addEventListener("click", function(){ $("libraryDialog").close(); });
|
||
$("layerFilter").addEventListener("change", renderLibrary);
|
||
$("librarySearch").addEventListener("input", renderLibrary);
|
||
$("loadAuditBtn").addEventListener("click", loadAudit);
|
||
$("abandonAllBtn").addEventListener("click", abandonAllUnreviewed);
|
||
$("finalizeBtn").addEventListener("click", finalize);
|
||
|
||
document.addEventListener("keydown", function(e){
|
||
const tag = document.activeElement ? document.activeElement.tagName : "";
|
||
if(["TEXTAREA","INPUT","SELECT"].includes(tag)) return;
|
||
if(e.key === "m" || e.key === "M") chooseStatus("MATCH");
|
||
else if(e.key === "p" || e.key === "P") chooseStatus("PARTIAL");
|
||
else if(e.key === "a" || e.key === "A") chooseStatus("AMBIGUOUS");
|
||
else if(e.key === "c" || e.key === "C") chooseStatus("COMPONENT");
|
||
else if(e.key === "u" || e.key === "U") chooseStatus("UNKNOWN");
|
||
else if(e.key === "ArrowLeft") move(-1);
|
||
else if(e.key === "ArrowRight") move(1);
|
||
else if(e.key === "Enter") saveDecision(true);
|
||
});
|
||
|
||
load().catch(function(e){
|
||
document.body.innerHTML = '<pre style="padding:20px;color:#b42318">' + esc(e.stack || e.message) + "</pre>";
|
||
});
|