feat: 初始化船长·HeiXiu 桌面应用项目(错误是svg的问题)

Electron + React 19 + TypeScript + Ant Design 6 + Tailwind CSS 4

  【核心架构】
  - Electron 主进程(窗口管理、平台检测、图标适配)
  - Vite 构建链(双产物 dist/ + dist-electron/)
  - 共享层 shared/(类型、常量、工具函数)
  - IPC 通信框架 + 安全守卫

  【主题系统】
  - 蓝紫渐变主题(亮色/暗色双模式)
  - Ant Design ConfigProvider 适配(lightAlgorithm / darkAlgorithm)
  - Tailwind CSS 4 @theme 指令 + CSS 变量
  - 设计令牌三处同步(tokens.ts / globals.css / antd-theme.ts)

  【导航栏】
  - 自定义 H5 导航栏替代系统菜单
  - 个人版/企业版双布局
  - 未登录拦截 + 事件总线驱动登录弹窗

  【登录注册】
  - Modal 弹层(Portal)+ Tabs 切换
  - 登录/注册表单动态字段渲染
  - 记住密码 / 自动登录 / 用户协议

  【更新系统】
  - 自定义 API 版本检查(替代 electron-updater generic provider)
  - 手动下载 + SHA512 校验 + spawn NSIS 安装
  - 渲染进程更新状态机 Hook(useUpdater)
  - 检查更新按钮 + 下载进度展示

  【构建工具链】
  - build.mjs 构建总管(--win/--mac/--linux --personal/--enterprise --build/--clean/--sign/--upload/--publish)
  - scripts/ 自动化脚本(clean / generate-update-info / upload-oss / publish-release)
  - electron-builder NSIS 安装器(可选路径、多版本打包)
  - update-info.json 自动生成(fileSize/sha512/changelog/releaseDate)

  【代码质量】
  - ESLint + TypeScript strict + Prettier
  - husky + lint-staged(提交前自动检查)
  - commitlint(规范提交信息)
  - Playwright E2E + Vitest 单元测试框架
  - rollup-plugin-visualizer 打包体积分析

  【文档】
  - README.md(快速开始 + 目录结构 + 命令速查)
  - UpdateA.md(发布手册:API/签名/发版流程/数据库)
  - CHANGELOG.md(更新日志)
  - .env.example(构建环境变量模板)
This commit is contained in:
2026-06-03 15:00:51 +08:00
commit 767bb8b297
95 changed files with 20814 additions and 0 deletions

324
build.mjs Normal file
View File

