3588AdminBackend/internal/web/ui/templates/api.html
2026-01-10 21:30:28 +08:00

174 lines
6.4 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.

{{define "api"}}
<div class="card">
<div class="muted">同域调用不需要独立前端构建。OpenAPI<a class="mono" href="/openapi.json">/openapi.json</a></div>
</div>
<div class="card">
<h2>常用变量</h2>
<div class="row" style="margin-top:8px">
<div>
<div class="muted small">device_id</div>
<input id="deviceId" placeholder="..." />
</div>
<div>
<div class="muted small">task_id用于 SSE</div>
<input id="taskId" placeholder="..." />
</div>
<div>
<div class="muted small">template name</div>
<input id="tplName" placeholder="..." />
</div>
</div>
</div>
<div class="card">
<h2>发起请求</h2>
<div class="row" style="margin-top:8px">
<div style="max-width:160px">
<div class="muted small">method</div>
<select id="method">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
</div>
<div>
<div class="muted small">path</div>
<input id="path" value="/api/devices" />
</div>
<div style="align-self:end">
<button type="button" onclick="doCall()">发送</button>
</div>
</div>
<div style="margin-top:10px">
<div class="muted small">bodyJSONGET/DELETE 会忽略)</div>
<textarea id="body" spellcheck="false">{}</textarea>
</div>
<div class="row" style="margin-top:10px">
<div>
<div class="muted small">status</div>
<input id="status" readonly />
</div>
</div>
<pre id="resp"></pre>
</div>
<div class="card">
<h2>快速预设</h2>
<div class="actions" style="margin-top:8px">
<button type="button" onclick="preset('POST','/api/discovery/search', JSON.stringify({timeout_ms:1200}))">发现</button>
<button type="button" onclick="preset('GET','/api/devices','')">设备列表</button>
<button type="button" onclick="preset('GET','/api/devices/' + v('deviceId'),'')">设备详情</button>
<button type="button" onclick="preset('POST','/api/devices/' + v('deviceId') + '/reload','')">重载</button>
<button type="button" onclick="preset('POST','/api/devices/' + v('deviceId') + '/rollback','')">回滚</button>
<button type="button" onclick="preset('GET','/api/devices/' + v('deviceId') + '/graphs','')">图表</button>
<button type="button" onclick="preset('GET','/api/devices/' + v('deviceId') + '/logs?limit=200','')">日志</button>
<button type="button" onclick="preset('POST','/api/devices/' + v('deviceId') + '/config/apply', JSON.stringify({config:{}}))">下发配置</button>
<button type="button" onclick="preset('GET','/api/templates','')">模板列表</button>
<button type="button" onclick="preset('GET','/api/templates/' + v('tplName'),'')">模板详情</button>
<button type="button" onclick="preset('GET','/api/tasks','')">任务列表</button>
<button type="button" onclick="preset('POST','/api/tasks', JSON.stringify({type:'config_apply', device_ids:[v('deviceId')], payload:{config:{}}}))">创建任务</button>
</div>
</div>
<div class="card">
<h2>SSE</h2>
<div class="muted small">订阅:<code class="mono">/api/tasks/{id}/events</code></div>
<div class="actions" style="margin-top:8px">
<button type="button" onclick="connectSSE()">连接</button>
<button type="button" onclick="disconnectSSE()">断开</button>
<span class="muted" id="sseState">未连接</span>
</div>
<pre id="sseOut" style="margin-top:10px;max-height:320px"></pre>
</div>
<div class="card">
<h2>模型上传</h2>
<div class="muted small"><code class="mono">/api/devices/{id}/models/upload</code></div>
<form id="uploadForm" class="row" style="margin-top:8px" onsubmit="return uploadModel()">
<div>
<div class="muted small">name</div>
<input name="name" placeholder="模型名" />
</div>
<div>
<div class="muted small">file</div>
<input type="file" name="file" />
</div>
<div style="align-self:end">
<button type="submit">上传</button>
</div>
</form>
<pre id="uploadResp"></pre>
</div>
<script>
let es;
function v(id){ return (document.getElementById(id).value || '').trim(); }
function preset(m,p,b){
document.getElementById('method').value = m;
document.getElementById('path').value = p;
document.getElementById('body').value = b || '';
}
async function doCall(){
const method = document.getElementById('method').value;
const path = document.getElementById('path').value;
const body = document.getElementById('body').value;
const opts = { method, headers: {} };
if(method !== 'GET' && method !== 'DELETE'){
opts.headers['Content-Type'] = 'application/json';
opts.body = body || '{}';
}
const respBox = document.getElementById('resp');
respBox.textContent = '...';
try{
const res = await fetch(path, opts);
document.getElementById('status').value = res.status;
const txt = await res.text();
respBox.textContent = pretty(txt);
}catch(e){
document.getElementById('status').value = 'ERR';
respBox.textContent = String(e);
}
}
function pretty(txt){
try{ return JSON.stringify(JSON.parse(txt), null, 2); }catch(e){ return txt; }
}
function connectSSE(){
disconnectSSE();
const id = v('taskId');
if(!id){ alert('请先填写 task_id'); return; }
const url = `/api/tasks/${id}/events`;
es = new EventSource(url);
document.getElementById('sseState').textContent = '连接中...';
es.onopen = () => document.getElementById('sseState').textContent = '已连接';
es.onerror = () => document.getElementById('sseState').textContent = '连接异常(自动重试)';
es.addEventListener('device_update', (ev) => {
const out = document.getElementById('sseOut');
out.textContent += ev.data + "\n";
out.scrollTop = out.scrollHeight;
});
}
function disconnectSSE(){
if(es){ es.close(); es = null; }
document.getElementById('sseState').textContent = '未连接';
}
async function uploadModel(){
const did = v('deviceId');
if(!did){ alert('device_id required'); return false; }
const form = document.getElementById('uploadForm');
const fd = new FormData(form);
const box = document.getElementById('uploadResp');
box.textContent = '...';
try{
const res = await fetch(`/api/devices/${did}/models/upload`, { method: 'POST', body: fd });
const txt = await res.text();
box.textContent = `status ${res.status}\n` + pretty(txt);
}catch(e){
box.textContent = String(e);
}
return false;
}
</script>
{{end}}