初始化

This commit is contained in:
2025-10-10 19:41:11 +08:00
commit 096ea34568
501 changed files with 12161 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

268
index.html Normal file
View File

@@ -0,0 +1,268 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>YOLO 更新检查</title>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'unsafe-inline' 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:;">
<meta name="color-scheme" content="light dark">
<style>
:root {
--bg: #0b1220;
--card: #0f172a;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #60a5fa;
--accent-2: #34d399;
--bar-bg: #1f2937;
--bar-fg: linear-gradient(90deg, var(--accent), var(--accent-2));
--ok: #34d399;
--error: #ef4444;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f8fafc;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--bar-bg: #e5e7eb;
}
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background: radial-gradient(1200px 600px at 20% -10%, rgba(96, 165, 250, .25), transparent 50%),
radial-gradient(1200px 600px at 120% 120%, rgba(52, 211, 153, .25), transparent 50%),
var(--bg);
color: var(--text);
display: grid;
place-items: center;
}
.card {
width: 560px;
max-width: calc(100% - 40px);
background: linear-gradient(180deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, 0)), var(--card);
border: 1px solid rgba(255, 255, 255, .08);
border-radius: 20px;
padding: 28px;
box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
backdrop-filter: blur(6px);
}
.header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 12px;
}
.logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
display: grid;
place-items: center;
color: white;
font-weight: 900;
text-shadow: 0 1px 2px rgba(0, 0, 0, .3);
}
h1 {
font-size: 18px;
margin: 0;
}
.sub {
color: var(--muted);
font-size: 13px;
margin-top: 2px;
}
.status {
margin: 20px 0 16px;
padding: 12px 14px;
background: rgba(255, 255, 255, .04);
border: 1px dashed rgba(255, 255, 255, .18);
border-radius: 12px;
font-size: 14px;
}
.progress-wrap {
background: var(--bar-bg);
border-radius: 999px;
overflow: hidden;
height: 14px;
width: 100%;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .25);
}
.progress-bar {
height: 100%;
width: 0%;
background: var(--bar-fg);
transition: width .25s ease;
}
.row {
display: flex;
justify-content: space-between;
gap: 14px;
margin-top: 10px;
font-size: 12px;
color: var(--muted);
}
.actions {
margin-top: 18px;
display: flex;
gap: 10px;
}
button {
appearance: none;
border: 0;
border-radius: 12px;
padding: 10px 14px;
cursor: pointer;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
color: white;
font-weight: 600;
font-size: 14px;
box-shadow: 0 6px 18px rgba(56, 189, 248, .35);
}
button.ghost {
background: transparent;
border: 1px solid rgba(255, 255, 255, .18);
color: var(--text);
}
.ok {
color: var(--ok);
}
.err {
color: var(--error);
}
.fade {
animation: fade .3s ease both;
}
@keyframes fade {
from {
opacity: .0;
transform: translateY(6px)
}
to {
opacity: 1;
transform: none
}
}
</style>
</head>
<body>
<div class="card fade">
<div class="header">
<div class="logo">PY</div>
<div>
<h1>YOLO 启动 / 更新检查</h1>
<div class="sub">为你自动检查可用更新并在后台下载</div>
</div>
</div>
<div id="status" class="status">正在初始化…</div>
<div class="progress-wrap" aria-label="下载进度">
<div id="bar" class="progress-bar"></div>
</div>
<div class="row">
<div>速度:<span id="speed">0</span> MB/s</div>
<div>已下:<span id="transferred">0</span>/<span id="total">0</span> MB</div>
<div>进度:<span id="percent">0</span>%</div>
</div>
<div class="actions">
<button id="btn-install" style="display:none;">立即重启安装</button>
<button id="btn-hide" class="ghost">最小化到托盘</button>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const statusEl = $('status');
const bar = $('bar');
const sp = $('speed'), tf = $('transferred'), tt = $('total'), pc = $('percent');
const btnInstall = $('btn-install');
const btnHide = $('btn-hide');
// 订阅主进程事件
window.electronAPI.onUpdateStatus(({ status, version, notes, currentVersion, message }) => {
switch (status) {
case 'checking':
statusEl.textContent = '正在检查更新…';
break;
case 'available':
statusEl.innerHTML = `检测到新版本 <b>v${version}</b>,正在下载…`;
break;
case 'none':
statusEl.innerHTML = `已是最新版本 <span class="ok">(v${currentVersion})</span>。3 秒后自动最小化。`;
bar.style.width = '100%'; pc.textContent = '100';
setTimeout(() => window.close(), 3000);
break;
case 'downloaded':
statusEl.innerHTML = `下载完成!可立即安装更新 <b>v${version}</b>。`;
bar.style.width = '100%'; pc.textContent = '100';
btnInstall.style.display = '';
// 也可自动倒计时安装:
// setTimeout(() => btnInstall.click(), 3000);
break;
case 'error':
statusEl.innerHTML = `更新失败:<span class="err">${message || '未知错误'}</span>`;
break;
default:
statusEl.textContent = '状态:' + status;
}
});
window.electronAPI.onUpdateProgress(({ percent, transferred, total, speed }) => {
bar.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
sp.textContent = speed ?? 0;
tf.textContent = transferred ?? 0;
tt.textContent = total ?? 0;
pc.textContent = (percent ?? 0).toFixed(1);
});
// 交互
btnInstall.addEventListener('click', () => {
statusEl.textContent = '即将重启并安装更新…';
window.electronAPI.installNow();
});
btnHide.addEventListener('click', () => {
window.close(); // 在 main.js 里拦截 close -> hide()
});
// 首次加载立即请求检查
window.electronAPI.checkForUpdates();
</script>
</body>
</html>

