新增了客户端打包成sdk的功能和服务端docker镜像打包的功能,并修复了一些问题
This commit is contained in:
+528
-71
@@ -5,65 +5,339 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Marsco 发布控制台</title>
|
||||
<style>
|
||||
body { font-family: Arial, Helvetica, sans-serif; margin: 20px; background: #f4f6fb; color: #222; }
|
||||
h1, h2 { margin: 0 0 12px; }
|
||||
.section { background: #fff; border: 1px solid #d8dbe2; border-radius: 10px; padding: 16px; margin-bottom: 18px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||
.row label { min-width: 110px; margin: 0; }
|
||||
.row input, .row select, .row button { padding: 8px 10px; font-size: 14px; }
|
||||
.row input, .row select { min-width: 240px; border: 1px solid #c9cdd5; border-radius: 6px; }
|
||||
.row button { border: 1px solid #5a8efc; background: #5a8efc; color: #fff; border-radius: 6px; cursor: pointer; }
|
||||
.row button:hover { background: #4b7ce6; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { border: 1px solid #e1e4ea; padding: 10px; text-align: left; vertical-align: middle; }
|
||||
th { background: #f1f5fb; }
|
||||
td button { margin-right: 6px; }
|
||||
pre { background: #0f172a; color: #f8fafc; border-radius: 10px; padding: 14px; min-height: 120px; overflow-x: auto; }
|
||||
.small { font-size: 13px; color: #6b7280; }
|
||||
.wide { width: 100%; }
|
||||
|
||||
:root { --ink:#172033; --muted:#667085; --line:#e4e9f1; --paper:#fff; --brand:#3157d5; --brand-dark:#2447bc; --soft:#f4f6fa; --danger:#d92d20; --success:#067647; }
|
||||
:root {
|
||||
--bg:#f6f7fb;
|
||||
--surface:#fff;
|
||||
--ink:#172033;
|
||||
--muted:#667085;
|
||||
--line:#dfe5ee;
|
||||
--field:#fdfefe;
|
||||
--brand:#3157d5;
|
||||
--brand-hover:#2447bc;
|
||||
--danger:#d92d20;
|
||||
--success:#067647;
|
||||
--warning:#9a6700;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body { max-width:1180px; margin:0 auto 60px; padding:0 24px; font-family:Inter,"Microsoft YaHei",system-ui,-apple-system,sans-serif; color:var(--ink); background:var(--soft); font-size:14px; }
|
||||
body::before { content:""; position:absolute; inset:0 0 auto; height:190px; z-index:-1; background:linear-gradient(120deg,#121a32,#294681); }
|
||||
h1 { margin:0 -24px 28px; padding:32px 24px 55px; color:#fff; font-size:30px; letter-spacing:-.03em; }
|
||||
h1::before { content:"MARSCO UPDATE SERVICE"; display:block; margin-bottom:7px; color:#adc0ff; font-size:11px; letter-spacing:.16em; }
|
||||
h1::after { content:" · 软件发布与升级管理"; color:#d7e0ff; font-size:14px; font-weight:400; letter-spacing:0; }
|
||||
h2 { margin:0; font-size:17px; }
|
||||
.section { border:1px solid var(--line); border-radius:16px; padding:22px; margin-bottom:18px; box-shadow:0 8px 28px rgba(24,36,61,.06); }
|
||||
.section h2 { padding-bottom:14px; margin-bottom:17px; border-bottom:1px solid #edf0f4; }
|
||||
.row { gap:12px; margin-top:12px; align-items:flex-end; }
|
||||
.row label { min-width:auto; color:#344054; font-size:13px; font-weight:650; }
|
||||
.row input,.row select { min-width:220px; padding:10px 12px; border:1px solid #d0d5dd; border-radius:9px; outline:none; transition:.18s ease; }
|
||||
.row input:focus,.row select:focus { border-color:#7893e8; box-shadow:0 0 0 3px rgba(49,87,213,.12); }
|
||||
.row button,td button { padding:9px 13px; border:1px solid #d0d5dd; border-radius:9px; background:#fff; color:#344054; font-weight:650; cursor:pointer; transition:.16s ease; }
|
||||
.row button:hover,td button:hover { transform:translateY(-1px); border-color:#98a2b3; box-shadow:0 3px 9px rgba(16,24,40,.08); }
|
||||
#saveTokenBtn,#addAppBtn,#publishBtn { color:#fff; border-color:var(--brand); background:var(--brand); }
|
||||
#saveTokenBtn:hover,#addAppBtn:hover,#publishBtn:hover { background:var(--brand-dark); }
|
||||
button:disabled { opacity:.55; cursor:not-allowed; transform:none!important; }
|
||||
input[type=file] { padding:8px; background:#fafbfc; }
|
||||
input[type=file]::file-selector-button { border:0; border-radius:7px; padding:7px 11px; margin-right:10px; color:#344054; background:#e9edf5; cursor:pointer; }
|
||||
table { margin-top:14px; border-collapse:separate; border-spacing:0; border:1px solid var(--line); border-radius:12px; overflow:hidden; }
|
||||
th,td { padding:12px 14px; border:0; border-bottom:1px solid #edf0f4; }
|
||||
th { color:#475467; background:#f8f9fb; font-size:12px; letter-spacing:.02em; }
|
||||
html { background:var(--bg); }
|
||||
body {
|
||||
max-width:1280px;
|
||||
margin:0 auto;
|
||||
padding:0 24px 48px;
|
||||
color:var(--ink);
|
||||
background:var(--bg);
|
||||
font:14px/1.45 Inter,"Microsoft YaHei",system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin:0 -24px 22px;
|
||||
padding:24px;
|
||||
color:var(--ink);
|
||||
background:var(--surface);
|
||||
border-bottom:1px solid var(--line);
|
||||
box-shadow:0 1px 2px rgba(16,24,40,.04);
|
||||
font-size:26px;
|
||||
line-height:1.2;
|
||||
letter-spacing:0;
|
||||
}
|
||||
h1::before {
|
||||
content:"MARSCO UPDATE SERVICE";
|
||||
display:block;
|
||||
margin-bottom:6px;
|
||||
color:#5d6b82;
|
||||
font-size:11px;
|
||||
font-weight:700;
|
||||
letter-spacing:.14em;
|
||||
}
|
||||
h1::after {
|
||||
content:" · 软件发布与升级管理";
|
||||
color:var(--muted);
|
||||
font-size:14px;
|
||||
font-weight:400;
|
||||
}
|
||||
h2 {
|
||||
margin:0 0 16px;
|
||||
padding-bottom:12px;
|
||||
border-bottom:1px solid #edf0f4;
|
||||
font-size:16px;
|
||||
line-height:1.3;
|
||||
letter-spacing:0;
|
||||
}
|
||||
.section {
|
||||
margin-bottom:16px;
|
||||
padding:18px;
|
||||
overflow-x:auto;
|
||||
background:var(--surface);
|
||||
border:1px solid var(--line);
|
||||
border-radius:8px;
|
||||
box-shadow:0 1px 3px rgba(16,24,40,.06);
|
||||
}
|
||||
.row {
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
align-items:center;
|
||||
gap:10px 12px;
|
||||
margin-top:12px;
|
||||
}
|
||||
.section h2 + .row { margin-top:0; }
|
||||
.row label {
|
||||
flex:0 0 116px;
|
||||
min-width:0;
|
||||
margin:0;
|
||||
color:#344054;
|
||||
font-size:13px;
|
||||
font-weight:650;
|
||||
line-height:1.25;
|
||||
}
|
||||
.row label input[type="checkbox"] { margin:0 7px 0 0; }
|
||||
.row input,
|
||||
.row select {
|
||||
flex:1 1 220px;
|
||||
width:auto;
|
||||
min-width:180px;
|
||||
max-width:360px;
|
||||
height:38px;
|
||||
padding:8px 11px;
|
||||
color:var(--ink);
|
||||
background:var(--field);
|
||||
border:1px solid #cfd6e2;
|
||||
border-radius:6px;
|
||||
outline:none;
|
||||
transition:border-color .16s ease, box-shadow .16s ease, background .16s ease;
|
||||
}
|
||||
.row input[type="number"] { flex-basis:120px; max-width:160px; }
|
||||
.row input[type="checkbox"] {
|
||||
flex:0 0 auto;
|
||||
width:16px;
|
||||
min-width:16px;
|
||||
height:16px;
|
||||
padding:0;
|
||||
}
|
||||
.row input[type="file"] {
|
||||
flex:1 1 420px;
|
||||
max-width:none;
|
||||
min-height:38px;
|
||||
height:auto;
|
||||
padding:7px;
|
||||
background:#fafbfc;
|
||||
}
|
||||
.row input[type="file"]::file-selector-button {
|
||||
margin-right:10px;
|
||||
padding:7px 11px;
|
||||
color:#344054;
|
||||
background:#e9edf5;
|
||||
border:0;
|
||||
border-radius:6px;
|
||||
cursor:pointer;
|
||||
}
|
||||
.row input:focus,
|
||||
.row select:focus {
|
||||
border-color:#7893e8;
|
||||
background:#fff;
|
||||
box-shadow:0 0 0 3px rgba(49,87,213,.12);
|
||||
}
|
||||
.row .wide,
|
||||
.row input.wide {
|
||||
flex:1 1 540px;
|
||||
max-width:none;
|
||||
}
|
||||
.row button,
|
||||
td button {
|
||||
min-height:38px;
|
||||
padding:8px 12px;
|
||||
color:#344054;
|
||||
background:#fff;
|
||||
border:1px solid #cfd6e2;
|
||||
border-radius:6px;
|
||||
font-weight:650;
|
||||
white-space:nowrap;
|
||||
cursor:pointer;
|
||||
transition:border-color .16s ease, box-shadow .16s ease, transform .16s ease, background .16s ease;
|
||||
}
|
||||
.row button {
|
||||
flex:0 0 auto;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
height:38px;
|
||||
}
|
||||
.action-row {
|
||||
justify-content:flex-start;
|
||||
padding-left:128px;
|
||||
}
|
||||
.checkbox-label {
|
||||
flex:0 0 auto!important;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:7px;
|
||||
min-width:96px!important;
|
||||
color:#344054;
|
||||
}
|
||||
.checkbox-label input[type="checkbox"] { margin:0; }
|
||||
.row button:hover,
|
||||
td button:hover {
|
||||
border-color:#98a2b3;
|
||||
box-shadow:0 3px 9px rgba(16,24,40,.08);
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
#saveTokenBtn,
|
||||
#addAppBtn,
|
||||
#publishBtn,
|
||||
#saveChannelBtn,
|
||||
#savePolicyBtn,
|
||||
#createLicenseBtn {
|
||||
color:#fff;
|
||||
border-color:var(--brand);
|
||||
background:var(--brand);
|
||||
}
|
||||
#saveTokenBtn:hover,
|
||||
#addAppBtn:hover,
|
||||
#publishBtn:hover,
|
||||
#saveChannelBtn:hover,
|
||||
#savePolicyBtn:hover,
|
||||
#createLicenseBtn:hover { background:var(--brand-hover); }
|
||||
#changeTokenBtn,
|
||||
#confirmChangeTokenBtn {
|
||||
color:var(--brand);
|
||||
border-color:#b9c7ef;
|
||||
background:#f7f9ff;
|
||||
}
|
||||
#confirmChangeTokenBtn {
|
||||
color:#fff;
|
||||
border-color:var(--brand);
|
||||
background:var(--brand);
|
||||
}
|
||||
#toggleTokenBtn { min-width:64px; }
|
||||
button:disabled {
|
||||
opacity:.55;
|
||||
cursor:not-allowed;
|
||||
box-shadow:none!important;
|
||||
transform:none!important;
|
||||
}
|
||||
table {
|
||||
width:100%;
|
||||
min-width:720px;
|
||||
margin-top:14px;
|
||||
overflow:hidden;
|
||||
border:1px solid var(--line);
|
||||
border-radius:8px;
|
||||
border-collapse:separate;
|
||||
border-spacing:0;
|
||||
background:#fff;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding:11px 12px;
|
||||
border:0;
|
||||
border-bottom:1px solid #edf0f4;
|
||||
text-align:left;
|
||||
vertical-align:middle;
|
||||
}
|
||||
th {
|
||||
color:#475467;
|
||||
background:#f8f9fb;
|
||||
font-size:12px;
|
||||
font-weight:700;
|
||||
letter-spacing:0;
|
||||
white-space:nowrap;
|
||||
}
|
||||
td { color:#253047; }
|
||||
td:last-child { white-space:nowrap; }
|
||||
td button { margin:2px 4px 2px 0; }
|
||||
tbody tr:last-child td { border-bottom:0; }
|
||||
tbody tr:hover { background:#fafbff; }
|
||||
td button[data-action=delete] { color:var(--danger); border-color:#fecdca; background:#fff7f6; }
|
||||
.badge { display:inline-flex; border-radius:999px; padding:4px 8px; font-size:12px; font-weight:700; color:#475467; background:#f2f4f7; }
|
||||
td button[data-action="delete"] {
|
||||
color:var(--danger);
|
||||
border-color:#fecdca;
|
||||
background:#fff7f6;
|
||||
}
|
||||
.badge {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
min-height:22px;
|
||||
padding:3px 8px;
|
||||
color:#475467;
|
||||
background:#f2f4f7;
|
||||
border-radius:999px;
|
||||
font-size:12px;
|
||||
font-weight:700;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.badge.success { color:var(--success); background:#ecfdf3; }
|
||||
.badge.fail { color:var(--danger); background:#fef3f2; }
|
||||
.empty { padding:30px 12px!important; color:var(--muted); text-align:center; }
|
||||
pre { margin:0; min-height:105px; max-height:320px; padding:15px; color:#d8e4ff; background:#11182a; border-radius:11px; font:12px/1.65 "Cascadia Code",Consolas,monospace; white-space:pre-wrap; }
|
||||
.small { margin-top:10px; color:var(--muted); }
|
||||
.token-change { margin-top:16px; padding:14px; border:1px solid #dce3f1; border-radius:11px; background:#f8faff; }
|
||||
.empty {
|
||||
padding:28px 12px!important;
|
||||
color:var(--muted);
|
||||
text-align:center;
|
||||
}
|
||||
.small {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
min-height:28px;
|
||||
margin:0;
|
||||
color:var(--muted);
|
||||
font-size:13px;
|
||||
line-height:1.35;
|
||||
}
|
||||
pre {
|
||||
margin:14px 0 0;
|
||||
min-height:105px;
|
||||
max-height:320px;
|
||||
padding:14px;
|
||||
overflow:auto;
|
||||
color:#d8e4ff;
|
||||
background:#11182a;
|
||||
border-radius:8px;
|
||||
font:12px/1.65 "Cascadia Code",Consolas,monospace;
|
||||
white-space:pre-wrap;
|
||||
}
|
||||
.token-change {
|
||||
margin-top:14px;
|
||||
padding:14px;
|
||||
background:#f8faff;
|
||||
border:1px solid #dce3f1;
|
||||
border-radius:8px;
|
||||
}
|
||||
.token-change[hidden] { display:none; }
|
||||
#toggleTokenBtn { min-width:64px; }
|
||||
#changeTokenBtn { color:var(--brand); border-color:#b9c7ef; background:#f7f9ff; }
|
||||
#confirmChangeTokenBtn { color:#fff; border-color:var(--brand); background:var(--brand); }
|
||||
.toast { position:fixed; right:22px; bottom:22px; z-index:20; max-width:380px; padding:12px 15px; color:#fff; background:#1d2939; border-radius:10px; box-shadow:0 12px 32px rgba(16,24,40,.25); opacity:0; transform:translateY(12px); pointer-events:none; transition:.22s ease; }
|
||||
.toast {
|
||||
position:fixed;
|
||||
right:22px;
|
||||
bottom:22px;
|
||||
z-index:20;
|
||||
max-width:380px;
|
||||
padding:12px 15px;
|
||||
color:#fff;
|
||||
background:#1d2939;
|
||||
border-radius:8px;
|
||||
box-shadow:0 12px 32px rgba(16,24,40,.25);
|
||||
opacity:0;
|
||||
transform:translateY(12px);
|
||||
pointer-events:none;
|
||||
transition:.22s ease;
|
||||
}
|
||||
.toast.show { opacity:1; transform:translateY(0); }
|
||||
.toast.error { background:#b42318; }
|
||||
@media(max-width:760px){body{padding:0 12px}h1{margin:0 -12px 20px;padding:24px 12px 46px;font-size:24px}.section{padding:17px}.row{align-items:stretch;flex-direction:column}.row input,.row select,.row button{width:100%;min-width:0}table{display:block;overflow-x:auto;white-space:nowrap}}
|
||||
@media(max-width:820px) {
|
||||
body { padding:0 12px 36px; }
|
||||
h1 { margin:0 -12px 16px; padding:20px 12px; font-size:23px; }
|
||||
h1::after { display:block; margin-top:4px; }
|
||||
.section { padding:15px; margin-bottom:12px; }
|
||||
.row { display:grid; grid-template-columns:1fr; align-items:stretch; }
|
||||
.row label,
|
||||
.row input,
|
||||
.row select,
|
||||
.row button,
|
||||
.row .wide,
|
||||
.row input.wide {
|
||||
width:100%;
|
||||
max-width:none;
|
||||
min-width:0;
|
||||
}
|
||||
.row label { min-height:auto; }
|
||||
.action-row { padding-left:0; }
|
||||
.row input[type="checkbox"] { width:16px; min-width:16px; }
|
||||
table { min-width:760px; }
|
||||
.toast { right:12px; left:12px; max-width:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body><div id="toast" class="toast"></div>
|
||||
@@ -77,9 +351,19 @@
|
||||
<button id="saveTokenBtn" type="button">登录并保存</button>
|
||||
<button id="changeTokenBtn" type="button">更改令牌</button>
|
||||
</div>
|
||||
<div class="small">令牌默认隐藏,仅保存在当前浏览器;服务端只保存令牌哈希。</div>
|
||||
<div class="small">令牌默认隐藏,仅保存在当前浏览器;服务端按 .env 中的 ADMIN_TOKEN 校验。</div>
|
||||
<div class="small">当前连接服务端:<strong id="apiBaseLabel"></strong></div>
|
||||
<div id="tokenChangePanel" class="token-change" hidden>
|
||||
<div class="row"><label for="newAdminToken">新令牌</label><input id="newAdminToken" type="password" autocomplete="new-password" placeholder="至少 8 个字符" /><label for="confirmAdminToken">确认新令牌</label><input id="confirmAdminToken" type="password" autocomplete="new-password" placeholder="再次输入新令牌" /><button id="confirmChangeTokenBtn" type="button">确认更改</button><button id="cancelChangeTokenBtn" type="button">取消</button></div>
|
||||
<div class="row">
|
||||
<label for="newAdminToken">新令牌</label>
|
||||
<input id="newAdminToken" type="password" autocomplete="new-password" placeholder="至少 8 个字符" />
|
||||
<label for="confirmAdminToken">确认新令牌</label>
|
||||
<input id="confirmAdminToken" type="password" autocomplete="new-password" placeholder="再次输入新令牌" />
|
||||
</div>
|
||||
<div class="row action-row">
|
||||
<button id="confirmChangeTokenBtn" type="button">确认更改</button>
|
||||
<button id="cancelChangeTokenBtn" type="button">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +383,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>渠道管理</h2>
|
||||
<div class="row">
|
||||
<label for="channelCode">渠道代码</label><input id="channelCode" placeholder="例如 enterprise">
|
||||
<label for="channelName">显示名称</label><input id="channelName" placeholder="例如 企业版">
|
||||
<label for="channelOrder">排序</label><input id="channelOrder" type="number" value="100">
|
||||
<label class="checkbox-label"><input id="channelEnabled" type="checkbox" checked>启用</label>
|
||||
</div>
|
||||
<div class="row action-row"><button id="saveChannelBtn" type="button">新增或保存</button><button id="refreshChannelsBtn" type="button">刷新渠道</button></div>
|
||||
<table><thead><tr><th>代码</th><th>名称</th><th>状态</th><th>排序</th><th>操作</th></tr></thead><tbody id="channelRows"></tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>发布新版本</h2>
|
||||
<div class="row">
|
||||
@@ -107,7 +403,8 @@
|
||||
<label for="publishVersion">版本号</label>
|
||||
<input id="publishVersion" type="text" value="1.0.0" />
|
||||
<label for="publishChannel">渠道</label>
|
||||
<select id="publishChannel"><option value="stable">stable(正式版)</option><option value="preview">preview(预览版)</option><option value="dev">dev(开发版)</option></select>
|
||||
<select id="publishChannel"></select>
|
||||
<label for="publishProtocol">客户端协议</label><input id="publishProtocol" type="number" min="1" value="3" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label for="publishFiles">软件根目录</label>
|
||||
@@ -131,6 +428,7 @@
|
||||
<th>ID</th>
|
||||
<th>版本</th>
|
||||
<th>渠道</th>
|
||||
<th>协议</th>
|
||||
<th>最新</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
@@ -143,7 +441,7 @@
|
||||
<div class="section">
|
||||
<h2>版本运行策略</h2>
|
||||
<div class="row">
|
||||
<label for="policyChannel">渠道</label><select id="policyChannel"><option>stable</option><option>preview</option><option>dev</option></select>
|
||||
<label for="policyChannel">渠道</label><select id="policyChannel"></select>
|
||||
<label><input id="policyForceUpdate" type="checkbox" /> 强制升级</label>
|
||||
<label><input id="policyAllowRollback" type="checkbox" /> 允许降级</label>
|
||||
<label><input id="policyOfflineAllowed" type="checkbox" checked /> 允许离线启动</label>
|
||||
@@ -160,6 +458,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>License 授权</h2>
|
||||
<div class="row">
|
||||
<label for="licenseCustomer">客户</label><input id="licenseCustomer" placeholder="客户或项目名称">
|
||||
<label for="licenseChannel">渠道</label><select id="licenseChannel"></select>
|
||||
<label for="licenseMaxDevices">最大设备数</label><input id="licenseMaxDevices" type="number" min="1" value="1">
|
||||
<label for="licenseValidUntil">有效期</label><input id="licenseValidUntil" value="2099-12-31T23:59:59Z">
|
||||
</div>
|
||||
<div class="row action-row"><button id="createLicenseBtn" type="button">创建授权</button><button id="refreshLicensesBtn" type="button">刷新授权</button></div>
|
||||
<div class="row"><label for="createdLicenseKey">新授权密钥</label><input id="createdLicenseKey" class="wide" readonly placeholder="仅显示一次;创建后请立即复制到客户端 app_config.json 的 license_key"></div>
|
||||
<table><thead><tr><th>License ID</th><th>客户</th><th>渠道</th><th>设备</th><th>有效期</th><th>状态</th><th>操作</th></tr></thead><tbody id="licenseRows"></tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>设备管理</h2>
|
||||
<div class="row"><button id="refreshDevicesBtn" type="button">刷新设备</button><span class="small">显示当前应用登记的设备;禁用后客户端请求会立即被拒绝</span></div>
|
||||
<table><thead><tr><th>设备 ID</th><th>状态</th><th>凭证序列</th><th>最近访问</th><th>IP</th><th>操作</th></tr></thead><tbody id="deviceRows"></tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>升级日志</h2>
|
||||
<div class="row">
|
||||
@@ -179,14 +496,26 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>文件下载日志</h2><div class="row"><button id="refreshDownloadLogsBtn">刷新下载日志</button></div>
|
||||
<table><thead><tr><th>设备</th><th>License</th><th>版本/渠道</th><th>文件</th><th>大小</th><th>结果</th><th>IP</th><th>时间</th></tr></thead><tbody id="downloadLogRows"></tbody></table>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>管理员审计日志</h2><div class="row"><button id="refreshAuditLogsBtn">刷新审计日志</button></div>
|
||||
<table><thead><tr><th>管理员指纹</th><th>操作</th><th>结果</th><th>状态码</th><th>IP</th><th>时间</th></tr></thead><tbody id="auditLogRows"></tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>调试输出</h2>
|
||||
<pre id="output">准备就绪...</pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const apiBase = 'http://' + (location.hostname || '127.0.0.1') + ':8000';
|
||||
const apiBase = (location.protocol === 'http:' || location.protocol === 'https:')
|
||||
? location.origin
|
||||
: 'http://127.0.0.1:8000';
|
||||
const adminTokenInput = document.getElementById('adminToken');
|
||||
const apiBaseLabel = document.getElementById('apiBaseLabel');
|
||||
const toggleTokenBtn = document.getElementById('toggleTokenBtn');
|
||||
const tokenChangePanel = document.getElementById('tokenChangePanel');
|
||||
const newAdminTokenInput = document.getElementById('newAdminToken');
|
||||
@@ -198,16 +527,23 @@
|
||||
const publishAppIdInput = document.getElementById('publishAppId');
|
||||
const publishVersionInput = document.getElementById('publishVersion');
|
||||
const publishChannelInput = document.getElementById('publishChannel');
|
||||
const publishProtocolInput = document.getElementById('publishProtocol');
|
||||
const publishFilesInput = document.getElementById('publishFiles');
|
||||
const publishFileSummary = document.getElementById('publishFileSummary');
|
||||
const publishFilePreview = document.getElementById('publishFilePreview');
|
||||
const publishBtn = document.getElementById('publishBtn');
|
||||
const versionRows = document.getElementById('versionRows');
|
||||
const reportRows = document.getElementById('reportRows');
|
||||
const deviceRows = document.getElementById('deviceRows');
|
||||
const licenseRows = document.getElementById('licenseRows');
|
||||
const channelRows = document.getElementById('channelRows');
|
||||
const downloadLogRows = document.getElementById('downloadLogRows');
|
||||
const auditLogRows = document.getElementById('auditLogRows');
|
||||
|
||||
let currentToken = localStorage.getItem('admin_token') || '';
|
||||
let apps = [];
|
||||
let currentAppId = '';
|
||||
let channels = [];
|
||||
|
||||
function log(message) {
|
||||
if (typeof message === 'string') {
|
||||
@@ -239,6 +575,7 @@
|
||||
const url = apiBase + path;
|
||||
const reqOptions = {
|
||||
...options,
|
||||
cache: 'no-store',
|
||||
headers: { ...(options.headers || {}), ...getHeaders(options.contentType) },
|
||||
};
|
||||
delete reqOptions.contentType;
|
||||
@@ -321,28 +658,43 @@
|
||||
}
|
||||
|
||||
const chrBackslash = String.fromCharCode(92);
|
||||
let releaseMainExecutable = 'MainApp.exe';
|
||||
function selectedUploadEntries() {
|
||||
const excludedFiles = new Set(['client.ini', 'bootstrap.exe', 'config/app_config.json', 'config/local_state.json', 'config/client_identity.dat', 'config/version_policy.dat']);
|
||||
return Array.from(publishFilesInput.files).map(file => ({ file, path: relativePathForFile(file) })).filter(item => {
|
||||
const path = item.path.toLowerCase();
|
||||
return !excludedFiles.has(path) && !path.startsWith('update/') && !path.startsWith('update_temp/') && !path.endsWith('.pdb') && !path.endsWith('.ilk');
|
||||
const parts = path.split('/');
|
||||
const blockedDirectory = parts.slice(0, -1).some(part => part === '.git' || part === '.vs' || part === 'cmakefiles' || part === 'debug' || part === 'update' || part === 'update_temp' || part.startsWith('build') || part.endsWith('_autogen'));
|
||||
return !excludedFiles.has(path) && !blockedDirectory && !path.endsWith('.pdb') && !path.endsWith('.ilk') && !path.endsWith('.obj');
|
||||
});
|
||||
}
|
||||
|
||||
function validateReleaseRoot(entries) {
|
||||
if (!entries.length) return '请选择软件发布根目录';
|
||||
const lowerPaths = entries.map(item => item.path.toLowerCase());
|
||||
if (!lowerPaths.includes('mainapp.exe')) {
|
||||
const nestedMain = lowerPaths.find(path => path.endsWith('/mainapp.exe'));
|
||||
const mainName = releaseMainExecutable.toLowerCase();
|
||||
if (!lowerPaths.includes(mainName)) {
|
||||
const nestedMain = lowerPaths.find(path => path.endsWith('/' + mainName));
|
||||
if (nestedMain) {
|
||||
const expectedRoot = nestedMain.slice(0, -'/mainapp.exe'.length);
|
||||
return '选择层级过高:MainApp.exe 位于 ' + expectedRoot + '/ 下,请直接选择该目录';
|
||||
const expectedRoot = nestedMain.slice(0, -(mainName.length + 1));
|
||||
return '选择层级过高:' + releaseMainExecutable + ' 位于 ' + expectedRoot + '/ 下,请直接选择该目录';
|
||||
}
|
||||
return '所选目录根级没有 MainApp.exe,不能作为软件发布根目录';
|
||||
return '所选目录根级没有 ' + releaseMainExecutable + ',不能作为软件发布根目录';
|
||||
}
|
||||
const duplicateMain = lowerPaths.find(path => path.endsWith('/' + mainName));
|
||||
if (duplicateMain) return '目录中存在嵌套的重复主程序 ' + duplicateMain + ',请选择干净的 Release 输出目录';
|
||||
return '';
|
||||
}
|
||||
|
||||
async function loadRuntimeConfig() {
|
||||
try {
|
||||
const data = await request('/admin/runtime-config');
|
||||
releaseMainExecutable = data.release_main_executable || releaseMainExecutable;
|
||||
} catch (err) {
|
||||
log({ warning: '读取发布配置失败,将使用默认主程序名', details: err.message || err });
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedDirectory() {
|
||||
const allCount = publishFilesInput.files.length;
|
||||
const entries = selectedUploadEntries();
|
||||
@@ -356,19 +708,29 @@
|
||||
publishFilePreview.textContent = paths.length ? paths.join('\n') + (entries.length > 30 ? '\n... 其余 ' + (entries.length - 30) + ' 个文件' : '') : '尚未选择发布目录';
|
||||
}
|
||||
|
||||
function renderChannels() {
|
||||
const enabled=channels.filter(c=>c.enabled); const selects=[publishChannelInput,document.getElementById('policyChannel'),document.getElementById('licenseChannel')];
|
||||
selects.forEach(select=>{const previous=select.value;select.innerHTML='';enabled.forEach(c=>{const o=document.createElement('option');o.value=c.channel_code;o.textContent=`${c.channel_code}(${c.display_name})`;select.appendChild(o);});if(enabled.some(c=>c.channel_code===previous))select.value=previous;});
|
||||
channelRows.innerHTML=channels.length?'':'<tr><td colspan="5" class="empty">暂无渠道</td></tr>';channels.forEach(c=>{const tr=document.createElement('tr');tr.innerHTML=`<td>${c.channel_code}</td><td>${c.display_name}</td><td><span class="badge ${c.enabled?'success':'fail'}">${c.enabled?'启用':'停用'}</span></td><td>${c.sort_order}</td><td><button data-channel-code="${c.channel_code}">编辑</button></td>`;channelRows.appendChild(tr);});
|
||||
}
|
||||
async function loadChannels(){const appId=currentAppId||publishAppIdInput.value.trim();if(!appId){channels=[];renderChannels();return;}try{const data=await request(`/admin/channel/list?app_id=${encodeURIComponent(appId)}`);channels=data.list||[];renderChannels();}catch(err){log(err);notify('读取渠道失败',true);}}
|
||||
async function saveChannel(){const appId=currentAppId||publishAppIdInput.value.trim();if(!appId){notify('请先选择应用',true);return;}const body={app_id:appId,channel_code:document.getElementById('channelCode').value.trim(),display_name:document.getElementById('channelName').value.trim(),sort_order:Number(document.getElementById('channelOrder').value),enabled:document.getElementById('channelEnabled').checked};try{await request('/admin/channel/save',{method:'POST',contentType:'application/json',body:JSON.stringify(body)});notify('渠道已保存');await loadChannels();}catch(err){log(err);notify('保存渠道失败',true);}}
|
||||
|
||||
async function publishVersion() {
|
||||
const appId = publishAppIdInput.value.trim();
|
||||
const version = publishVersionInput.value.trim();
|
||||
const channel = publishChannelInput.value.trim() || 'stable';
|
||||
const channel = publishChannelInput.value.trim();
|
||||
const entries = selectedUploadEntries();
|
||||
if (!appId) { log('请选择或填写 App ID'); return; }
|
||||
if (!version) { log('请输入版本号'); return; }
|
||||
if (!channel) { notify('当前应用没有已启用渠道', true); return; }
|
||||
const rootError = validateReleaseRoot(entries);
|
||||
if (rootError) { log(rootError); notify(rootError, true); return; }
|
||||
const formData = new FormData();
|
||||
formData.append('app_id', appId);
|
||||
formData.append('version', version);
|
||||
formData.append('channel', channel);
|
||||
formData.append('client_protocol', String(Math.max(1, Number(publishProtocolInput.value) || 3)));
|
||||
for (const item of entries) {
|
||||
formData.append('files', item.file, item.file.name);
|
||||
formData.append('relative_paths', item.path);
|
||||
@@ -412,17 +774,20 @@
|
||||
try {
|
||||
const data = await request(`/admin/version/list?app_id=${encodeURIComponent(appId)}`);
|
||||
const rows = data.list || [];
|
||||
versionRows.innerHTML = rows.length ? '' : '<tr><td colspan="6" class="empty">该应用还没有发布版本</td></tr>';
|
||||
versionRows.innerHTML = rows.length ? '' : '<tr><td colspan="7" class="empty">该应用还没有发布版本</td></tr>';
|
||||
rows.forEach(item => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${item.id}</td>
|
||||
<td>${item.version}</td>
|
||||
<td>${item.channel}</td>
|
||||
<td><span class="badge">P${item.client_protocol || 1}</span></td>
|
||||
<td><span class="badge ${item.latest ? 'success' : ''}">${item.latest ? '当前最新' : '历史版本'}</span></td>
|
||||
<td>${item.create_time || ''}</td>
|
||||
<td>
|
||||
<button type="button" data-action="set-latest" data-id="${item.id}">设为最新</button>
|
||||
<button type="button" data-action="set-protocol" data-id="${item.id}" data-protocol="${item.client_protocol || 1}">修改协议</button>
|
||||
<button type="button" data-action="offline-package" data-id="${item.id}" data-version="${item.version}">下载 .upd</button>
|
||||
<button type="button" data-action="delete" data-id="${item.id}">删除</button>
|
||||
</td>
|
||||
`;
|
||||
@@ -434,6 +799,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadOfflinePackage(versionId, version) {
|
||||
notify('正在生成离线包,请保持页面打开');
|
||||
try { const resp=await fetch(`${apiBase}/admin/version/offline-package`,{method:'POST',headers:getHeaders('application/json'),body:JSON.stringify({version_id:Number(versionId)})}); if(!resp.ok)throw new Error(`HTTP ${resp.status}: ${await resp.text()}`); const blob=await resp.blob(); const url=URL.createObjectURL(blob); const a=document.createElement('a');a.href=url;a.download=`${currentAppId}_${version}.upd`;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);notify('离线包已生成'); } catch(err){log(err);notify('离线包生成失败',true);}
|
||||
}
|
||||
|
||||
async function setLatest(versionId) {
|
||||
log(`正在设置版本 ${versionId} 为最新...`);
|
||||
try {
|
||||
@@ -449,6 +819,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function setVersionProtocol(versionId, currentProtocol) {
|
||||
const raw = window.prompt('请输入该版本实际支持的客户端协议号', String(currentProtocol || 1));
|
||||
if (raw === null) return;
|
||||
const protocol = Number(raw);
|
||||
if (!Number.isInteger(protocol) || protocol < 1) { notify('协议号必须是正整数', true); return; }
|
||||
try {
|
||||
const body = await request('/admin/version/set-protocol', {method:'POST', contentType:'application/json',
|
||||
body:JSON.stringify({version_id:Number(versionId), client_protocol:protocol})});
|
||||
log(body); notify('版本协议已更新为 P' + protocol); await loadVersions();
|
||||
} catch (err) { log(err); notify('修改协议失败', true); }
|
||||
}
|
||||
|
||||
async function deleteVersion(versionId) {
|
||||
if (!window.confirm(`确定删除版本记录 #${versionId} 及其云端文件吗?此操作不可撤销。`)) return;
|
||||
log(`正在删除版本 ${versionId} ...`);
|
||||
@@ -502,6 +884,36 @@
|
||||
} catch (err) { log(err); notify('保存策略失败', true); }
|
||||
}
|
||||
|
||||
async function loadLicenses() {
|
||||
const appId=currentAppId||publishAppIdInput.value.trim(); if(!appId){licenseRows.innerHTML='<tr><td colspan="7" class="empty">请先选择应用</td></tr>';return;}
|
||||
try { const data=await request(`/admin/license/list?app_id=${encodeURIComponent(appId)}`); const rows=data.list||[]; licenseRows.innerHTML=rows.length?'':'<tr><td colspan="7" class="empty">暂无授权</td></tr>'; rows.forEach(item=>{const tr=document.createElement('tr');tr.innerHTML=`<td>${item.license_id}</td><td>${item.customer_name}</td><td>${item.channel_code}</td><td>${item.used_devices} / ${item.max_devices}</td><td>${item.valid_until}</td><td><span class="badge ${item.status==='active'?'success':'fail'}">${item.status==='active'?'有效':'已禁用'}</span></td><td><button data-license-id="${item.license_id}" data-status="${item.status}">${item.status==='active'?'禁用':'启用'}</button></td>`;licenseRows.appendChild(tr);}); } catch(err){log(err);notify('读取授权失败',true);}
|
||||
}
|
||||
async function createLicense(){
|
||||
const appId=currentAppId||publishAppIdInput.value.trim();if(!appId){notify('请先选择应用',true);return;} const body={app_id:appId,customer_name:document.getElementById('licenseCustomer').value.trim(),channel:document.getElementById('licenseChannel').value,max_devices:Number(document.getElementById('licenseMaxDevices').value),valid_until:document.getElementById('licenseValidUntil').value.trim()};
|
||||
try{const result=await request('/admin/license/create',{method:'POST',contentType:'application/json',body:JSON.stringify(body)});document.getElementById('createdLicenseKey').value=result.license_key;log(result);notify('授权已创建,请立即复制密钥');await loadLicenses();}catch(err){log(err);notify('创建授权失败',true);}
|
||||
}
|
||||
async function toggleLicense(id,status){try{await request('/admin/license/set-status',{method:'POST',contentType:'application/json',body:JSON.stringify({license_id:id,status:status==='active'?'disabled':'active'})});await loadLicenses();notify('授权状态已更新');}catch(err){log(err);notify('修改授权失败',true);}}
|
||||
|
||||
async function loadDevices() {
|
||||
const appId = currentAppId || publishAppIdInput.value.trim();
|
||||
if (!appId) { deviceRows.innerHTML = '<tr><td colspan="6" class="empty">请先选择应用</td></tr>'; return; }
|
||||
try {
|
||||
const data = await request(`/admin/device/list?app_id=${encodeURIComponent(appId)}`);
|
||||
const rows = data.list || [];
|
||||
deviceRows.innerHTML = rows.length ? '' : '<tr><td colspan="6" class="empty">暂无登记设备</td></tr>';
|
||||
rows.forEach(item => { const tr=document.createElement('tr'); tr.innerHTML=`<td>${item.device_id}</td><td><span class="badge ${item.disabled?'fail':'success'}">${item.disabled?'已禁用':'正常'}</span></td><td>${item.credential_seq}</td><td>${item.last_seen_at||''}</td><td>${item.last_ip||''}</td><td><button data-device-id="${item.device_id}" data-disabled="${item.disabled}">${item.disabled?'恢复':'禁用'}</button></td>`; deviceRows.appendChild(tr); });
|
||||
} catch (err) { log(err); notify('读取设备列表失败', true); }
|
||||
}
|
||||
|
||||
async function toggleDevice(deviceId, disabled) {
|
||||
let reason=''; if (!disabled) { reason=window.prompt('请输入禁用原因', '管理员禁用') || ''; if (!reason) return; }
|
||||
try { const result=await request('/admin/device/set-disabled',{method:'POST',contentType:'application/json',body:JSON.stringify({device_id:deviceId,disabled:!disabled,reason})}); log(result); notify(result.msg); await loadDevices(); } catch(err) { log(err); notify('设备状态修改失败',true); }
|
||||
}
|
||||
|
||||
function formatLogBytes(bytes){const n=Number(bytes)||0;return n>=1048576?(n/1048576).toFixed(2)+' MB':n>=1024?(n/1024).toFixed(1)+' KB':n+' B';}
|
||||
async function loadDownloadLogs(){const appId=currentAppId||publishAppIdInput.value.trim();try{const data=await request(`/admin/download-log/list?app_id=${encodeURIComponent(appId)}&limit=300`);const rows=data.list||[];downloadLogRows.innerHTML=rows.length?'':'<tr><td colspan="8" class="empty">暂无下载日志</td></tr>';rows.forEach(x=>{const tr=document.createElement('tr');tr.innerHTML=`<td>${x.device_id}</td><td>${x.license_id}</td><td>${x.version} / ${x.channel_code}</td><td>${x.file_path}</td><td>${formatLogBytes(x.file_size)}</td><td><span class="badge ${x.result==='success'?'success':x.result==='fail'?'fail':''}">${x.result}</span></td><td>${x.ip||''}</td><td>${x.created_at||''}</td>`;downloadLogRows.appendChild(tr);});}catch(err){log(err);notify('读取下载日志失败',true);}}
|
||||
async function loadAuditLogs(){try{const data=await request('/admin/audit-log/list?limit=300');const rows=data.list||[];auditLogRows.innerHTML=rows.length?'':'<tr><td colspan="6" class="empty">暂无审计日志</td></tr>';rows.forEach(x=>{const tr=document.createElement('tr');tr.innerHTML=`<td>${x.actor_hash}</td><td>${x.method} ${x.path}</td><td><span class="badge ${x.result==='success'?'success':'fail'}">${x.result}</span></td><td>${x.status_code}</td><td>${x.ip||''}</td><td>${x.created_at||''}</td>`;auditLogRows.appendChild(tr);});}catch(err){log(err);notify('读取审计日志失败',true);}}
|
||||
|
||||
async function loadReports() {
|
||||
log('正在加载升级日志...');
|
||||
try {
|
||||
@@ -539,12 +951,17 @@
|
||||
document.getElementById('saveTokenBtn').addEventListener('click', async () => {
|
||||
const token = adminTokenInput.value.trim();
|
||||
if (!token) { notify('令牌不能为空', true); return; }
|
||||
const previousToken = currentToken;
|
||||
currentToken = token;
|
||||
localStorage.setItem('admin_token', currentToken);
|
||||
try {
|
||||
await loadApps();
|
||||
await request('/admin/auth/check');
|
||||
localStorage.setItem('admin_token', currentToken);
|
||||
await loadConsoleData();
|
||||
notify('令牌验证成功并已保存');
|
||||
} catch (err) {
|
||||
currentToken = previousToken;
|
||||
updateTokenInput();
|
||||
log(err);
|
||||
notify('令牌验证失败', true);
|
||||
}
|
||||
});
|
||||
@@ -561,10 +978,13 @@
|
||||
});
|
||||
|
||||
document.getElementById('confirmChangeTokenBtn').addEventListener('click', async () => {
|
||||
const currentInputToken = adminTokenInput.value.trim();
|
||||
const nextToken = newAdminTokenInput.value.trim();
|
||||
const confirmation = confirmAdminTokenInput.value.trim();
|
||||
if (!currentInputToken) { notify('请先输入当前管理员令牌', true); return; }
|
||||
if (nextToken.length < 8) { notify('新令牌至少需要 8 个字符', true); return; }
|
||||
if (nextToken !== confirmation) { notify('两次输入的新令牌不一致', true); return; }
|
||||
currentToken = currentInputToken;
|
||||
try {
|
||||
const result = await request('/admin/token/change', { method: 'POST', contentType: 'application/json', body: JSON.stringify({ new_token: nextToken }) });
|
||||
currentToken = nextToken;
|
||||
@@ -574,19 +994,30 @@
|
||||
newAdminTokenInput.value = '';
|
||||
confirmAdminTokenInput.value = '';
|
||||
log(result);
|
||||
notify('管理员令牌已更改');
|
||||
notify('管理员令牌已在当前服务进程生效,请同步修改 .env');
|
||||
} catch (err) {
|
||||
log(err);
|
||||
notify('更改令牌失败,请确认当前令牌正确', true);
|
||||
const detail = err && err.body && err.body.detail ? err.body.detail : '请确认当前令牌正确';
|
||||
notify('更改令牌失败:' + detail, true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('refreshAppsBtn').addEventListener('click', loadApps);
|
||||
document.getElementById('refreshChannelsBtn').addEventListener('click', loadChannels);
|
||||
document.getElementById('saveChannelBtn').addEventListener('click', saveChannel);
|
||||
channelRows.addEventListener('click',event=>{const b=event.target.closest('button[data-channel-code]');if(!b)return;const c=channels.find(x=>x.channel_code===b.dataset.channelCode);if(!c)return;document.getElementById('channelCode').value=c.channel_code;document.getElementById('channelName').value=c.display_name;document.getElementById('channelOrder').value=c.sort_order;document.getElementById('channelEnabled').checked=c.enabled;});
|
||||
document.getElementById('addAppBtn').addEventListener('click', addApp);
|
||||
publishBtn.addEventListener('click', publishVersion);
|
||||
publishFilesInput.addEventListener('change', renderSelectedDirectory);
|
||||
document.getElementById('refreshVersionsBtn').addEventListener('click', loadVersions);
|
||||
document.getElementById('refreshReportsBtn').addEventListener('click', loadReports);
|
||||
document.getElementById('refreshDownloadLogsBtn').addEventListener('click', loadDownloadLogs);
|
||||
document.getElementById('refreshAuditLogsBtn').addEventListener('click', loadAuditLogs);
|
||||
document.getElementById('refreshDevicesBtn').addEventListener('click', loadDevices);
|
||||
document.getElementById('refreshLicensesBtn').addEventListener('click', loadLicenses);
|
||||
document.getElementById('createLicenseBtn').addEventListener('click', createLicense);
|
||||
licenseRows.addEventListener('click', event=>{const b=event.target.closest('button[data-license-id]');if(b)toggleLicense(b.dataset.licenseId,b.dataset.status);});
|
||||
deviceRows.addEventListener('click', event => { const b=event.target.closest('button[data-device-id]'); if(b) toggleDevice(b.dataset.deviceId, b.dataset.disabled === 'true'); });
|
||||
document.getElementById('loadPolicyBtn').addEventListener('click', loadPolicy);
|
||||
document.getElementById('savePolicyBtn').addEventListener('click', savePolicy);
|
||||
document.getElementById('policyChannel').addEventListener('change', loadPolicy);
|
||||
@@ -597,7 +1028,7 @@
|
||||
publishAppIdInput.value = currentAppId;
|
||||
}
|
||||
currentAppLabel.textContent = currentAppId || '未选择';
|
||||
if (currentAppId) loadPolicy();
|
||||
if (currentAppId) { loadChannels().then(() => loadPolicy()); loadLicenses(); loadDevices(); loadDownloadLogs(); }
|
||||
});
|
||||
|
||||
versionRows.addEventListener('click', async (event) => {
|
||||
@@ -607,15 +1038,41 @@
|
||||
const id = button.dataset.id;
|
||||
if (action === 'set-latest') {
|
||||
await setLatest(id);
|
||||
} else if (action === 'set-protocol') {
|
||||
await setVersionProtocol(id, button.dataset.protocol);
|
||||
} else if (action === 'offline-package') {
|
||||
await downloadOfflinePackage(id, button.dataset.version);
|
||||
} else if (action === 'delete') {
|
||||
await deleteVersion(id);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
updateTokenInput();
|
||||
async function loadConsoleData() {
|
||||
await loadRuntimeConfig();
|
||||
renderSelectedDirectory();
|
||||
await loadApps();
|
||||
await loadReports();
|
||||
await loadLicenses();
|
||||
await loadDevices();
|
||||
await loadDownloadLogs();
|
||||
await loadAuditLogs();
|
||||
}
|
||||
|
||||
function renderLoggedOutState() {
|
||||
versionRows.innerHTML = '<tr><td colspan="7" class="empty">登录后显示版本列表</td></tr>';
|
||||
reportRows.innerHTML = '<tr><td colspan="5" class="empty">登录后显示升级日志</td></tr>';
|
||||
downloadLogRows.innerHTML = '<tr><td colspan="8" class="empty">登录后显示下载日志</td></tr>';
|
||||
auditLogRows.innerHTML = '<tr><td colspan="6" class="empty">登录后显示审计日志</td></tr>';
|
||||
deviceRows.innerHTML = '<tr><td colspan="6" class="empty">登录后显示设备</td></tr>';
|
||||
licenseRows.innerHTML = '<tr><td colspan="7" class="empty">登录后显示授权</td></tr>';
|
||||
channelRows.innerHTML = '<tr><td colspan="5" class="empty">登录后显示渠道</td></tr>';
|
||||
log('请输入管理员令牌后点击“登录并保存”。页面不会在打开时自动读取服务端日志。当前连接服务端:' + apiBase);
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
apiBaseLabel.textContent = apiBase;
|
||||
updateTokenInput();
|
||||
renderLoggedOutState();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user