software copyright
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 自然写互动课堂PC端应用软件 V1.0
|
||||
*
|
||||
* device_manager.ts - USB/BLE设备管理
|
||||
*
|
||||
* 功能说明:
|
||||
* - USB HID点阵笔连接管理
|
||||
* - BLE蓝牙点阵笔扫描与连接
|
||||
* - 设备数据解析(7字节紧凑坐标解码)
|
||||
* - 设备热插拔监听
|
||||
* - 多设备并行管理
|
||||
*/
|
||||
|
||||
/* ======================== 类型定义 ======================== */
|
||||
|
||||
/** 设备连接方式 */
|
||||
enum DeviceInterface {
|
||||
USB_HID = 'usb',
|
||||
BLE = 'ble'
|
||||
}
|
||||
|
||||
/** 设备状态 */
|
||||
enum DeviceStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
/** 点阵笔设备信息 */
|
||||
interface PenDevice {
|
||||
id: string; /* 设备唯一ID */
|
||||
name: string; /* 设备名称 */
|
||||
macAddress: string; /* MAC地址 */
|
||||
interface: DeviceInterface; /* 连接方式 */
|
||||
status: DeviceStatus; /* 连接状态 */
|
||||
battery: number; /* 电量百分比 */
|
||||
firmwareVersion: string; /* 固件版本 */
|
||||
lastConnected: number; /* 最后连接时间戳 */
|
||||
}
|
||||
|
||||
/** 笔迹坐标点 */
|
||||
interface StrokePoint {
|
||||
x: number; /* X坐标(毫米) */
|
||||
y: number; /* Y坐标(毫米) */
|
||||
pressure: number; /* 压力值(0-1) */
|
||||
timestamp: number; /* 时间戳(毫秒) */
|
||||
penDown: boolean; /* 落笔标志 */
|
||||
}
|
||||
|
||||
/** 设备事件回调 */
|
||||
interface DeviceEventCallbacks {
|
||||
onDeviceDiscovered: (device: PenDevice) => void;
|
||||
onDeviceConnected: (device: PenDevice) => void;
|
||||
onDeviceDisconnected: (deviceId: string) => void;
|
||||
onStrokeData: (deviceId: string, points: StrokePoint[]) => void;
|
||||
onBatteryUpdate: (deviceId: string, level: number) => void;
|
||||
onError: (deviceId: string, error: string) => void;
|
||||
}
|
||||
|
||||
/* ======================== USB HID常量 ======================== */
|
||||
|
||||
/** 自然写点阵笔USB VendorID */
|
||||
const WRITECH_USB_VID = 0x1234;
|
||||
/** 自然写点阵笔USB ProductID */
|
||||
const WRITECH_USB_PID = 0x5678;
|
||||
/** USB HID报文最大长度 */
|
||||
const USB_REPORT_SIZE = 64;
|
||||
/** USB轮询间隔(毫秒) */
|
||||
const USB_POLL_INTERVAL = 5;
|
||||
|
||||
/* ======================== BLE常量 ======================== */
|
||||
|
||||
/** 自然写笔迹服务UUID */
|
||||
const BLE_SERVICE_UUID = '0000ffe0-0000-1000-8000-00805f9b34fb';
|
||||
/** 笔迹数据特征UUID(Notify) */
|
||||
const BLE_STROKE_CHAR_UUID = '0000ffe1-0000-1000-8000-00805f9b34fb';
|
||||
/** 电量特征UUID */
|
||||
const BLE_BATTERY_CHAR_UUID = '0000ffe2-0000-1000-8000-00805f9b34fb';
|
||||
/** 控制特征UUID(Write) */
|
||||
const BLE_CONTROL_CHAR_UUID = '0000ffe3-0000-1000-8000-00805f9b34fb';
|
||||
|
||||
/* ======================== 坐标解码 ======================== */
|
||||
|
||||
/**
|
||||
* 解码7字节紧凑坐标编码
|
||||
* 编码格式: 20位X + 20位Y + 12位压力 + 4位标志
|
||||
*/
|
||||
function decodeCompactPoint(data: Buffer, offset: number): StrokePoint {
|
||||
/* 提取20位X坐标 */
|
||||
const rawX = (data[offset] << 12) |
|
||||
(data[offset + 1] << 4) |
|
||||
((data[offset + 2] >> 4) & 0x0F);
|
||||
|
||||
/* 提取20位Y坐标 */
|
||||
const rawY = ((data[offset + 2] & 0x0F) << 16) |
|
||||
(data[offset + 3] << 8) |
|
||||
data[offset + 4];
|
||||
|
||||
/* 提取12位压力值 */
|
||||
const rawPressure = (data[offset + 5] << 4) |
|
||||
((data[offset + 6] >> 4) & 0x0F);
|
||||
|
||||
/* 提取4位标志 */
|
||||
const flags = data[offset + 6] & 0x0F;
|
||||
|
||||
return {
|
||||
x: rawX * 0.3, /* 点阵码单位转毫米 */
|
||||
y: rawY * 0.3,
|
||||
pressure: rawPressure / 4095, /* 归一化到0-1 */
|
||||
timestamp: Date.now(),
|
||||
penDown: (flags & 0x01) !== 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算CRC-16 CCITT校验
|
||||
*/
|
||||
function crc16CCITT(data: Buffer, length: number): number {
|
||||
let crc = 0xFFFF;
|
||||
for (let i = 0; i < length; i++) {
|
||||
crc ^= data[i] << 8;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
if (crc & 0x8000) {
|
||||
crc = ((crc << 1) ^ 0x1021) & 0xFFFF;
|
||||
} else {
|
||||
crc = (crc << 1) & 0xFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* ======================== 设备管理器 ======================== */
|
||||
|
||||
/**
|
||||
* 点阵笔设备管理器
|
||||
* 统一管理USB和BLE连接的点阵笔设备
|
||||
*/
|
||||
class DeviceManager {
|
||||
/** 已连接设备列表 */
|
||||
private devices: Map<string, PenDevice> = new Map();
|
||||
/** 事件回调 */
|
||||
private callbacks: DeviceEventCallbacks;
|
||||
/** USB轮询定时器 */
|
||||
private usbPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
/** BLE扫描状态 */
|
||||
private bleScanning: boolean = false;
|
||||
/** 是否运行中 */
|
||||
private running: boolean = false;
|
||||
|
||||
constructor(callbacks: DeviceEventCallbacks) {
|
||||
this.callbacks = callbacks;
|
||||
console.log('[设备管理] 初始化');
|
||||
}
|
||||
|
||||
/* ==================== USB HID管理 ==================== */
|
||||
|
||||
/**
|
||||
* 启动USB设备监听
|
||||
* 使用node-usb库检测设备热插拔
|
||||
*/
|
||||
startUSBMonitor(): void {
|
||||
console.log('[设备管理] 启动USB监听');
|
||||
this.running = true;
|
||||
|
||||
/* 枚举已连接的USB设备 */
|
||||
this.scanUSBDevices();
|
||||
|
||||
/* 监听USB热插拔事件
|
||||
usb.on('attach', (device) => this.onUSBAttach(device));
|
||||
usb.on('detach', (device) => this.onUSBDetach(device)); */
|
||||
|
||||
/* 启动USB数据轮询 */
|
||||
this.usbPollTimer = setInterval(() => {
|
||||
this.pollUSBData();
|
||||
}, USB_POLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描已连接的USB HID设备
|
||||
*/
|
||||
private scanUSBDevices(): void {
|
||||
/* const devices = HID.devices()
|
||||
.filter(d => d.vendorId === WRITECH_USB_VID &&
|
||||
d.productId === WRITECH_USB_PID); */
|
||||
|
||||
console.log('[设备管理] USB扫描完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* USB设备接入处理
|
||||
*/
|
||||
private onUSBAttach(usbDevice: any): void {
|
||||
const deviceId = `usb_${usbDevice.serialNumber || Date.now()}`;
|
||||
|
||||
const pen: PenDevice = {
|
||||
id: deviceId,
|
||||
name: `WritechPen-USB-${deviceId.slice(-4)}`,
|
||||
macAddress: '',
|
||||
interface: DeviceInterface.USB_HID,
|
||||
status: DeviceStatus.CONNECTED,
|
||||
battery: 100,
|
||||
firmwareVersion: '1.0.0',
|
||||
lastConnected: Date.now()
|
||||
};
|
||||
|
||||
this.devices.set(deviceId, pen);
|
||||
this.callbacks.onDeviceConnected(pen);
|
||||
console.log(`[设备管理] USB设备接入: ${pen.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* USB设备拔出处理
|
||||
*/
|
||||
private onUSBDetach(usbDevice: any): void {
|
||||
const deviceId = `usb_${usbDevice.serialNumber || ''}`;
|
||||
if (this.devices.has(deviceId)) {
|
||||
this.devices.delete(deviceId);
|
||||
this.callbacks.onDeviceDisconnected(deviceId);
|
||||
console.log(`[设备管理] USB设备断开: ${deviceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询USB设备数据
|
||||
* 读取HID报文并解析坐标
|
||||
*/
|
||||
private pollUSBData(): void {
|
||||
this.devices.forEach((device, deviceId) => {
|
||||
if (device.interface !== DeviceInterface.USB_HID) return;
|
||||
if (device.status !== DeviceStatus.CONNECTED) return;
|
||||
|
||||
/* const report = hidDevice.readSync();
|
||||
if (report && report.length > 0) {
|
||||
this.parseUSBReport(deviceId, Buffer.from(report));
|
||||
} */
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析USB HID报文
|
||||
* 报文格式: [报文类型][数据长度][坐标数据...]
|
||||
*/
|
||||
private parseUSBReport(deviceId: string, report: Buffer): void {
|
||||
const reportType = report[0];
|
||||
const dataLen = report[1];
|
||||
|
||||
if (reportType === 0x01) {
|
||||
/* 笔迹数据报文: 每11字节一个坐标点(7字节坐标+4字节时间戳) */
|
||||
const points: StrokePoint[] = [];
|
||||
const pointSize = 11;
|
||||
|
||||
for (let offset = 2; offset + pointSize <= 2 + dataLen; offset += pointSize) {
|
||||
const point = decodeCompactPoint(report, offset);
|
||||
/* 时间戳从报文中提取 */
|
||||
point.timestamp = report.readUInt32LE(offset + 7);
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
if (points.length > 0) {
|
||||
this.callbacks.onStrokeData(deviceId, points);
|
||||
}
|
||||
} else if (reportType === 0x04) {
|
||||
/* 电量报文 */
|
||||
const battery = report[2];
|
||||
this.callbacks.onBatteryUpdate(deviceId, battery);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== BLE管理 ==================== */
|
||||
|
||||
/**
|
||||
* 启动BLE蓝牙扫描
|
||||
*/
|
||||
startBLEScan(): void {
|
||||
if (this.bleScanning) return;
|
||||
|
||||
console.log('[设备管理] 启动BLE扫描');
|
||||
this.bleScanning = true;
|
||||
|
||||
/* noble.on('discover', (peripheral) => {
|
||||
if (peripheral.advertisement.localName?.startsWith('WritechPen')) {
|
||||
this.onBLEDiscover(peripheral);
|
||||
}
|
||||
});
|
||||
noble.startScanning([BLE_SERVICE_UUID], true); */
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止BLE扫描
|
||||
*/
|
||||
stopBLEScan(): void {
|
||||
this.bleScanning = false;
|
||||
/* noble.stopScanning(); */
|
||||
console.log('[设备管理] BLE扫描已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* BLE设备发现回调
|
||||
*/
|
||||
private onBLEDiscover(peripheral: any): void {
|
||||
const deviceId = `ble_${peripheral.address.replace(/:/g, '')}`;
|
||||
|
||||
if (this.devices.has(deviceId)) return;
|
||||
|
||||
const pen: PenDevice = {
|
||||
id: deviceId,
|
||||
name: peripheral.advertisement.localName || 'WritechPen',
|
||||
macAddress: peripheral.address,
|
||||
interface: DeviceInterface.BLE,
|
||||
status: DeviceStatus.DISCONNECTED,
|
||||
battery: 0,
|
||||
firmwareVersion: '',
|
||||
lastConnected: 0
|
||||
};
|
||||
|
||||
this.callbacks.onDeviceDiscovered(pen);
|
||||
console.log(`[设备管理] 发现BLE设备: ${pen.name} [${pen.macAddress}]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接BLE设备
|
||||
*/
|
||||
async connectBLE(deviceId: string): Promise<boolean> {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (!device || device.interface !== DeviceInterface.BLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
device.status = DeviceStatus.CONNECTING;
|
||||
console.log(`[设备管理] 连接BLE设备: ${device.name}`);
|
||||
|
||||
try {
|
||||
/* peripheral.connect((err) => { ... });
|
||||
peripheral.discoverServices([BLE_SERVICE_UUID], (err, services) => {
|
||||
services[0].discoverCharacteristics([...], (err, chars) => {
|
||||
// 订阅笔迹数据Notify
|
||||
strokeChar.subscribe();
|
||||
strokeChar.on('data', (data) => this.onBLEData(deviceId, data));
|
||||
});
|
||||
}); */
|
||||
|
||||
device.status = DeviceStatus.CONNECTED;
|
||||
device.lastConnected = Date.now();
|
||||
this.devices.set(deviceId, device);
|
||||
this.callbacks.onDeviceConnected(device);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
device.status = DeviceStatus.ERROR;
|
||||
this.callbacks.onError(deviceId, err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BLE数据接收回调
|
||||
*/
|
||||
private onBLEData(deviceId: string, data: Buffer): void {
|
||||
/* BLE数据帧格式与USB类似:[帧头0xAA][类型][长度][数据...][CRC16] */
|
||||
if (data[0] !== 0xAA) return;
|
||||
|
||||
const frameType = data[1];
|
||||
const payloadLen = data[2];
|
||||
|
||||
/* CRC校验 */
|
||||
const expectedCrc = data.readUInt16LE(3 + payloadLen);
|
||||
const calcCrc = crc16CCITT(data.slice(0, 3 + payloadLen), 3 + payloadLen);
|
||||
if (expectedCrc !== calcCrc) {
|
||||
console.warn(`[设备管理] BLE数据CRC校验失败: ${deviceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameType === 0x01) {
|
||||
/* 笔迹坐标数据 */
|
||||
const points: StrokePoint[] = [];
|
||||
const pointSize = 11;
|
||||
for (let i = 3; i + pointSize <= 3 + payloadLen; i += pointSize) {
|
||||
points.push(decodeCompactPoint(data, i));
|
||||
}
|
||||
if (points.length > 0) {
|
||||
this.callbacks.onStrokeData(deviceId, points);
|
||||
}
|
||||
} else if (frameType === 0x04) {
|
||||
/* 电量数据 */
|
||||
this.callbacks.onBatteryUpdate(deviceId, data[3]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 公共接口 ==================== */
|
||||
|
||||
/** 获取所有已连接设备 */
|
||||
getConnectedDevices(): PenDevice[] {
|
||||
return Array.from(this.devices.values())
|
||||
.filter(d => d.status === DeviceStatus.CONNECTED);
|
||||
}
|
||||
|
||||
/** 获取设备数量 */
|
||||
getDeviceCount(): number {
|
||||
return this.devices.size;
|
||||
}
|
||||
|
||||
/** 断开指定设备 */
|
||||
disconnect(deviceId: string): void {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (device) {
|
||||
device.status = DeviceStatus.DISCONNECTED;
|
||||
this.callbacks.onDeviceDisconnected(deviceId);
|
||||
console.log(`[设备管理] 断开设备: ${device.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止所有设备管理 */
|
||||
shutdown(): void {
|
||||
this.running = false;
|
||||
if (this.usbPollTimer) {
|
||||
clearInterval(this.usbPollTimer);
|
||||
}
|
||||
this.stopBLEScan();
|
||||
this.devices.clear();
|
||||
console.log('[设备管理] 已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
export { DeviceManager, PenDevice, StrokePoint, DeviceStatus, DeviceInterface };
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* 自然写互动课堂PC端应用软件 V1.0
|
||||
*
|
||||
* main.ts - Electron主进程入口
|
||||
*
|
||||
* 功能说明:
|
||||
* - Electron应用生命周期管理
|
||||
* - 主窗口创建与配置
|
||||
* - 系统托盘与菜单
|
||||
* - IPC通信注册
|
||||
* - 自动更新检测
|
||||
* - 单实例锁定
|
||||
* - 全局异常处理
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, Menu, Tray, ipcMain, dialog, shell } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/* ======================== 应用配置 ======================== */
|
||||
|
||||
/** 应用版本号 */
|
||||
const APP_VERSION = '1.0.0';
|
||||
/** 应用名称 */
|
||||
const APP_NAME = '自然写互动课堂';
|
||||
/** 窗口默认尺寸 */
|
||||
const DEFAULT_WIDTH = 1440;
|
||||
const DEFAULT_HEIGHT = 900;
|
||||
/** 最小窗口尺寸 */
|
||||
const MIN_WIDTH = 1024;
|
||||
const MIN_HEIGHT = 680;
|
||||
/** 开发模式标志 */
|
||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||
|
||||
/* ======================== 全局变量 ======================== */
|
||||
|
||||
/** 主窗口实例 */
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
/** 系统托盘实例 */
|
||||
let tray: Tray | null = null;
|
||||
/** 窗口状态保存路径 */
|
||||
const windowStatePath = path.join(app.getPath('userData'), 'window-state.json');
|
||||
|
||||
/* ======================== 窗口状态管理 ======================== */
|
||||
|
||||
/** 保存的窗口状态 */
|
||||
interface WindowState {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isMaximized: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载上次保存的窗口状态
|
||||
*/
|
||||
function loadWindowState(): WindowState {
|
||||
try {
|
||||
if (fs.existsSync(windowStatePath)) {
|
||||
const data = fs.readFileSync(windowStatePath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[主进程] 加载窗口状态失败:', err);
|
||||
}
|
||||
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, isMaximized: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前窗口状态
|
||||
*/
|
||||
function saveWindowState(win: BrowserWindow): void {
|
||||
const bounds = win.getBounds();
|
||||
const state: WindowState = {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
isMaximized: win.isMaximized()
|
||||
};
|
||||
try {
|
||||
fs.writeFileSync(windowStatePath, JSON.stringify(state, null, 2));
|
||||
} catch (err) {
|
||||
console.error('[主进程] 保存窗口状态失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================== 窗口创建 ======================== */
|
||||
|
||||
/**
|
||||
* 创建主窗口
|
||||
* 配置安全选项、预加载脚本和窗口参数
|
||||
*/
|
||||
function createMainWindow(): void {
|
||||
const savedState = loadWindowState();
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
title: APP_NAME,
|
||||
width: savedState.width,
|
||||
height: savedState.height,
|
||||
x: savedState.x,
|
||||
y: savedState.y,
|
||||
minWidth: MIN_WIDTH,
|
||||
minHeight: MIN_HEIGHT,
|
||||
show: false,
|
||||
frame: true,
|
||||
backgroundColor: '#ffffff',
|
||||
webPreferences: {
|
||||
/* 安全选项:渲染进程沙箱化 */
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
/* 预加载脚本路径 */
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
/* 禁用远程模块 */
|
||||
webSecurity: true,
|
||||
/* 禁止打开新窗口 */
|
||||
allowRunningInsecureContent: false
|
||||
}
|
||||
});
|
||||
|
||||
/* 加载渲染进程页面 */
|
||||
if (IS_DEV) {
|
||||
mainWindow.loadURL('http://localhost:5173');
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
|
||||
}
|
||||
|
||||
/* 窗口就绪后显示(避免白屏闪烁) */
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
if (savedState.isMaximized) {
|
||||
mainWindow?.maximize();
|
||||
}
|
||||
mainWindow?.show();
|
||||
console.log('[主进程] 主窗口已显示');
|
||||
});
|
||||
|
||||
/* 窗口关闭前保存状态 */
|
||||
mainWindow.on('close', (event) => {
|
||||
if (mainWindow) {
|
||||
saveWindowState(mainWindow);
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
/* 拦截外部链接在系统浏览器打开 */
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
console.log(`[主进程] 窗口创建完成: ${savedState.width}x${savedState.height}`);
|
||||
}
|
||||
|
||||
/* ======================== 系统托盘 ======================== */
|
||||
|
||||
/**
|
||||
* 创建系统托盘图标和菜单
|
||||
*/
|
||||
function createTray(): void {
|
||||
const iconPath = path.join(__dirname, '../assets/tray-icon.png');
|
||||
tray = new Tray(iconPath);
|
||||
tray.setToolTip(APP_NAME);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: '显示主窗口', click: () => mainWindow?.show() },
|
||||
{ type: 'separator' },
|
||||
{ label: '设备管理', click: () => sendToRenderer('navigate', '/devices') },
|
||||
{ label: '设置', click: () => sendToRenderer('navigate', '/settings') },
|
||||
{ type: 'separator' },
|
||||
{ label: `版本 ${APP_VERSION}`, enabled: false },
|
||||
{ label: '退出', click: () => app.quit() }
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
tray.on('click', () => mainWindow?.show());
|
||||
}
|
||||
|
||||
/* ======================== IPC通信处理 ======================== */
|
||||
|
||||
/**
|
||||
* 向渲染进程发送消息
|
||||
*/
|
||||
function sendToRenderer(channel: string, data: any): void {
|
||||
mainWindow?.webContents.send(channel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册IPC通信处理器
|
||||
* 渲染进程通过IPC调用主进程的系统API
|
||||
*/
|
||||
function setupIpcHandlers(): void {
|
||||
/* 获取应用信息 */
|
||||
ipcMain.handle('app:getInfo', () => ({
|
||||
version: APP_VERSION,
|
||||
name: APP_NAME,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
userDataPath: app.getPath('userData')
|
||||
}));
|
||||
|
||||
/* 文件选择对话框 */
|
||||
ipcMain.handle('dialog:openFile', async (_, options) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow!, {
|
||||
title: options.title || '选择文件',
|
||||
filters: options.filters || [{ name: '所有文件', extensions: ['*'] }],
|
||||
properties: options.properties || ['openFile']
|
||||
});
|
||||
return result.filePaths;
|
||||
});
|
||||
|
||||
/* 保存文件对话框 */
|
||||
ipcMain.handle('dialog:saveFile', async (_, options) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow!, {
|
||||
title: options.title || '保存文件',
|
||||
defaultPath: options.defaultPath,
|
||||
filters: options.filters || [{ name: '所有文件', extensions: ['*'] }]
|
||||
});
|
||||
return result.filePath;
|
||||
});
|
||||
|
||||
/* 文件读取 */
|
||||
ipcMain.handle('fs:readFile', async (_, filePath: string) => {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
});
|
||||
|
||||
/* 文件写入 */
|
||||
ipcMain.handle('fs:writeFile', async (_, filePath: string, content: string) => {
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
return true;
|
||||
});
|
||||
|
||||
/* 打印功能 */
|
||||
ipcMain.handle('print:start', async (_, options) => {
|
||||
mainWindow?.webContents.print({
|
||||
silent: options.silent || false,
|
||||
printBackground: true,
|
||||
copies: options.copies || 1,
|
||||
pageSize: options.pageSize || 'A4'
|
||||
});
|
||||
});
|
||||
|
||||
/* 窗口控制 */
|
||||
ipcMain.on('window:minimize', () => mainWindow?.minimize());
|
||||
ipcMain.on('window:maximize', () => {
|
||||
if (mainWindow?.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow?.maximize();
|
||||
}
|
||||
});
|
||||
ipcMain.on('window:close', () => mainWindow?.close());
|
||||
|
||||
console.log('[主进程] IPC处理器注册完成');
|
||||
}
|
||||
|
||||
/* ======================== 自动更新 ======================== */
|
||||
|
||||
/**
|
||||
* 检查应用更新
|
||||
* 使用electron-updater检查并安装更新
|
||||
*/
|
||||
function checkForUpdates(): void {
|
||||
if (IS_DEV) return;
|
||||
|
||||
console.log('[主进程] 检查应用更新...');
|
||||
/* autoUpdater.checkForUpdatesAndNotify()
|
||||
.then(result => { ... })
|
||||
.catch(err => { ... }); */
|
||||
/* autoUpdater.on('update-available', (info) => {
|
||||
sendToRenderer('update:available', info);
|
||||
});
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
sendToRenderer('update:progress', progress);
|
||||
});
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
sendToRenderer('update:downloaded', info);
|
||||
}); */
|
||||
}
|
||||
|
||||
/* ======================== 应用生命周期 ======================== */
|
||||
|
||||
/** 确保单实例运行 */
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
console.log('[主进程] 已有实例运行,退出');
|
||||
app.quit();
|
||||
}
|
||||
|
||||
app.on('second-instance', () => {
|
||||
/* 用户尝试打开第二个实例时,聚焦已有窗口 */
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* 应用就绪 */
|
||||
app.whenReady().then(() => {
|
||||
console.log(`[主进程] ${APP_NAME} v${APP_VERSION} 启动`);
|
||||
|
||||
createMainWindow();
|
||||
createTray();
|
||||
setupIpcHandlers();
|
||||
|
||||
/* 延迟检查更新 */
|
||||
setTimeout(checkForUpdates, 5000);
|
||||
});
|
||||
|
||||
/* macOS特殊处理:所有窗口关闭后重新创建 */
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
/* 所有窗口关闭时退出(macOS除外) */
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
/* 全局异常处理 */
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[主进程] 未捕获异常:', error);
|
||||
dialog.showErrorBox('应用错误', `发生未预期的错误:\n${error.message}`);
|
||||
});
|
||||
Reference in New Issue
Block a user