feat(testing): Flows/FromCase、增量 swagger 与 Doc-Driven 门禁

补齐多接口流程联调与控制台体验,并落地仓内 Doc-Driven+TDD 总规则与文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 18:11:08 +08:00
parent 9fe4ab2758
commit 16496a8dc0
14 changed files with 4178 additions and 418 deletions
+199 -34
View File
@@ -47,11 +47,24 @@ input,select,textarea,button{font:inherit}pre{margin:0}
.cs{border:1px solid var(--border);border-radius:var(--r);margin-bottom:6px;overflow:hidden}
.csh{padding:6px 10px;display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;background:var(--bg2)}
.csh:hover{background:var(--bg3)}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0}
.dot.ok{background:var(--green)}.dot.er{background:var(--red)}.csh .ml{margin-left:auto;color:#555;font-size:11px}
.dot.ok{background:var(--green)}.dot.er{background:var(--red)}.csh .ml{margin-left:auto;color:#555;font-size:11px;display:flex;align-items:center;gap:6px;flex-shrink:0;max-width:55%}
.path-link{color:var(--accent);cursor:pointer;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px;display:inline-block;vertical-align:bottom}
.path-link:hover{text-decoration:underline}
.bind-tag{font-size:10px;color:#888;padding:1px 6px;border:1px solid var(--border);border-radius:10px;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis}
.flow-prep{margin-top:8px;padding:8px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);color:var(--fg2);font-size:12px;line-height:1.6;white-space:pre-wrap}
.flow-prep .lbl{display:inline-block;color:var(--accent);font-size:10px;margin-bottom:4px;letter-spacing:.5px}
.flow-note{color:#aaa;font-size:12px;line-height:1.5;margin-bottom:8px;white-space:pre-wrap}
.flow-note.empty{color:#555}
.csb{display:none;border-top:1px solid var(--border);font-size:12px}.csb.o{display:block}
.csr{display:flex}.csr>.csc{flex:1;padding:8px 10px;min-width:0}.csr>.csc+.csc{border-left:1px solid var(--border)}
.csc h5{color:var(--accent);margin:0 0 4px;font-size:11px;letter-spacing:.5px;display:flex;align-items:center}
.csc h5 .cp-sm{margin-left:auto}.csc h5:not(:first-child){margin-top:8px}
.modal-mask{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px}
.modal-box{background:var(--bg);border:1px solid var(--border);border-radius:8px;width:min(920px,96vw);max-height:88vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 40px rgba(0,0,0,.45)}
.modal-hd{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0}
.modal-hd .x{margin-left:auto;background:var(--bg3);border:1px solid var(--border);color:var(--fg2);border-radius:var(--r);cursor:pointer;padding:2px 10px;font-size:14px}
.modal-hd .x:hover{color:var(--accent);border-color:var(--accent)}
.modal-bd{overflow:auto;padding:12px;flex:1}
pre.j{background:#0a0a1a;padding:6px 8px;border-radius:var(--r);font-size:12px;line-height:1.4;color:#aaa;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto;font-family:Consolas,Monaco,monospace}
.resp-wrap{display:flex;gap:0}.resp-main{flex:1;min-width:0}
.resp-main .rsp-hd{display:flex;align-items:center;margin-bottom:3px;font-size:10px;color:var(--fg2);letter-spacing:.5px}
@@ -136,7 +149,7 @@ function epPass(e){if(!e.cases?.length)return false;return e.cases.every(x=>x.pa
function epFail(e){if(!e.cases?.length)return e.tested;return e.cases.some(x=>!x.passed);}
function render(){
if(!D)return;const q=document.getElementById('q').value.toLowerCase();const T={};
for(const e of D.endpoints){
for(const e of D.endpoints||[]){
if(F==='yes'&&!e.tested)continue;
if(F==='no'&&e.tested)continue;
if(F==='pass'&&!epPass(e))continue;
@@ -145,13 +158,35 @@ function render(){
if(!T[e.project])T[e.project]={};if(!T[e.project][e.ctr])T[e.project][e.ctr]=[];T[e.project][e.ctr].push(e);
}
let h='';
const flows=(D.flows||[]).filter(f=>{
if(q&&!f.name.toLowerCase().includes(q)&&!(f.desc||'').toLowerCase().includes(q))return false;
if(F==='pass'&&!f.passed)return false;
if(F==='fail'&&f.passed)return false;
if(F==='no')return false;
return true;
});
if(flows.length){
const byG={};
for(const f of flows){const g=f.group||'default';if(!byG[g])byG[g]=[];byG[g].push(f);}
let fh='';
for(const g of Object.keys(byG).sort()){
let gh='';
for(const f of byG[g]){
const sc=f.steps?.length||0;
const tg=f.passed?'<span class="tg tg-ok">通过</span>':'<span class="tg tg-err">失败</span>';
gh+='<div class="ep'+(C?.flowName===f.name?' act':'')+'" data-flow="'+esc(f.name)+'" onclick="goFlow(this)"><span class="mt mt-post">FLOW</span><span class="nm" title="'+esc(f.desc||f.name)+'">'+esc(f.name)+'</span>'+tg+'<span class="cn">'+sc+'</span></div>';
}
fh+='<div class="l2h" onclick="tog(this)"><span class="ar o">&#9654;</span>'+esc(g)+'<span class="cn">'+byG[g].length+'</span></div><div class="sub o">'+gh+'</div>';
}
h+='<div class="l1"><div class="l1h" onclick="tog(this)"><span class="ar o">&#9654;</span>业务流程<span class="cn">'+flows.length+'</span></div><div class="sub o">'+fh+'</div></div>';
}
for(const p of Object.keys(T).sort()){let ph='';
for(const c of Object.keys(T[p]).sort()){const es=T[p][c];let eh='';
for(const e of es){const m=e.method.toLowerCase();
let tg='';if(!e.tested)tg='<span class="tg tg-no">未测试</span>';
else if(e.cases?.length){const pc=e.cases.filter(x=>x.passed).length;tg=pc===e.cases.length?'<span class="tg tg-ok">'+pc+'/'+e.cases.length+'</span>':'<span class="tg tg-err">'+pc+'/'+e.cases.length+'</span>';}
const mn=e.path.split('/').pop();const lbl=e.tested&&e.summary!==mn?mn+' '+e.summary:e.summary;
eh+='<div class="ep'+(C?.path===e.path?' act':'')+'" data-p="'+e.path+'" onclick="go(this)"><span class="mt mt-'+m+'">'+e.method+'</span><span class="nm" title="'+e.path+'">'+lbl+'</span>'+tg+'</div>';
eh+='<div class="ep'+(C?.path===e.path&&!C?.flowName?' act':'')+'" data-p="'+e.path+'" onclick="go(this)"><span class="mt mt-'+m+'">'+e.method+'</span><span class="nm" title="'+e.path+'">'+lbl+'</span>'+tg+'</div>';
}
ph+='<div class="l2h" onclick="tog(this)"><span class="ar">&#9654;</span>'+c+'<span class="cn">'+es.length+'</span></div><div class="sub">'+eh+'</div>';
}
@@ -162,7 +197,92 @@ function render(){
}
function sf(f,b){F=f;document.querySelectorAll('.sft button').forEach(x=>x.classList.remove('on'));b.classList.add('on');render();}
function tog(e){e.querySelector('.ar').classList.toggle('o');e.nextElementSibling.classList.toggle('o');}
function go(el){C=D.endpoints.find(e=>e.path===el.dataset.p);if(!C)return;document.querySelectorAll('.ep').forEach(e=>e.classList.remove('act'));el.classList.add('act');show();}
function go(el){C=D.endpoints.find(e=>e.path===el.dataset.p);if(!C)return;C.flowName=undefined;document.querySelectorAll('.ep').forEach(e=>e.classList.remove('act'));el.classList.add('act');show();}
function goFlow(el){const f=(D.flows||[]).find(x=>x.name===el.dataset.flow);if(!f)return;C={flowName:f.name,flow:f};document.querySelectorAll('.ep').forEach(e=>e.classList.remove('act'));el.classList.add('act');showFlow(f);}
function stepCurl(s){
const base=window.location.origin;
let url=base+(s.path||'');
const m=s.method||'POST';
if(s.query){const qs=Object.keys(s.query).filter(k=>s.query[k]!==undefined&&s.query[k]!=='').map(k=>encodeURIComponent(k)+'='+encodeURIComponent(s.query[k])).join('&');if(qs)url+='?'+qs;}
let cmd="curl -X "+m+" '"+url+"'";
if(s.json){cmd+="\\\n -H 'Content-Type: application/json'";cmd+="\\\n -d '"+JSON.stringify(s.json).replace(/'/g,"'\\''")+"'";}
else if(s.form){const fd=Object.keys(s.form).map(k=>"-F '"+k+"="+s.form[k]+"'").join("\\\n ");if(fd)cmd+="\\\n "+fd;}
return cmd;
}
function hasReqData(s){return !!(s&&((s.query&&Object.keys(s.query).length)||s.json||(s.form&&Object.keys(s.form).length)));}
function closeEpModal(){const m=document.getElementById('flow-ep-modal');if(m)m.remove();}
function openEpModal(path,bindCase){
const e=(D.endpoints||[]).find(x=>x.path===path);
closeEpModal();
const mask=document.createElement('div');mask.id='flow-ep-modal';mask.className='modal-mask';
mask.onclick=ev=>{if(ev.target===mask)closeEpModal();};
let hd='<div class="modal-hd">';
if(e){const m=(e.method||'POST').toLowerCase();hd+='<span class="mt mt-'+m+'" style="font-size:12px;padding:2px 8px">'+esc(e.method||'')+'</span><span class="pa" style="font-size:14px">'+esc(e.path)+'</span><span class="ds">'+esc(e.summary||'')+'</span><span class="case-cnt">只读详情</span>';}
else{hd+='<span class="pa" style="font-size:14px">'+esc(path)+'</span><span class="ds">未找到接口定义</span>';}
hd+='<button class="x" onclick="closeEpModal()">关闭</button></div>';
let bd='<div class="modal-bd">';
if(!e){bd+='<div style="color:#555;padding:20px">swagger 中无此接口</div>';}
else{
let view=e;
if(bindCase&&e.cases?.length){
const idx=e.cases.findIndex(c=>c.name===bindCase);
if(idx>=0){view=Object.assign({},e,{cases:[e.cases[idx]].concat(e.cases.filter((_,i)=>i!==idx))});}
}
bd+=cases(view);
}
bd+='</div>';
mask.innerHTML='<div class="modal-box" onclick="event.stopPropagation()">'+hd+bd+'</div>';
document.body.appendChild(mask);
}
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeEpModal();});
function showFlow(f){
let h='<div class="ct-hd"><div class="ehd"><span class="mt mt-post" style="font-size:13px;padding:3px 8px">FLOW</span><span class="pa">'+esc(f.name)+'</span>';
if(f.desc)h+='<span class="ds">'+esc(f.desc)+'</span>';
h+='<span class="'+(f.passed?'tg tg-ok':'tg tg-err')+'">'+(f.passed?'通过':'失败')+'</span>';
h+='<span class="case-cnt">'+(f.steps?.length||0)+'个步骤</span></div>';
if(f.prep)h+='<div class="flow-prep"><div class="lbl">业务说明</div>'+esc(f.prep)+'</div>';
h+='</div>';
h+='<div class="flow-steps" style="padding:12px;overflow:auto;flex:1">';
if(!f.steps?.length){h+='<div style="color:#555;padding:20px">暂无步骤</div>';}
else{for(let i=0;i<f.steps.length;i++){const s=f.steps[i];
const m=(s.method||'POST').toLowerCase();
const curlId='fc'+i;const respId='fr'+i;
const failCls=(!s.passed)?' flow-step-fail':'';
h+='<div class="cs'+failCls+'" data-step="'+i+'"><div class="csh" onclick="flowStepToggle(this)"><span class="ar o">&#9654;</span><span class="dot '+(s.passed?'ok':'er')+'"></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc((s.index||i+1)+'. '+(s.name||s.caseName||''))+'</span>';
h+='<span class="ml"><span class="mt mt-'+m+'" style="font-size:10px;padding:1px 5px">'+(s.method||'')+'</span>';
if(s.path)h+='<a class="path-link" title="'+esc(s.path)+'" href="javascript:void(0)" onclick="event.stopPropagation();openEpModal(\''+esc(s.path)+'\''+(s.bindCase?',\''+esc(s.bindCase)+'\'':'')+')">'+esc(s.path)+'</a>';
h+='</span></div>';
h+='<div class="csb o"><div class="csr">';
h+='<div class="csc">';
if(s.bindCase)h+='<h5>关联验收用例</h5><div class="flow-note"><a class="path-link" href="javascript:void(0)" onclick="event.stopPropagation();openEpModal(\''+esc(s.path||'')+'\',\''+esc(s.bindCase)+'\')">'+esc(s.bindCase)+'</a></div>';
h+='<h5>备注</h5>';
if(s.note)h+='<div class="flow-note">'+esc(s.note)+'</div>';
else h+='<div class="flow-note empty">无备注</div>';
if(!s.passed&&s.failReason)h+='<div class="verify-err"><span class="verify-label">失败原因</span>'+esc(s.failReason)+'</div>';
else if(s.verifyError)h+='<div class="verify-err"><span class="verify-label">Verify</span>'+esc(s.verifyError)+'</div>';
h+='<h5>请求<button class="cp-sm" onclick="event.stopPropagation();copyEl(\''+curlId+'\')">复制</button></h5>';
h+='<div class="curl" id="'+curlId+'">'+esc(stepCurl(s))+'</div>';
if(hasReqData(s)){
if(s.query)h+='<h5>Query</h5><pre class="j">'+fj(s.query)+'</pre>';
if(s.json)h+='<h5>JSON</h5><pre class="j">'+fj(s.json)+'</pre>';
if(s.form)h+='<h5>Form</h5><pre class="j">'+fj(s.form)+'</pre>';
}else{h+='<div style="color:#555;font-size:11px;margin-top:6px">无请求参数</div>';}
h+='</div>';
h+='<div class="csc"><h5>响应<span class="'+(s.passed?'tg tg-ok':'tg tg-err')+'" style="margin-left:6px">'+(s.passed?'通过':'失败')+'</span><button class="cp-sm" onclick="event.stopPropagation();copyEl(\''+respId+'\')">复制</button></h5>';
if(s.response)h+='<pre class="j" id="'+respId+'" style="max-height:320px">'+fj(s.response)+'</pre>';
else h+='<span style="color:#555" id="'+respId+'">无响应</span>';
h+='</div></div></div></div>';
}}
h+='</div>';
document.getElementById('ct').innerHTML=h;
const failEl=document.querySelector('#ct .flow-step-fail');
if(failEl)failEl.scrollIntoView({block:'nearest',behavior:'smooth'});
}
function flowStepToggle(el){
const bd=el.nextElementSibling;if(!bd)return;
bd.classList.toggle('o');
el.querySelector('.ar')?.classList.toggle('o');
}
function show(){
const e=C;if(!e)return;const m=e.method.toLowerCase();const cnt=e.cases?.length||0;
@@ -184,6 +304,8 @@ function show(){
let defIdx=-1;
if(cnt>0){for(let i=e.cases.length-1;i>=0;i--){if(e.cases[i].passed){defIdx=i;break;}}if(defIdx<0)defIdx=e.cases.length-1;}
pfSelect(defIdx>=0?defIdx:'');
bindLiveCurl();
refreshLiveCurl();
}
function rswitch(i){
document.querySelectorAll('.rtab').forEach((t,j)=>t.classList.toggle('on',j===i));
@@ -225,8 +347,8 @@ function cases(e){
return h;
}
function accordion(el){
const bd=el.nextElementSibling;const wasOpen=bd.classList.contains('o');
const panel=el.closest('.rpn');
const bd=el.nextElementSibling;if(!bd)return;const wasOpen=bd.classList.contains('o');
const panel=el.closest('.modal-bd')||el.closest('.rpn')||el.closest('#ct')||document;
panel.querySelectorAll('.csb').forEach(b=>b.classList.remove('o'));
panel.querySelectorAll('.csh .ar').forEach(a=>a.classList.remove('o'));
if(!wasOpen){bd.classList.add('o');el.querySelector('.ar').classList.add('o');}
@@ -236,7 +358,7 @@ function respPanel(){
let r='';
r+='<div id="note-box" style="display:none;color:#aaa;font-size:12px;margin-bottom:8px;line-height:1.5"></div>';
r+='<div class="row-hd" style="margin-bottom:4px"><span class="sec-title">cURL</span><button class="cp-sm" onclick="copyEl(\'curl-box\')">复制</button></div>';
r+='<div class="curl" id="curl-box">发送请求后生成</div>';
r+='<div class="curl" id="curl-box">根据左侧参数实时生成</div>';
r+='<div class="row-hd" style="margin:10px 0 4px"><span class="sec-title">响应 <span id="resp-st"></span></span><button class="cp-sm" onclick="copyEl(\'resp-body\')">复制</button></div>';
r+='<pre class="j" id="resp-body" style="min-height:60px;max-height:calc(100vh - 200px)">发送请求后显示</pre>';
return r;
@@ -253,15 +375,15 @@ function buildLeft(e){
l+='<div class="pf-wrap"><button class="pf-btn" id="pf-btn" onclick="togglePfDd(event)"><span class="dot" id="pf-dot" style="display:none;flex-shrink:0"></span><span id="pf-label" style="flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--fg2)">选择用例...</span><span style="color:var(--fg2);font-size:10px;flex-shrink:0">▾</span></button><ul class="pf-dd" id="pf-dd"></ul></div>';
l+='<button class="sbtn" onclick="send()" id="sbtn">发送</button>';
l+='</div>';
let defH='<div class="kv"><input value="Content-Type"><input id="ct-val" value="'+(defBody==='json'?'application/json':'application/x-www-form-urlencoded')+'"><button onclick="this.parentElement.remove()">&#215;</button></div>';
if(tk&&at==='header')defH+='<div class="kv" data-auth="1"><input value="Authorization"><input value="'+esc(tk)+'"><button onclick="this.parentElement.remove()">&#215;</button></div>';
if(tk&&at==='cookie')defH+='<div class="kv" data-auth="1"><input value="Cookie"><input value="HOTIME='+esc(tk)+'"><button onclick="this.parentElement.remove()">&#215;</button></div>';
let defH='<div class="kv"><input value="Content-Type"><input id="ct-val" value="'+(defBody==='json'?'application/json':'application/x-www-form-urlencoded')+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div>';
if(tk&&at==='header')defH+='<div class="kv" data-auth="1"><input value="Authorization"><input value="'+esc(tk)+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div>';
if(tk&&at==='cookie')defH+='<div class="kv" data-auth="1"><input value="Cookie"><input value="HOTIME='+esc(tk)+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div>';
l+='<div class="sec" style="margin-top:0"><div class="row-hd"><span class="sec-title">请求头</span><button class="addbtn" onclick="addKV(\'h-rows\')">+ 添加</button></div><div id="h-rows">'+defH+'</div></div>';
let qDef='';
if(tk&&at==='query')qDef='<div class="kv" data-auth="1"><input value="token"><input value="'+esc(tk)+'"><button onclick="this.parentElement.remove()">&#215;</button></div>';
qDef+='<div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove()">&#215;</button></div>';
if(tk&&at==='query')qDef='<div class="kv" data-auth="1"><input value="token"><input value="'+esc(tk)+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div>';
qDef+='<div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div>';
l+='<div class="sec"><div class="row-hd"><span class="sec-title">Query 参数</span><button class="addbtn" onclick="addKV(\'q-rows\')">+ 添加</button></div><div id="q-rows">'+qDef+'</div></div>';
l+='<div class="sec"><div class="row-hd"><span class="sec-title">文件上传</span><button class="addbtn" onclick="addFileRow()">+ 添加</button></div><div id="file-rows"><div class="kv"><input placeholder="字段名" value="" style="max-width:120px"><input type="file"><button onclick="this.parentElement.remove()">&#215;</button></div></div></div>';
l+='<div class="sec"><div class="row-hd"><span class="sec-title">文件上传</span><button class="addbtn" onclick="addFileRow();refreshLiveCurl()">+ 添加</button></div><div id="file-rows"><div class="kv"><input placeholder="字段名" value="" style="max-width:120px"><input type="file"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div></div></div>';
let jsonPH='';
if(e.params){
const jp=e.params.filter(p=>p.in==='json');
@@ -269,7 +391,7 @@ function buildLeft(e){
}
l+='<div class="sec"><div class="sec-title" style="margin-bottom:4px">请求体</div><div class="bd-acc">';
l+='<div class="bd-hd'+(defBody==='form'?' on':'')+'" data-t="form" onclick="togBody(\'form\')"><span class="ar'+(defBody==='form'?' o':'')+'">&#9654;</span>表单 (Form)<button class="addbtn" onclick="event.stopPropagation();setAcc(\'form\');addKV(\'f-rows\')">+ 添加</button></div>';
l+='<div id="bt-form" class="bd-bd'+(defBody==='form'?' on':'')+'"><div id="f-rows"><div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove()">&#215;</button></div></div></div>';
l+='<div id="bt-form" class="bd-bd'+(defBody==='form'?' on':'')+'"><div id="f-rows"><div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button></div></div></div>';
l+='<div class="bd-hd'+(defBody==='json'?' on':'')+'" data-t="json" onclick="togBody(\'json\')"><span class="ar'+(defBody==='json'?' o':'')+'">&#9654;</span>JSON</div>';
l+='<div id="bt-json" class="bd-bd'+(defBody==='json'?' on':'')+'">';
if(e.params){
@@ -295,18 +417,18 @@ function buildLeft(e){
function addKV(id,k,v){
const row=document.createElement('div');row.className='kv';
row.innerHTML='<input placeholder="key" value="'+esc(k||'')+'"><input placeholder="value" value="'+esc(v||'')+'"><button onclick="this.parentElement.remove()">&#215;</button>';
row.innerHTML='<input placeholder="key" value="'+esc(k||'')+'"><input placeholder="value" value="'+esc(v||'')+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button>';
document.getElementById(id).appendChild(row);
}
function addKVReq(id,k,v,req){
const row=document.createElement('div');row.className='kv';
const star=req?'<span class="req-star">*</span>':'';
row.innerHTML=star+'<input placeholder="key" value="'+esc(k||'')+'"><input placeholder="value" value="'+esc(v||'')+'"><button onclick="this.parentElement.remove()">&#215;</button>';
row.innerHTML=star+'<input placeholder="key" value="'+esc(k||'')+'"><input placeholder="value" value="'+esc(v||'')+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button>';
document.getElementById(id).appendChild(row);
}
function addFileRow(field){
const row=document.createElement('div');row.className='kv';
row.innerHTML='<input placeholder="字段名" value="'+esc(field||'')+'" style="max-width:120px"><input type="file"><button onclick="this.parentElement.remove()">&#215;</button>';
row.innerHTML='<input placeholder="字段名" value="'+esc(field||'')+'" style="max-width:120px"><input type="file"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button>';
document.getElementById('file-rows').appendChild(row);
}
function setAcc(t){
@@ -315,23 +437,26 @@ function setAcc(t){
document.getElementById('bt-form').classList.toggle('on',t==='form');
const ctVal=document.getElementById('ct-val');
if(ctVal)ctVal.value=t==='json'?'application/json':'application/x-www-form-urlencoded';
refreshLiveCurl();
}
function togBody(t){
const cur=document.getElementById(t==='json'?'bt-json':'bt-form').classList.contains('on');
setAcc(cur?(t==='json'?'form':'json'):t);
}
function autoH(el){el.style.height='36px';el.style.height=Math.min(el.scrollHeight,window.innerHeight*0.4)+'px';}
function autoH(el){el.style.height='36px';el.style.height=Math.min(el.scrollHeight,window.innerHeight*0.4)+'px';refreshLiveCurl();}
function syncAuth(){
const tk=document.getElementById('tk').value;const at=document.getElementById('at').value;
const hr=document.getElementById('h-rows');const qr=document.getElementById('q-rows');
if(!hr||!qr)return;
if(!hr||!qr){refreshLiveCurl();return;}
hr.querySelectorAll('[data-auth]').forEach(e=>e.remove());
qr.querySelectorAll('[data-auth]').forEach(e=>e.remove());
if(!tk)return;
function mkAuth(k,v,parent){const d=document.createElement('div');d.className='kv';d.dataset.auth='1';d.innerHTML='<input value="'+esc(k)+'"><input value="'+esc(v)+'"><button onclick="this.parentElement.remove()">&#215;</button>';parent.insertBefore(d,parent.querySelector('.kv:last-child'));}
if(at==='header')mkAuth('Authorization',tk,hr);
if(at==='cookie')mkAuth('Cookie','HOTIME='+tk,hr);
if(at==='query')mkAuth('token',tk,qr);
if(tk){
function mkAuth(k,v,parent){const d=document.createElement('div');d.className='kv';d.dataset.auth='1';d.innerHTML='<input value="'+esc(k)+'"><input value="'+esc(v)+'"><button onclick="this.parentElement.remove();refreshLiveCurl()">&#215;</button>';parent.insertBefore(d,parent.querySelector('.kv:last-child'));}
if(at==='header')mkAuth('Authorization',tk,hr);
if(at==='cookie')mkAuth('Cookie','HOTIME='+tk,hr);
if(at==='query')mkAuth('token',tk,qr);
}
refreshLiveCurl();
}
function buildPfList(e){
@@ -449,6 +574,7 @@ function pfFill(v){
document.getElementById('file-rows').innerHTML='';
if(c.hasFile&&c.fileField)addFileRow(c.fileField);
addFileRow();syncAuth();
refreshLiveCurl();
}
function getKV(id){
@@ -457,6 +583,52 @@ function getKV(id){
return Object.keys(obj).length?obj:null;
}
function buildLiveCurl(){
if(!C)return '';
let url=C.path;
const qp=getKV('q-rows');
if(qp){const ps=new URLSearchParams();for(const[k,v]of Object.entries(qp))ps.set(k,v);url+='?'+ps.toString();}
const token=document.getElementById('tk')?.value||'';
const authType=document.getElementById('at')?.value||'none';
const headers={};
const ch=getKV('h-rows');if(ch)for(const[k,v]of Object.entries(ch)){
if(k.toLowerCase()==='authorization'&&token&&authType==='header')continue;
if(k.toLowerCase()==='cookie'&&token&&authType==='cookie')continue;
headers[k]=v;
}
if(token&&authType==='header')headers['Authorization']=token;
const fileEls=document.querySelectorAll('#file-rows .kv');
const files=[];fileEls.forEach(r=>{const fnEl=r.querySelector('input:not([type="file"])');const fi=r.querySelector('input[type="file"]');if(!fnEl||!fi)return;const fn=fnEl.value.trim();if(!fn)return;if(fi.files.length)files.push({field:fn,name:fi.files[0].name});else files.push({field:fn,name:'filename'});});
const isJson=document.getElementById('bt-json')?.classList.contains('on');
const bodyEl=document.getElementById('xb');const formKV=getKV('f-rows');let curlParts='';
if(files.length){
for(const f of files)curlParts+=' -F "'+f.field+'=@'+f.name+'"';
if(!isJson&&formKV)for(const[k,v]of Object.entries(formKV))curlParts+=' -F "'+k+'='+v+'"';
}else if(isJson){
const b=bodyEl?.value?.trim();
if(b&&C.method!=='GET')curlParts=" -d '"+b.replace(/'/g,"'\\''")+"'";
}else if(formKV){
for(const[k,v]of Object.entries(formKV))curlParts+=' -F "'+k+'='+v+'"';
}
let curl='curl -X '+C.method;
for(const[k,v]of Object.entries(headers)){if(k&&v)curl+=" -H '"+k+": "+v+"'";}
if(token&&authType==='cookie')curl+=" --cookie 'HOTIME="+token+"'";
curl+=curlParts+' '+location.origin+url;
return curl;
}
function refreshLiveCurl(){
const curlBox=document.getElementById('curl-box');
if(!curlBox||!C)return;
curlBox.textContent=buildLiveCurl();
}
function bindLiveCurl(){
const left=document.querySelector('#ct .split-l');
if(!left||left.dataset.liveCurl==='1')return;
left.dataset.liveCurl='1';
left.addEventListener('input',refreshLiveCurl);
left.addEventListener('change',refreshLiveCurl);
}
async function send(){
if(!C)return;const btn=document.getElementById('sbtn');btn.disabled=true;btn.textContent='请求中...';
let url=C.path;
@@ -476,25 +648,18 @@ async function send(){
const fileEls=document.querySelectorAll('#file-rows .kv');
const files=[];fileEls.forEach(r=>{const fnEl=r.querySelector('input:not([type="file"])');const fi=r.querySelector('input[type="file"]');if(!fnEl||!fi)return;const fn=fnEl.value.trim();if(fn&&fi.files.length)files.push({field:fn,file:fi.files[0]});});
const isJson=document.getElementById('bt-json')?.classList.contains('on');
const bodyEl=document.getElementById('xb');const formKV=getKV('f-rows');let curlParts='';
const bodyEl=document.getElementById('xb');const formKV=getKV('f-rows');
if(files.length){
const fd=new FormData();for(const f of files)fd.append(f.field,f.file);
if(!isJson&&formKV)for(const[k,v]of Object.entries(formKV))fd.append(k,v);
opts.body=fd;delete opts.headers['Content-Type'];
for(const f of files)curlParts+=' -F "'+f.field+'=@'+f.file.name+'"';
if(!isJson&&formKV)for(const[k,v]of Object.entries(formKV))curlParts+=' -F "'+k+'='+v+'"';
}else if(isJson){
const b=bodyEl?.value?.trim();
if(b&&C.method!=='GET'){opts.body=b;curlParts=" -d '"+b+"'";}
if(b&&C.method!=='GET')opts.body=b;
}else if(formKV){
const fd=new FormData();for(const[k,v]of Object.entries(formKV))fd.append(k,v);opts.body=fd;delete opts.headers['Content-Type'];
for(const[k,v]of Object.entries(formKV))curlParts+=' -F "'+k+'='+v+'"';
}
let curl='curl -X '+C.method;
for(const[k,v]of Object.entries(opts.headers)){if(k&&v)curl+=" -H '"+k+": "+v+"'";}
if(token&&authType==='cookie')curl+=" --cookie 'HOTIME="+token+"'";
curl+=curlParts+' '+location.origin+url;
const curlBox=document.getElementById('curl-box');if(curlBox)curlBox.textContent=curl;
refreshLiveCurl();
rswitch(1);
const t0=Date.now();
try{