first commit

This commit is contained in:
Michelle Zan
2026-09-03 15:35:23 +08:00
commit 9b26161bcf
510 changed files with 29402 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
<!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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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>