Files
get_wechat/electron-launcher/index.html

968 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信知识库 - 服务控制台</title>
<style>
:root {
--bg: #030405;
--panel: rgba(20, 22, 26, 0.78);
--panel-strong: rgba(28, 30, 36, 0.92);
--line: rgba(255, 255, 255, 0.1);
--line-strong: rgba(255, 255, 255, 0.18);
--text: #f3f7fb;
--muted: #9aa5b1;
--subtle: #5f6875;
--cyan: #20d3d2;
--blue: #7ca5ff;
--green: #2cd889;
--red: #ff6c91;
--warning: #ffd166;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
--robot-x: 0deg;
--robot-y: 0deg;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: var(--bg);
color: var(--text);
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
}
body {
min-width: 1180px;
min-height: 760px;
}
button {
border: 0;
border-radius: 10px;
color: #06100f;
cursor: pointer;
font: inherit;
font-weight: 700;
transition: transform 0.18s ease, opacity 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
button:hover:not(:disabled) {
transform: translateY(-1px);
}
button:disabled {
cursor: not-allowed;
opacity: 0.42;
transform: none;
}
.launcher {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at 52% 48%, rgba(255, 255, 255, 0.08), transparent 22%),
radial-gradient(circle at 75% 35%, rgba(32, 211, 210, 0.08), transparent 28%),
linear-gradient(180deg, #050607 0%, #020303 100%);
}
.launcher::before {
content: "";
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,0.45), transparent 68%);
pointer-events: none;
}
.launcher::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(90deg, rgba(0,0,0,0.58), transparent 38%, rgba(0,0,0,0.62)),
linear-gradient(180deg, rgba(0,0,0,0.1), rgba(0,0,0,0.78));
pointer-events: none;
}
.brand {
position: absolute;
top: 32px;
left: 36px;
z-index: 5;
display: flex;
align-items: center;
gap: 12px;
color: rgba(255,255,255,0.86);
font-size: 20px;
font-weight: 800;
}
.brand img {
width: 58px;
height: 36px;
object-fit: contain;
border-radius: 4px;
}
.brand span {
text-shadow: 0 0 24px rgba(32, 211, 210, 0.24);
}
.copy {
position: absolute;
z-index: 4;
left: 36px;
top: 320px;
width: 820px;
}
.copy h1 {
margin: 0;
font-size: 46px;
line-height: 1.18;
font-weight: 900;
white-space: nowrap;
color: transparent;
background: linear-gradient(90deg, #8d8cff 0%, #42b9e8 38%, #22d0b0 78%);
-webkit-background-clip: text;
background-clip: text;
text-shadow: 0 22px 60px rgba(20, 150, 165, 0.26);
}
.copy p {
margin: 24px 0 0;
color: rgba(255,255,255,0.66);
font-size: 19px;
font-style: italic;
font-weight: 700;
}
.robot-stage {
position: absolute;
z-index: 3;
left: 50%;
top: 53%;
width: 560px;
height: 660px;
transform: translate(-50%, -50%);
pointer-events: none;
filter: drop-shadow(0 48px 55px rgba(0,0,0,0.78));
}
.robot {
width: 100%;
height: 100%;
overflow: visible;
}
.robot-head {
transform-box: fill-box;
transform-origin: center;
transition: transform 0.12s ease-out;
}
.robot-left-arm {
transform-origin: 206px 277px;
animation: armFloatLeft 3.4s ease-in-out infinite;
}
.robot-right-arm {
transform-origin: 354px 277px;
animation: armFloatRight 3.2s ease-in-out infinite;
}
@keyframes armFloatLeft {
0%, 100% { transform: translateY(0) rotate(-3deg); }
50% { transform: translateY(-18px) rotate(4deg); }
}
@keyframes armFloatRight {
0%, 100% { transform: translateY(-4px) rotate(3deg); }
50% { transform: translateY(16px) rotate(-4deg); }
}
.control-panel {
position: absolute;
z-index: 4;
right: 72px;
top: 105px;
width: 420px;
padding: 26px;
border: 1px solid var(--line-strong);
border-radius: 24px;
background: linear-gradient(180deg, rgba(27,29,34,0.94), rgba(15,16,19,0.9));
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.panel-title {
margin-bottom: 18px;
text-align: center;
}
.panel-title strong {
display: block;
color: #f6f8fb;
font-size: 28px;
line-height: 1.2;
}
.panel-title span {
display: block;
margin-top: 8px;
color: var(--muted);
font-size: 14px;
}
.service-list {
display: grid;
gap: 10px;
}
.service-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(0,0,0,0.24);
}
.service-name {
color: var(--text);
font-size: 15px;
font-weight: 800;
}
.status {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}
.dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--red);
box-shadow: 0 0 16px rgba(255,108,145,0.45);
}
.dot.active {
background: var(--green);
box-shadow: 0 0 18px rgba(44,216,137,0.65);
}
.service-actions {
display: flex;
gap: 8px;
}
.btn-start,
.btn-stop {
width: 48px;
height: 34px;
font-size: 13px;
}
.btn-start {
background: var(--green);
}
.btn-stop {
background: var(--red);
}
.action-row {
display: grid;
gap: 10px;
margin-top: 18px;
}
.btn-open,
.btn-refresh-account {
width: 100%;
min-height: 50px;
padding: 0 18px;
font-size: 17px;
}
.btn-open {
background: linear-gradient(90deg, #7d94f3, #20d3d2, #2ba35f);
color: #fff;
box-shadow: 0 16px 30px rgba(32, 211, 210, 0.18);
}
.btn-open.ready {
animation: readyPulse 1.8s ease-in-out infinite;
}
.btn-refresh-account {
background: rgba(255,255,255,0.1);
color: #dbe3ee;
border: 1px solid var(--line);
}
@keyframes readyPulse {
0%, 100% { box-shadow: 0 0 0 rgba(44,216,137,0.0), 0 16px 30px rgba(32,211,210,0.18); }
50% { box-shadow: 0 0 28px rgba(44,216,137,0.34), 0 16px 30px rgba(32,211,210,0.18); }
}
.account-refresh-status {
min-height: 20px;
margin-top: 12px;
color: var(--subtle);
font-size: 12px;
line-height: 1.5;
}
.account-refresh-status.success {
color: var(--green);
}
.account-refresh-status.error {
color: var(--red);
}
.decrypt-bar-wrap {
display: none;
margin-top: 18px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(0,0,0,0.25);
}
.decrypt-bar-wrap.show {
display: block;
}
.decrypt-label {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 9px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.decrypt-track {
height: 7px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,0.1);
}
.decrypt-fill {
width: 35%;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--blue), var(--cyan));
animation: slide 1.2s ease-in-out infinite;
}
.decrypt-fill.done {
width: 100%;
background: var(--green);
animation: none;
}
@keyframes slide {
0% { transform: translateX(-105%); }
100% { transform: translateX(310%); }
}
.log-panel {
position: absolute;
z-index: 4;
left: 36px;
right: 36px;
bottom: 28px;
height: 170px;
}
.log-title {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 10px;
color: #e7ecff;
font-size: 18px;
font-weight: 900;
}
.log-title::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 16px rgba(32,211,210,0.8);
}
.logs {
height: calc(100% - 32px);
overflow-y: auto;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(6, 7, 11, 0.84);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02);
color: #b9c4d4;
font-family: "Cascadia Mono", Consolas, monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
}
.log-entry {
margin-bottom: 4px;
}
.log-source {
color: var(--cyan);
font-weight: 800;
}
#close-warning {
display: none;
position: absolute;
z-index: 30;
top: 24px;
left: 50%;
transform: translateX(-50%);
padding: 12px 18px;
border: 1px solid rgba(255,108,145,0.4);
border-radius: 12px;
background: rgba(255,108,145,0.92);
color: #120407;
font-weight: 900;
box-shadow: 0 18px 50px rgba(0,0,0,0.35);
}
.modal-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 100;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(8px);
}
.modal-overlay.show {
display: flex;
}
.modal-box {
width: min(720px, 88vw);
padding: 34px 38px;
border: 1px solid var(--line-strong);
border-radius: 22px;
background: linear-gradient(180deg, rgba(35,37,44,0.96), rgba(18,19,24,0.96));
box-shadow: var(--shadow);
}
.modal-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 18px;
color: #9fc0ff;
font-size: 22px;
font-weight: 900;
}
.modal-body {
color: #d7ddeb;
font-size: 15px;
line-height: 1.8;
}
.modal-steps {
display: grid;
gap: 12px;
margin: 18px 0;
padding: 0;
list-style: none;
counter-reset: step;
}
.modal-steps li {
display: flex;
gap: 12px;
align-items: center;
counter-increment: step;
}
.modal-steps li::before {
content: counter(step);
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: #9fc0ff;
color: #07101f;
font-size: 13px;
font-weight: 900;
flex-shrink: 0;
}
.modal-body .tip {
margin-top: 14px;
padding: 12px 14px;
border-left: 3px solid var(--cyan);
border-radius: 8px;
background: rgba(0,0,0,0.28);
color: #d0b8ff;
font-size: 13px;
}
.modal-footer {
display: flex;
align-items: center;
gap: 10px;
margin-top: 22px;
color: var(--muted);
font-size: 13px;
}
.modal-spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.18);
border-top-color: var(--cyan);
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="launcher" id="launcher">
<div id="close-warning">请先停止所有运行中的服务再退出。</div>
<div class="brand">
<img src="build/company-logo.jpg" alt="">
<span>灵泽万川 ChatLab</span>
</div>
<section class="copy" aria-label="启动页介绍">
<h1>智能售后知识库服务控制台</h1>
<p>Next-Generation Intelligent Service Knowledge Platform</p>
</section>
<section class="robot-stage" aria-hidden="true">
<svg class="robot" viewBox="0 0 560 660" role="img">
<defs>
<radialGradient id="metal" cx="38%" cy="22%" r="75%">
<stop offset="0%" stop-color="#f4f7fb"/>
<stop offset="18%" stop-color="#8e949c"/>
<stop offset="46%" stop-color="#20242a"/>
<stop offset="100%" stop-color="#050607"/>
</radialGradient>
<linearGradient id="body" x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stop-color="#5c626c"/>
<stop offset="35%" stop-color="#111419"/>
<stop offset="100%" stop-color="#020304"/>
</linearGradient>
<linearGradient id="edge" x1="0" x2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="45%" stop-color="#252a31"/>
<stop offset="100%" stop-color="#000000"/>
</linearGradient>
<filter id="softGlow" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="9" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<ellipse cx="280" cy="612" rx="145" ry="26" fill="rgba(0,0,0,0.75)"/>
<g class="robot-left-arm">
<path d="M210 282 C158 272 126 310 120 370" stroke="url(#edge)" stroke-width="46" stroke-linecap="round" fill="none"/>
<ellipse cx="116" cy="384" rx="48" ry="40" fill="url(#metal)" transform="rotate(-15 116 384)"/>
<path d="M85 386 C103 403 132 404 150 384" stroke="#070809" stroke-width="10" stroke-linecap="round" fill="none"/>
</g>
<g class="robot-right-arm">
<path d="M350 282 C402 272 434 310 440 370" stroke="url(#edge)" stroke-width="46" stroke-linecap="round" fill="none"/>
<ellipse cx="444" cy="384" rx="48" ry="40" fill="url(#metal)" transform="rotate(15 444 384)"/>
<path d="M410 384 C428 404 457 402 475 386" stroke="#070809" stroke-width="10" stroke-linecap="round" fill="none"/>
</g>
<path d="M232 248 L328 248 C355 294 362 394 338 482 C322 515 238 515 222 482 C198 394 205 294 232 248Z" fill="url(#body)"/>
<path d="M248 508 L236 610" stroke="url(#edge)" stroke-width="24" stroke-linecap="round"/>
<path d="M312 508 L324 610" stroke="url(#edge)" stroke-width="24" stroke-linecap="round"/>
<ellipse cx="230" cy="622" rx="44" ry="18" fill="url(#metal)"/>
<ellipse cx="330" cy="622" rx="44" ry="18" fill="url(#metal)"/>
<path d="M248 230 L246 198 L314 198 L312 230" stroke="url(#edge)" stroke-width="22" stroke-linecap="round" fill="none"/>
<g class="robot-head">
<ellipse cx="280" cy="132" rx="78" ry="88" fill="url(#metal)"/>
<ellipse cx="280" cy="139" rx="57" ry="65" fill="#030405"/>
<path d="M222 106 C245 58 315 58 338 106 C325 88 236 88 222 106Z" fill="rgba(255,255,255,0.78)"/>
<g filter="url(#softGlow)" fill="#d9f7ff">
<circle cx="254" cy="136" r="2.2"/>
<circle cx="260" cy="136" r="2.2"/>
<circle cx="266" cy="136" r="2.2"/>
<circle cx="254" cy="143" r="2.2"/>
<circle cx="260" cy="143" r="2.2"/>
<circle cx="266" cy="143" r="2.2"/>
<circle cx="294" cy="136" r="2.2"/>
<circle cx="300" cy="136" r="2.2"/>
<circle cx="306" cy="136" r="2.2"/>
<circle cx="294" cy="143" r="2.2"/>
<circle cx="300" cy="143" r="2.2"/>
<circle cx="306" cy="143" r="2.2"/>
</g>
</g>
</svg>
</section>
<aside class="control-panel" aria-label="服务控制面板">
<div class="panel-title">
<strong>服务控制台</strong>
<span>启动本地服务并进入售后知识库</span>
</div>
<div class="service-list">
<div class="service-row" id="card-chatlog">
<div>
<div class="service-name">底层服务 (Go)</div>
<div class="status"><div class="dot" id="dot-chatlog"></div><span id="text-chatlog">未运行</span></div>
</div>
<div class="service-actions">
<button class="btn-start" onclick="window.electronAPI.startChatlog()">启动</button>
<button class="btn-stop" onclick="window.electronAPI.stopChatlog()">停止</button>
</div>
</div>
<div class="service-row" id="card-fastapi">
<div>
<div class="service-name">业务层 (Python)</div>
<div class="status"><div class="dot" id="dot-fastapi"></div><span id="text-fastapi">未运行</span></div>
</div>
<div class="service-actions">
<button class="btn-start" onclick="window.electronAPI.startFastapi()">启动</button>
<button class="btn-stop" onclick="window.electronAPI.stopFastapi()">停止</button>
</div>
</div>
<div class="service-row" id="card-frontend">
<div>
<div class="service-name">UI 界面 (React)</div>
<div class="status"><div class="dot" id="dot-frontend"></div><span id="text-frontend">未运行</span></div>
</div>
<div class="service-actions">
<button class="btn-start" onclick="window.electronAPI.startFrontend()">启动</button>
<button class="btn-stop" onclick="window.electronAPI.stopFrontend()">停止</button>
</div>
</div>
</div>
<div class="decrypt-bar-wrap" id="decrypt-wrap">
<div class="decrypt-label">
<span id="decrypt-text">数据库解密中...</span>
<span id="decrypt-elapsed"></span>
</div>
<div class="decrypt-track"><div class="decrypt-fill" id="decrypt-fill"></div></div>
</div>
<div class="action-row">
<button class="btn-open" id="btn-enter">启动并进入系统</button>
<button class="btn-refresh-account" id="btn-refresh-account">重新识别当前微信账号</button>
</div>
<div class="account-refresh-status" id="account-refresh-status"></div>
</aside>
<section class="log-panel" aria-label="终端运行日志">
<h2 class="log-title">终端运行日志</h2>
<div class="logs" id="logs-container"></div>
</section>
<div class="modal-overlay" id="decrypt-dialog">
<div class="modal-box">
<div class="modal-title">检测到新账号,正在解密数据</div>
<div class="modal-body">
系统正在为此微信账号首次解密数据库和图片,同时已开启自动解密。
<ol class="modal-steps">
<li>请切换到微信窗口</li>
<li>随意点击任意一个聊天对话</li>
<li>翻看一下历史消息记录</li>
<li>等待本窗口自动关闭即可</li>
</ol>
<div class="tip">这与您在 chatlog.exe 界面中手动解密的操作完全相同,只需配合操作微信一次,后续将全程自动化。</div>
</div>
<div class="modal-footer">
<div class="modal-spinner"></div>
<span id="decrypt-dialog-status">解密进行中,请在微信里随意点击一个聊天窗口...</span>
<span id="decrypt-dialog-elapsed" style="margin-left:auto;"></span>
</div>
</div>
</div>
</div>
<script>
const logsContainer = document.getElementById('logs-container');
const btnEnter = document.getElementById('btn-enter');
const btnRefreshAccount = document.getElementById('btn-refresh-account');
const accountRefreshStatus = document.getElementById('account-refresh-status');
const decryptWrap = document.getElementById('decrypt-wrap');
const decryptText = document.getElementById('decrypt-text');
const decryptElapsed = document.getElementById('decrypt-elapsed');
const decryptFill = document.getElementById('decrypt-fill');
const decryptDialog = document.getElementById('decrypt-dialog');
const decryptDialogStatus = document.getElementById('decrypt-dialog-status');
const decryptDialogElapsed = document.getElementById('decrypt-dialog-elapsed');
const launcher = document.getElementById('launcher');
const robotHead = document.querySelector('.robot-head');
let decryptReady = false;
let frontendRunning = false;
let startingAll = false;
let refreshingAccount = false;
launcher.addEventListener('mousemove', (event) => {
const rect = launcher.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
const y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
const clampedX = Math.max(-1, Math.min(1, x));
const clampedY = Math.max(-1, Math.min(1, y));
robotHead.style.transform = `translate(${clampedX * 9}px, ${clampedY * 6}px) rotate(${clampedX * 7}deg)`;
});
launcher.addEventListener('mouseleave', () => {
robotHead.style.transform = 'translate(0, 0) rotate(0deg)';
});
function setRefreshStatus(text, type = '') {
accountRefreshStatus.textContent = text || '';
accountRefreshStatus.className = `account-refresh-status${type ? ` ${type}` : ''}`;
}
function setRefreshButtonBusy(busy) {
refreshingAccount = busy;
btnRefreshAccount.disabled = busy;
btnRefreshAccount.textContent = busy ? '识别中...' : '重新识别当前微信账号';
}
function updateEnterButton() {
if (decryptReady) {
btnEnter.disabled = false;
btnEnter.className = 'btn-open ready';
btnEnter.textContent = '进入系统界面';
btnEnter.onclick = () => window.electronAPI.openInApp();
}
}
function setServiceUI(key, running) {
const dot = document.getElementById(`dot-${key}`);
const text = document.getElementById(`text-${key}`);
if (!dot || !text) return;
if (running) {
dot.classList.add('active');
text.innerText = '运行中';
text.style.color = 'var(--green)';
} else {
dot.classList.remove('active');
text.innerText = '未运行';
text.style.color = 'var(--muted)';
}
if (key === 'frontend') frontendRunning = running;
}
function appendLog(source, text) {
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = `<span class="log-source">[${source}]</span> ${String(text || '').trim()}`;
logsContainer.appendChild(div);
logsContainer.scrollTop = logsContainer.scrollHeight;
}
window.electronAPI.getProcessStatus().then((status) => {
['chatlog', 'fastapi', 'frontend'].forEach((key) => setServiceUI(key, Boolean(status[key])));
if (status.chatlog) {
fetch('http://127.0.0.1:5030/api/v1/chatroom?format=json&limit=1')
.then((r) => r.json())
.then((data) => {
if (!data.error || !data.error.includes('decrypting')) {
decryptReady = true;
decryptWrap.classList.remove('show');
updateEnterButton();
} else {
decryptWrap.classList.add('show');
decryptText.textContent = '数据库解密中...';
}
})
.catch(() => {
decryptWrap.classList.add('show');
decryptText.textContent = '等待底层服务就绪...';
});
}
});
btnEnter.onclick = async () => {
if (startingAll || refreshingAccount) return;
startingAll = true;
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '正在启动本地服务...';
try {
await window.electronAPI.startAll();
} catch (e) {
btnEnter.disabled = false;
btnEnter.textContent = '启动并进入系统';
appendLog('App(Error)', e?.message || '启动失败');
} finally {
startingAll = false;
}
};
btnRefreshAccount.onclick = async () => {
if (refreshingAccount || startingAll) return;
setRefreshButtonBusy(true);
decryptReady = false;
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '正在重新识别账号...';
setRefreshStatus('正在重新识别当前微信账号,并重建消息索引...');
try {
const result = await window.electronAPI.refreshCurrentAccount();
decryptReady = Boolean(result?.messageIndexReady);
if (decryptReady) {
const removed = Number(result?.removedMessageCache || 0);
setRefreshStatus(removed > 0 ? `已重新识别并重建消息索引,清理了 ${removed} 个消息缓存。` : '已重新识别当前微信账号,消息索引已就绪。', 'success');
updateEnterButton();
} else {
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '消息索引未就绪,请重试重新识别';
setRefreshStatus('重新识别已完成,但消息索引仍未就绪。请在微信打开聊天窗口翻看历史消息后重试。', 'error');
}
} catch (e) {
decryptReady = false;
btnEnter.disabled = false;
btnEnter.className = 'btn-open';
btnEnter.textContent = '启动并进入系统';
setRefreshStatus(e?.message || '重新识别失败,请确认 PC 微信已登录。', 'error');
appendLog('App(Error)', e?.message || '重新识别失败');
} finally {
setRefreshButtonBusy(false);
}
};
window.electronAPI.onLog((data) => {
appendLog(data.source, data.text);
});
window.electronAPI.onProcessError?.((data) => {
decryptDialog.classList.remove('show');
decryptDialogElapsed.textContent = '';
decryptDialogStatus.textContent = data?.message || '服务启动失败';
btnEnter.disabled = false;
btnEnter.className = 'btn-open';
btnEnter.textContent = '启动并进入系统';
setRefreshButtonBusy(false);
setRefreshStatus(data?.message || '服务启动失败', 'error');
appendLog('Error', data?.message || '服务启动失败');
});
window.electronAPI.onProcessStarted((key) => {
setServiceUI(key, true);
if (key === 'chatlog') {
decryptWrap.classList.add('show');
decryptText.textContent = '数据库解密中...';
decryptFill.className = 'decrypt-fill';
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '等待数据库解密完成...';
btnEnter.onclick = null;
}
});
window.electronAPI.onProcessStopped((key) => {
setServiceUI(key, false);
if (key === 'chatlog') {
decryptReady = false;
decryptWrap.classList.remove('show');
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '请先启动底层服务并等待解密完成';
btnEnter.onclick = null;
}
});
window.electronAPI.onDecryptStatus(({ elapsed }) => {
const min = Math.floor(elapsed / 60);
const sec = elapsed % 60;
const timeStr = min > 0 ? `${min}${sec}` : `${sec}`;
decryptElapsed.textContent = timeStr;
if (decryptDialog.classList.contains('show')) {
decryptDialogElapsed.textContent = timeStr;
}
});
window.electronAPI.onDecryptReady(() => {
decryptReady = true;
decryptDialog.classList.remove('show');
decryptText.textContent = '解密完成';
decryptElapsed.textContent = '';
decryptFill.className = 'decrypt-fill done';
updateEnterButton();
});
window.electronAPI.onShowDecryptDialog(() => {
decryptDialog.classList.add('show');
decryptDialogStatus.textContent = '解密进行中,请在微信里随意点击一个聊天窗口...';
decryptDialogElapsed.textContent = '';
});
window.electronAPI.onDecryptReset(() => {
decryptReady = false;
decryptWrap.classList.remove('show');
btnEnter.disabled = true;
btnEnter.className = 'btn-open';
btnEnter.textContent = '请先启动底层服务并等待解密完成';
btnEnter.onclick = null;
});
let warningTimer = null;
window.electronAPI.onShowCloseWarning(() => {
const el = document.getElementById('close-warning');
el.style.display = 'block';
if (warningTimer) clearTimeout(warningTimer);
warningTimer = setTimeout(() => { el.style.display = 'none'; }, 3000);
});
</script>
</body>
</html>