build / build-push (push) Successful in 1m59s
- 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 项测试全绿
253 lines
11 KiB
HTML
253 lines
11 KiB
HTML
<!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 { '&':'&', '<':'<', '>':'>', '"':'"' }[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>
|