first commit

This commit is contained in:
2026-08-13 21:24:16 +08:00
commit a306eee7d5
24 changed files with 2457 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台 - ResLib License</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="icon" href="/static/img/logo.svg" type="image/svg+xml">
<style>
.filters { display: flex; gap: 8px; flex-wrap: wrap; margin: 18px 0; }
.filter-btn {
padding: 6px 16px; border-radius: 20px; border: 1px solid var(--line);
background: #fff; color: var(--ink-light); font-size: 0.85rem; cursor: pointer; font-family: inherit;
}
.filter-btn.active { background: var(--blue); color: #fff; border-color: var(--blue); }
.empty { text-align: center; color: var(--ink-light); padding: 40px 0; }
.modal-mask {
position: fixed; inset: 0; background: rgba(15, 30, 50, 0.5);
display: none; align-items: center; justify-content: center; z-index: 200; padding: 20px;
}
.modal-mask.show { display: flex; }
.modal {
background: #fff; border-radius: 10px; width: 100%; max-width: 560px;
max-height: 90vh; overflow: auto; padding: 26px;
}
.modal h3 { color: var(--blue-dark); margin-bottom: 14px; }
.kv { display: grid; grid-template-columns: 110px 1fr; gap: 8px 12px; font-size: 0.92rem; margin-bottom: 16px; }
.kv dt { color: var(--ink-light); }
.kv dd { word-break: break-all; }
.lic-preview {
background: #0d2b4a; color: #7cc0ff; font-family: Consolas, monospace;
border-radius: 6px; padding: 12px 14px; font-size: 0.85rem; margin: 12px 0; letter-spacing: 0.05em;
}
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; flex-wrap: wrap; }
.deny-reason { width: 100%; margin-top: 10px; display: none; }
.deny-reason.show { display: block; }
</style>
</head>
<body>
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="/admin">
<img src="/static/img/logo.svg" alt="ResLib">
<span class="brand-name">ResLib License 管理后台</span>
</a>
<div>
<a href="/reslib/" class="btn btn-ghost btn-sm" style="margin-right:8px;">前台</a>
<a href="/admin/logout" class="btn btn-danger btn-sm">退出</a>
</div>
</div>
</header>
<main class="section" style="padding-top: 30px;">
<div class="container">
<div class="filters" id="filters">
<button class="filter-btn active" data-f="all">全部</button>
<button class="filter-btn" data-f="pending">待审核</button>
<button class="filter-btn" data-f="approved">已批准</button>
<button class="filter-btn" data-f="denied">已拒绝</button>
<button id="refresh" class="filter-btn" style="margin-left:auto; border-color: var(--blue); color: var(--blue);">刷新</button>
</div>
<div class="table-wrap">
<table class="admin" id="table">
<thead>
<tr>
<th>申请编号</th><th>提交时间</th><th>公司</th><th>联系人</th><th>邮箱</th>
<th>MAC</th><th>状态</th><th>操作</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="empty" id="empty" style="display:none;">暂无申请记录</div>
</div>
</main>
<div class="modal-mask" id="mask">
<div class="modal">
<h3 id="m-title">申请详情</h3>
<dl class="kv" id="m-body"></dl>
<div id="m-lic"></div>
<div class="deny-reason" id="deny-box">
<label style="font-size:0.85rem; font-weight:600;">拒绝原因(可选):</label>
<input type="text" id="deny-reason-input" style="width:100%; margin-top:6px; padding:9px 12px; border:1px solid #cfdcea; border-radius:6px;">
</div>
<div class="modal-actions">
<button class="btn btn-ghost btn-sm" id="m-close">关闭</button>
<button class="btn btn-danger btn-sm" id="m-deny" style="display:none;">拒绝</button>
<button class="btn btn-primary btn-sm" id="m-approve" style="display:none;">批准并生成 License</button>
</div>
</div>
</div>
<script>
(function () {
var tbody = document.getElementById("tbody");
var empty = document.getElementById("empty");
var filter = "all";
var items = [];
var current = null;
function badge(s) {
if (s === "approved") return '<span class="badge badge-approved">已批准</span>';
if (s === "denied") return '<span class="badge badge-denied">已拒绝</span>';
return '<span class="badge badge-pending">待审核</span>';
}
async function load() {
try {
var resp = await fetch("/api/admin/list");
if (resp.status === 401) { window.location.href = "/admin/login"; return; }
items = await resp.json();
render();
} catch (e) {
alert("加载失败:" + e.message);
}
}
function render() {
var list = filter === "all" ? items : items.filter(function (i) { return i.status === filter; });
tbody.innerHTML = "";
empty.style.display = list.length ? "none" : "block";
list.forEach(function (i) {
var tr = document.createElement("tr");
tr.innerHTML =
'<td class="mono">' + i.id + "</td>" +
"<td>" + i.created + "</td>" +
"<td>" + esc(i.company) + "</td>" +
"<td>" + esc(i.contact) + "</td>" +
"<td>" + esc(i.email) + "</td>" +
'<td class="mono">' + esc(i.mac) + "</td>" +
"<td>" + badge(i.status) + "</td>" +
'<td class="actions"><button class="btn btn-ghost btn-sm" data-view="' + i.id + '">详情</button></td>';
tbody.appendChild(tr);
});
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function showDetail(id) {
current = items.find(function (i) { return i.id === id; });
if (!current) return;
document.getElementById("m-body").innerHTML =
"<dt>申请编号</dt><dd class='mono'>" + current.id + "</dd>" +
"<dt>提交时间</dt><dd>" + current.created + "</dd>" +
"<dt>状态</dt><dd>" + badge(current.status) + "</dd>" +
"<dt>MAC 地址</dt><dd class='mono'>" + esc(current.mac) + "</dd>" +
"<dt>公司</dt><dd>" + esc(current.company) + "</dd>" +
"<dt>联系人</dt><dd>" + esc(current.contact) + "</dd>" +
"<dt>邮箱</dt><dd>" + esc(current.email) + "</dd>" +
"<dt>Creo 版本</dt><dd>" + esc(current.creo) + "</dd>" +
"<dt>授权套数</dt><dd>" + esc(current.seats) + "</dd>" +
"<dt>备注</dt><dd>" + esc(current.notes) || "(无)" + "</dd>" +
(current.reason ? "<dt>拒绝原因</dt><dd>" + esc(current.reason) + "</dd>" : "");
var licBox = document.getElementById("m-lic");
licBox.innerHTML = "";
if (current.status === "approved") {
licBox.innerHTML =
"<b style='font-size:0.88rem;'>已生成授权文件:</b>" +
"<div class='lic-preview' id='lic-content'></div>" +
"<a class='btn btn-primary btn-sm' href='/api/license/" + encodeURIComponent(current.id) + "' download>下载 res_center.lic</a>";
fetch("/api/license/" + encodeURIComponent(current.id)).then(function (r) { return r.text(); }).then(function (t) {
var el = document.getElementById("lic-content");
if (el) el.textContent = t.trim();
}).catch(function () {});
}
document.getElementById("m-approve").style.display =
current.status === "pending" ? "" : "none";
document.getElementById("m-deny").style.display =
current.status === "pending" ? "" : "none";
document.getElementById("deny-box").classList.remove("show");
document.getElementById("deny-reason-input").value = "";
document.getElementById("mask").classList.add("show");
}
document.getElementById("filters").addEventListener("click", function (e) {
var b = e.target.closest(".filter-btn");
if (!b || b.id === "refresh") return;
document.querySelectorAll(".filter-btn").forEach(function (x) { x.classList.remove("active"); });
b.classList.add("active");
filter = b.dataset.f;
render();
});
document.getElementById("refresh").addEventListener("click", load);
tbody.addEventListener("click", function (e) {
var b = e.target.closest("[data-view]");
if (b) showDetail(b.dataset.view);
});
document.getElementById("m-close").addEventListener("click", function () {
document.getElementById("mask").classList.remove("show");
});
document.getElementById("mask").addEventListener("click", function (e) {
if (e.target === this) this.classList.remove("show");
});
document.getElementById("m-approve").addEventListener("click", async function () {
if (!confirm("确认批准该申请并生成 License?")) return;
var resp = await fetch("/api/admin/approve/" + encodeURIComponent(current.id), { method: "POST" });
if (resp.ok) { closeAndReload(); } else { alert("操作失败"); }
});
document.getElementById("m-deny").addEventListener("click", async function () {
if (!confirm("确认拒绝该申请?")) return;
var reason = document.getElementById("deny-reason-input").value;
var resp = await fetch("/api/admin/deny/" + encodeURIComponent(current.id), {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "reason=" + encodeURIComponent(reason)
});
if (resp.ok) { closeAndReload(); } else { alert("操作失败"); }
});
function closeAndReload() {
document.getElementById("mask").classList.remove("show");
load();
}
load();
})();
</script>
</body>
</html>