Files
open-gateway-service/src/main/resources/static/admin.html
T
chenxing 2389d2fa9a
build / build-push (push) Successful in 1m59s
feat(admin): 管理面信封化+分页化,OpenAPI 文档端点旁路(B1-B6)
- pom: 引 yh-platform-dependencies(import) / yh-api-std / springdoc-openapi-ui 1.7.0 + nexus-public 仓库
- RoutingFilter.isGatewayInternal: /v3/api-docs、/swagger-ui/**、/swagger-ui.html 纳入自身路径旁路,避免被 TargetRouter 当 target 裸 404
- InternalAdminController: page/routes/targets/logs 三接口分页化返回 BasePageResult;补 @Operation(operationId=GatewayAdmin.*);错误契约去 @ResponseStatus(400) 改 200+code=2;logs 兼容旧 limit 入参
- RouteRefreshController: 补 @Tag/@Operation(operationId=GatewayAdmin.refresh)
- static/admin.html: 适配 {code,msg,data} 信封与 content/total 分页
- InternalAdminControllerTest: 重写 12 用例,77 项测试全绿
2026-09-23 12:17:02 +08:00

253 lines
11 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">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>网关治理</title>
<style>
:root { --line:#e2e8f0; --txt:#1a202c; --sub:#718096; --ok:#276749; --err:#c53030; }
* { box-sizing:border-box; }
body { font:14px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif; color:var(--txt);
margin:0; background:#f7fafc; }
header { background:#fff; border-bottom:1px solid var(--line); padding:14px 24px;
display:flex; align-items:center; gap:16px; position:sticky; top:0; z-index:5; }
header h1 { font-size:17px; margin:0; }
header .sp { flex:1; }
button { padding:5px 14px; border:1px solid var(--line); background:#fff; border-radius:6px;
cursor:pointer; font-size:13px; }
button:hover { background:#edf2f7; }
button.primary { background:#2b6cb0; color:#fff; border-color:#2b6cb0; }
main { max-width:1280px; margin:0 auto; padding:20px 24px 60px; }
section { background:#fff; border:1px solid var(--line); border-radius:8px; margin-bottom:22px;
overflow:hidden; }
section > h2 { font-size:15px; margin:0; padding:12px 18px; border-bottom:1px solid var(--line);
background:#fbfcfd; display:flex; align-items:center; gap:10px; }
section > h2 .sp { flex:1; }
.bar { padding:10px 18px; display:flex; gap:10px; align-items:center; border-bottom:1px solid var(--line);
flex-wrap:wrap; }
.bar label { color:var(--sub); font-size:13px; }
.bar input, .bar select { padding:4px 8px; border:1px solid var(--line); border-radius:5px; font-size:13px; }
table { border-collapse:collapse; width:100%; font-size:13px; }
th, td { padding:7px 10px; border-bottom:1px solid var(--line); text-align:left;
white-space:nowrap; max-width:340px; overflow:hidden; text-overflow:ellipsis; }
th { color:var(--sub); font-weight:600; background:#fbfcfd; }
td input, td select { padding:3px 6px; border:1px solid var(--line); border-radius:4px;
font-size:12.5px; width:100%; }
td .num { width:72px; }
.tag { display:inline-block; padding:1px 8px; border-radius:10px; font-size:12px; }
.tag.on { background:#c6f6d5; color:var(--ok); }
.tag.off { background:#fed7d7; color:var(--err); }
.st2 { color:var(--ok); } .st4xx { color:#b7791f; } .st5xx { color:var(--err); }
.idle { color:var(--sub); padding:26px; text-align:center; }
#toast { position:fixed; left:50%; bottom:34px; transform:translateX(-50%); background:#2d3748;
color:#fff; padding:9px 20px; border-radius:8px; font-size:13px; opacity:0;
transition:opacity .25s; pointer-events:none; max-width:70vw; }
#toast.show { opacity:.96; }
#toast.err { background:var(--err); }
</style>
</head>
<body>
<header>
<h1>网关治理</h1>
<span class="sp"></span>
<span id="stat" style="color:var(--sub);font-size:12.5px"></span>
<button class="primary" onclick="reloadRegistry()">重载注册表</button>
</header>
<main>
<section>
<h2>路由治理旋钮 <span class="sp"></span>
<span style="color:var(--sub);font-weight:400;font-size:12px">值域 visibility=INTERNAL/EXTERNAL · authMode=SIGNED/ANONYMOUS · cacheScope=CALLER/GLOBAL</span></h2>
<div class="tableWrap"><table id="routeT">
<thead><tr>
<th>id</th><th>target</th><th>path</th><th>method</th>
<th style="width:110px">visibility</th><th style="width:120px">authMode</th>
<th style="width:80px">cacheTtl</th><th style="width:100px">cacheScope</th>
<th style="width:80px">qps</th><th>启用</th><th>备注</th><th></th>
</tr></thead>
<tbody></tbody>
</table></div>
</section>
<section>
<h2>目标地址</h2>
<div class="tableWrap"><table id="tgtT">
<thead><tr><th>id</th><th>target</th><th>base_url</th><th>启用</th><th>更新时间</th><th></th></tr></thead>
<tbody></tbody>
</table></div>
</section>
<section>
<h2>访问台账</h2>
<div class="bar">
<label>条数</label><input id="qLimit" type="number" value="50" min="1" max="500" style="width:76px">
<label>target</label><input id="qTarget" placeholder="如 notice" style="width:110px">
<label>caller</label><input id="qCaller" placeholder="如 100001" style="width:110px">
<button onclick="loadLogs()">查询</button>
<span id="logTotal" style="color:var(--sub);font-size:12px"></span>
</div>
<div class="tableWrap"><table id="logT">
<thead><tr><th>id</th><th>时间</th><th>caller</th><th>target</th><th>method</th>
<th>path</th><th>status</th><th>backend</th><th>cost(ms)</th></tr></thead>
<tbody></tbody>
</table></div>
</section>
</main>
<div id="toast"></div>
<script>
var $ = function(id){ return document.getElementById(id); };
function toast(msg, isErr) {
var t = $('toast');
t.textContent = msg;
t.className = 'show' + (isErr ? ' err' : '');
setTimeout(function(){ t.className = ''; }, isErr ? 4200 : 2200);
}
/* 管理面响应 = 平台信封 {code,msg,data}(yh-api-std 统一包装);成功时把 data 交给调用方 */
function api(method, url, body) {
var opt = { method: method, headers: {} };
if (body !== undefined) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
return fetch(url, opt).then(function(r) {
if (!r.ok) { throw new Error('HTTP ' + r.status); }
return r.json().then(function(j) {
if (j.code !== 0) { throw new Error(j.msg || ('业务错误 code=' + j.code)); }
return j.data;
});
});
}
function esc(s) {
return s == null ? '' : String(s).replace(/[&<>"]/g, function(c) {
return { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;' }[c];
});
}
/* ---------- 路由表 ---------- */
function loadRoutes() {
return api('GET', 'routes?pageIndex=1&pageSize=200').then(function(pg) {
var rows = (pg && pg.content) || [];
var tb = $('routeT').tBodies[0];
tb.innerHTML = rows.map(function(r) { return routeRow(r); }).join('');
});
}
function routeRow(r) {
return '<tr data-id="' + r.id + '">'
+ '<td>' + r.id + '</td><td><b>' + esc(r.targetCode) + '</b></td>'
+ '<td title="' + esc(r.pathPattern) + '">' + esc(r.pathPattern) + '</td>'
+ '<td>' + esc(r.httpMethod) + '</td>'
+ '<td><select data-k="visibility">'
+ opt('INTERNAL', r.visibility) + opt('EXTERNAL', r.visibility) + '</select></td>'
+ '<td><select data-k="authMode">'
+ opt('SIGNED', r.authMode) + opt('ANONYMOUS', r.authMode) + '</select></td>'
+ '<td><input class="num" type="number" min="0" data-k="cacheTtlSeconds" value="' + r.cacheTtlSeconds + '"></td>'
+ '<td><select data-k="cacheScope">'
+ opt('CALLER', r.cacheScope) + opt('GLOBAL', r.cacheScope) + '</select></td>'
+ '<td><input class="num" type="number" min="0" data-k="rateLimitQps" value="' + (r.rateLimitQps || 0) + '"></td>'
+ '<td><input type="checkbox" data-k="enabled"' + (r.enabled ? ' checked' : '') + '></td>'
+ '<td><input data-k="remark" value="' + esc(r.remark || '') + '" placeholder="-"></td>'
+ '<td><button onclick="saveRoute(this)">保存</button></td>'
+ '</tr>';
}
function opt(v, cur) {
return '<option value="' + v + '"' + (v === cur ? ' selected' : '') + '>' + v + '</option>';
}
function saveRoute(btn) {
var tr = btn.closest('tr');
var body = {};
tr.querySelectorAll('[data-k]').forEach(function(el) {
var k = el.getAttribute('data-k');
if (el.type === 'checkbox') { body[k] = el.checked; }
else if (el.type === 'number') { body[k] = parseInt(el.value || '0', 10); }
else { body[k] = el.value; }
});
api('PUT', 'routes/' + tr.dataset.id, body)
.then(function(d) { toast('已保存,注册表加载 ' + d.loaded + ' 条路由'); })
.catch(function(e) { toast('保存失败:' + e.message, true); });
}
/* ---------- 目标表 ---------- */
function loadTargets() {
return api('GET', 'targets?pageIndex=1&pageSize=200').then(function(pg) {
var rows = (pg && pg.content) || [];
var tb = $('tgtT').tBodies[0];
tb.innerHTML = rows.map(function(t) {
return '<tr data-id="' + t.id + '">'
+ '<td>' + t.id + '</td><td><b>' + esc(t.targetCode) + '</b></td>'
+ '<td><input data-k="baseUrl" value="' + esc(t.baseUrl) + '" style="width:300px"></td>'
+ '<td><input type="checkbox" data-k="enabled"' + (t.enabled ? ' checked' : '') + '></td>'
+ '<td>' + esc((t.gmtModified || '').replace('T', ' ').slice(0, 19)) + '</td>'
+ '<td><button onclick="saveTarget(this)">保存</button></td>'
+ '</tr>';
}).join('');
});
}
function saveTarget(btn) {
var tr = btn.closest('tr');
var body = {};
tr.querySelectorAll('[data-k]').forEach(function(el) {
var k = el.getAttribute('data-k');
body[k] = el.type === 'checkbox' ? el.checked : el.value;
});
api('PUT', 'targets/' + tr.dataset.id, body)
.then(function(d) { toast('已保存,目标表加载 ' + d.targets + ' 条'); })
.catch(function(e) { toast('保存失败:' + e.message, true); });
}
/* ---------- 台账 ---------- */
function loadLogs() {
var size = parseInt($('qLimit').value || '50', 10) || 50;
var q = 'logs?pageIndex=1&pageSize=' + size;
if ($('qTarget').value.trim()) q += '&targetCode=' + encodeURIComponent($('qTarget').value.trim());
if ($('qCaller').value.trim()) q += '&callerAppKey=' + encodeURIComponent($('qCaller').value.trim());
return api('GET', q).then(function(pg) {
$('logTotal').textContent = '共 ' + pg.total + ' 条';
var rows = (pg && pg.content) || [];
var tb = $('logT').tBodies[0];
if (!rows.length) {
tb.innerHTML = '<tr><td colspan="9" class="idle">无匹配记录</td></tr>';
return;
}
tb.innerHTML = rows.map(function(l) {
var st = l.status;
var cls = st >= 500 ? 'st5xx' : (st >= 400 ? 'st4xx' : 'st2');
return '<tr>'
+ '<td>' + l.id + '</td>'
+ '<td>' + esc((l.ts || '').replace('T', ' ').slice(0, 23)) + '</td>'
+ '<td>' + esc(l.callerAppKey == null ? '-' : l.callerAppKey) + '</td>'
+ '<td>' + esc(l.targetCode) + '</td><td>' + esc(l.method) + '</td>'
+ '<td title="' + esc(l.path) + '">' + esc(l.path) + '</td>'
+ '<td class="' + cls + '">' + st + '</td>'
+ '<td>' + (l.backendCode == null ? '-' : l.backendCode) + '</td>'
+ '<td>' + l.costMs + '</td></tr>';
}).join('');
});
}
/* ---------- 顶部 ---------- */
function reloadRegistry() {
api('POST', '../routes/refresh')
.then(function(d) { toast('注册表已重载:路由 ' + d.loaded + ' 条 / 目标 ' + d.targets + ' 条'); })
.catch(function(e) { toast('重载失败:' + e.message, true); });
}
function refreshAll() {
Promise.all([loadRoutes(), loadTargets(), loadLogs()])
.then(function() { $('stat').textContent = '更新于 ' + new Date().toLocaleTimeString(); })
.catch(function(e) { toast('加载失败:' + e.message, true); });
}
refreshAll();
</script>
</body>
</html>