@@ -0,0 +1,324 @@
// ============================================================
// build.mjs — 构建总管
//
// 用法node build.mjs [选项]
//
// 平台(必选其一):
// --win Windows
// --mac macOS
// --linux Linux
//
// 版本(可选,默认 personal
// --personal 个人版
// --enterprise 企业版
//
// 操作(可选组合,默认只执行 --build
// --build 仅构建(默认)
// --clean 构建前清理 release/ dist/ dist-electron/
// --sign 代码签名
// --upload 上传安装包到 OSS
// --publish 发布版本信息到后端 API
// --all 全流程clean + build + sign + upload + publish
//
// 签名(仅 --sign 时可用):
// --cert <path> 证书文件路径 (.pfx)
// --cert-password <p> 证书密码
//
// 示例:
// node build.mjs --win --personal # 仅打包
// node build.mjs --win --personal --upload --publish # 打包 + 上传 + 发布
// node build.mjs --win --enterprise --all # 全流程
// node build.mjs --win --personal --sign --cert ./cert.pfx --cert-password xxx
// ============================================================
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = __dirname;
// ANSI 颜色工具(在其它代码之前定义,因为 printHelp() 在参数解析时就会调用)
const c = (code, text) => `\x1b[${code}m${text}\x1b[0m`;
const dim = (t) => c(2, t);
const bold = (t) => c(1, t);
const cyan = (t) => c(36, t);
const green = (t) => c(32, t);
const yellow = (t) => c(33, t);
// ---------- 参数解析 ----------
const args = process.argv.slice(2);
const opts = {
win: false,
mac: false,
linux: false,
personal: false,
enterprise: false,
clean: false,
build: false,
sign: false,
upload: false,
publish: false,
cert: '',
certPassword: '',
};
let i = 0;
while (i < args.length) {
const a = args[i];
switch (a) {
case '--win': opts.win = true; break;
case '--mac': opts.mac = true; break;
case '--linux': opts.linux = true; break;
case '--personal': opts.personal = true; break;
case '--enterprise': opts.enterprise = true; break;
case '--clean': opts.clean = true; break;
case '--build': opts.build = true; break;
case '--sign': opts.sign = true; break;
case '--upload': opts.upload = true; break;
case '--publish': opts.publish = true; break;
case '--all':
opts.clean = opts.build = opts.sign = opts.upload = opts.publish = true;
break;
case '--cert':
opts.cert = args[++i] || '';
break;
case '--cert-password':
opts.certPassword = args[++i] || '';
break;
case '--help': // fallthrough
case '-h':
printHelp();
process.exit(0);
break; // eslint 友好
default:
// 忽略未知参数
break;
}
i++;
}
// ---------- 校验 ----------
const platformCount = [opts.win, opts.mac, opts.linux].filter(Boolean).length;
if (platformCount !== 1) {
console.error('错误: 请指定且仅指定一个平台(--win / --mac / --linux');
process.exit(1);
}
const editionCount = [opts.personal, opts.enterprise].filter(Boolean).length;
if (editionCount === 0) {
opts.personal = true; // 默认个人版
} else if (editionCount > 1) {
console.error('错误: --personal 和 --enterprise 不能同时使用');
process.exit(1);
}
// 默认执行 build
if (!opts.clean && !opts.build && !opts.sign && !opts.upload && !opts.publish) {
opts.build = true;
}
const platform = opts.win ? 'win32' : opts.mac ? 'darwin' : 'linux';
const edition = opts.personal ? 'personal' : 'enterprise';
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const version = pkg.version;
// ---------- 工具函数 ----------
function run(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, {
cwd: ROOT,
stdio: 'inherit',
shell: true,
...opts,
env: { ...process.env, ...(opts.env || {}) },
});
child.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${cmd} ${args.join(' ')} 退出码: ${code}`));
});
child.on('error', reject);
});
}
function runScript(name, ...args) {
const scriptPath = join(ROOT, 'scripts', `${name}.mjs`);
return run('node', [scriptPath, ...args]);
}
function step(title) {
console.log(`\n${'='.repeat(60)}`);
console.log(` ${title}`);
console.log(`${'='.repeat(60)}\n`);
}
// ---------- 主流程 ----------
async function main() {
const electronBuilderPlatform = opts.win ? '--win' : opts.mac ? '--mac' : '--linux';
const editionEnv = { EDITION: edition };
console.log('┌──────────────────────────────────────┐');
console.log(`│ HeiXiu 构建 v${version} ${platform}/${edition}`.padEnd(45) + '│');
console.log('└──────────────────────────────────────┘');
console.log(`操作: ${[
opts.clean && '清理',
opts.build && '构建',
opts.sign && '签名',
opts.upload && '上传',
opts.publish && '发布',
].filter(Boolean).join(' → ')}`);
// ── Step 1: 清理 ──
if (opts.clean) {
step('1/ 清理构建产物');
await runScript('clean');
}
// ── Step 2: 构建 ──
if (opts.build) {
step('2/ 构建应用');
// 签名环境变量electron-builder 原生支持 CSC_LINK / CSC_KEY_PASSWORD
const signEnv = {};
if (opts.sign) {
if (opts.cert) signEnv.CSC_LINK = opts.cert;
if (opts.certPassword) signEnv.CSC_KEY_PASSWORD = opts.certPassword;
console.log(opts.cert ? `签名证书: ${opts.cert}` : '签名: 使用系统证书存储');
}
// 2a. TypeScript 编译
console.log('[tsc] 类型检查...');
await run('npx', ['tsc'], { env: editionEnv });
// 2b. Vite 构建
console.log('[vite] 打包前端 + Electron...');
await run('npx', ['vite', 'build'], { env: editionEnv });
// 2c. electron-builder 打包
console.log(`[electron-builder] ${electronBuilderPlatform} 打包...`);
await run('npx', ['electron-builder', electronBuilderPlatform], {
env: { ...editionEnv, ...signEnv },
});
// 2d. 生成 update-info.json
console.log('[info] 生成 update-info.json...');
await runScript('generate-update-info', platform, edition, version);
}
// ── Step 3: 上传 OSS ──
if (opts.upload) {
step('3/ 上传 OSS');
await runScript('upload-oss', platform, edition, version);
}
// ── Step 4: 发布到 API ──
if (opts.publish) {
step('4/ 发布版本到 API');
await runScript('publish-release', platform, edition, version);
}
console.log('\n✅ 全部完成');
}
function printHelp() {
const W = 62;
const box = (title) => {
const pad = Math.max(0, W - 4 - title.length);
console.log(`\n${cyan('┌')}${cyan('─'.repeat(W - 2))}${cyan('┐')}`);
console.log(`${cyan('│')} ${bold(title)}${' '.repeat(pad)}${cyan('│')}`);
console.log(`${cyan('└')}${cyan('─'.repeat(W - 2))}${cyan('┘')}`);
};
const row = (flag, desc) => {
console.log(` ${green(flag.padEnd(22))}${dim(desc)}`);
};
const hdr = (t) => console.log(`\n ${bold(yellow(t))}`);
console.log('');
console.log(cyan(' ╔') + cyan('═══════════════════════════════════════════════════') + cyan('╗'));
console.log(cyan(' ║') + bold(' 船长·HeiXiu — 构建工具') + ' ' + cyan('║'));
console.log(cyan(' ╚') + cyan('═══════════════════════════════════════════════════') + cyan('╝'));
// 用法
box('用法');
console.log(` ${bold('node build.mjs')} ${dim('<平台> [版本] [操作...]')}`);
console.log(` ${bold('npm run build:win')} ${dim('— 等效快捷命令')}`);
// 平台
hdr('平台(必选其一)');
row('--win', 'Windows');
row('--mac', 'macOS');
row('--linux', 'Linux');
// 版本
hdr('版本(可选,默认 --personal');
row('--personal', '个人版');
row('--enterprise', '企业版');
// 操作
hdr('操作(可选组合,不指定则默认 --build');
row('--build', '构建应用tsc + vite + electron-builder + info');
row('--clean', '构建前清理 release/ dist/ dist-electron/');
row('--sign', '代码签名(需配合 --cert 或环境变量)');
row('--upload', '上传安装包到 OSS');
row('--publish', '发布版本信息到后端 API');
row('--all', '全流程 = clean + build + sign + upload + publish');
// 签名
hdr('签名参数(配合 --sign');
row('--cert <path>', '证书文件路径(.pfx / .p12');
row('--cert-password <p>', '证书密码');
console.log(` ${dim('也可通过 CSC_LINK / CSC_KEY_PASSWORD 环境变量设置')}`);
// 示例
box('示例');
console.log(` ${dim('# 日常开发')}`);
console.log(` ${bold('node build.mjs --win --personal')}`);
console.log(` ${bold('npm run build:win')} ${dim('— 等效')}`);
console.log('');
console.log(` ${dim('# 打包 + 上传 OSS + 发布到 API')}`);
console.log(` ${bold('node build.mjs --win --personal --upload --publish')}`);
console.log('');
console.log(` ${dim('# 全流程(清理 → 构建 → 签名 → 上传 → 发布)')}`);
console.log(` ${bold('node build.mjs --win --enterprise --all --cert ./cert.pfx --cert-password xxx')}`);
console.log('');
console.log(` ${dim('# 仅清理')}`);
console.log(` ${bold('npm run build:clean')}`);
// 环境变量
box('环境变量');
console.log(` ${yellow('UPDATE_API_URL')} ${dim('版本检查 API 基地址')}`);
console.log(` ${yellow('UPDATE_API_KEY')} ${dim('发布 API 鉴权 Key')}`);
console.log(` ${yellow('UPDATE_DOWNLOAD_BASE')} ${dim('OSS 下载基地址(预判 URL 用)')}`);
console.log(` ${yellow('OSS_ENDPOINT')} ${dim('阿里云 OSS endpoint')}`);
console.log(` ${yellow('OSS_BUCKET')} ${dim('阿里云 OSS bucket 名称')}`);
console.log(` ${yellow('OSS_ACCESS_KEY_ID')} ${dim('阿里云 AccessKey ID')}`);
console.log(` ${yellow('OSS_ACCESS_KEY_SECRET')} ${dim('阿里云 AccessKey Secret')}`);
console.log(` ${yellow('CSC_LINK')} ${dim('签名证书路径(与 --cert 等效)')}`);
console.log(` ${yellow('CSC_KEY_PASSWORD')} ${dim('签名证书密码(与 --cert-password 等效)')}`);
// 快捷命令
box('npm 快捷命令');
console.log(` ${bold('npm run build:win')} ${dim('→ node build.mjs --win --personal --build')}`);
console.log(` ${bold('npm run build:win:enterprise')} ${dim('→ node build.mjs --win --enterprise --build')}`);
console.log(` ${bold('npm run build:mac')} ${dim('→ node build.mjs --mac --personal --build')}`);
console.log(` ${bold('npm run build:linux')} ${dim('→ node build.mjs --linux --personal --build')}`);
console.log(` ${bold('npm run build:clean')} ${dim('→ node scripts/clean.mjs')}`);
console.log(` ${bold('npm run upload:oss')} ${dim('→ node scripts/upload-oss.mjs')}`);
console.log(` ${bold('npm run publish:release')} ${dim('→ node scripts/publish-release.mjs')}`);
console.log('');
}
// ---------- 入口 ----------
main().catch((err) => {
console.error(`\n❌ 构建失败: ${err.message}`);
process.exit(1);
});