u
This commit is contained in:
1 parent
7bd5f1dde9
commit
f3936cec61
70 files changed
+13074
-187
No files matched your search
@@ -0,0 +1,414 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>ERP 日志分析</title>
|
||||
<style>
|
||||
:root { --bd:#dfe3e8; --mut:#6b7280; --bg:#f7f8fa; --err:#c0392b; --warn:#b7791f; --ok:#2f855a; --hi:#2563eb; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font:14px/1.5 "Microsoft YaHei",system-ui,Segoe UI,Arial,sans-serif; color:#1f2937; background:var(--bg); }
|
||||
header { position:sticky; top:0; z-index:10; background:#fff; border-bottom:1px solid var(--bd); padding:14px 18px; }
|
||||
h1 { margin:0 0 4px; font-size:18px; }
|
||||
h2 { margin:22px 0 8px; font-size:15px; border-left:4px solid var(--hi); padding-left:8px; }
|
||||
.hint { margin:0 0 10px; color:var(--mut); font-size:12px; }
|
||||
.controls { display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
|
||||
.controls label { color:var(--mut); font-size:13px; }
|
||||
.btn { border:1px solid var(--bd); background:#fff; padding:6px 12px; border-radius:6px; cursor:pointer; font-size:13px; }
|
||||
.btn:hover { border-color:var(--hi); color:var(--hi); }
|
||||
input[type=number] { width:70px; padding:4px 6px; border:1px solid var(--bd); border-radius:4px; }
|
||||
input[type=text] { padding:6px 8px; border:1px solid var(--bd); border-radius:4px; width:260px; }
|
||||
#drop { margin:12px 18px 0; border:2px dashed var(--bd); border-radius:10px; padding:18px; text-align:center; color:var(--mut); background:#fff; }
|
||||
#drop.on { border-color:var(--hi); background:#eef4ff; color:var(--hi); }
|
||||
main { padding:0 18px 40px; }
|
||||
#status { color:var(--mut); font-size:13px; }
|
||||
.cards { display:flex; flex-wrap:wrap; gap:10px; margin-top:14px; }
|
||||
.card { flex:1 1 140px; background:#fff; border:1px solid var(--bd); border-radius:8px; padding:10px 12px; }
|
||||
.card .n { font-size:20px; font-weight:600; }
|
||||
.card .t { color:var(--mut); font-size:12px; }
|
||||
.card.bad .n { color:var(--err); }
|
||||
.card.warn .n { color:var(--warn); }
|
||||
table { border-collapse:collapse; width:100%; background:#fff; font-size:13px; }
|
||||
th,td { border:1px solid var(--bd); padding:5px 8px; text-align:left; vertical-align:top; }
|
||||
th { background:#f1f3f5; font-weight:600; white-space:nowrap; }
|
||||
tbody tr:nth-child(even) { background:#fafbfc; }
|
||||
td.num { text-align:right; font-variant-numeric:tabular-nums; }
|
||||
.empty { color:var(--mut); font-size:13px; padding:8px 0; }
|
||||
.tag { display:inline-block; padding:0 6px; border-radius:10px; font-size:12px; border:1px solid var(--bd); }
|
||||
.lvl-ERROR { color:#fff; background:var(--err); border-color:var(--err); }
|
||||
.lvl-WARN { color:#fff; background:var(--warn); border-color:var(--warn); }
|
||||
.lvl-INFO { color:#fff; background:var(--ok); border-color:var(--ok); }
|
||||
.lvl-DEBUG { color:#fff; background:#6b7280; border-color:#6b7280; }
|
||||
.lnk { color:var(--hi); cursor:pointer; text-decoration:underline; word-break:break-all; }
|
||||
.mono { font-family:Consolas,Menlo,monospace; }
|
||||
.params { max-width:460px; word-break:break-all; font-family:Consolas,Menlo,monospace; font-size:12px; color:#334155; }
|
||||
.st { white-space:pre-wrap; font-family:Consolas,Menlo,monospace; font-size:12px; color:#7f1d1d; margin-top:4px; display:none; }
|
||||
details > summary { cursor:pointer; color:var(--hi); }
|
||||
.scroll { max-height:420px; overflow:auto; border:1px solid var(--bd); border-radius:6px; }
|
||||
.scroll table { border:0; }
|
||||
footer { color:var(--mut); font-size:12px; padding:8px 18px 24px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>ERP 日志分析</h1>
|
||||
<p class="hint">双击本文件用浏览器打开(推荐 Edge / Chrome)→ 选择日志文件或整个 logs 文件夹,也可以直接把文件拖进来。</p>
|
||||
<div class="controls">
|
||||
<input type="file" id="pickFiles" multiple hidden>
|
||||
<input type="file" id="pickDir" webkitdirectory hidden>
|
||||
<button class="btn" id="btnFiles">选择日志文件</button>
|
||||
<button class="btn" id="btnDir">选择 logs 文件夹</button>
|
||||
<label>慢请求阈值 <input type="number" id="slowMs" value="1000" min="0" step="100"> ms</label>
|
||||
<label>Top <input type="number" id="topN" value="50" min="1" step="10"></label>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="drop">把 <span class="mono">.jsonl</span> / <span class="mono">.jsonl.gz</span> 日志文件或整个 logs 文件夹拖到这里</div>
|
||||
<main id="out"></main>
|
||||
<footer>数据只在浏览器本地解析,不会上传任何地方。</footer>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
var rows = []; // 解析后的日志对象
|
||||
var badLines = 0; // 解析失败的行数
|
||||
var fileCount = 0; // 读取的文件数
|
||||
var traceQuery = ""; // 当前 traceId 查询
|
||||
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
|
||||
/* ---------- 读取文件(含 .gz 解压) ---------- */
|
||||
function readAsText(file) {
|
||||
var stream = file.stream();
|
||||
if (/\.gz$/i.test(file.name)) {
|
||||
if (typeof DecompressionStream === "undefined") {
|
||||
return Promise.reject(new Error("当前浏览器不支持 gzip 解压,请用最新版 Edge/Chrome"));
|
||||
}
|
||||
stream = stream.pipeThrough(new DecompressionStream("gzip"));
|
||||
}
|
||||
return new Response(stream).text();
|
||||
}
|
||||
|
||||
function isLogFile(name) {
|
||||
return /\.(jsonl|ndjson|gz|log|txt)$/i.test(name);
|
||||
}
|
||||
|
||||
async function load(files) {
|
||||
var list = Array.prototype.slice.call(files).filter(function (f) { return isLogFile(f.name); });
|
||||
var skipped = files.length - list.length;
|
||||
rows = []; badLines = 0; fileCount = 0; traceQuery = "";
|
||||
var problems = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var f = list[i];
|
||||
try {
|
||||
var text = await readAsText(f);
|
||||
ingest(text);
|
||||
fileCount++;
|
||||
} catch (e) {
|
||||
problems.push(f.name + ": " + e.message);
|
||||
}
|
||||
}
|
||||
var msg = "已读取 " + fileCount + " 个文件," + rows.length + " 条日志";
|
||||
if (badLines) msg += "(" + badLines + " 行无法解析已跳过)";
|
||||
if (skipped) msg += ";忽略 " + skipped + " 个非日志文件";
|
||||
if (problems.length) msg += ";失败: " + problems.join(";");
|
||||
$("status").textContent = msg;
|
||||
render();
|
||||
}
|
||||
|
||||
function ingest(text) {
|
||||
var lines = text.split(/\r?\n/);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var s = lines[i].trim();
|
||||
if (!s) continue;
|
||||
try { rows.push(JSON.parse(s)); } catch (e) { badLines++; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 小工具 ---------- */
|
||||
function esc(v) {
|
||||
if (v === null || v === undefined) return "";
|
||||
return String(v).replace(/[&<>"]/g, function (c) {
|
||||
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
|
||||
});
|
||||
}
|
||||
function groupCount(arr, keyFn) {
|
||||
var m = new Map();
|
||||
arr.forEach(function (r) {
|
||||
var k = keyFn(r);
|
||||
if (k === null || k === undefined || k === "") k = "(空)";
|
||||
m.set(k, (m.get(k) || 0) + 1);
|
||||
});
|
||||
return Array.from(m.entries()).sort(function (a, b) { return b[1] - a[1]; });
|
||||
}
|
||||
function isAccess(r) { return r.logger_name === "ACCESS"; }
|
||||
function slowLimit() { var v = parseInt($("slowMs").value, 10); return isNaN(v) ? 1000 : v; }
|
||||
function topLimit() { var v = parseInt($("topN").value, 10); return isNaN(v) ? 50 : v; }
|
||||
function traceLink(tid) { return '<span class="lnk mono" data-trace="' + esc(tid) + '">' + esc(tid) + '</span>'; }
|
||||
|
||||
function tableHTML(headers, bodyRows) {
|
||||
var h = "<table><thead><tr>";
|
||||
headers.forEach(function (x) { h += "<th>" + x + "</th>"; });
|
||||
h += "</tr></thead><tbody>";
|
||||
if (!bodyRows.length) {
|
||||
h += '<tr><td colspan="' + headers.length + '" class="empty">无数据</td></tr>';
|
||||
} else {
|
||||
bodyRows.forEach(function (r) {
|
||||
h += "<tr>";
|
||||
r.forEach(function (c) { h += "<td>" + c + "</td>"; });
|
||||
h += "</tr>";
|
||||
});
|
||||
}
|
||||
return h + "</tbody></table>";
|
||||
}
|
||||
|
||||
/* ---------- 渲染 ---------- */
|
||||
function render() {
|
||||
var out = $("out");
|
||||
if (!rows.length) {
|
||||
out.innerHTML = '<h2>概览</h2><div class="empty">还没有数据。请选择或拖入日志文件。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var access = rows.filter(isAccess);
|
||||
var errors = rows.filter(function (r) { return r.level === "ERROR"; });
|
||||
var warns = rows.filter(function (r) { return r.level === "WARN"; });
|
||||
var lim = slowLimit();
|
||||
var slow = access.filter(function (r) { return (Number(r.durationMs) || 0) > lim; })
|
||||
.sort(function (a, b) { return (Number(b.durationMs) || 0) - (Number(a.durationMs) || 0); });
|
||||
|
||||
var times = rows.map(function (r) { return r["@timestamp"] || ""; }).filter(Boolean).sort();
|
||||
var tMin = times.length ? times[0].replace("T", " ").slice(0, 19) : "-";
|
||||
var tMax = times.length ? times[times.length - 1].replace("T", " ").slice(0, 19) : "-";
|
||||
|
||||
var durs = access.map(function (r) { return Number(r.durationMs) || 0; });
|
||||
var avg = durs.length ? Math.round(durs.reduce(function (a, b) { return a + b; }, 0) / durs.length) : 0;
|
||||
var maxD = durs.length ? Math.max.apply(null, durs) : 0;
|
||||
|
||||
var html = "";
|
||||
|
||||
/* 概览卡片 */
|
||||
html += '<h2>概览</h2><div class="cards">'
|
||||
+ card(rows.length, "日志条数")
|
||||
+ card(access.length, "请求数(ACCESS)")
|
||||
+ card(errors.length, "错误 ERROR", errors.length ? "bad" : "")
|
||||
+ card(warns.length, "警告 WARN", warns.length ? "warn" : "")
|
||||
+ card(slow.length, "慢请求 > " + lim + "ms", slow.length ? "warn" : "")
|
||||
+ card(avg + " ms", "平均耗时")
|
||||
+ card(maxD + " ms", "最大耗时")
|
||||
+ "</div>"
|
||||
+ '<div class="hint" style="margin-top:8px">时间范围:' + esc(tMin) + " ~ " + esc(tMax) + "</div>";
|
||||
|
||||
/* 错误 */
|
||||
html += "<h2>错误(ERROR)</h2>";
|
||||
if (!errors.length) {
|
||||
html += '<div class="empty">没有 ERROR 级别日志。</div>';
|
||||
} else {
|
||||
var er = errors.slice().sort(function (a, b) { return (a["@timestamp"] || "").localeCompare(b["@timestamp"] || ""); });
|
||||
var body = er.slice(0, topLimit()).map(function (r) {
|
||||
var st = r.stack_trace ? '<details><summary>堆栈</summary><div class="st">' + esc(r.stack_trace) + "</div></details>" : "";
|
||||
return [
|
||||
'<span class="tag lvl-ERROR">ERROR</span>',
|
||||
'<span class="mono">' + esc((r["@timestamp"] || "").replace("T", " ").slice(0, 19)) + "</span>",
|
||||
esc(r.uri || r.method || ""),
|
||||
esc(r.message || ""),
|
||||
r.params ? '<span class="params">' + esc(r.params) + "</span>" : "",
|
||||
r.traceId ? traceLink(r.traceId) : "",
|
||||
st
|
||||
];
|
||||
});
|
||||
html += '<div class="scroll">' + tableHTML(["级别", "时间", "接口", "消息", "参数", "traceId", "详情"], body) + "</div>";
|
||||
if (er.length > topLimit()) html += '<div class="hint">仅显示前 ' + topLimit() + " 条,共 " + er.length + " 条。</div>";
|
||||
}
|
||||
|
||||
/* 慢请求 */
|
||||
html += "<h2>慢请求(耗时 > " + lim + " ms)</h2>";
|
||||
if (!slow.length) {
|
||||
html += '<div class="empty">没有超过阈值的请求。</div>';
|
||||
} else {
|
||||
var sbody = slow.slice(0, topLimit()).map(function (r) {
|
||||
return [
|
||||
'<span class="mono">' + esc((r["@timestamp"] || "").replace("T", " ").slice(0, 19)) + "</span>",
|
||||
esc(r.method || ""),
|
||||
esc(r.uri || ""),
|
||||
'<span class="num">' + esc(r.durationMs) + "</span>",
|
||||
esc(r.orgId || "anonymous"),
|
||||
esc(r.userId || ""),
|
||||
r.params ? '<span class="params">' + esc(r.params) + "</span>" : "",
|
||||
r.traceId ? traceLink(r.traceId) : "",
|
||||
esc(r.clientIp || "")
|
||||
];
|
||||
});
|
||||
html += '<div class="scroll">' + tableHTML(["时间", "方法", "接口", "耗时(ms)", "机构", "用户", "参数", "traceId", "客户端IP"], sbody) + "</div>";
|
||||
if (slow.length > topLimit()) html += '<div class="hint">仅显示最慢的 ' + topLimit() + " 条,共 " + slow.length + " 条。</div>";
|
||||
}
|
||||
|
||||
/* 按机构 */
|
||||
html += "<h2>按机构统计(请求数)</h2>";
|
||||
var orgs = groupCount(access, function (r) { return r.orgId || "anonymous"; });
|
||||
html += '<div class="scroll">' + tableHTML(["机构", "请求数"], orgs.slice(0, topLimit() + 20).map(function (e) {
|
||||
return [esc(e[0]), '<span class="num">' + e[1] + "</span>"];
|
||||
})) + "</div>";
|
||||
|
||||
/* 按接口 */
|
||||
html += "<h2>按接口统计(请求数 / 最大耗时)</h2>";
|
||||
var uriMap = new Map();
|
||||
access.forEach(function (r) {
|
||||
var k = r.uri || "(空)";
|
||||
var o = uriMap.get(k) || { n: 0, max: 0, sum: 0 };
|
||||
o.n++; var d = Number(r.durationMs) || 0; o.max = Math.max(o.max, d); o.sum += d;
|
||||
uriMap.set(k, o);
|
||||
});
|
||||
var uriRows = Array.from(uriMap.entries()).sort(function (a, b) { return b[1].max - a[1].max; });
|
||||
html += '<div class="scroll">' + tableHTML(["接口", "请求数", "最大耗时(ms)", "平均耗时(ms)"], uriRows.slice(0, topLimit() + 20).map(function (e) {
|
||||
return [esc(e[0]), '<span class="num">' + e[1].n + "</span>", '<span class="num">' + e[1].max + "</span>", '<span class="num">' + Math.round(e[1].sum / e[1].n) + "</span>"];
|
||||
})) + "</div>";
|
||||
|
||||
/* 状态码 */
|
||||
html += "<h2>状态码分布</h2>";
|
||||
var st = groupCount(access, function (r) { return r.status; });
|
||||
html += '<div class="scroll">' + tableHTML(["状态码", "次数"], st.map(function (e) {
|
||||
return [esc(e[0]), '<span class="num">' + e[1] + "</span>"];
|
||||
})) + "</div>";
|
||||
|
||||
/* 按小时 */
|
||||
html += "<h2>按小时统计</h2>";
|
||||
var hMap = new Map();
|
||||
rows.forEach(function (r) {
|
||||
var ts = r["@timestamp"] || "";
|
||||
var hh = ts.slice(0, 13);
|
||||
if (!hh) return;
|
||||
var o = hMap.get(hh) || { total: 0, err: 0 };
|
||||
o.total++;
|
||||
if (r.level === "ERROR") o.err++;
|
||||
hMap.set(hh, o);
|
||||
});
|
||||
var hRows = Array.from(hMap.entries()).sort(function (a, b) { return a[0].localeCompare(b[0]); });
|
||||
html += '<div class="scroll">' + tableHTML(["小时", "日志数", "其中 ERROR"], hRows.map(function (e) {
|
||||
return [esc(e[0].replace("T", " ")), '<span class="num">' + e[1].total + "</span>", '<span class="num">' + e[1].err + "</span>"];
|
||||
})) + "</div>";
|
||||
|
||||
/* traceId 查询 */
|
||||
html += '<h2>按 traceId 追踪</h2><div class="controls" style="margin-bottom:8px">'
|
||||
+ '<input type="text" id="traceInput" placeholder="输入 traceId 回车,或点击上方链接" value="' + esc(traceQuery) + '">'
|
||||
+ '<button class="btn" id="btnTrace">查询</button>'
|
||||
+ '<button class="btn" id="btnTraceClear">清除</button></div>';
|
||||
html += '<div id="traceOut">' + renderTrace() + "</div>";
|
||||
|
||||
out.innerHTML = html;
|
||||
|
||||
/* 绑定事件 */
|
||||
document.querySelectorAll("[data-trace]").forEach(function (el) {
|
||||
el.addEventListener("click", function () {
|
||||
traceQuery = el.getAttribute("data-trace");
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
render();
|
||||
var inp = $("traceInput"); if (inp) inp.focus();
|
||||
});
|
||||
});
|
||||
var ti = $("traceInput");
|
||||
if (ti) {
|
||||
ti.addEventListener("keydown", function (e) { if (e.key === "Enter") { traceQuery = ti.value.trim(); render(); } });
|
||||
}
|
||||
var bt = $("btnTrace");
|
||||
if (bt) bt.addEventListener("click", function () { traceQuery = ($("traceInput").value || "").trim(); render(); });
|
||||
var bc = $("btnTraceClear");
|
||||
if (bc) bc.addEventListener("click", function () { traceQuery = ""; render(); });
|
||||
}
|
||||
|
||||
function card(n, t, cls) {
|
||||
return '<div class="card ' + (cls || "") + '"><div class="n">' + esc(n) + '</div><div class="t">' + esc(t) + "</div></div>";
|
||||
}
|
||||
|
||||
function renderTrace() {
|
||||
if (!traceQuery) return '<div class="empty">输入 traceId 后,会列出该次请求的全链路日志(访问行 + 业务日志 + 异常)。</div>';
|
||||
var hit = rows.filter(function (r) { return r.traceId === traceQuery; })
|
||||
.sort(function (a, b) { return (a["@timestamp"] || "").localeCompare(b["@timestamp"] || ""); });
|
||||
if (!hit.length) return '<div class="empty">没有找到 traceId = ' + esc(traceQuery) + " 的日志。</div>";
|
||||
var body = hit.map(function (r) {
|
||||
var lvl = r.level || "";
|
||||
var detail = r.stack_trace ? '<details><summary>堆栈</summary><div class="st">' + esc(r.stack_trace) + "</div></details>" : "";
|
||||
var extra = [];
|
||||
if (r.method || r.uri) extra.push((r.method || "") + " " + (r.uri || ""));
|
||||
if (r.status !== undefined) extra.push("status=" + r.status);
|
||||
if (r.durationMs !== undefined) extra.push("耗时=" + r.durationMs + "ms");
|
||||
if (r.orgId) extra.push("机构=" + r.orgId);
|
||||
if (r.userId) extra.push("用户=" + r.userId);
|
||||
if (r.clientIp) extra.push("IP=" + r.clientIp);
|
||||
if (r.params) extra.push("参数=" + r.params);
|
||||
return [
|
||||
'<span class="tag lvl-' + esc(lvl) + '">' + esc(lvl) + "</span>",
|
||||
'<span class="mono">' + esc((r["@timestamp"] || "").replace("T", " ").slice(0, 23)) + "</span>",
|
||||
esc(r.logger_name || ""),
|
||||
esc(r.message || ""),
|
||||
esc(extra.join(",")) + detail
|
||||
];
|
||||
});
|
||||
return '<div class="hint">共 ' + hit.length + " 条</div><div class=\"scroll\">"
|
||||
+ tableHTML(["级别", "时间", "来源", "消息", "上下文"], body) + "</div>";
|
||||
}
|
||||
|
||||
/* ---------- 事件绑定 ---------- */
|
||||
$("btnFiles").addEventListener("click", function () { $("pickFiles").click(); });
|
||||
$("btnDir").addEventListener("click", function () { $("pickDir").click(); });
|
||||
$("pickFiles").addEventListener("change", function (e) { if (e.target.files.length) load(e.target.files); });
|
||||
$("pickDir").addEventListener("change", function (e) { if (e.target.files.length) load(e.target.files); });
|
||||
$("slowMs").addEventListener("change", render);
|
||||
$("topN").addEventListener("change", render);
|
||||
|
||||
var drop = $("drop");
|
||||
["dragenter", "dragover"].forEach(function (ev) {
|
||||
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add("on"); });
|
||||
});
|
||||
["dragleave", "drop"].forEach(function (ev) {
|
||||
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove("on"); });
|
||||
});
|
||||
drop.addEventListener("drop", async function (e) {
|
||||
var files = await collectDropped(e.dataTransfer);
|
||||
if (files.length) load(files);
|
||||
});
|
||||
|
||||
/* 支持拖入文件夹(递归收集) */
|
||||
async function collectDropped(dt) {
|
||||
var out = [];
|
||||
var entries = [];
|
||||
if (dt.items) {
|
||||
for (var i = 0; i < dt.items.length; i++) {
|
||||
var en = dt.items[i].webkitGetAsEntry && dt.items[i].webkitGetAsEntry();
|
||||
if (en) entries.push(en);
|
||||
}
|
||||
}
|
||||
if (!entries.length) return Array.prototype.slice.call(dt.files);
|
||||
for (var j = 0; j < entries.length; j++) await walkEntry(entries[j], out);
|
||||
return out;
|
||||
}
|
||||
function walkEntry(entry, out) {
|
||||
return new Promise(function (resolve) {
|
||||
if (entry.isFile) {
|
||||
entry.file(function (f) { out.push(f); resolve(); }, function () { resolve(); });
|
||||
} else if (entry.isDirectory) {
|
||||
var reader = entry.createReader();
|
||||
var all = [];
|
||||
(function readBatch() {
|
||||
reader.readEntries(async function (ents) {
|
||||
if (!ents.length) {
|
||||
for (var i = 0; i < all.length; i++) await walkEntry(all[i], out);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
all.push.apply(all, ents);
|
||||
readBatch();
|
||||
}, function () { resolve(); });
|
||||
})();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* 启动时给出提示 */
|
||||
$("status").textContent = "等待选择日志…";
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user