Files
twoJob-demo/xinrenxinshi/docs/审核.html
T
2026-09-03 15:35:23 +08:00

186 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>