feat(0.2.1): 合并 Windows 外壳 + 离线编辑, 版本 0.2.1, 正式域名 https://iboard.honor3.com

This commit is contained in:
jif2.zhang
2026-10-26 20:53:38 +08:00
parent e689e44923
commit eef296942f
74 changed files with 2734 additions and 437 deletions
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env node
// Windows 外壳一键构建 (开发用): 安装包 (NSIS) + 便携版 (portable zip)
//
// 用法:
// npm run build:win # 完整流程
// node scripts/build-windows.mjs --skip-check # 跳过版本一致性校验
// node scripts/build-windows.mjs --debug # debug 构建 (快速验证, 产物带 -debug 后缀)
//
// 流程:
// 1) 版本一致性校验 (scripts/ci/check-version.mjs, 外壳三件套)
// 2) 前端构建 (build:shell, 注入 VITE_API_BASE 指向远端后端)
// 3) tauri build --bundles nsis → 编译一次 release exe + NSIS 安装包
// 4) 复用同一个 exe 组装便携版目录并压成 zip (不二次编译)
//
// 产物 (artifacts/windows/):
// iBoard_<ver>_x64-setup.exe 安装包 (当前用户安装, 免管理员)
// iBoard_<ver>_x64-portable.zip 便携版 (解压即用)
// portable/ 便携版解压目录 (iBoard.exe + README.txt)
//
// 说明: 便携版不内置 WebView2 —— 目标机需 Win10 19041+ / Win11 (系统自带 WebView2 Runtime)。
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2)
const skipCheck = args.includes('--skip-check')
const isDebug = args.includes('--debug')
const profile = isDebug ? 'debug' : 'release'
const conf = JSON.parse(fs.readFileSync(path.join(root, 'src-tauri', 'tauri.conf.json'), 'utf8'))
const version = String(conf.version || '0.0.0')
const product = conf.productName || 'iBoard'
const suffix = isDebug ? '-debug' : ''
const artifactsDir = path.join(root, 'artifacts', 'windows')
const targetDir = path.join(root, 'src-tauri', 'target', profile)
function log(msg) {
console.log(`\x1b[36m[build-win]\x1b[0m ${msg}`)
}
function fail(msg) {
console.error(`\x1b[31m[build-win] 失败: ${msg}\x1b[0m`)
process.exit(1)
}
function run(cmd, cmdArgs, opts = {}) {
log(`$ ${cmd} ${cmdArgs.join(' ')}`)
const res = spawnSync(cmd, cmdArgs, {
cwd: root,
stdio: 'inherit',
shell: process.platform === 'win32',
...opts,
})
if (res.status !== 0) fail(`${cmd} ${cmdArgs.join(' ')} 退出码 ${res.status}`)
}
// ---------- 0. 前置检查 ----------
if (process.platform !== 'win32') fail('本脚本仅支持 Windows')
if (!fs.existsSync(path.join(root, 'node_modules'))) fail('缺少 node_modules, 请先运行 npm ci')
log(`项目 ${product} 外壳版本 ${version} (${profile})`)
// ---------- 1. 版本一致性 ----------
if (!skipCheck) {
log('校验版本一致性 (外壳 tauri.conf.json / Cargo.toml / Cargo.lock)')
run('node', ['scripts/ci/check-version.mjs'], {
env: { ...process.env, BRANCH: process.env.BRANCH || '' },
})
} else {
log('跳过版本一致性校验 (--skip-check)')
}
// ---------- 2. 前端构建 ----------
log('构建前端 (build:shell, VITE_API_BASE 指向远端后端)')
run('npm', ['run', 'build:shell'])
// ---------- 3. 安装包 (NSIS) ----------
const tauriArgs = ['tauri', 'build']
if (isDebug) tauriArgs.push('--debug')
// 只出 NSIS: 避免 bundle.targets="all" 在 Windows 上拉 WiX 打 MSI
tauriArgs.push('--bundles', 'nsis')
log('编译外壳并生成 NSIS 安装包')
run('npx', tauriArgs)
// ---------- 4. 便携版 ----------
const exePath = path.join(targetDir, `iboard${suffix}.exe`)
if (!fs.existsSync(exePath)) fail(`未找到构建产物 ${exePath}`)
fs.rmSync(artifactsDir, { recursive: true, force: true })
fs.mkdirSync(artifactsDir, { recursive: true })
const portableDir = path.join(artifactsDir, 'portable')
fs.mkdirSync(portableDir, { recursive: true })
const portableExe = path.join(portableDir, `${product}.exe`)
fs.copyFileSync(exePath, portableExe)
const readme = `${product} ${version} 便携版 (Windows x64)
=================================================
免安装: 双击 ${product}.exe 即可运行。
系统要求
- Windows 10 19041+ 或 Windows 11 (x64)
- WebView2 Runtime: Win10/11 系统自带; 若提示缺失, 请安装
https://developer.microsoft.com/microsoft-edge/webview2/
使用说明
- 关闭窗口 = 隐藏到托盘, 不会退出; 退出请用托盘右键菜单「退出」。
- 窗口贴屏幕边缘会自动收起为半圆悬浮球, 鼠标停留或单击唤回。
- 托盘菜单可开关「开机启动」「边缘吸附」。
- 配置保存在 %APPDATA%\\com.iboard.app\\shell-config.json (删除即恢复默认)。
注意: 便携版不写注册表、不装服务; 但若开启了「开机启动」, 会在
HKCU\\...\\Run 写入自启动项, 关闭开关即可移除。
`
fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8')
const zipName = `${product}_${version}_x64-portable${suffix}.zip`
const zipPath = path.join(artifactsDir, zipName)
log('打包便携版 zip')
const psZip = `Compress-Archive -Path '${portableDir}\\*' -DestinationPath '${zipPath}' -Force`
const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psZip], {
cwd: root,
stdio: 'inherit',
})
if (res.status !== 0) fail('压缩便携版失败 (Compress-Archive)')
// ---------- 5. 汇总 ----------
const setupExe = path.join(
targetDir,
'bundle',
'nsis',
`${product}_${version}_x64-setup${suffix}.exe`,
)
const outputs = []
if (fs.existsSync(setupExe)) {
const dest = path.join(artifactsDir, path.basename(setupExe))
fs.copyFileSync(setupExe, dest)
outputs.push(['安装包 (NSIS)', dest])
} else {
outputs.push(['安装包 (NSIS)', `未找到: ${setupExe}`])
}
outputs.push(['便携版 (zip)', zipPath])
outputs.push(['便携版 (目录)', `${portableDir} (${fs.readdirSync(portableDir).length} 个文件)`])
log('构建完成:')
for (const [label, p] of outputs) {
const size = fs.existsSync(p) ? `${(fs.statSync(p).size / 1024 / 1024).toFixed(1)} MB` : ''
console.log(` ${label.padEnd(14)} ${p}${size ? ' (' + size + ')' : ''}`)
}
process.exit(0)