client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// EveryPublish 客户端渲染进程逻辑
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ---- 页签切换 ----
|
||||
document.querySelectorAll('.tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
|
||||
document.querySelectorAll('.page').forEach((p) => p.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
$('page-' + tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 状态页 ----
|
||||
let currentServer = '';
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const s = await window.api.status();
|
||||
if (s && s.server) currentServer = s.server;
|
||||
$('st-server').textContent = (s && s.server) || '-';
|
||||
$('st-device').textContent = s && s.deviceId > 0 ? '#' + s.deviceId + '(' + (s.name || '') + ')' : '未配对';
|
||||
$('st-session').textContent = (s && s.session) || '-';
|
||||
const dot = $('st-dot');
|
||||
if (!s) { $('st-online').textContent = '核心未就绪'; dot.className = 'dot gray'; }
|
||||
else if (s.online) { $('st-online').textContent = '在线'; dot.className = 'dot green'; }
|
||||
else { $('st-online').textContent = s.deviceId > 0 ? '离线(自动重连中)' : '未配对'; dot.className = 'dot orange'; }
|
||||
} catch (e) {
|
||||
$('st-online').textContent = '核心未就绪';
|
||||
$('st-dot').className = 'dot gray';
|
||||
}
|
||||
}
|
||||
setInterval(refreshStatus, 3000);
|
||||
refreshStatus();
|
||||
|
||||
$('btn-open-web').addEventListener('click', () => {
|
||||
const url = currentServer || 'http://127.0.0.1:8090';
|
||||
window.api.openWeb(url);
|
||||
});
|
||||
$('btn-open-data').addEventListener('click', () => window.api.openDataDir());
|
||||
|
||||
// ---- 配对页 ----
|
||||
$('btn-pair').addEventListener('click', async () => {
|
||||
const code = $('pair-code').value.trim();
|
||||
const name = $('pair-name').value.trim();
|
||||
const res = $('pair-result');
|
||||
if (!code) { res.textContent = '请输入配对码'; res.className = 'msg err'; return; }
|
||||
$('btn-pair').disabled = true;
|
||||
res.textContent = '配对中...';
|
||||
res.className = 'msg';
|
||||
try {
|
||||
const r = await window.api.pair(code, name);
|
||||
if (r && r.code === 0) {
|
||||
res.textContent = '配对成功,正在连接服务器(可到「状态」页查看)';
|
||||
res.className = 'msg ok';
|
||||
} else {
|
||||
res.textContent = (r && r.message) || '配对失败';
|
||||
res.className = 'msg err';
|
||||
}
|
||||
} catch (e) {
|
||||
res.textContent = '配对失败:' + e;
|
||||
res.className = 'msg err';
|
||||
}
|
||||
$('btn-pair').disabled = false;
|
||||
});
|
||||
|
||||
// ---- 挑战页 ----
|
||||
let selectedChallenge = null;
|
||||
let pollTimer = null;
|
||||
const KIND_LABEL = { qr: '扫码', sms: '短信验证码', confirm: 'APP 确认', captcha: '图形验证码', pending: '等待设备' };
|
||||
|
||||
async function refreshChallenges() {
|
||||
try {
|
||||
const r = await window.api.challenges();
|
||||
const list = (r && r.list) || [];
|
||||
const badge = $('ch-badge');
|
||||
if (list.length > 0) { badge.textContent = list.length; badge.classList.remove('hidden'); }
|
||||
else badge.classList.add('hidden');
|
||||
const box = $('ch-list');
|
||||
box.innerHTML = '';
|
||||
list.forEach((ch) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'item' + (selectedChallenge && selectedChallenge.challengeId === ch.challengeId ? ' selected' : '');
|
||||
const inner = [
|
||||
'<span><b>',
|
||||
ch.platform || '?',
|
||||
'</b> · ',
|
||||
KIND_LABEL[ch.kind] || ch.kind,
|
||||
'</span><span class="p">',
|
||||
ch.prompt || '',
|
||||
'</span>',
|
||||
].join('');
|
||||
div.innerHTML = inner;
|
||||
div.addEventListener('click', () => selectChallenge(ch));
|
||||
box.appendChild(div);
|
||||
});
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<p class="hint">暂无待处理挑战</p>';
|
||||
}
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
function selectChallenge(ch) {
|
||||
selectedChallenge = ch;
|
||||
refreshChallenges();
|
||||
const detail = $('ch-detail');
|
||||
detail.classList.remove('hidden');
|
||||
const qr = ch.qrUrl || ch.qrToken || '';
|
||||
const img = $('ch-qr');
|
||||
if (qr && (qr.startsWith('http') || qr.startsWith('file://'))) {
|
||||
img.src = qr;
|
||||
img.classList.remove('hidden');
|
||||
} else {
|
||||
img.classList.add('hidden');
|
||||
}
|
||||
$('ch-value').value = '';
|
||||
$('ch-result').textContent = '';
|
||||
}
|
||||
|
||||
$('btn-solve').addEventListener('click', async () => {
|
||||
if (!selectedChallenge) return;
|
||||
const value = $('ch-value').value.trim() || 'confirmed';
|
||||
const res = $('ch-result');
|
||||
try {
|
||||
const r = await window.api.solve(selectedChallenge.challengeId, value);
|
||||
if (r && r.code === 0) {
|
||||
res.textContent = '已提交,等待服务器确认';
|
||||
res.className = 'msg ok';
|
||||
selectedChallenge = null;
|
||||
$('ch-detail').classList.add('hidden');
|
||||
refreshChallenges();
|
||||
} else {
|
||||
res.textContent = (r && r.message) || '提交失败';
|
||||
res.className = 'msg err';
|
||||
}
|
||||
} catch (e) {
|
||||
res.textContent = '提交失败:' + e;
|
||||
res.className = 'msg err';
|
||||
}
|
||||
});
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer) return;
|
||||
pollTimer = setInterval(refreshChallenges, 2000);
|
||||
refreshChallenges();
|
||||
}
|
||||
startPolling();
|
||||
|
||||
// ---- 设置页 ----
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const cfg = await window.api.getConfig();
|
||||
$('set-server').value = (cfg && cfg.server) || '';
|
||||
$('set-fake').checked = !!(cfg && cfg.fakeAll);
|
||||
$('set-autostart').checked = await window.api.getAutoStart();
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
loadSettings();
|
||||
|
||||
$('btn-save').addEventListener('click', async () => {
|
||||
const server = $('set-server').value.trim();
|
||||
const res = $('set-result');
|
||||
if (!server) { res.textContent = '请填写服务器地址(内网部署填 http://服务器IP:8090)'; res.className = 'msg err'; return; }
|
||||
const cfg = { server: server, fakeAll: $('set-fake').checked };
|
||||
await window.api.saveConfig(cfg);
|
||||
await window.api.setAutoStart($('set-autostart').checked);
|
||||
currentServer = server;
|
||||
res.textContent = '已保存,核心进程已重启';
|
||||
res.className = 'msg ok';
|
||||
});
|
||||
|
||||
$('btn-quit').addEventListener('click', () => window.api.quit());
|
||||
|
||||
// 核心就绪提示
|
||||
if (window.api.onCoreReady) {
|
||||
window.api.onCoreReady(() => {
|
||||
$('st-online').textContent = '核心已就绪';
|
||||
refreshStatus();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>EveryPublish 客户端</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand">EveryPublish <span class="sub">发布客户端</span></div>
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="status">状态</button>
|
||||
<button class="tab" data-tab="pair">配对</button>
|
||||
<button class="tab" data-tab="challenges">挑战<span id="ch-badge" class="badge hidden">0</span></button>
|
||||
<button class="tab" data-tab="settings">设置</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<!-- 状态页 -->
|
||||
<section id="page-status" class="page active">
|
||||
<div class="card">
|
||||
<h2>运行状态</h2>
|
||||
<div class="kv"><span>连接状态</span><b id="st-online">检测中...</b><span id="st-dot" class="dot gray"></span></div>
|
||||
<div class="kv"><span>服务器</span><b id="st-server">-</b></div>
|
||||
<div class="kv"><span>设备</span><b id="st-device">-</b></div>
|
||||
<div class="kv"><span>会话</span><b id="st-session">-</b></div>
|
||||
</div>
|
||||
<div class="card actions">
|
||||
<button id="btn-open-web" class="btn primary">打开网页控制台</button>
|
||||
<button id="btn-open-data" class="btn">打开数据目录</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 配对页 -->
|
||||
<section id="page-pair" class="page">
|
||||
<div class="card">
|
||||
<h2>设备配对</h2>
|
||||
<ol class="steps">
|
||||
<li>打开网页控制台(需与服务器内网互通)</li>
|
||||
<li>登录后进入「设置 → Agent 设备」生成配对码</li>
|
||||
<li>在下方输入配对码并点击「配对」</li>
|
||||
</ol>
|
||||
<label>配对码(6 位,5 分钟有效)</label>
|
||||
<input id="pair-code" placeholder="例如:AB12CD" maxlength="6" />
|
||||
<label>设备名(选填)</label>
|
||||
<input id="pair-name" placeholder="例如:办公室电脑-01" />
|
||||
<button id="btn-pair" class="btn primary">配对</button>
|
||||
<p id="pair-result" class="msg"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 挑战页 -->
|
||||
<section id="page-challenges" class="page">
|
||||
<div class="card">
|
||||
<h2>二次验证挑战</h2>
|
||||
<p class="hint">平台要求扫码/验证码时在此处理;超时可在网页端一键重发。</p>
|
||||
<div id="ch-list" class="list"></div>
|
||||
<div id="ch-detail" class="detail hidden">
|
||||
<img id="ch-qr" alt="二维码" />
|
||||
<label>验证码 / 确认值</label>
|
||||
<input id="ch-value" placeholder="扫码后输入验证码;扫码挑战留空直接提交" />
|
||||
<button id="btn-solve" class="btn primary">提交</button>
|
||||
<p id="ch-result" class="msg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 设置页 -->
|
||||
<section id="page-settings" class="page">
|
||||
<div class="card">
|
||||
<h2>设置</h2>
|
||||
<label>服务器地址(内网部署时填 http://服务器IP:8090)</label>
|
||||
<input id="set-server" placeholder="http://192.168.x.x:8090" />
|
||||
<label class="switch-row">
|
||||
<input type="checkbox" id="set-fake" />
|
||||
<span>模拟执行(无真实账号的安装联调)</span>
|
||||
</label>
|
||||
<label class="switch-row">
|
||||
<input type="checkbox" id="set-autostart" />
|
||||
<span>开机自启</span>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button id="btn-save" class="btn primary">保存并重启核心</button>
|
||||
<button id="btn-quit" class="btn danger">退出客户端</button>
|
||||
</div>
|
||||
<p id="set-result" class="msg"></p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f5f6f8; color: #1f2329; height: 100vh; display: flex; flex-direction: column; }
|
||||
.topbar { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 0 24px; display: flex; align-items: center; height: 56px; gap: 32px; }
|
||||
.brand { font-size: 18px; font-weight: 700; color: #0a63f0; }
|
||||
.brand .sub { font-size: 13px; font-weight: 400; color: #86909c; margin-left: 6px; }
|
||||
.tabs { display: flex; gap: 8px; }
|
||||
.tab { border: none; background: none; padding: 8px 16px; font-size: 14px; cursor: pointer; border-radius: 6px; color: #4e5969; }
|
||||
.tab:hover { background: #f2f4f7; }
|
||||
.tab.active { background: #e8f1ff; color: #0a63f0; font-weight: 600; }
|
||||
.badge { display: inline-block; margin-left: 4px; background: #f53f3f; color: #fff; border-radius: 8px; font-size: 11px; padding: 0 6px; }
|
||||
.hidden { display: none !important; }
|
||||
#main { flex: 1; overflow-y: auto; padding: 24px; }
|
||||
.page { display: none; max-width: 680px; margin: 0 auto; }
|
||||
.page.active { display: block; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
|
||||
.card h2 { font-size: 16px; margin-bottom: 14px; }
|
||||
.kv { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px dashed #f0f1f3; font-size: 14px; }
|
||||
.kv span { color: #86909c; width: 80px; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-left: 6px; }
|
||||
.dot.gray { background: #c9cdd4; } .dot.green { background: #00b42a; } .dot.orange { background: #ff7d00; } .dot.red { background: #f53f3f; }
|
||||
label { display: block; font-size: 13px; color: #4e5969; margin: 12px 0 6px; }
|
||||
input[type="text"], input:not([type]) { width: 100%; padding: 9px 12px; border: 1px solid #d9dde3; border-radius: 6px; font-size: 14px; outline: none; }
|
||||
input:focus { border-color: #0a63f0; }
|
||||
.btn { padding: 9px 20px; border: 1px solid #d9dde3; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }
|
||||
.btn:hover { background: #f7f8fa; }
|
||||
.btn.primary { background: #0a63f0; border-color: #0a63f0; color: #fff; }
|
||||
.btn.primary:hover { background: #0b57d0; }
|
||||
.btn.danger { color: #f53f3f; border-color: #f53f3f; }
|
||||
.actions { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.msg { margin-top: 10px; font-size: 13px; color: #4e5969; min-height: 18px; }
|
||||
.msg.ok { color: #00b42a; } .msg.err { color: #f53f3f; }
|
||||
.hint { font-size: 13px; color: #86909c; margin-bottom: 10px; }
|
||||
.steps { margin: 0 0 14px 18px; font-size: 13px; color: #4e5969; line-height: 1.9; }
|
||||
.list .item { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 8px; cursor: pointer; font-size: 13px; }
|
||||
.list .item:hover { border-color: #0a63f0; }
|
||||
.list .item.selected { border-color: #0a63f0; background: #f0f7ff; }
|
||||
.list .item .p { color: #86909c; }
|
||||
.detail { margin-top: 14px; }
|
||||
.detail img { max-width: 220px; max-height: 220px; border: 1px solid #e5e7eb; border-radius: 6px; display: block; margin-bottom: 10px; }
|
||||
.switch-row { display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 10px 0; cursor: pointer; }
|
||||
Reference in New Issue
Block a user