feat(desktop): 新增 Electron 桌面端壳与自绘标题栏

- 新增 desktop/ Electron 工程:启动后端并等待 /api/health,就绪后加载页面;打包模式从 extraResources 读取 UI/后端
- 新增 DesktopTitleBar 组件,适配 frame:false 自绘标题栏,并修复桌面端 100vh 布局导致的外层滚动条
- chat 页面右侧布局调整更接近原生微信;detection-result 调试输出仅在 dev 环境启用
- .gitignore 忽略 desktop 构建产物/依赖,保留 .gitkeep 占位文件
- README 补充 Windows 桌面端 EXE 打包(npm run dist)与产物路径说明
This commit is contained in:
2977094657
2026-01-17 18:23:52 +08:00
parent 848847c162
commit 6eb161c726
15 changed files with 5793 additions and 6 deletions

View File

@@ -0,0 +1,40 @@
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const repoRoot = path.resolve(__dirname, "..", "..");
const entry = path.join(repoRoot, "src", "wechat_decrypt_tool", "backend_entry.py");
const distDir = path.join(repoRoot, "desktop", "resources", "backend");
const workDir = path.join(repoRoot, "desktop", "build", "pyinstaller");
const specDir = path.join(repoRoot, "desktop", "build", "pyinstaller-spec");
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(workDir, { recursive: true });
fs.mkdirSync(specDir, { recursive: true });
const nativeDir = path.join(repoRoot, "src", "wechat_decrypt_tool", "native");
const addData = `${nativeDir};wechat_decrypt_tool/native`;
const args = [
"run",
"pyinstaller",
"--noconfirm",
"--clean",
"--name",
"wechat-backend",
"--onefile",
"--distpath",
distDir,
"--workpath",
workDir,
"--specpath",
specDir,
"--add-data",
addData,
entry,
];
const r = spawnSync("uv", args, { cwd: repoRoot, stdio: "inherit" });
process.exit(r.status ?? 1);

View File

@@ -0,0 +1,49 @@
const fs = require("fs");
const path = require("path");
const pngToIco = require("png-to-ico").default;
const { PNG } = require("pngjs");
const repoRoot = path.resolve(__dirname, "..", "..");
const srcPng = path.join(repoRoot, "frontend", "public", "logo.png");
const dstIco = path.join(repoRoot, "desktop", "resources", "icon.ico");
async function main() {
if (!fs.existsSync(srcPng)) {
// eslint-disable-next-line no-console
console.error(`Logo not found: ${srcPng}`);
process.exit(1);
}
const raw = fs.readFileSync(srcPng);
const input = PNG.sync.read(raw);
const size = Math.max(input.width, input.height);
const square = new PNG({ width: size, height: size });
const dx = Math.floor((size - input.width) / 2);
const dy = Math.floor((size - input.height) / 2);
for (let y = 0; y < input.height; y += 1) {
const srcStart = y * input.width * 4;
const srcEnd = srcStart + input.width * 4;
const dstStart = ((y + dy) * size + dx) * 4;
input.data.copy(square.data, dstStart, srcStart, srcEnd);
}
const tmpDir = path.join(repoRoot, "desktop", "build", "icon");
fs.mkdirSync(tmpDir, { recursive: true });
const tmpPng = path.join(tmpDir, "logo-square.png");
fs.writeFileSync(tmpPng, PNG.sync.write(square));
const buf = await pngToIco(tmpPng);
fs.mkdirSync(path.dirname(dstIco), { recursive: true });
fs.writeFileSync(dstIco, buf);
// eslint-disable-next-line no-console
console.log(`Generated icon: ${dstIco}`);
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,24 @@
const fs = require("fs");
const path = require("path");
const repoRoot = path.resolve(__dirname, "..", "..");
const srcDir = path.join(repoRoot, "frontend", ".output", "public");
const dstDir = path.join(repoRoot, "desktop", "resources", "ui");
if (!fs.existsSync(path.join(srcDir, "index.html"))) {
// eslint-disable-next-line no-console
console.error(
`Nuxt static output not found at ${srcDir}. Run: npm --prefix frontend run generate`
);
process.exit(1);
}
fs.mkdirSync(dstDir, { recursive: true });
for (const ent of fs.readdirSync(dstDir, { withFileTypes: true })) {
if (ent.name === ".gitkeep") continue;
fs.rmSync(path.join(dstDir, ent.name), { recursive: true, force: true });
}
fs.cpSync(srcDir, dstDir, { recursive: true });
// eslint-disable-next-line no-console
console.log(`Copied UI: ${srcDir} -> ${dstDir}`);