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:
27
electron/electron-env.d.ts
vendored
Normal file
27
electron/electron-env.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types="vite-plugin-electron/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The built directory structure
|
||||
*
|
||||
* ```tree
|
||||
* ├─┬─┬ dist
|
||||
* │ │ └── index.html
|
||||
* │ │
|
||||
* │ ├─┬ dist-electron
|
||||
* │ │ ├── main.js
|
||||
* │ │ └── preload.js
|
||||
* │
|
||||
* ```
|
||||
*/
|
||||
APP_ROOT: string;
|
||||
/** /dist/ or /public/ */
|
||||
VITE_PUBLIC: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
interface Window {
|
||||
ipcRenderer: import('electron').IpcRenderer;
|
||||
}
|
||||
126
electron/main.ts
Normal file
126
electron/main.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
import { getPlatform, isMacOS } from './main/utils/platform';
|
||||
import { getWindowIconPath } from './main/utils/logo';
|
||||
import { buildWindowTitle, parseEdition } from '../shared/constants/app';
|
||||
import { initUpdater, registerUpdateIpcHandlers } from './main/updater';
|
||||
import { setupAppMenu } from './main/menu';
|
||||
|
||||
createRequire(import.meta.url);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
process.env.APP_ROOT = path.join(__dirname, '..');
|
||||
|
||||
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'];
|
||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron');
|
||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
|
||||
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||
? path.join(process.env.APP_ROOT, 'public')
|
||||
: RENDERER_DIST;
|
||||
|
||||
// ---------- 平台 & 版本信息 ----------
|
||||
const currentPlatform = getPlatform();
|
||||
const currentEdition = parseEdition(process.env.EDITION);
|
||||
const WINDOW_TITLE = buildWindowTitle(currentPlatform, currentEdition);
|
||||
|
||||
// ============================================================
|
||||
// 窗口引用
|
||||
// ============================================================
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
// ============================================================
|
||||
// 主窗口
|
||||
// ============================================================
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const iconPath = getWindowIconPath(app.getAppPath());
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: WINDOW_TITLE,
|
||||
icon: iconPath,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
show: false,
|
||||
...(isMacOS() ? { titleBarStyle: 'hiddenInset' as const } : {}),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.setTitle(WINDOW_TITLE);
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// macOS 全屏事件
|
||||
if (isMacOS()) {
|
||||
mainWindow.on('enter-full-screen', () =>
|
||||
mainWindow?.webContents.send('window-fullscreen-changed', true),
|
||||
);
|
||||
mainWindow.on('leave-full-screen', () =>
|
||||
mainWindow?.webContents.send('window-fullscreen-changed', false),
|
||||
);
|
||||
}
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString());
|
||||
mainWindow?.webContents.send('platform-info', {
|
||||
platform: currentPlatform,
|
||||
edition: currentEdition,
|
||||
});
|
||||
});
|
||||
|
||||
// ready-to-show 后显示,避免白屏闪烁
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow?.show();
|
||||
});
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(VITE_DEV_SERVER_URL);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(RENDERER_DIST, 'index.html'));
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 应用生命周期
|
||||
// ============================================================
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (!isMacOS()) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// 清理工作
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
setupAppMenu();
|
||||
app.setName(WINDOW_TITLE);
|
||||
registerUpdateIpcHandlers();
|
||||
|
||||
// 始终打开主窗口
|
||||
// 登录/注册由渲染进程内的 Modal 弹层处理,不再新开窗口
|
||||
const win = createMainWindow();
|
||||
initUpdater(win);
|
||||
});
|
||||
58
electron/main/menu/index.ts
Normal file
58
electron/main/menu/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// ============================================================
|
||||
// 最小化系统菜单
|
||||
//
|
||||
// 策略:已全面采用 H5 自定义导航栏,系统菜单仅保留 macOS 必需的 Edit 菜单
|
||||
// (否则 Cmd+C/V/A 等系统级快捷键会失效)。
|
||||
//
|
||||
// macOS → 仅 Edit 菜单(undo/redo/cut/copy/paste/selectAll)
|
||||
// Windows → 完全移除系统菜单栏
|
||||
// Linux → 完全移除系统菜单栏
|
||||
// ============================================================
|
||||
|
||||
import { app, Menu, type MenuItemConstructorOptions } from 'electron';
|
||||
import { isMacOS } from '../utils/platform';
|
||||
import { APP_NAME } from '../../../shared/constants/app';
|
||||
import { APP_VERSION } from '../../../shared/constants/version';
|
||||
|
||||
/**
|
||||
* macOS 最小 Edit 菜单
|
||||
* 必须保留,否则 Cmd+C / Cmd+V / Cmd+A / Cmd+Z 等快捷键全部失效
|
||||
*/
|
||||
function buildMinimalEditMenu(): MenuItemConstructorOptions {
|
||||
return {
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ label: '撤销', accelerator: 'Cmd+Z', role: 'undo' },
|
||||
{ label: '重做', accelerator: 'Shift+Cmd+Z', role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ label: '剪切', accelerator: 'Cmd+X', role: 'cut' },
|
||||
{ label: '复制', accelerator: 'Cmd+C', role: 'copy' },
|
||||
{ label: '粘贴', accelerator: 'Cmd+V', role: 'paste' },
|
||||
{ label: '全选', accelerator: 'Cmd+A', role: 'selectAll' },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置应用菜单
|
||||
* macOS → 最小 Edit 菜单
|
||||
* Windows/Linux → 无菜单(导航栏由 H5 组件实现)
|
||||
*/
|
||||
export function setupAppMenu(): void {
|
||||
if (isMacOS()) {
|
||||
const template: MenuItemConstructorOptions[] = [buildMinimalEditMenu()];
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
} else {
|
||||
// Windows / Linux:完全移除菜单栏
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
|
||||
// 关于面板信息(macOS 原生 About / Windows 自定义)
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: APP_NAME,
|
||||
applicationVersion: APP_VERSION,
|
||||
version: '',
|
||||
credits: 'Powered by Electron + React 19 + TypeScript',
|
||||
});
|
||||
}
|
||||
316
electron/main/updater.ts
Normal file
316
electron/main/updater.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
// ============================================================
|
||||
// 自动更新模块 — 自定义 API 版本检查 + OSS 文件下载
|
||||
//
|
||||
// 架构:
|
||||
// 客户端 → GET /api/v1/update/check → ECS (API) → 返回 JSON
|
||||
// 客户端 → GET <downloadUrl> → OSS → 下载安装包
|
||||
// 下载完成 → SHA512 校验 → spawn 安装器 → app.quit()
|
||||
//
|
||||
// 与旧版 (electron-updater generic provider) 的区别:
|
||||
// - 版本检查走自定义 API,后端可做灰度/强更/统计
|
||||
// - 不再依赖 latest.yml 静态文件
|
||||
// - 安装包仍托管于 OSS,下载地址由 API 动态返回
|
||||
//
|
||||
// 环境变量:
|
||||
// UPDATE_API_URL — 版本检查 API 基地址(默认 https://www.heixiu.com)
|
||||
// ============================================================
|
||||
|
||||
import { BrowserWindow, net, app, ipcMain } from 'electron';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { unlink } from 'node:fs/promises';
|
||||
|
||||
import type { UpdateCheckResult } from '../../shared/types';
|
||||
import { APP_VERSION } from '../../shared/constants/version';
|
||||
|
||||
// ---------- IPC 通道(对渲染进程暴露,与旧版兼容)----------
|
||||
|
||||
export const UPDATE_CHANNELS = {
|
||||
UPDATE_AVAILABLE: 'update-available',
|
||||
UPDATE_PROGRESS: 'update-progress',
|
||||
UPDATE_DOWNLOADED: 'update-downloaded',
|
||||
UPDATE_ERROR: 'update-error',
|
||||
UPDATE_NOT_AVAILABLE: 'update-not-available',
|
||||
INSTALL_UPDATE: 'install-update',
|
||||
CHECK_FOR_UPDATE: 'check-for-update',
|
||||
} as const;
|
||||
|
||||
// ---------- 配置 ----------
|
||||
|
||||
function getApiBaseUrl(): string {
|
||||
return process.env.UPDATE_API_URL || 'https://www.heixiu.com';
|
||||
}
|
||||
|
||||
// ---------- 状态 ----------
|
||||
|
||||
let updateWindow: BrowserWindow | null = null;
|
||||
let updateChecked = false;
|
||||
/** 当前正在下载的临时文件路径(用于安装后清理) */
|
||||
let pendingInstallerPath: string | null = null;
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
|
||||
export function initUpdater(mainWindow: BrowserWindow): void {
|
||||
updateWindow = mainWindow;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Updater] 开发模式,跳过自动检查更新。API:', getApiBaseUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动 5 秒后静默检查
|
||||
setTimeout(() => {
|
||||
checkForUpdates();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 版本检查(调用 API)
|
||||
// ============================================================
|
||||
|
||||
async function fetchUpdateInfo(): Promise<UpdateCheckResult | null> {
|
||||
const platform = process.platform; // 'win32' | 'darwin' | 'linux'
|
||||
const edition = process.env.EDITION || 'personal';
|
||||
const apiUrl = `${getApiBaseUrl()}/api/v1/update/check?platform=${platform}&version=${APP_VERSION}&edition=${edition}`;
|
||||
|
||||
console.log('[Updater] 请求版本检查 API:', apiUrl);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = net.request({ method: 'GET', url: apiUrl });
|
||||
|
||||
req.on('response', (response) => {
|
||||
let body = '';
|
||||
response.on('data', (chunk) => (body += chunk.toString()));
|
||||
response.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
|
||||
// 兼容两种 API 响应格式:
|
||||
// 格式 A: { code: 0, data: UpdateCheckResult }
|
||||
// 格式 B: UpdateCheckResult 直接返回
|
||||
const data: UpdateCheckResult = json.data ?? json;
|
||||
|
||||
if (!data.hasUpdate) {
|
||||
console.log('[Updater] 当前已是最新版本');
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
console.log('[Updater] 发现新版本:', data.latestVersion);
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(new Error(`API 响应解析失败: ${(err as Error).message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => reject(new Error(`API 请求失败: ${err.message}`)));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 下载安装包
|
||||
// ============================================================
|
||||
|
||||
async function downloadInstaller(info: UpdateCheckResult): Promise<string> {
|
||||
const ext =
|
||||
process.platform === 'win32' ? '.exe' : process.platform === 'darwin' ? '.dmg' : '.AppImage';
|
||||
const tempPath = join(tmpdir(), `HeiXiu-${info.latestVersion}-update${ext}`);
|
||||
|
||||
console.log('[Updater] 开始下载:', info.downloadUrl);
|
||||
console.log('[Updater] 临时文件:', tempPath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = net.request({ method: 'GET', url: info.downloadUrl });
|
||||
const fileStream = createWriteStream(tempPath);
|
||||
const hash = createHash('sha512');
|
||||
const totalSize = info.fileSize || 0;
|
||||
let downloadedSize = 0;
|
||||
let lastReportTime = 0;
|
||||
|
||||
req.on('response', (response) => {
|
||||
const statusCode = response.statusCode;
|
||||
if (statusCode !== 200) {
|
||||
fileStream.close();
|
||||
unlink(tempPath).catch(() => {});
|
||||
return reject(new Error(`下载失败,HTTP ${statusCode}`));
|
||||
}
|
||||
|
||||
response.on('data', (chunk: Buffer) => {
|
||||
fileStream.write(chunk);
|
||||
hash.update(chunk);
|
||||
downloadedSize += chunk.length;
|
||||
|
||||
// 限频:最多每 250ms 推送一次进度
|
||||
const now = Date.now();
|
||||
if (totalSize > 0 && now - lastReportTime >= 250) {
|
||||
lastReportTime = now;
|
||||
const percent = Math.round((downloadedSize / totalSize) * 100);
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_PROGRESS, {
|
||||
percent,
|
||||
bytesPerSecond: 0, // 简化处理,不计算实时速度
|
||||
transferred: downloadedSize,
|
||||
total: totalSize,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
fileStream.end();
|
||||
fileStream.on('finish', () => {
|
||||
// SHA512 校验
|
||||
const actualSha512 = hash.digest('base64');
|
||||
if (info.sha512 && actualSha512 !== info.sha512) {
|
||||
unlink(tempPath).catch(() => {});
|
||||
return reject(
|
||||
new Error(
|
||||
`SHA512 校验失败\n期望: ${info.sha512.slice(0, 16)}...\n实际: ${actualSha512.slice(0, 16)}...`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Updater] 下载完成,SHA512 校验通过');
|
||||
pendingInstallerPath = tempPath;
|
||||
resolve(tempPath);
|
||||
});
|
||||
});
|
||||
|
||||
response.on('error', (err) => {
|
||||
fileStream.close();
|
||||
unlink(tempPath).catch(() => {});
|
||||
reject(new Error(`下载中断: ${err.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
fileStream.close();
|
||||
unlink(tempPath).catch(() => {});
|
||||
reject(new Error(`下载请求失败: ${err.message}`));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 操作方法
|
||||
// ============================================================
|
||||
|
||||
export async function checkForUpdates(): Promise<void> {
|
||||
if (updateChecked) {
|
||||
console.log('[Updater] 已检查过更新,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Updater] 开发模式,跳过更新检查');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateChecked = true;
|
||||
const info = await fetchUpdateInfo();
|
||||
|
||||
if (!info) {
|
||||
// 无更新,不发通知(静默)
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知渲染进程:有新版本
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, {
|
||||
version: info.latestVersion,
|
||||
releaseDate: info.releaseDate,
|
||||
releaseNotes: info.changelog,
|
||||
forceUpdate: info.forceUpdate,
|
||||
});
|
||||
|
||||
// 自动开始下载
|
||||
await downloadInstaller(info);
|
||||
|
||||
// 下载完成 → 通知渲染进程
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED);
|
||||
} catch (error) {
|
||||
console.error('[Updater] 更新流程失败:', (error as Error).message);
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
|
||||
message: (error as Error).message || '更新检查失败',
|
||||
});
|
||||
updateChecked = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUpdatesManual(): Promise<void> {
|
||||
try {
|
||||
updateChecked = false;
|
||||
const info = await fetchUpdateInfo();
|
||||
|
||||
if (!info) {
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE);
|
||||
return;
|
||||
}
|
||||
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, {
|
||||
version: info.latestVersion,
|
||||
releaseDate: info.releaseDate,
|
||||
releaseNotes: info.changelog,
|
||||
forceUpdate: info.forceUpdate,
|
||||
});
|
||||
|
||||
await downloadInstaller(info);
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED);
|
||||
} catch (error) {
|
||||
console.error('[Updater] 手动更新失败:', (error as Error).message);
|
||||
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
|
||||
message: (error as Error).message || '检查更新失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function installUpdateNow(): void {
|
||||
if (!pendingInstallerPath) {
|
||||
console.error('[Updater] 没有待安装的更新文件');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Updater] 开始安装更新:', pendingInstallerPath);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: NSIS 安装器,/S 静默安装
|
||||
spawn(pendingInstallerPath, ['/S'], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} else if (process.platform === 'darwin') {
|
||||
// macOS: 挂载 DMG
|
||||
spawn('hdiutil', ['attach', pendingInstallerPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} else {
|
||||
// Linux: AppImage,chmod +x 后执行(通常需要手动替换)
|
||||
console.log('[Updater] Linux 更新请手动替换 AppImage 文件');
|
||||
}
|
||||
|
||||
// 退出应用,让安装器接管
|
||||
setImmediate(() => {
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IPC 注册
|
||||
// ============================================================
|
||||
|
||||
export function registerUpdateIpcHandlers(): void {
|
||||
ipcMain.handle(UPDATE_CHANNELS.CHECK_FOR_UPDATE, async () => {
|
||||
await checkForUpdatesManual();
|
||||
});
|
||||
|
||||
ipcMain.on(UPDATE_CHANNELS.INSTALL_UPDATE, () => {
|
||||
installUpdateNow();
|
||||
});
|
||||
}
|
||||
100
electron/main/utils/logo.ts
Normal file
100
electron/main/utils/logo.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// ============================================================
|
||||
// 主进程 Logo 自适应工具
|
||||
// 根据平台返回正确的图标路径,处理 .ico / .png / .icns 格式差异
|
||||
// ============================================================
|
||||
|
||||
import path from 'node:path';
|
||||
import { isMacOS, isWindows } from './platform';
|
||||
|
||||
// Logo 资源目录(相对于构建后的 dist-electron 目录或开发时的 electron/ 目录)
|
||||
// 开发时 app.getAppPath() 指向项目根目录
|
||||
// 生产时指向 app.asar 所在目录
|
||||
|
||||
/** Logo 目录在项目中的相对路径 */
|
||||
const LOGO_DIR = 'src/assets/logo';
|
||||
|
||||
/**
|
||||
* 获取 Logo 资源目录的绝对路径
|
||||
* @param appPath - Electron app.getAppPath() 的返回值
|
||||
*/
|
||||
export function getLogoDir(appPath: string): string {
|
||||
return path.join(appPath, LOGO_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按平台获取窗口图标路径
|
||||
*
|
||||
* 平台格式要求:
|
||||
* Windows → .ico(支持多尺寸)
|
||||
* macOS → .icns(首选)或 .png(回退)
|
||||
* Linux → .png
|
||||
*
|
||||
* @param appPath - Electron app.getAppPath()
|
||||
* @returns 图标文件的绝对路径
|
||||
*/
|
||||
export function getWindowIconPath(appPath: string): string {
|
||||
const logoDir = path.join(appPath, LOGO_DIR);
|
||||
|
||||
if (isWindows()) {
|
||||
// Windows 首选 .ico
|
||||
return path.join(logoDir, 'heixiu.ico');
|
||||
}
|
||||
|
||||
if (isMacOS()) {
|
||||
return path.join(logoDir, 'heixiu.icns');
|
||||
}
|
||||
|
||||
// Linux 使用 .png
|
||||
return path.join(logoDir, 'HX.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 按平台获取系统托盘图标路径
|
||||
* 托盘图标通常用 .png(跨平台通用),且尺寸较小
|
||||
*
|
||||
* @param appPath - Electron app.getAppPath()
|
||||
* @returns 托盘图标绝对路径
|
||||
*/
|
||||
export function getTrayIconPath(appPath: string): string {
|
||||
const logoDir = path.join(appPath, LOGO_DIR);
|
||||
|
||||
if (isWindows()) {
|
||||
// Windows 托盘可用 .ico 或小尺寸 .png
|
||||
return path.join(logoDir, 'heixiu.ico');
|
||||
}
|
||||
|
||||
// macOS / Linux 托盘用 .png
|
||||
return path.join(logoDir, 'HX.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 按平台获取应用安装包图标路径
|
||||
* electron-builder 打包时使用,各平台格式不同:
|
||||
* Windows → .ico
|
||||
* macOS → .icns
|
||||
* Linux → .png(通常 256x256 或 512x512)
|
||||
*
|
||||
* @param appPath - 应用根目录路径
|
||||
* @returns 安装包图标的绝对路径
|
||||
*/
|
||||
export function getInstallerIconPath(appPath: string): string {
|
||||
const logoDir = path.join(appPath, LOGO_DIR);
|
||||
|
||||
if (isWindows()) {
|
||||
return path.join(logoDir, 'heixiu.ico');
|
||||
}
|
||||
|
||||
if (isMacOS()) {
|
||||
return path.join(logoDir, 'heixiu.icns');
|
||||
}
|
||||
|
||||
// Linux
|
||||
return path.join(logoDir, 'HX.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用 PNG Logo(不区分平台,用于 About 对话框等)
|
||||
*/
|
||||
export function getPngLogoPath(appPath: string): string {
|
||||
return path.join(appPath, LOGO_DIR, 'HX.png');
|
||||
}
|
||||
53
electron/main/utils/platform.ts
Normal file
53
electron/main/utils/platform.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// ============================================================
|
||||
// 主进程平台判定工具 —— 直接使用 Node.js process.platform
|
||||
// ============================================================
|
||||
|
||||
import type { Platform } from '../../../shared/types';
|
||||
|
||||
/** 获取当前平台标识 */
|
||||
export function getPlatform(): Platform {
|
||||
return process.platform as Platform;
|
||||
}
|
||||
|
||||
/** 是否为 macOS */
|
||||
export function isMacOS(): boolean {
|
||||
return process.platform === 'darwin';
|
||||
}
|
||||
|
||||
/** 是否为 Windows */
|
||||
export function isWindows(): boolean {
|
||||
return process.platform === 'win32';
|
||||
}
|
||||
|
||||
/** 是否为 Linux */
|
||||
export function isLinux(): boolean {
|
||||
return process.platform === 'linux';
|
||||
}
|
||||
|
||||
/** 获取平台名称(中文) */
|
||||
export function getPlatformName(): string {
|
||||
if (isMacOS()) return 'macOS';
|
||||
if (isWindows()) return 'Windows';
|
||||
if (isLinux()) return 'Linux';
|
||||
return '未知平台';
|
||||
}
|
||||
|
||||
/** 获取平台专用的快捷键修饰符描述(macOS 用 ⌘,Windows/Linux 用 Ctrl) */
|
||||
export function getShortcutModifier(): string {
|
||||
return isMacOS() ? 'Cmd' : 'Ctrl';
|
||||
}
|
||||
|
||||
/** 判断是否需要在任务栏显示图标(macOS 不需要,Windows/Linux 需要) */
|
||||
export function shouldShowTaskbarIcon(): boolean {
|
||||
return !isMacOS();
|
||||
}
|
||||
|
||||
/** 获取用户数据目录的基础路径 */
|
||||
export function getUserDataPath(): string {
|
||||
// Electron app.getPath('userData') 在各平台自动适配
|
||||
// 此处仅作为工具函数的补充
|
||||
const home = process.env.HOME || process.env.USERPROFILE || '';
|
||||
if (isMacOS()) return `${home}/Library/Application Support`;
|
||||
if (isWindows()) return process.env.APPDATA || `${home}\\AppData\\Roaming`;
|
||||
return `${home}/.config`;
|
||||
}
|
||||
24
electron/preload.ts
Normal file
24
electron/preload.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ipcRenderer, contextBridge } from 'electron';
|
||||
|
||||
// --------- Expose some API to the Renderer process ---------
|
||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
on(...args: Parameters<typeof ipcRenderer.on>) {
|
||||
const [channel, listener] = args;
|
||||
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args));
|
||||
},
|
||||
off(...args: Parameters<typeof ipcRenderer.off>) {
|
||||
const [channel, ...omit] = args;
|
||||
return ipcRenderer.off(channel, ...omit);
|
||||
},
|
||||
send(...args: Parameters<typeof ipcRenderer.send>) {
|
||||
const [channel, ...omit] = args;
|
||||
return ipcRenderer.send(channel, ...omit);
|
||||
},
|
||||
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
|
||||
const [channel, ...omit] = args;
|
||||
return ipcRenderer.invoke(channel, ...omit);
|
||||
},
|
||||
|
||||
// You can expose other APTs you need here.
|
||||
// ...
|
||||
});
|
||||
Reference in New Issue
Block a user