186 lines
12 KiB
HTML
186 lines
12 KiB
HTML
<!DOCTYPE html>
|
||||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8"/>
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|||
|
|
<title>薪人薪事 · 功能调研审核</title>
|
|||
|
|
<style>
|
|||
|
|
:root{--green:#2f6bff;--bd:#e5e7eb;--muted:#6b7280;--bg:#f5f6f8;--keep:#16a34a;--drop:#dc2626;}
|
|||
|
|
*{box-sizing:border-box;}
|
|||
|
|
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:#1f2937;background:var(--bg);}
|
|||
|
|
header{position:sticky;top:0;z-index:20;background:#fff;border-bottom:1px solid var(--bd);padding:12px 20px;}
|
|||
|
|
header h1{margin:0 0 4px;font-size:18px;}
|
|||
|
|
.sub{color:var(--muted);font-size:13px;}
|
|||
|
|
.toolbar{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
|
|||
|
|
.toolbar input[type=search],.toolbar select{padding:7px 10px;border:1px solid var(--bd);border-radius:6px;font-size:13px;}
|
|||
|
|
.toolbar input[type=search]{width:230px;}
|
|||
|
|
button{cursor:pointer;border:1px solid var(--bd);background:#fff;border-radius:6px;padding:7px 12px;font-size:13px;}
|
|||
|
|
button.primary{background:var(--green);color:#fff;border-color:var(--green);}
|
|||
|
|
button:hover{filter:brightness(0.97);}
|
|||
|
|
.stat{font-size:13px;color:var(--muted);margin-left:auto;}
|
|||
|
|
.stat b{color:var(--green);font-size:15px;}
|
|||
|
|
.wrap{padding:16px 20px 90px;max-width:1180px;margin:0 auto;}
|
|||
|
|
.mod{margin-top:22px;}
|
|||
|
|
.mod-h{font-size:16px;font-weight:700;color:#0f2d66;border-left:4px solid var(--green);padding-left:8px;margin-bottom:6px;}
|
|||
|
|
.grp-h{font-size:13px;font-weight:600;color:#4b5563;margin:12px 0 6px;}
|
|||
|
|
.card{background:#fff;border:1px solid var(--bd);border-radius:10px;padding:14px 16px;margin-bottom:12px;display:flex;gap:16px;}.card.dropped{opacity:.5;}
|
|||
|
|
.card.dropped .name{text-decoration:line-through;}
|
|||
|
|
.left{flex:1;min-width:0;}
|
|||
|
|
.name{font-size:15px;font-weight:600;}
|
|||
|
|
.name .path{color:#9ca3af;font-weight:400;font-size:11px;margin-left:8px;}
|
|||
|
|
.desc{color:#374151;font-size:13px;margin-top:5px;line-height:1.6;}
|
|||
|
|
.und{color:#8a6d3b;background:#fcf8ee;border:1px solid #f3e9cf;border-radius:4px;padding:3px 7px;font-size:12px;margin-top:6px;display:inline-block;}
|
|||
|
|
.url{color:#9ca3af;font-size:11px;margin-top:5px;word-break:break-all;}
|
|||
|
|
.note{margin-top:8px;width:100%;border:1px solid var(--bd);border-radius:6px;padding:6px 8px;font-size:12.5px;resize:vertical;min-height:34px;font-family:inherit;}
|
|||
|
|
.right{flex:0 0 260px;display:flex;flex-direction:column;gap:8px;align-items:flex-end;}
|
|||
|
|
.thumb img{width:250px;max-height:170px;object-fit:cover;object-position:top;border:1px solid var(--bd);border-radius:6px;cursor:zoom-in;background:#fafafa;}
|
|||
|
|
.actions{display:flex;gap:6px;}
|
|||
|
|
.btn-keep{border-color:var(--keep);color:var(--keep);}
|
|||
|
|
.btn-keep.on{background:var(--keep);color:#fff;}
|
|||
|
|
.btn-drop{border-color:var(--drop);color:var(--drop);}
|
|||
|
|
.btn-drop.on{background:var(--drop);color:#fff;}
|
|||
|
|
.deep{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;}
|
|||
|
|
.deep img{width:78px;height:52px;object-fit:cover;object-position:top;border:1px solid var(--bd);border-radius:4px;cursor:zoom-in;}
|
|||
|
|
.dls{margin-top:6px;width:100%;}
|
|||
|
|
.dl-item{display:inline-flex;align-items:center;gap:6px;font-size:12px;background:#f0f5ff;border:1px solid #d6e2ff;border-radius:5px;padding:3px 8px;margin:3px 3px 0 0;}
|
|||
|
|
.dl-item a{color:var(--green);text-decoration:none;}
|
|||
|
|
.dl-item .prev{cursor:pointer;color:#6b7280;text-decoration:underline;}
|
|||
|
|
.hidden{display:none;}
|
|||
|
|
#lb{position:fixed;inset:0;background:rgba(0,0,0,.85);display:none;z-index:100;overflow:auto;text-align:center;}
|
|||
|
|
#lb img{max-width:96%;margin:2% auto;border-radius:6px;}
|
|||
|
|
#lb .close{position:fixed;top:14px;right:20px;color:#fff;font-size:30px;cursor:pointer;}
|
|||
|
|
#pv{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;z-index:110;padding:3% 4%;}
|
|||
|
|
#pv .box{background:#fff;border-radius:8px;height:100%;overflow:auto;padding:14px;}
|
|||
|
|
#pv table{border-collapse:collapse;font-size:12px;}
|
|||
|
|
#pv td{border:1px solid #e5e7eb;padding:3px 7px;white-space:nowrap;}
|
|||
|
|
#pv tr:first-child td{background:#f3f4f6;font-weight:600;position:sticky;top:0;}
|
|||
|
|
#pv .close{float:right;cursor:pointer;font-size:20px;color:#6b7280;}
|
|||
|
|
.tabs{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;}
|
|||
|
|
.tabs button.on{background:var(--green);color:#fff;border-color:var(--green);}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<header>
|
|||
|
|
<h1>薪人薪事 · 功能调研审核 <span class="sub">逐条勾选保留/删除,可写开发备注,导出 selection.json</span></h1>
|
|||
|
|
<div class="sub" id="srcline">数据来源:https://s-demob.xinrenxinshi.show 全站功能调研</div>
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<input type="search" id="q" placeholder="搜索功能名称/释义…"/>
|
|||
|
|
<select id="modsel"><option value="">全部模块</option></select>
|
|||
|
|
<button onclick="setAll(1)">全选保留</button>
|
|||
|
|
<button onclick="setAll(0)">全部删除</button>
|
|||
|
|
<button onclick="showAtt()">📎 附件总览</button>
|
|||
|
|
<button class="primary" onclick="exportSel()">⬇ 导出 selection.json</button>
|
|||
|
|
<span class="stat">保留 <b id="cntKeep">0</b> / <span id="cntAll">0</span> 项</span>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
<div class="wrap" id="wrap"></div>
|
|||
|
|
<div id="lb" onclick="this.style.display='none'"><span class="close">×</span><img id="lbimg"/></div>
|
|||
|
|
<div id="pv"><div class="box"><span class="close" onclick="document.getElementById('pv').style.display='none'">×</span><div class="tabs" id="pvtabs"></div><div id="pvbody"></div></div></div>
|
|||
|
|
<script>
|
|||
|
|
let FEATS=[];
|
|||
|
|
function esc(s){return (s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
|||
|
|
function fmtSize(n){if(!n)return '';if(n<1024)return n+'B';if(n<1048576)return (n/1024).toFixed(0)+'KB';return (n/1048576).toFixed(1)+'MB';}
|
|||
|
|
|
|||
|
|
async function load(){
|
|||
|
|
const r=await fetch('/api/features'); const d=await r.json();
|
|||
|
|
FEATS=d.items;
|
|||
|
|
const sel=document.getElementById('modsel'); const mods=[...new Set(FEATS.map(f=>f.module))];
|
|||
|
|
mods.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;sel.appendChild(o);});
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
function render(){
|
|||
|
|
const q=(document.getElementById('q').value||'').trim().toLowerCase();
|
|||
|
|
const mf=document.getElementById('modsel').value;
|
|||
|
|
const wrap=document.getElementById('wrap'); wrap.innerHTML='';
|
|||
|
|
let curMod='',curGrp='',modBox=null;
|
|||
|
|
FEATS.filter(f=>{
|
|||
|
|
if(mf&&f.module!==mf)return false;
|
|||
|
|
if(q){const t=(f.name+f.description+f.understanding+f.full_path).toLowerCase();if(!t.includes(q))return false;}
|
|||
|
|
return true;
|
|||
|
|
}).forEach(f=>{
|
|||
|
|
if(f.module!==curMod){curMod=f.module;curGrp='';
|
|||
|
|
const h=document.createElement('div');h.className='mod';h.innerHTML='<div class="mod-h">'+esc(f.module)+'</div>';
|
|||
|
|
wrap.appendChild(h);modBox=h;}
|
|||
|
|
if(f.group&&f.group!==curGrp){curGrp=f.group;
|
|||
|
|
const g=document.createElement('div');g.className='grp-h';g.textContent='▸ '+f.group;modBox.appendChild(g);}
|
|||
|
|
modBox.appendChild(card(f));
|
|||
|
|
});
|
|||
|
|
updateStats();
|
|||
|
|
}
|
|||
|
|
function card(f){
|
|||
|
|
const c=document.createElement('div');c.className='card'+(f.keep?'':' dropped');c.dataset.key=f.node_key;
|
|||
|
|
let deep='';
|
|||
|
|
if(f.deep_shots&&f.deep_shots.length){deep='<div class="deep">'+f.deep_shots.map(s=>'<img title="'+esc(s.kind_cn+(s.label?(' · '+s.label):''))+'" src="'+s.shot+'" onclick="zoom(this.src)"/>').join('')+'</div>';}
|
|||
|
|
let dls='';
|
|||
|
|
if(f.downloads&&f.downloads.length){dls='<div class="dls">'+f.downloads.map(d=>{
|
|||
|
|
const isXls=/\.(xlsx|xls)$/i.test(d.name);
|
|||
|
|
return '<span class="dl-item">📄 '+esc(d.name)+' <span style="color:#9ca3af">'+fmtSize(d.size)+'</span> '+
|
|||
|
|
(isXls?'<span class="prev" onclick="preview(\''+encodeURIComponent(d.rel)+'\')">预览</span> ':'')+
|
|||
|
|
'<a href="/dl/'+d.rel.split('/').map(encodeURIComponent).join('/')+'">下载</a></span>';
|
|||
|
|
}).join('')+'</div>';}
|
|||
|
|
c.innerHTML=
|
|||
|
|
'<div class="left">'+
|
|||
|
|
'<div class="name">'+esc(f.name)+'<span class="path">'+esc(f.full_path||'')+'</span></div>'+
|
|||
|
|
(f.description?'<div class="desc">'+esc(f.description)+'</div>':'')+
|
|||
|
|
(f.understanding?'<div class="und">理解:'+esc(f.understanding)+'</div>':'')+
|
|||
|
|
(f.url?'<div class="url">'+esc(f.url)+'</div>':'')+
|
|||
|
|
dls+
|
|||
|
|
'<textarea class="note" placeholder="开发备注/审核意见…" onchange="saveNote(this,\''+f.node_key+'\')">'+esc(f.note)+'</textarea>'+
|
|||
|
|
'</div>'+
|
|||
|
|
'<div class="right">'+
|
|||
|
|
(f.shot?'<div class="thumb"><img loading="lazy" src="'+f.shot+'" onclick="zoom(this.src)" onerror="this.style.opacity=.2;this.title=\'截图待生成\'"/></div>':'<div class="sub" style="font-size:11px;color:#bbb">(截图待生成)</div>')+
|
|||
|
|
'<div class="actions"><button class="btn-keep'+(f.keep?' on':'')+'" onclick="setKeep(\''+f.node_key+'\',1)">保留</button>'+
|
|||
|
|
'<button class="btn-drop'+(f.keep?'':' on')+'" onclick="setKeep(\''+f.node_key+'\',0)">删除</button></div>'+
|
|||
|
|
deep+
|
|||
|
|
'</div>';
|
|||
|
|
return c;
|
|||
|
|
}
|
|||
|
|
async function setKeep(k,v){
|
|||
|
|
await fetch('/api/keep',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({node_key:k,keep:v})});
|
|||
|
|
const f=FEATS.find(x=>x.node_key===k);if(f)f.keep=v;render();
|
|||
|
|
}
|
|||
|
|
async function saveNote(el,k){
|
|||
|
|
await fetch('/api/note',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({node_key:k,note:el.value})});
|
|||
|
|
const f=FEATS.find(x=>x.node_key===k);if(f)f.note=el.value;
|
|||
|
|
}
|
|||
|
|
async function setAll(v){
|
|||
|
|
const keys=FEATS.map(f=>f.node_key);
|
|||
|
|
await fetch('/api/keep_batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({node_keys:keys,keep:v})});
|
|||
|
|
FEATS.forEach(f=>f.keep=v);render();
|
|||
|
|
}
|
|||
|
|
function updateStats(){const keep=FEATS.filter(f=>f.keep).length;
|
|||
|
|
document.getElementById('cntKeep').textContent=keep;document.getElementById('cntAll').textContent=FEATS.length;}
|
|||
|
|
function zoom(src){document.getElementById('lbimg').src=src;document.getElementById('lb').style.display='block';}
|
|||
|
|
async function exportSel(){const r=await fetch('/api/export',{method:'POST'});const d=await r.json();
|
|||
|
|
alert('已导出 '+d.path+'\n共 '+d.total+' 项,保留 '+d.keep+',删除 '+d.dropped);}
|
|||
|
|
async function preview(relEnc){
|
|||
|
|
const rel=decodeURIComponent(relEnc);
|
|||
|
|
const r=await fetch('/api/preview?rel='+encodeURIComponent(rel));const d=await r.json();
|
|||
|
|
if(!d.ok){alert('无法预览:'+(d.err||'未知'));return;}
|
|||
|
|
const pv=document.getElementById('pv');const tabs=document.getElementById('pvtabs');const body=document.getElementById('pvbody');
|
|||
|
|
tabs.innerHTML='';body.innerHTML='';
|
|||
|
|
d.sheets.forEach((s,i)=>{const b=document.createElement('button');b.textContent=s.name+' ('+s.total_rows+'×'+s.total_cols+')';
|
|||
|
|
b.className=i===0?'on':'';b.onclick=()=>{[...tabs.children].forEach(x=>x.className='');b.className='on';showSheet(s);};tabs.appendChild(b);});
|
|||
|
|
if(d.sheets.length)showSheet(d.sheets[0]);
|
|||
|
|
pv.style.display='block';
|
|||
|
|
}
|
|||
|
|
function showSheet(s){const body=document.getElementById('pvbody');
|
|||
|
|
body.innerHTML='<table>'+s.rows.map(r=>'<tr>'+r.map(c=>'<td>'+esc(c)+'</td>').join('')+'</tr>').join('')+'</table>';}
|
|||
|
|
async function showAtt(){
|
|||
|
|
const r=await fetch('/api/attachments');const d=await r.json();
|
|||
|
|
const pv=document.getElementById('pv');const tabs=document.getElementById('pvtabs');const body=document.getElementById('pvbody');
|
|||
|
|
tabs.innerHTML='<b style="padding:6px">📎 真实导出附件 共 '+d.total+' 个</b>';
|
|||
|
|
body.innerHTML=d.groups.map(g=>'<div style="margin:8px 0"><div style="font-weight:600;color:#0f2d66">'+esc(g.dir)+'</div>'+
|
|||
|
|
g.files.map(f=>{const isXls=/\.(xlsx|xls)$/i.test(f.name);
|
|||
|
|
return '<div class="dl-item">📄 '+esc(f.name)+' <span style="color:#9ca3af">'+fmtSize(f.size)+'</span> '+
|
|||
|
|
(isXls?'<span class="prev" onclick="preview(\''+encodeURIComponent(f.rel)+'\')">预览</span> ':'')+
|
|||
|
|
'<a href="/dl/'+f.rel.split('/').map(encodeURIComponent).join('/')+'">下载</a></div>';}).join('')+'</div>').join('')||'<div class="sub">暂无附件</div>';
|
|||
|
|
pv.style.display='block';
|
|||
|
|
}
|
|||
|
|
document.getElementById('q').addEventListener('input',render);
|
|||
|
|
document.getElementById('modsel').addEventListener('change',render);
|
|||
|
|
load();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|