first commit
This commit is contained in:
+4605
File diff suppressed because it is too large
Load Diff
+11293
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+469
@@ -0,0 +1,469 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>2号人事部 · 功能审核(一页一功能)</title>
|
||||
<style>
|
||||
:root{--green:#00b389;--red:#ef4444;--bd:#e5e7eb;--muted:#6b7280;--bg:#f4f6f8;--ink:#1f2937;}
|
||||
*{box-sizing:border-box;}
|
||||
html,body{margin:0;height:100%;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--ink);background:var(--bg);}
|
||||
body{display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
||||
|
||||
/* 顶部进度条 */
|
||||
header{flex:0 0 auto;background:#fff;border-bottom:1px solid var(--bd);padding:10px 18px;}
|
||||
.htop{display:flex;align-items:center;gap:14px;}
|
||||
.htop h1{margin:0;font-size:16px;white-space:nowrap;}
|
||||
.htop .sub{color:var(--muted);font-size:12px;}
|
||||
.progress{flex:1;height:8px;background:#eef1f4;border-radius:99px;overflow:hidden;min-width:120px;}
|
||||
.progress > i{display:block;height:100%;background:var(--green);width:0;transition:width .2s;}
|
||||
.counts{font-size:13px;color:var(--muted);white-space:nowrap;}
|
||||
.counts b{color:var(--green);font-size:15px;}
|
||||
.counts .drop{color:var(--red);}
|
||||
.btn{cursor:pointer;border:1px solid var(--bd);background:#fff;border-radius:7px;padding:7px 12px;font-size:13px;}
|
||||
.btn:hover{filter:brightness(.97);}
|
||||
.btn.primary{background:var(--green);color:#fff;border-color:var(--green);}
|
||||
|
||||
/* 主体: 左目录 + 中截图 + 右信息 */
|
||||
main{flex:1;display:flex;min-height:0;}
|
||||
#sidebar{flex:0 0 262px;background:#fff;border-right:1px solid var(--bd);overflow:auto;transition:margin .2s;}
|
||||
#sidebar.collapsed{margin-left:-262px;}
|
||||
.side-search{position:sticky;top:0;background:#fff;padding:10px;border-bottom:1px solid var(--bd);}
|
||||
.side-search input{width:100%;padding:7px 9px;border:1px solid var(--bd);border-radius:6px;font-size:13px;}
|
||||
.mod{padding:2px 0;}
|
||||
.mod-h{padding:7px 12px;font-size:13px;font-weight:700;color:#0f766e;background:#f0f9f6;position:sticky;top:52px;cursor:pointer;display:flex;justify-content:space-between;}
|
||||
.mod-h .mc{font-weight:400;color:var(--muted);font-size:11px;}
|
||||
.leaf{padding:6px 12px 6px 22px;font-size:12.5px;cursor:pointer;display:flex;align-items:center;gap:7px;border-left:3px solid transparent;}
|
||||
.leaf:hover{background:#f7f8fa;}
|
||||
.leaf.active{background:#eef7f4;border-left-color:var(--green);font-weight:600;}
|
||||
.dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--green);}
|
||||
.leaf.dropped .dot{background:var(--red);}
|
||||
.leaf.dropped .lname{text-decoration:line-through;color:#9ca3af;}
|
||||
.lname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.leaf-att{flex:0 0 auto;font-size:10.5px;color:#0f766e;background:#e6f4f0;border-radius:9px;padding:1px 6px;line-height:1.5;font-weight:600;}
|
||||
.att-browse{color:var(--green);cursor:pointer;text-decoration:underline;margin-left:4px;}
|
||||
|
||||
/* 中间截图区 */
|
||||
#stage{flex:1;min-width:0;overflow:auto;padding:18px;display:flex;flex-direction:column;align-items:center;background:#eceef1;}
|
||||
#shotwrap{width:100%;max-width:1180px;background:#fff;border:1px solid var(--bd);border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.05);overflow:hidden;}
|
||||
#shotwrap .shot-h{padding:9px 14px;border-bottom:1px solid var(--bd);font-size:12px;color:var(--muted);display:flex;justify-content:space-between;align-items:center;}
|
||||
#shotwrap .shot-body{max-height:calc(100vh - 150px);overflow:auto;}
|
||||
#shotwrap img{width:100%;display:block;cursor:zoom-in;transition:opacity .18s;}
|
||||
#shotwrap.loading img{opacity:.3;}
|
||||
.shot-h .badge2{font-size:12px;color:#0f766e;font-weight:700;}
|
||||
#deepwrap{width:100%;max-width:1180px;margin-top:14px;background:#fff;border:1px solid var(--bd);border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.05);overflow:hidden;}
|
||||
#deepwrap .deep-h{padding:9px 14px;border-bottom:1px solid var(--bd);font-size:12px;color:var(--muted);font-weight:700;}
|
||||
#deepwrap .deep-h span{color:#0f766e;margin-left:6px;}
|
||||
#deepGrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px;padding:14px;}
|
||||
.deep-card{border:1px solid var(--bd);border-radius:8px;overflow:hidden;cursor:zoom-in;background:#fafafa;transition:box-shadow .15s;}
|
||||
.deep-card:hover{box-shadow:0 2px 10px rgba(0,0,0,.12);}
|
||||
.deep-card img{width:100%;display:block;height:150px;object-fit:cover;object-position:top;}
|
||||
.deep-card .cap{padding:6px 9px;font-size:12px;color:#374151;border-top:1px solid var(--bd);}
|
||||
.deep-card .cap b{color:#0f766e;}
|
||||
#noshot{padding:60px;color:#9ca3af;text-align:center;}
|
||||
|
||||
/* 右信息栏 */
|
||||
#info{flex:0 0 348px;background:#fff;border-left:1px solid var(--bd);overflow:auto;padding:0;display:flex;flex-direction:column;}
|
||||
.info-body{padding:18px;flex:1;}
|
||||
.crumb{font-size:12px;color:var(--muted);margin-bottom:6px;}
|
||||
.crumb .sep{margin:0 5px;color:#cbd5e1;}
|
||||
.fname{font-size:22px;font-weight:800;margin:0 0 4px;}
|
||||
.furl{font-size:11px;color:#9ca3af;word-break:break-all;margin-bottom:16px;}
|
||||
.sec{margin-bottom:16px;}
|
||||
.sec .lab{font-size:12px;font-weight:700;color:#0f766e;margin-bottom:5px;display:flex;align-items:center;gap:5px;}
|
||||
.sec .lab::before{content:"";width:3px;height:13px;background:var(--green);border-radius:2px;}
|
||||
.sec .txt{font-size:14px;line-height:1.7;color:#374151;}
|
||||
.sec.und .txt{background:#fcf8ee;border:1px solid #f3e9cf;color:#8a6d3b;border-radius:8px;padding:10px 12px;}
|
||||
.sec.note textarea{width:100%;min-height:82px;resize:vertical;border:1px solid var(--bd);border-radius:8px;padding:9px 11px;font-size:13.5px;line-height:1.6;font-family:inherit;color:#1f2937;}
|
||||
.sec.note textarea:focus{outline:none;border-color:var(--green);box-shadow:0 0 0 2px rgba(0,179,137,.15);}
|
||||
.sec.note .lab .st{font-weight:400;font-size:11px;color:#9ca3af;margin-left:auto;}
|
||||
.sec.note .notebar{display:flex;align-items:center;gap:9px;margin-top:8px;}
|
||||
.sec.note .btn-note{background:var(--green);color:#fff;border:1px solid var(--green);border-radius:7px;padding:8px 15px;font-size:13px;cursor:pointer;font-weight:600;}
|
||||
.sec.note .btn-note:hover{filter:brightness(.96);}
|
||||
.sec.note .btn-note.dirty{animation:pulse 1.1s infinite;}
|
||||
.sec.note .btn-note.saved{background:#fff;color:#059669;border-color:#a7f3d0;}
|
||||
.sec.note .tip{font-size:11px;color:#9ca3af;line-height:1.4;}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(0,179,137,.45);}50%{box-shadow:0 0 0 5px rgba(0,179,137,0);}}
|
||||
.leaf.hasnote .lname::after{content:"\270e";color:#f59e0b;margin-left:4px;font-size:11px;}
|
||||
.badge{display:inline-block;font-size:12px;padding:3px 9px;border-radius:99px;margin-bottom:14px;}
|
||||
.badge.keep{background:#e7f7f1;color:#059669;}
|
||||
.badge.drop{background:#fee2e2;color:#dc2626;}
|
||||
|
||||
/* 导出样本(真实数据附件) */
|
||||
.sec.att .lab .cnt{margin-left:auto;font-weight:400;font-size:11px;color:#9ca3af;}
|
||||
.att-list{display:flex;flex-direction:column;gap:6px;}
|
||||
.att-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid var(--bd);border-radius:8px;font-size:12.5px;text-decoration:none;color:#1f2937;background:#fafafa;}
|
||||
.att-item:hover{border-color:var(--green);background:#f0f9f6;}
|
||||
.att-item .ico{flex:0 0 auto;font-size:15px;}
|
||||
.att-item .an{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.att-item .asz{flex:0 0 auto;color:#9ca3af;font-size:11px;}
|
||||
.att-row{display:flex;align-items:stretch;gap:4px;}
|
||||
.att-row .att-item{flex:1;min-width:0;cursor:pointer;}
|
||||
.att-prev:hover{border-color:var(--green);background:#f0f9f6;}
|
||||
.att-dl{flex:0 0 auto;display:flex;align-items:center;padding:0 9px;border:1px solid var(--bd);border-radius:8px;text-decoration:none;color:#6b7280;background:#fafafa;font-size:13px;}
|
||||
.att-dl:hover{border-color:var(--green);color:var(--green);background:#f0f9f6;}
|
||||
#pvmodal{position:fixed;inset:0;background:rgba(0,0,0,.5);display:none;z-index:120;}
|
||||
#pvmodal .box{position:absolute;top:3%;left:50%;transform:translateX(-50%);width:min(1100px,96vw);max-height:92vh;background:#fff;border-radius:12px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.25);}
|
||||
#pvmodal .mh{padding:12px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:10px;}
|
||||
#pvmodal .mh h3{margin:0;font-size:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%;}
|
||||
#pvmodal .mh .sub{color:var(--muted);font-size:12px;}
|
||||
#pvmodal .mh .x{margin-left:auto;cursor:pointer;font-size:22px;color:var(--muted);}
|
||||
#pvmodal .tabs{display:flex;gap:4px;padding:8px 18px 0;flex-wrap:wrap;}
|
||||
#pvmodal .tab{padding:5px 12px;border:1px solid var(--bd);border-bottom:none;border-radius:7px 7px 0 0;font-size:12px;cursor:pointer;background:#f5f6f7;color:#4b5563;}
|
||||
#pvmodal .tab.on{background:#fff;color:#0f766e;font-weight:700;border-color:var(--green);}
|
||||
#pvmodal .mb{padding:0 18px 18px;overflow:auto;}
|
||||
#pvmodal table{border-collapse:collapse;font-size:12px;width:max-content;min-width:100%;}
|
||||
#pvmodal th,#pvmodal td{border:1px solid #e5e7eb;padding:4px 8px;text-align:left;white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis;}
|
||||
#pvmodal thead th{position:sticky;top:0;background:#0f766e;color:#fff;font-weight:600;z-index:1;}
|
||||
#pvmodal tbody tr:nth-child(even){background:#f8fafa;}
|
||||
#pvmodal .rowno{background:#f1f5f9;color:#94a3b8;text-align:right;position:sticky;left:0;}
|
||||
.att-empty{font-size:12.5px;color:#9ca3af;}
|
||||
.att-more{font-size:11.5px;color:#0f766e;cursor:pointer;text-align:center;padding:4px;}
|
||||
.att-more:hover{text-decoration:underline;}
|
||||
/* 全量附件弹窗 */
|
||||
#attmodal{position:fixed;inset:0;background:rgba(0,0,0,.5);display:none;z-index:110;}
|
||||
#attmodal .box{position:absolute;top:4%;left:50%;transform:translateX(-50%);width:min(760px,94vw);max-height:90vh;background:#fff;border-radius:12px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.25);}
|
||||
#attmodal .mh{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:10px;}
|
||||
#attmodal .mh h3{margin:0;font-size:16px;}
|
||||
#attmodal .mh .sub{color:var(--muted);font-size:12px;}
|
||||
#attmodal .mh .x{margin-left:auto;cursor:pointer;font-size:22px;color:var(--muted);}
|
||||
#attmodal .mb{padding:12px 18px 20px;overflow:auto;}
|
||||
#attmodal .grp{margin-bottom:16px;}
|
||||
#attmodal .grp .gh{font-size:13px;font-weight:700;color:#0f766e;padding:6px 0;border-bottom:1px solid #eef1f4;margin-bottom:8px;}
|
||||
#attmodal .grp .gh .gc{font-weight:400;color:#9ca3af;font-size:11px;margin-left:6px;}
|
||||
|
||||
/* 底部决策条 */
|
||||
.decide{flex:0 0 auto;border-top:1px solid var(--bd);padding:14px 18px;display:flex;flex-direction:column;gap:10px;background:#fff;}
|
||||
.decide .big{display:flex;gap:10px;}
|
||||
.decide .big button{flex:1;padding:14px 0;font-size:15px;font-weight:700;border-radius:9px;border:2px solid;cursor:pointer;}
|
||||
.bkeep{background:var(--green);color:#fff;border-color:var(--green);}
|
||||
.bkeep.off{background:#fff;color:var(--green);}
|
||||
.bdrop{background:#fff;color:var(--red);border-color:var(--red);}
|
||||
.bdrop.on{background:var(--red);color:#fff;}
|
||||
.nav{display:flex;gap:8px;align-items:center;}
|
||||
.nav button{flex:1;padding:9px 0;font-size:13px;border:1px solid var(--bd);border-radius:8px;background:#fff;cursor:pointer;}
|
||||
.nav .pos{font-size:12px;color:var(--muted);white-space:nowrap;padding:0 4px;}
|
||||
.hint{font-size:11px;color:#9ca3af;text-align:center;}
|
||||
|
||||
/* lightbox */
|
||||
#lb{position:fixed;inset:0;background:rgba(0,0,0,.9);display:none;z-index:100;overflow:auto;text-align:center;}
|
||||
#lb img{max-width:98%;margin:1% auto;}
|
||||
#lb .x{position:fixed;top:12px;right:20px;color:#fff;font-size:32px;cursor:pointer;}
|
||||
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:9px 18px;border-radius:99px;font-size:13px;opacity:0;transition:opacity .2s;z-index:120;pointer-events:none;}
|
||||
.toast.show{opacity:.95;}
|
||||
@media(max-width:1100px){#info{flex-basis:300px;}#sidebar{flex-basis:220px;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="htop">
|
||||
<button class="btn" id="btnSide" title="显示/隐藏目录">☰</button>
|
||||
<h1>2号人事部 · 功能审核 <span class="sub">一页一功能 · 决策实时写入数据库</span></h1>
|
||||
<div class="progress"><i id="pbar"></i></div>
|
||||
<div class="counts">已审 <b id="cReviewed">0</b>/<span id="cTotal">0</span> · 保留 <b id="cKeep">0</b> · 删除 <span class="drop" id="cDrop">0</span></div>
|
||||
<button class="btn" id="btnAtt" title="查看已抓取的真实导出样本">📎 导出样本 <b id="attN">0</b></button>
|
||||
<button class="btn primary" id="btnExport">⬇ 导出 selection.json</button>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<aside id="sidebar">
|
||||
<div class="side-search"><input id="q" type="search" placeholder="搜索功能…"/></div>
|
||||
<div id="side-list"></div>
|
||||
</aside>
|
||||
<section id="stage">
|
||||
<div id="shotwrap">
|
||||
<div class="shot-h"><span id="shotPath"></span><span>点击图片可放大 · 长图可在框内滚动</span></div>
|
||||
<div class="shot-body" id="shotBody">
|
||||
<img id="shotImg" onclick="zoom()" onerror="noShot()"/>
|
||||
<div id="noshot" style="display:none">(此功能暂无整页截图)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="deepwrap" style="display:none">
|
||||
<div class="deep-h">深层界面(点击按钮抓取)<span id="deepCnt"></span></div>
|
||||
<div id="deepGrid"></div>
|
||||
</div>
|
||||
</section>
|
||||
<aside id="info">
|
||||
<div class="info-body">
|
||||
<div class="crumb" id="crumb"></div>
|
||||
<h2 class="fname" id="fname"></h2>
|
||||
<div class="furl" id="furl"></div>
|
||||
<span class="badge" id="badge"></span>
|
||||
<div class="sec"><div class="lab">功能释义</div><div class="txt" id="desc"></div></div>
|
||||
<div class="sec und"><div class="lab">我的理解 / 开发提示</div><div class="txt" id="und"></div></div>
|
||||
<div class="sec att"><div class="lab">导出样本(真实数据)<span class="cnt" id="attCnt"></span></div>
|
||||
<div class="att-list" id="attList"></div></div>
|
||||
<div class="sec note"><div class="lab">审核备注 / 开发建议<span class="st" id="noteSt"></span></div>
|
||||
<textarea id="note" placeholder="在此记录对该功能的处理建议(如:保留但简化字段 / 与某模块合并 / 二期再做…)"></textarea>
|
||||
<div class="notebar">
|
||||
<button class="btn-note" id="btnSaveNote" onclick="saveNote(false)">💾 保存备注到数据库</button>
|
||||
<span class="tip">可提前预填,审核时会自动显示已备注信息</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="decide">
|
||||
<div class="big">
|
||||
<button class="bkeep" id="bKeep" onclick="decide(1)">✓ 保留 (K)</button>
|
||||
<button class="bdrop" id="bDrop" onclick="decide(0)">✕ 删除 (D)</button>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<button onclick="go(idx-1)">← 上一个</button>
|
||||
<span class="pos" id="pos">0 / 0</span>
|
||||
<button onclick="go(idx+1)">下一个 →</button>
|
||||
</div>
|
||||
<div class="hint">快捷键:← → 翻页 · K 保留 · D 删除(决策后自动跳下一个)</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<div id="lb" onclick="this.style.display='none'"><span class="x">×</span><img id="lbimg"/></div>
|
||||
<div id="attmodal">
|
||||
<div class="box">
|
||||
<div class="mh"><h3>📎 导出样本</h3><span class="sub" id="attSub"></span><span class="x" onclick="closeAtt()">×</span></div>
|
||||
<div class="mb" id="attModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pvmodal">
|
||||
<div class="box">
|
||||
<div class="mh"><h3 id="pvTitle">📊 预览</h3><span class="sub" id="pvSub"></span><span class="x" onclick="closePv()">×</span></div>
|
||||
<div class="tabs" id="pvTabs"></div>
|
||||
<div class="mb" id="pvBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
let FEATS=[], idx=0;
|
||||
let ATT={total:0,groups:[]}, ATT_BY_MOD={};
|
||||
const $=s=>document.querySelector(s);
|
||||
|
||||
async function boot(){
|
||||
const r=await fetch('/api/features'); const d=await r.json();
|
||||
FEATS=d.items;
|
||||
$('#cTotal').textContent=FEATS.length;
|
||||
await loadAttachments();
|
||||
buildSidebar();
|
||||
const saved=parseInt(localStorage.getItem('twohr_review_idx')||'0');
|
||||
go(isNaN(saved)?0:Math.min(saved,FEATS.length-1));
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
async function loadAttachments(){
|
||||
try{
|
||||
const r=await fetch('/api/attachments'); ATT=await r.json();
|
||||
}catch(e){ ATT={total:0,groups:[]}; }
|
||||
ATT_BY_MOD={};
|
||||
(ATT.groups||[]).forEach(g=>{ (ATT_BY_MOD[g.module]=ATT_BY_MOD[g.module]||[]).push(g); });
|
||||
$('#attN').textContent=ATT.total||0;
|
||||
}
|
||||
function fmtSize(n){ return n>=1048576?(n/1048576).toFixed(1)+'MB':(n/1024).toFixed(1)+'KB'; }
|
||||
function isSheet(n){ return /\.(xlsx|xls)$/i.test(n||''); }
|
||||
function escj(s){ return (s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
|
||||
function attItem(fobj){
|
||||
const sheet=isSheet(fobj.name), rel=encodeURI(fobj.rel);
|
||||
const inner='<span class="ico">'+(sheet?'📊':'📄')+'</span><span class="an">'+esc(fobj.name)+'</span>'
|
||||
+'<span class="asz">'+fmtSize(fobj.size)+'</span>';
|
||||
const main=sheet
|
||||
? '<span class="att-item att-prev" title="点击预览:'+esc(fobj.name)+'" onclick="previewFile(\''+escj(fobj.rel)+'\',\''+escj(fobj.name)+'\')">'+inner+'</span>'
|
||||
: '<a class="att-item" href="/dl/'+rel+'" title="'+esc(fobj.name)+'">'+inner+'</a>';
|
||||
return '<div class="att-row">'+main+'<a class="att-dl" href="/dl/'+rel+'" title="下载">⬇</a></div>';
|
||||
}
|
||||
function renderFeatAtt(){
|
||||
const f=FEATS[idx]; const box=$('#attList'); const cnt=$('#attCnt'); if(!box) return;
|
||||
const files=(f.downloads||[]);
|
||||
cnt.textContent=files.length?('本页 '+files.length+' 个'):'';
|
||||
if(!files.length){ box.innerHTML='<div class="att-empty">该页面未抓到导出文件(多为设置/模板页,或测试账号无数据)。<a class="att-browse" onclick="openAtt()">浏览全部导出 ›</a></div>'; return; }
|
||||
let html=files.map(attItem).join('');
|
||||
html+='<div class="att-more" onclick="openAtt()">浏览全站全部导出 ›</div>';
|
||||
box.innerHTML=html;
|
||||
}
|
||||
function openAtt(filterMod){
|
||||
const body=$('#attModalBody');
|
||||
let groups=ATT.groups||[];
|
||||
if(filterMod) groups=groups.filter(g=>g.module===filterMod);
|
||||
$('#attSub').textContent=(filterMod?('模块:'+filterMod+' · '):'共 ')+ (filterMod?'':(ATT.total+' 个文件,'))+groups.length+' 个目录';
|
||||
body.innerHTML=groups.map(g=>'<div class="grp"><div class="gh">'+esc(g.dir)+'<span class="gc">'+g.files.length+' 个</span></div>'
|
||||
+'<div class="att-list">'+g.files.map(attItem).join('')+'</div></div>').join('')
|
||||
|| '<div class="att-empty">暂无导出文件</div>';
|
||||
$('#attmodal').style.display='block';
|
||||
}
|
||||
function closeAtt(){ $('#attmodal').style.display='none'; }
|
||||
|
||||
let PV={sheets:[],cur:0,name:''};
|
||||
async function previewFile(rel,name){
|
||||
$('#pvTitle').textContent='📊 '+name; $('#pvSub').textContent='加载中…';
|
||||
$('#pvTabs').innerHTML=''; $('#pvBody').innerHTML='<div class="att-empty">正在解析…</div>';
|
||||
$('#pvmodal').style.display='block';
|
||||
let d;
|
||||
try{ const r=await fetch('/api/preview?rel='+encodeURIComponent(rel)); d=await r.json(); }
|
||||
catch(e){ d={ok:false,err:'请求失败'}; }
|
||||
if(!d.ok){ $('#pvSub').textContent=''; $('#pvBody').innerHTML='<div class="att-empty">无法预览:'+esc(d.err||'未知错误')+'</div>'; return; }
|
||||
PV={sheets:d.sheets||[],cur:0,name:d.name};
|
||||
const st=PV.sheets.reduce((a,s)=>a+ (s.total_rows||0),0);
|
||||
$('#pvSub').textContent=PV.sheets.length+' 个工作表 · 约 '+st+' 行(预览上限 '+d.max_rows+'×'+d.max_cols+')';
|
||||
$('#pvTabs').innerHTML=PV.sheets.map((s,i)=>'<div class="tab'+(i===0?' on':'')+'" onclick="pvTab('+i+')">'+esc(s.name)+'</div>').join('');
|
||||
renderSheet(0);
|
||||
}
|
||||
function pvTab(i){ PV.cur=i; document.querySelectorAll('#pvTabs .tab').forEach((t,j)=>t.classList.toggle('on',j===i)); renderSheet(i); }
|
||||
function renderSheet(i){
|
||||
const s=PV.sheets[i]; const box=$('#pvBody');
|
||||
if(!s||!s.rows||!s.rows.length){ box.innerHTML='<div class="att-empty">空工作表</div>'; return; }
|
||||
const rows=s.rows;
|
||||
let h='<table><thead><tr><th class="rowno">#</th>';
|
||||
rows[0].forEach(c=>h+='<th>'+esc(c)+'</th>'); h+='</tr></thead><tbody>';
|
||||
for(let r=1;r<rows.length;r++){ h+='<tr><td class="rowno">'+r+'</td>'; rows[r].forEach(c=>h+='<td title="'+esc(c)+'">'+esc(c)+'</td>'); h+='</tr>'; }
|
||||
h+='</tbody></table>';
|
||||
if((s.total_rows||0)>rows.length) h+='<div class="att-empty">仅预览前 '+(rows.length-1)+' 行,完整数据请下载文件</div>';
|
||||
box.innerHTML=h;
|
||||
}
|
||||
function closePv(){ $('#pvmodal').style.display='none'; }
|
||||
|
||||
function buildSidebar(){
|
||||
const list=$('#side-list'); list.innerHTML='';
|
||||
let curMod=null, modBox=null;
|
||||
FEATS.forEach((f,i)=>{
|
||||
if(f.module!==curMod){
|
||||
curMod=f.module;
|
||||
const first=i;
|
||||
const mod=document.createElement('div'); mod.className='mod';
|
||||
const h=document.createElement('div'); h.className='mod-h';
|
||||
const cnt=FEATS.filter(x=>x.module===curMod).length;
|
||||
h.innerHTML='<span>'+esc(curMod)+'</span><span class="mc">'+cnt+'</span>';
|
||||
h.title='跳到该模块第一个功能';
|
||||
h.onclick=()=>go(first);
|
||||
mod.appendChild(h); list.appendChild(mod); modBox=mod;
|
||||
}
|
||||
const el=document.createElement('div'); el.className='leaf'; el.dataset.i=i;
|
||||
const nd=(f.downloads||[]).length;
|
||||
const clip=nd?'<span class="leaf-att" title="有 '+nd+' 个导出文件">📎'+nd+'</span>':'';
|
||||
el.innerHTML='<span class="dot"></span><span class="lname">'+esc(f.name)+'</span>'+clip;
|
||||
el.onclick=()=>go(i);
|
||||
modBox.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function render(){
|
||||
const f=FEATS[idx]; if(!f) return;
|
||||
$('#crumb').innerHTML=esc(f.module)+(f.group?'<span class="sep">›</span>'+esc(f.group):'')+'<span class="sep">›</span>'+esc(f.name);
|
||||
$('#fname').textContent=f.name;
|
||||
$('#furl').textContent=f.url;
|
||||
$('#desc').textContent=f.description||'—';
|
||||
$('#und').textContent=f.understanding||'—';
|
||||
$('#note').value=f.note||'';
|
||||
$('#noteSt').textContent=f.note?'已有备注':'';
|
||||
renderFeatAtt();
|
||||
markNoteDirty();
|
||||
$('#pos').textContent=(idx+1)+' / '+FEATS.length;
|
||||
$('#shotPath').textContent=f.full_path;
|
||||
const img=$('#shotImg'), wrap=$('#shotwrap');
|
||||
if(f.shot){
|
||||
$('#noshot').style.display='none'; img.style.display='block';
|
||||
if(decodeURIComponent(img.src)!==location.origin+decodeURIComponent(f.shot)){
|
||||
wrap.classList.add('loading');
|
||||
img.onload=()=>wrap.classList.remove('loading');
|
||||
img.src=f.shot;
|
||||
}
|
||||
}
|
||||
else { noShot(); }
|
||||
renderDeep(f);
|
||||
paintDecision(f);
|
||||
// sidebar active
|
||||
document.querySelectorAll('.leaf').forEach(el=>el.classList.toggle('active',+el.dataset.i===idx));
|
||||
const act=document.querySelector('.leaf.active'); if(act) act.scrollIntoView({block:'nearest'});
|
||||
$('#stage').scrollTop=0;
|
||||
const sb=$('#shotBody'); if(sb) sb.scrollTop=0;
|
||||
localStorage.setItem('twohr_review_idx',idx);
|
||||
}
|
||||
function paintDecision(f){
|
||||
const keep=f.keep===1;
|
||||
$('#bKeep').classList.toggle('off',!keep);
|
||||
$('#bDrop').classList.toggle('on',!keep);
|
||||
const b=$('#badge');
|
||||
b.className='badge '+(keep?'keep':'drop');
|
||||
b.textContent=keep?'当前:保留':'当前:删除';
|
||||
}
|
||||
function noShot(){ $('#shotImg').style.display='none'; $('#noshot').style.display='block'; }
|
||||
function renderDeep(f){
|
||||
const wrap=$('#deepwrap'), grid=$('#deepGrid'); const ds=(f.deep_shots||[]);
|
||||
if(!ds.length){ wrap.style.display='none'; grid.innerHTML=''; return; }
|
||||
$('#deepCnt').textContent=ds.length+' 张';
|
||||
grid.innerHTML=ds.map(d=>{
|
||||
const cap=d.kind_cn+(d.label&&d.label!==d.kind?(' · '+esc(d.label)):'');
|
||||
return '<div class="deep-card" onclick="zoom(\''+d.shot+'\')"><img src="'+d.shot+'" loading="lazy"/><div class="cap"><b>'+esc(d.kind_cn)+'</b>'+(d.label&&d.label!==d.kind?(' '+esc(d.label)):'')+'</div></div>';
|
||||
}).join('');
|
||||
wrap.style.display='block';
|
||||
}
|
||||
|
||||
async function decide(keep){
|
||||
const f=FEATS[idx]; f.keep=keep?1:0;
|
||||
paintDecision(f); markSidebar(idx); updateCounts();
|
||||
toast(keep?'已保留 ✓':'已删除 ✕');
|
||||
try{ await fetch('/api/keep',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({node_key:f.node_key,keep:f.keep})}); }catch(e){ toast('⚠ 写入失败'); }
|
||||
// 自动跳下一个未到末尾
|
||||
if(idx<FEATS.length-1) setTimeout(()=>go(idx+1),180);
|
||||
}
|
||||
function markSidebar(i){ const el=document.querySelector('.leaf[data-i="'+i+'"]');
|
||||
if(el){ el.classList.toggle('dropped',FEATS[i].keep===0);
|
||||
el.classList.toggle('hasnote',!!(FEATS[i].note && FEATS[i].note.trim())); } }
|
||||
function go(i){ if(i<0||i>=FEATS.length) return; idx=i; render(); }
|
||||
|
||||
function updateCounts(){
|
||||
const keep=FEATS.filter(f=>f.keep===1).length;
|
||||
$('#cKeep').textContent=keep; $('#cDrop').textContent=FEATS.length-keep;
|
||||
const reviewed=parseInt(localStorage.getItem('twohr_review_idx')||'0')+1;
|
||||
$('#cReviewed').textContent=Math.min(idx+1,FEATS.length);
|
||||
$('#pbar').style.width=((idx+1)/FEATS.length*100)+'%';
|
||||
FEATS.forEach((f,i)=>markSidebar(i));
|
||||
}
|
||||
function zoom(src){ const s=src||FEATS[idx].shot; if(!s)return; $('#lbimg').src=s; $('#lb').style.display='block'; }
|
||||
function esc(s){ return (s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
let tT; function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show');
|
||||
clearTimeout(tT); tT=setTimeout(()=>t.classList.remove('show'),1100); }
|
||||
|
||||
$('#btnSide').onclick=()=>$('#sidebar').classList.toggle('collapsed');
|
||||
$('#btnAtt').onclick=()=>openAtt(null);
|
||||
$('#attmodal').onclick=e=>{ if(e.target.id==='attmodal') closeAtt(); };
|
||||
$('#btnExport').onclick=async()=>{
|
||||
const r=await fetch('/api/export',{method:'POST'}); const d=await r.json();
|
||||
if(d.ok) toast('已导出 docs/selection.json(保留 '+d.keep+'/'+d.total+')');
|
||||
};
|
||||
$('#q').addEventListener('input',e=>{
|
||||
const q=e.target.value.trim().toLowerCase();
|
||||
document.querySelectorAll('.leaf').forEach(el=>{
|
||||
const f=FEATS[+el.dataset.i];
|
||||
const hit=!q||(f.name+f.module+(f.group||'')+f.description).toLowerCase().includes(q);
|
||||
el.style.display=hit?'':'none';
|
||||
});
|
||||
});
|
||||
let noteT;
|
||||
function markNoteDirty(){
|
||||
const f=FEATS[idx]; const btn=$('#btnSaveNote'); if(!f||!btn) return;
|
||||
const dirty=$('#note').value!==(f.note||'');
|
||||
btn.classList.toggle('dirty',dirty); btn.classList.toggle('saved',!dirty&&!!(f.note&&f.note.trim()));
|
||||
if(dirty) $('#noteSt').textContent='● 未保存';
|
||||
}
|
||||
async function saveNote(silent){
|
||||
const f=FEATS[idx]; if(!f) return;
|
||||
const val=$('#note').value;
|
||||
if(val===(f.note||'')){ markNoteDirty(); return; }
|
||||
f.note=val; markSidebar(idx);
|
||||
$('#noteSt').textContent='保存中…'; $('#btnSaveNote').classList.remove('dirty');
|
||||
try{ await fetch('/api/note',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({node_key:f.node_key,note:val})});
|
||||
$('#noteSt').textContent='已保存到数据库 ✓'; markNoteDirty();
|
||||
if(!silent) toast('备注已保存到数据库 ✓'); }
|
||||
catch(e){ $('#noteSt').textContent='⚠ 保存失败'; if(!silent) toast('⚠ 备注保存失败'); }
|
||||
}
|
||||
$('#note').addEventListener('input',markNoteDirty);
|
||||
$('#note').addEventListener('blur',()=>{ clearTimeout(noteT); saveNote(true); }); // 失焦兵底保存, 防丢失
|
||||
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA') return;
|
||||
if(e.key==='ArrowLeft') go(idx-1);
|
||||
else if(e.key==='ArrowRight') go(idx+1);
|
||||
else if(e.key==='k'||e.key==='K') decide(1);
|
||||
else if(e.key==='d'||e.key==='D') decide(0);
|
||||
});
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1588
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user