228
main.js Normal file
View File

@@ -0,0 +1,228 @@
// main.js
const { app, Tray, Menu, dialog, BrowserWindow, ipcMain, nativeTheme } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const fs = require('fs');
const { spawn, spawnSync, exec } = require('child_process');
const log = require('electron-log');
Object.assign(console, log.functions);
const ALLOW_UNSIGNED = process.env.YOLO_ALLOW_UNSIGNED === '1';
let installTriggered = false; // NEW: 防止重复触发安装
// ===== 全局状态 =====
let tray = null;
let managedChild = null;
let quitting = false;
let updateWin = null;
let pythonStarted = false; // NEW: 已启动 tkpage
let updaterKicked = false; // NEW: 是否已触发检查
// ===== tkpage 路径 =====
function resolveExePath() {
const devExe = path.resolve(__dirname, 'resources', 'python', 'tkpage.exe');
const prodExe = path.join(process.resourcesPath, 'python', 'tkpage.exe');
return app.isPackaged ? prodExe : devExe;
}
// ===== 启动 tkpage只会启动一次 =====
function launchPythonIfNeeded() {
if (pythonStarted) return;
const exe = resolveExePath();
const cwd = path.dirname(exe);
console.info('[tkpage exe]', exe);
if (!fs.existsSync(exe)) {
dialog.showErrorBox('启动失败', `未找到可执行文件:\n${exe}`);
return;
}
pythonStarted = true;
managedChild = spawn(exe, [], {
cwd,
windowsHide: false,
detached: false,
stdio: 'ignore',
});
managedChild.on('exit', (code, signal) => {
console.info(`[tkpage] exit code=${code} signal=${signal}`);
if (!quitting) { quitting = true; app.quit(); }
});
managedChild.on('close', (code, signal) => {
console.info(`[tkpage] close code=${code} signal=${signal}`);
if (!quitting) { quitting = true; app.quit(); }
});
}
// ===== 结束 tkpage =====
function killPythonGUI(killSelf = true) {
if (managedChild && managedChild.pid) {
try { process.kill(managedChild.pid); } catch (e) {
console.warn('[kill] managed kill error:', e?.message || e);
} finally { managedChild = null; }
}
try { spawnSync('taskkill', ['/f', '/t', '/im', 'tkpage.exe'], { windowsHide: true }); } catch { }
if (killSelf) {
try { exec('taskkill /IM YOLO助手.exe /F'); } catch { }
}
}
// ===== 托盘 =====
function createTray() {
const iconPath = path.join(__dirname, 'assets', 'icon.ico');
tray = new Tray(iconPath);
const menu = Menu.buildFromTemplate([
{ label: '检查更新', click: () => startUpdateCheck(/*force*/true) },
{ type: 'separator' },
{ label: '显示更新窗口', click: () => { if (updateWin) { updateWin.show(); updateWin.focus(); } else { createUpdateWindow(); } } },
{ type: 'separator' },
{ label: '重启 Python GUI', click: () => { killPythonGUI(); pythonStarted = false; launchPythonIfNeeded(); } },
{ type: 'separator' },
{ label: '退出', click: () => { quitting = true; killPythonGUI(); app.quit(); } },
]);
tray.setToolTip('YOLO');
tray.setContextMenu(menu);
}
// ===== 更新窗口 =====
function createUpdateWindow() {
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); return; }
updateWin = new BrowserWindow({
width: 640, height: 420, resizable: false, fullscreenable: false, maximizable: false,
autoHideMenuBar: true, show: true, center: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#0b1220' : '#ffffff',
icon: path.join(__dirname, 'assets', 'icon.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, nodeIntegration: false, sandbox: true,
}
});
updateWin.on('close', (e) => { e.preventDefault(); updateWin.hide(); });
updateWin.loadFile(path.join(__dirname, 'index.html')).catch(err => console.error('[updateWin] load error', err));
}
// ===== Updater 事件桥接到渲染进程,并控制 tkpage 启动时机 =====
function wireUpdaterIpc() {
if (ALLOW_UNSIGNED) {
console.warn('[updater] verifyUpdateCodeSignature disabled (YOLO_ALLOW_UNSIGNED=1)');
autoUpdater.verifyUpdateCodeSignature = async () => null;
}
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;
const send = (ch, data) => { if (updateWin && !updateWin.isDestroyed()) updateWin.webContents.send(ch, data); };
autoUpdater.on('checking-for-update', () => {
console.info('[updater] Checking');
tray?.setToolTip('YOLO - 正在检查更新…');
send('update:status', { status: 'checking' });
});
autoUpdater.on('update-available', (info) => {
console.info('[updater] Available', info?.version);
tray?.setToolTip(`YOLO - 检测到新版本 v${info?.version},开始下载…`);
send('update:status', { status: 'available', version: info?.version, notes: info?.releaseNotes || '' });
// 此时不启动 tkpage等待下载完成
});
autoUpdater.on('update-not-available', (info) => {
console.info('[updater] None');
tray?.setToolTip('YOLO - 已是最新版本');
send('update:status', { status: 'none', currentVersion: app.getVersion(), info });
// ✅ 没有更新,立刻启动 tkpage
launchPythonIfNeeded();
// 可隐藏更新窗口(让应用留在托盘+tkpage前台
setTimeout(() => updateWin?.hide(), 1200);
});
autoUpdater.on('download-progress', (p) => {
const percent = Math.max(0, Math.min(100, p?.percent || 0));
const transferred = Math.floor((p?.transferred || 0) / 1024 / 1024);
const total = Math.floor((p?.total || 0) / 1024 / 1024);
const speed = Math.floor((p?.bytesPerSecond || 0) / 1024 / 1024);
tray?.setToolTip(`YOLO - 正在下载更新:${percent.toFixed(1)}%`);
send('update:progress', { percent, transferred, total, speed });
});
autoUpdater.on('update-downloaded', (info) => {
console.info('[updater] Downloaded', info?.version);
tray?.setToolTip('YOLO - 更新已下载,等待安装…');
// 仅通知渲染进程显示“立即安装”按钮;不自动安装
send('update:status', { status: 'downloaded', version: info?.version });
// 可选择把更新窗口确保前置
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); }
});
autoUpdater.on('error', (err) => {
const msg = err?.message || String(err);
console.error('[updater] error', msg);
tray?.setToolTip('YOLO - 更新失败');
dialog.showErrorBox('自动更新失败', msg);
send('update:status', { status: 'error', message: msg });
// ⚠️ 出错:不阻断主流程,直接启动 tkpage
launchPythonIfNeeded();
});
// 渲染侧手动触发
ipcMain.on('updater:check', () => startUpdateCheck(/*force*/true));
ipcMain.on('updater:install-now', () => {
if (installTriggered) return;
installTriggered = true;
console.info('[updater] quitAndInstall requested by renderer');
tray?.setToolTip('YOLO - 正在安装更新…');
quitting = true;
try { killPythonGUI(false); } catch (e) {
console.warn('[updater] killPythonGUI error:', e?.message || e);
}
setTimeout(() => {
try {
autoUpdater.quitAndInstall(false, true);
} catch (e) {
installTriggered = false;
console.error('[updater] quitAndInstall error:', e?.message || e);
dialog.showErrorBox('安装失败', String(e?.message || e));
}
}, 300);
});
}
// ===== 触发更新检查(仅触发一次,托盘点“检查更新”可强制再触发) =====
function startUpdateCheck(force = false) {
if (updaterKicked && !force) return;
updaterKicked = true;
// 显示更新窗口(可选:你也可以只在有更新时显示)
if (!updateWin || updateWin.isDestroyed()) createUpdateWindow();
else { updateWin.show(); updateWin.focus(); }
autoUpdater.checkForUpdates().catch(() => { /* 错误会在 on('error') 里处理 */ });
}
// ===== 单例 & 生命周期 =====
process.on('unhandledRejection', (r) => console.error('[unhandledRejection]', r));
process.on('uncaughtException', (e) => console.error('[uncaughtException]', e));
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => { if (updateWin) { updateWin.show(); updateWin.focus(); } });
app.whenReady().then(() => {
if (process.platform === 'win32') {
app.setAppUserModelId('com.yolozs.newPage');
}
createTray();
createUpdateWindow();
wireUpdaterIpc();
// ✅ 启动时:只检查更新,不启动 tkpage。无更新 → 启动;有更新 → 下载并重启安装。
startUpdateCheck();
});
app.on('before-quit', () => { quitting = true; killPythonGUI(true); });
app.on('window-all-closed', () => { /* 后台常驻 */ });
}

6840
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "YOLO",
"productName": "YOLO助手",
"version": "4.7.0",
"main": "main.js",
"scripts": {
"dev": "electron .",
"build": "electron-builder"
},
"dependencies": {
"electron-log": "^5",
"electron-updater": "^6"
},
"devDependencies": {
"electron": "^31",
"electron-builder": "^24"
},
"build": {
"appId": "com.yolozs.newPage",
"productName": "YOLO助手",
"asar": true,
"icon": "icon.ico",
"win": {
"target": "nsis"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"perMachine": false,
"deleteAppDataOnUninstall": false
},
"publish": [
{
"provider": "generic",
"url": "http://tkNewPage.yolozs.com/updata/"
}
],
"extraResources": [
{
"from": "resources/python",
"to": "python",
"filter": [
"**/*"
]
}
]
}
}

13
preload.js Normal file
View File

@@ -0,0 +1,13 @@
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 主动触发检查
checkForUpdates: () => ipcRenderer.send('updater:check'),
// 安装更新
installNow: () => ipcRenderer.send('updater:install-now'),
// 订阅状态
onUpdateStatus: (cb) => ipcRenderer.on('update:status', (_e, payload) => cb(payload)),
// 订阅进度
onUpdateProgress: (cb) => ipcRenderer.on('update:progress', (_e, payload) => cb(payload)),
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More