feat(pc): 码码码

master
chenyt 2025-08-29 22:16:38 +08:00
parent 865dcdae6d
commit 0c1308335b
26 changed files with 1501 additions and 1511 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,194 +0,0 @@
// // src/grpc/BTGrpcClient.ts 调用后端服务
// import * as grpc from '@grpc/grpc-js';
// import * as protoLoader from '@grpc/proto-loader';
// import path from 'path';
// import { app } from 'electron';
// export interface DownloadRequest {
// torrent_url: string;
// item_name: string;
// item_id?: string;
// }
// export interface ProgressCallback {
// (progress: ProgressUpdate): void;
// }
// export interface ProgressUpdate {
// download_id: string;
// progress: number;
// download_speed: number;
// upload_speed: number;
// eta: number;
// total_size: number;
// downloaded_size: number;
// state: string;
// }
// export class BTGrpcClient {
// private client: any;
// private progressCallbacks: Map<string, ProgressCallback[]> = new Map();
// private progressStream: any = null;
// constructor() {
// this.initializeClient();
// }
// private initializeClient() {
// try {
// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
// const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
// keepCase: true,
// longs: String,
// enums: String,
// defaults: true,
// oneofs: true,
// });
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
// const bittorrent = protoDescriptor.bittorrent as any;
// // 连接到后端 Agent假设运行在 localhost:50051
// this.client = new bittorrent.BTDownloadService(
// 'localhost:50051',
// grpc.credentials.createInsecure()
// );
// console.log('gRPC客户端初始化成功');
// this.setupProgressStream();
// } catch (error) {
// console.error('gRPC客户端初始化失败:', error);
// }
// }
// // 设置进度流监听
// private setupProgressStream() {
// try {
// this.progressStream = this.client.SubscribeProgress({});
// // 注册数据事件监听器,接收进度更新
// this.progressStream.on('data', (progress: ProgressUpdate) => {
// console.log('收到进度数据:', progress); // 添加调试日志
// // BTGrpcClient 通过IPC将进度发送给主进程
// this.progressCallbacks.forEach((callbacks, downloadId) => {
// if (downloadId === 'all' || downloadId === progress.download_id) {
// callbacks.forEach(callback => callback(progress));
// }
// });
// // 如果有针对特定下载ID的回调也要通知
// const specificCallbacks = this.progressCallbacks.get(progress.download_id);
// if (specificCallbacks) {
// specificCallbacks.forEach(callback => callback(progress));
// }
// });
// this.progressStream.on('error', (error: Error) => {
// console.error('进度流错误:', error);
// });
// this.progressStream.on('end', () => {
// console.log('进度流结束');
// });
// } catch (error) {
// console.error('设置进度流失败:', error);
// }
// }
// // 开始下载
// startDownload(request: DownloadRequest): Promise<any> {
// return new Promise((resolve, reject) => {
// // MockBTService 接收请求,创建下载任务
// this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => {
// if (error) {
// reject(error);
// } else {
// resolve(response);
// }
// });
// });
// }
// // 停止下载
// stopDownload(downloadId: string): Promise<any> {
// return new Promise((resolve, reject) => {
// this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => {
// if (error) {
// reject(error);
// } else {
// resolve(response);
// }
// });
// });
// }
// // 注册进度回调
// registerProgressCallback(downloadId: string, callback: ProgressCallback) {
// if (!this.progressCallbacks.has(downloadId)) {
// this.progressCallbacks.set(downloadId, []);
// }
// this.progressCallbacks.get(downloadId)!.push(callback);
// }
// // 移除进度回调
// removeProgressCallback(downloadId: string, callback: ProgressCallback) {
// const callbacks = this.progressCallbacks.get(downloadId);
// if (callbacks) {
// const index = callbacks.indexOf(callback);
// if (index > -1) {
// callbacks.splice(index, 1);
// }
// }
// }
// // 检查连接状态
// isConnected(): boolean {
// return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY;
// }
// // 重新连接
// reconnect() {
// try {
// this.client.close();
// this.initializeClient();
// } catch (error) {
// console.error('重新连接失败:', error);
// }
// }
// getConnectionState(): string {
// if (!this.client) return 'NOT_CREATED';
// const state = this.client.getChannel().getConnectivityState(false);
// const stateNames = {
// [grpc.connectivityState.IDLE]: 'IDLE',
// [grpc.connectivityState.CONNECTING]: 'CONNECTING',
// [grpc.connectivityState.READY]: 'READY',
// [grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE',
// [grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN'
// } as const;
// // 使用类型断言确保 state 是合法的 key
// return stateNames[state as keyof typeof stateNames] || 'UNKNOWN';
// }
// // 添加测试方法
// async testConnection(): Promise<boolean> {
// try {
// // 尝试调用一个简单的方法来测试连接
// await new Promise((resolve, reject) => {
// this.client.ListDownloads({}, (error: any, response: any) => {
// if (error && error.code !== grpc.status.UNIMPLEMENTED) {
// reject(error);
// } else {
// resolve(response);
// }
// });
// });
// return true;
// } catch (error) {
// console.error('连接测试失败:', error);
// return false;
// }
// }
// }

View File

@ -1,323 +0,0 @@
// // 本地测试用的 gRPC 服务器,用于模拟 BT 下载
// // src/grpc/MockBTService.ts
// import * as grpc from '@grpc/grpc-js';
// import * as protoLoader from '@grpc/proto-loader';
// import path from 'path';
// // 进度更新接口
// interface ProgressUpdate {
// download_id: string;
// progress: number;
// download_speed: number;
// item_name?: string; // 可选的文件名字段
// upload_speed: number;
// eta: number;
// total_size: number;
// downloaded_size: number;
// state: string;
// }
// // 模拟的下载任务
// interface MockDownloadTask {
// id: string;
// itemName: string;
// torrentUrl: string;
// progress: number;
// totalSize: number;
// downloadedSize: number;
// downloadSpeed: number;
// state: 'downloading' | 'completed' | 'error' | 'paused';
// startTime: number;
// }
// export class MockBTService {
// private server: grpc.Server;
// private activeDownloads: Map<string, MockDownloadTask> = new Map();
// private progressIntervals: Map<string, NodeJS.Timeout> = new Map();
// // 存储所有的进度回调函数,发送信息
// private progressCallbacks: ((progress: ProgressUpdate) => void)[] = [];
// constructor() {
// this.server = new grpc.Server();
// this.setupService();
// }
// private setupService() {
// // 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法
// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
// const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
// keepCase: true,
// longs: String,
// enums: String,
// defaults: true,
// oneofs: true,
// });
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
// const bittorrent = protoDescriptor.bittorrent as any;
// // 实现 gRPC 服务方法
// this.server.addService(bittorrent.BTDownloadService.service, {
// StartDownload: this.startDownload.bind(this),
// StopDownload: this.stopDownload.bind(this),
// GetDownloadStatus: this.getDownloadStatus.bind(this),
// ListDownloads: this.listDownloads.bind(this),
// SubscribeProgress: this.subscribeProgress.bind(this),
// });
// }
// // 开始下载
// // 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑
// private startDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
// const { torrent_url, item_name, item_id } = call.request;
// const downloadId = item_id || `download-${Date.now()}`;
// console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`);
// // 根据文件名或ID设置不同大小的文件
// let fileSize = 1024 * 1024 * 100; // 默认100MB
// if (item_name.includes('large') || item_name.includes('大文件')) {
// fileSize = 4 * 1024 * 1024 * 1024; // 4GB
// } else if (item_name.includes('medium') || item_name.includes('中等')) {
// fileSize = 1024 * 1024 * 1024; // 1GB
// } else if (item_name.includes('small') || item_name.includes('小文件')) {
// fileSize = 100 * 1024 * 1024; // 100MB
// }
// // 创建模拟下载任务
// const task: MockDownloadTask = {
// id: downloadId,
// itemName: item_name,
// torrentUrl: torrent_url,
// progress: 0,
// totalSize: fileSize,
// downloadedSize: 0,
// downloadSpeed: 1024 * 1024 * 2, // 2MB/s
// state: 'downloading',
// startTime: Date.now(),
// };
// this.activeDownloads.set(downloadId, task);
// console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`);
// // 启动进度模拟
// this.startProgressSimulation(downloadId);
// callback(null, {
// success: true,
// message: '下载已开始',
// download_id: downloadId,
// });
// }
// // 停止下载
// private stopDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
// const { download_id } = call.request;
// if (this.activeDownloads.has(download_id)) {
// const task = this.activeDownloads.get(download_id)!;
// task.state = 'paused';
// // 清除进度定时器
// if (this.progressIntervals.has(download_id)) {
// clearInterval(this.progressIntervals.get(download_id));
// this.progressIntervals.delete(download_id);
// }
// console.log(`模拟停止下载: ${download_id}`);
// callback(null, { success: true, message: '下载已停止' });
// } else {
// callback({
// code: grpc.status.NOT_FOUND,
// message: `下载任务不存在: ${download_id}`
// });
// }
// }
// // 获取下载状态
// private getDownloadStatus(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
// const { download_id } = call.request;
// if (this.activeDownloads.has(download_id)) {
// const task = this.activeDownloads.get(download_id)!;
// callback(null, {
// download_id: task.id,
// progress: task.progress,
// download_speed: task.downloadSpeed,
// state: task.state,
// total_size: task.totalSize,
// downloaded_size: task.downloadedSize,
// });
// } else {
// callback({
// code: grpc.status.NOT_FOUND,
// message: `下载任务不存在: ${download_id}`
// });
// }
// }
// // 列出所有下载
// private listDownloads(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
// const downloads = Array.from(this.activeDownloads.values()).map(task => ({
// download_id: task.id,
// item_name: task.itemName,
// progress: task.progress,
// download_speed: task.downloadSpeed,
// state: task.state,
// total_size: task.totalSize,
// downloaded_size: task.downloadedSize,
// }));
// callback(null, { downloads });
// }
// // 订阅进度更新(流式响应)
// private subscribeProgress(call: grpc.ServerWritableStream<any, any>) {
// console.log('客户端订阅了进度更新');
// // 存储回调以便发送进度更新
// const callback = (progress: ProgressUpdate) => {
// try {
// call.write(progress);
// } catch (error) {
// console.error('发送进度更新失败:', error);
// }
// };
// this.progressCallbacks.push(callback);
// // 当客户端断开连接时清理
// call.on('cancelled', () => {
// console.log('客户端取消了进度订阅');
// const index = this.progressCallbacks.indexOf(callback);
// if (index > -1) {
// this.progressCallbacks.splice(index, 1);
// }
// });
// call.on('error', (error) => {
// console.error('进度流错误:', error);
// const index = this.progressCallbacks.indexOf(callback);
// if (index > -1) {
// this.progressCallbacks.splice(index, 1);
// }
// });
// // 处理客户端断开连接
// call.on('end', () => {
// console.log('客户端断开连接');
// const index = this.progressCallbacks.indexOf(callback);
// if (index > -1) {
// this.progressCallbacks.splice(index, 1);
// }
// });
// }
// // 启动进度模拟
// // 在 MockBTService.ts 中修改 startProgressSimulation 方法
// private startProgressSimulation(downloadId: string) {
// const interval = setInterval(() => {
// if (this.activeDownloads.has(downloadId)) {
// const task = this.activeDownloads.get(downloadId)!;
// if (task.state === 'downloading' && task.progress < 100) {
// // // 更新进度但确保不超过100%
// // task.progress += Math.random() * 2;
// // 使用固定的进度增加而不是随机值,使下载更加稳定
// task.progress += 1.5; // 每秒增加1.5%的进度
// // 确保进度不超过100%
// if (task.progress >= 100) {
// task.progress = 100;
// task.state = 'completed';
// task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小
// console.log(`下载完成: ${downloadId}`);
// // 清除定时器
// clearInterval(interval);
// this.progressIntervals.delete(downloadId);
// } else {
// // 根据进度计算已下载大小
// task.downloadedSize = (task.totalSize * task.progress) / 100;
// }
// // 计算剩余时间(秒)
// let eta = 0;
// if (task.progress < 100) {
// // 基于当前速度计算剩余时间
// const progressPerSecond = 1.5; // 每秒进度百分比
// eta = Math.round(((100 - task.progress) / progressPerSecond));
// }
// // 发送进度更新
// this.sendProgressUpdate({
// download_id: task.id,
// item_name: task.itemName,
// progress: task.progress,
// download_speed: task.downloadSpeed,
// upload_speed: 1024 * 512, // 512KB/s 上传速度
// eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000),
// total_size: task.totalSize,
// downloaded_size: task.downloadedSize,
// state: task.state,
// });
// }
// } else {
// clearInterval(interval);
// this.progressIntervals.delete(downloadId);
// }
// }, 1000); // 每秒更新一次进度
// this.progressIntervals.set(downloadId, interval);
// }
// // 发送进度更新给所有订阅者
// private sendProgressUpdate(progress: ProgressUpdate) {
// console.log('发送进度更新:', progress);
// // 通过已注册的回调函数传递给 BTGrpcClient
// this.progressCallbacks.forEach((callback, index) => {
// try {
// callback(progress);
// } catch (error) {
// console.error(`发送进度更新给回调 ${index} 失败:`, error);
// }
// });
// }
// // 绑定端口并启动服务器
// start(port: number = 50051): Promise<void> {
// return new Promise((resolve, reject) => {
// this.server.bindAsync(
// `0.0.0.0:${port}`,
// grpc.ServerCredentials.createInsecure(),
// (error, port) => {
// if (error) {
// reject(error);
// } else {
// this.server.start();
// console.log(`Mock gRPC 服务器运行在端口 ${port}`);
// resolve();
// }
// }
// );
// });
// }
// // 停止服务器
// stop(): Promise<void> {
// return new Promise((resolve) => {
// // 清理所有定时器
// this.progressIntervals.forEach(interval => clearInterval(interval));
// this.progressIntervals.clear();
// this.activeDownloads.clear();
// this.progressCallbacks = [];
// this.server.tryShutdown(() => {
// console.log('Mock gRPC 服务器已停止');
// resolve();
// });
// });
// }
// }

View File

@ -1,219 +0,0 @@
梳理整个gRPC下载逻辑和模拟下载的执行流程。
## 1. 整体架构概览
整个gRPC下载系统分为以下几个主要部分
1. **前端页面** (`grpc.tsx`) - 用户交互界面
2. **预加载脚本** (`preload.ts`) - 桥接前端和主进程
3. **主进程** (`platform.ts`) - 管理gRPC客户端和服务端
4. **gRPC客户端** (`BTGrpcClient.ts`) - 调用后端服务
5. **gRPC服务端** (`MockBTService.ts`) - 模拟下载服务
6. **协议定义** (`bittorrent.proto`) - gRPC接口定义
## 2. 启动流程
### 2.1 应用启动时初始化gRPC
```mermaid
graph TD
A[应用启动] --> B[app.whenReady()]
B --> C[initializeGrpc()]
C --> D{IS_TEST_MODE}
D -->|true| E[创建MockBTService实例]
D -->|false| F[连接真实后端]
E --> G[启动Mock gRPC服务器]
G --> H[创建BTGrpcClient实例]
H --> I[注册进度回调]
I --> J[启动健康检查]
```
`platform.ts` 中:
- 应用启动时调用 `initializeGrpc()` 函数
- 根据 `IS_TEST_MODE` 决定是启动模拟服务还是连接真实后端
- 创建 `MockBTService` 实例并启动服务器
- 创建 `BTGrpcClient` 实例连接到gRPC服务
- 注册进度回调函数,用于接收下载进度更新
### 2.2 gRPC服务端启动 (MockBTService)
```mermaid
sequenceDiagram
participant M as MockBTService
participant S as gRPC Server
participant C as BTGrpcClient
M->>S: 创建gRPC服务器实例
M->>S: 加载proto定义
M->>S: 实现服务方法
M->>S: 绑定端口并启动
S-->>M: 服务器启动成功
C->>S: 连接服务器
S-->>C: 连接建立
```
`MockBTService.ts` 中:
1. 构造函数调用 [setupService()](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L43-L65) 设置gRPC服务
2. 加载 `bittorrent.proto` 定义
3. 实现所有gRPC服务方法
- `StartDownload` - 开始下载
- `StopDownload` - 停止下载
- `GetDownloadStatus` - 获取下载状态
- `ListDownloads` - 列出所有下载
- `SubscribeProgress` - 订阅进度更新
4. 通过 `server.bindAsync()` 绑定端口并启动服务
## 3. 下载流程详解
### 3.1 用户触发下载
```mermaid
graph TD
A[用户点击下载按钮] --> B[handleStartDownload()]
B --> C[调用window.electronAPI.grpcStartDownload()]
C --> D[IPC调用grpc-start-download]
D --> E[主进程处理]
E --> F[调用btGrpcClient.startDownload()]
F --> G[gRPC调用StartDownload()]
G --> H[MockBTService处理]
H --> I[创建下载任务]
I --> J[启动进度模拟]
J --> K[返回下载ID]
K --> L[前端接收响应]
```
详细步骤:
1. 用户点击"开始gRPC下载测试"按钮
2. 调用 `handleStartDownload()` 函数
3. 通过 `window.electronAPI.grpcStartDownload()` 发起IPC调用
4. 主进程 `platform.ts` 接收 `grpc-start-download` 请求
5. 调用 `btGrpcClient.startDownload()` 方法
6. gRPC客户端向服务端发起 `StartDownload` 调用
7. [MockBTService](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L32-L305) 接收请求,创建下载任务
8. 启动进度模拟定时器
9. 返回下载ID给前端
### 3.2 进度更新机制
```mermaid
sequenceDiagram
participant M as MockBTService
participant I as Interval
participant B as BTGrpcClient
participant P as Platform主进程
participant F as 前端
I->>M: 定时触发
M->>M: 更新任务进度
M->>M: 构造进度数据
M->>B: 发送进度更新
B->>P: 调用回调函数
P->>F: IPC发送grpc-progress-update
F->>F: 更新UI显示
```
详细步骤:
1. `startProgressSimulation()` 创建定时器
2. 定时器每秒触发一次,更新下载进度
3. 构造 `ProgressUpdate` 对象
4. 调用 `sendProgressUpdate()` 发送进度更新
5. 通过已注册的回调函数传递给 `BTGrpcClient`
6. `BTGrpcClient` 通过IPC将进度发送给主进程
7. 主进程通过 `webContents.send()` 将进度发送给前端
8. 前端接收并更新UI
## 4. 关键代码解析
### 4.1 前端监听进度更新
```typescript
// grpc.tsx
useEffect(() => {
// 设置进度监听
const handleProgress = (progress: DownloadProgress) => {
console.log('收到进度更新:', progress);
setDownloads(prev => {
const newMap = new Map(prev);
newMap.set(progress.download_id, progress);
return newMap;
});
};
window.electronAPI.onGrpcProgress(handleProgress);
return () => {
window.electronAPI.removeAllGrpcProgressListeners();
};
}, []);
```
### 4.2 主进程转发进度更新
```typescript
// platform.ts
btGrpcClient.registerProgressCallback('all', (progress) => {
const mainWindow = BrowserWindow.getFocusedWindow();
if (mainWindow) {
mainWindow.webContents.send('grpc-progress-update', progress);
}
});
```
### 4.3 Mock服务模拟进度
```typescript
// MockBTService.ts
private startProgressSimulation(downloadId: string) {
const interval = setInterval(() => {
if (this.activeDownloads.has(downloadId)) {
const task = this.activeDownloads.get(downloadId)!;
if (task.state === 'downloading' && task.progress < 100) {
// 更新进度
task.progress += Math.random() * 2;
task.downloadedSize = (task.totalSize * task.progress) / 100;
// 发送进度更新
this.sendProgressUpdate({
download_id: task.id,
item_name: task.itemName,
progress: task.progress,
download_speed: task.downloadSpeed,
upload_speed: 1024 * 512,
eta: Math.round(((100 - task.progress) / 2) * 1000),
total_size: task.totalSize,
downloaded_size: task.downloadedSize,
state: task.state,
});
}
}
}, 1000);
this.progressIntervals.set(downloadId, interval);
}
```
## 5. 数据流总结
```mermaid
graph LR
A[前端UI] --> B[preload.ts]
B --> C[主进程platform.ts]
C --> D[BTGrpcClient.ts]
D --> E[MockBTService.ts]
E --> F[定时模拟进度]
F --> G[发送进度更新]
G --> H[主进程转发]
H --> I[前端接收更新]
I --> A
```
整个流程形成了一个闭环:
1. 用户操作触发前端请求
2. 请求通过IPC传递到主进程
3. 主进程通过gRPC调用后端服务
4. Mock服务模拟下载过程
5. 进度更新通过相同路径返回前端
6. 前端更新UI显示最新进度
这就是整个gRPC下载逻辑的完整执行流程。

View File

@ -1,77 +0,0 @@
// bittorrent.proto gRPC
syntax = "proto3";
package bittorrent;
service BTDownloadService {
rpc StartDownload (DownloadRequest) returns (DownloadResponse) {}
rpc StopDownload (StopRequest) returns (StopResponse) {}
rpc GetDownloadStatus (StatusRequest) returns (StatusResponse) {}
rpc ListDownloads (ListRequest) returns (ListResponse) {}
rpc SubscribeProgress (SubscribeRequest) returns (stream ProgressUpdate) {}
}
message DownloadRequest {
string torrent_url = 1;
string item_name = 2;
string item_id = 3;
}
message DownloadResponse {
bool success = 1;
string message = 2;
string download_id = 3;
}
message StopRequest {
string download_id = 1;
}
message StopResponse {
bool success = 1;
string message = 2;
}
message ProgressUpdate {
string download_id = 1;
double progress = 2;
double download_speed = 3;
double upload_speed = 4;
int64 eta = 5;
int64 total_size = 6;
int64 downloaded_size = 7;
string state = 8;
}
message StatusRequest {
string download_id = 1;
}
message StatusResponse {
string download_id = 1;
double progress = 2;
double download_speed = 3;
string state = 4;
int64 total_size = 5;
int64 downloaded_size = 6;
}
message ListRequest {
}
message ListResponse {
repeated DownloadInfo downloads = 1;
}
message DownloadInfo {
string download_id = 1;
string item_name = 2;
double progress = 3;
double download_speed = 4;
string state = 5;
int64 total_size = 6;
int64 downloaded_size = 7;
}
message SubscribeRequest {
}

View File

@ -0,0 +1,105 @@
// bt下载
import { ipcMain } from 'electron';
import request from '../utils/request';
import { currentServerIp } from './sharedState'; // 修改导入
// 添加下载相关 IPC 处理
ipcMain.handle('start-download', async (event, downloadConfig) => {
// return {success:true,message:"开始下载"}
try {
console.log('开始下载:', downloadConfig);
// 检查服务器 IP 是否已设置
if (!currentServerIp) {
return {
success: false,
error: '服务器未连接,请先配置服务器'
};
}
// 使用保存的服务器 IP 构建 API 地址
const apiUrl = `http://${currentServerIp}:3001/api/downloads`;
// 发送 HTTP 请求到后端 API
const response = await request(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(downloadConfig)
});
let responseData;
try {
responseData = JSON.parse((response as any).data);
} catch (parseError) {
console.error('Failed to parse authentication response:', parseError);
return { success: false, message: '服务器响应格式错误' };
}
// const result = await response.json();
// 检查结果 - 只有code等于200时才算成功
if (responseData.code === '200' || responseData.code === 200) {
console.log('开始下载成功:', responseData);
return { success: true, message: `开始下载成功`, data: responseData };
} else {
// 认证失败,返回服务器提供的错误信息
const errorMessage = responseData.message || responseData.msg || responseData.error || '开始下载成功';
console.log('Authentication failed:', errorMessage);
return { success: false, message: errorMessage };
}
} catch (error) {
console.error('下载请求失败:', error);
return {
success: false,
message: error instanceof Error ? error.message : '未知错误'
};
}
});
ipcMain.handle('stop-download', async (event, downloadId) => {
// return {success:true,message:"停止下载成功"}
try {
console.log('停止下载:', downloadId);
// 检查服务器 IP 是否已设置
if (!currentServerIp) {
return {
success: false,
error: '服务器未连接,请先配置服务器'
};
}
// 使用保存的服务器 IP 构建 API 地址
const apiUrl = `http://${currentServerIp}:3001/api/downloads/${downloadId}`;
// 发送 HTTP 请求到后端 API
const response = await request(apiUrl, {
method: 'DELETE'
});
// const result = await response.json();
let responseData;
try {
responseData = JSON.parse((response as any).data);
} catch (parseError) {
console.error('Failed to parse authentication response:', parseError);
return { success: false, message: '服务器响应格式错误' };
}
// const result = await response.json();
// 检查结果 - 只有code等于200时才算成功
if (responseData.code === '200' || responseData.code === 200) {
console.log('停止下载成功:', responseData);
return { success: true, message: `停止下载成功`, data: responseData };
} else {
// 认证失败,返回服务器提供的错误信息
const errorMessage = responseData.message || responseData.msg || responseData.error || '停止下载失败';
console.log('Authentication failed:', errorMessage);
return { success: false, message: errorMessage };
}
} catch (error) {
console.error('停止下载失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : '未知错误'
};
}
});

View File

@ -0,0 +1,111 @@
// 通用
import { ipcMain,app } from 'electron';
import { BrowserWindow } from 'electron';
import { getDeviceIdAndMac } from '../utils/utils';
// 服务器IP获取
ipcMain.handle('get-current-server-ip', () => {
return (global as any).currentServerIp;
});
// 终端信息获取
ipcMain.handle('get-device-info', async () => {
// return { success: true, deviceId: "fsfs" };
try {
const deviceInfo = await getDeviceIdAndMac();
console.log(`Using device Info: ${deviceInfo}`);
return { success: true, deviceInfo };
} catch (error) {
console.error('获取设备信息失败:', error);
return { success: false, error: error instanceof Error ? error.message : '未知错误' };
}
});
// 添加处理窗口调整
ipcMain.handle('adjust-window-for-normal', async (event) => {
try {
const window = BrowserWindow.fromWebContents(event.sender);
if (window) {
// 调整窗口大小和配置
window.setKiosk(false); // 退出全屏模式
window.setMinimumSize(1200, 800);
window.setSize(1200, 800);
window.setResizable(true);
window.setMaximizable(true);
window.setMinimizable(true);
// 保持无边框和隐藏标题栏的设置
window.setFullScreen(false);
}
return { success: true };
} catch (error) {
console.error('调整窗口失败:', error);
return { success: false, };
}
});
/**拖动窗口 */
ipcMain.handle('drag-window', (event) => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
// 通知渲染进程开始拖拽
focusedWindow.webContents.send('start-drag');
}
});
// 监听渲染进程发送的消息
ipcMain.handle('getPlatform', () => {
return `hi, i'm from ${process.platform}`;
});
// 窗口控制:最小化,退出全屏,关闭,
// 获取窗口最大化状态
ipcMain.handle('get-window-maximized', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
return window?.isMaximized() || false;
});
ipcMain.on('close-app', () => {
app.quit();
});
// 最小化
ipcMain.on('minimize-app', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.minimize();
}
// window?.minimize();
});
// 退出全屏
ipcMain.on('restore-window', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.unmaximize();
}
// if (window) {
// window.setFullScreen(false);
// }
});
// 设置全屏
ipcMain.on('maximize-window', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.maximize();
}
})
// 监听窗口状态变化并通知渲染进程
ipcMain.on('register-window-state-listeners', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
if (window) {
window.on('maximize', () => {
event.sender.send('window-maximized');
});
window.on('unmaximize', () => {
event.sender.send('window-unmaximized');
});
}
});

View File

@ -1,5 +1,5 @@
import { ipcMain,app } from 'electron';
import { getDeviceId, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils';
import { getDeviceIdAndMac, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils';
import { BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
import request from '../utils/request';
@ -8,119 +8,19 @@ const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const window = getBrowserWindowRuntime();
let currentServerIp: string | undefined;
// 添加处理窗口调整
ipcMain.handle('adjust-window-for-normal', async (event) => {
try {
const window = BrowserWindow.fromWebContents(event.sender);
if (window) {
// 调整窗口大小和配置
window.setKiosk(false); // 退出全屏模式
window.setMinimumSize(1200, 800);
window.setSize(1200, 800);
window.setResizable(true);
window.setMaximizable(true);
window.setMinimizable(true);
// 保持无边框和隐藏标题栏的设置
window.setFullScreen(false);
}
return { success: true };
} catch (error) {
console.error('调整窗口失败:', error);
return { success: false, };
}
});
/**拖动窗口 */
ipcMain.handle('drag-window', (event) => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
// 通知渲染进程开始拖拽
focusedWindow.webContents.send('start-drag');
}
});
// 监听渲染进程发送的消息
ipcMain.handle('getPlatform', () => {
return `hi, i'm from ${process.platform}`;
});
// 窗口控制:最小化,退出全屏,关闭,
// 获取窗口最大化状态
ipcMain.handle('get-window-maximized', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
return window?.isMaximized() || false;
});
ipcMain.on('close-app', () => {
app.quit();
});
// 最小化
ipcMain.on('minimize-app', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.minimize();
}
// window?.minimize();
});
// 退出全屏
ipcMain.on('restore-window', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.unmaximize();
}
// if (window) {
// window.setFullScreen(false);
// }
});
// 设置全屏
ipcMain.on('maximize-window', () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) {
focusedWindow.maximize();
}
})
// 监听窗口状态变化并通知渲染进程
ipcMain.on('register-window-state-listeners', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
if (window) {
window.on('maximize', () => {
event.sender.send('window-maximized');
});
window.on('unmaximize', () => {
event.sender.send('window-unmaximized');
});
}
});
ipcMain.handle('get-device-id',async()=>{
const deviceId = await getDeviceId();
console.log(`Using device ID: ${deviceId}`);
// TODO:传给后端
})
/* 1. 平台网络配置IPC 处理应用有线网络配置 */
ipcMain.handle('apply-wired-config',async(event,config)=>{
/* 1. 平台网络配置IPC 处理应用有线网络配置 */
ipcMain.handle('apply-wired-config', async (event, config) => {
// return {
// success: true,
// message: '网络配置已成功应用'
// };
try{
try {
console.log('应用网络配置:', config);
// 获取有线连接名称
const connectionName = await getWiredConnectionName();
console.log('有线连接名称:', connectionName);
if(config.method==='static'){
if (config.method === 'static') {
// 使用nmcli配置静态IP需要使用sudo权限一次性设置所有参数
let modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method manual ipv4.addresses "${config.ipv4}/${netmaskToCidr(config.subnetMask)}" ipv4.gateway "${config.ipv4Gateway}"`;
const dnsServers = [config.primaryDns, config.secondaryDns].filter(Boolean).join(',');
@ -130,11 +30,11 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
if (config.ipv6 && config.ipv6Gateway) {
const ipv6PrefixLength = config.ipv6PrefixLength &&
config.ipv6PrefixLength >= 0 &&
config.ipv6PrefixLength <= 128 ?
config.ipv6PrefixLength : 64; // 默认使用64
config.ipv6PrefixLength >= 0 &&
config.ipv6PrefixLength <= 128 ?
config.ipv6PrefixLength : 64; // 默认使用64
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${ipv6PrefixLength}" ipv6.gateway "${config.ipv6Gateway}"`;
}else if (config.ipv6 || config.ipv6Gateway) {
} else if (config.ipv6 || config.ipv6Gateway) {
console.warn('IPv6配置不完整需要同时提供IPv6地址和网关');
}
@ -145,7 +45,7 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
// 重新激活连接
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
}else{
} else {
// DHCP配置一次性设置所有参数
const modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""`;
@ -160,7 +60,7 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
success: true,
message: '网络配置已成功应用'
};
}catch(error:unknown){
} catch (error: unknown) {
console.error('应用网络配置失败:', error);
return {
success: false,
@ -169,14 +69,16 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
}
})
/**2. 服务器配置 */
/**2. 服务器配置认证 */
ipcMain.handle('connect-server', async (event, { serverIp }) => {
console.log(`Connecting to server: ${serverIp}`);
// (global as any).currentServerIp = serverIp;
// console.log('Authentication successful, server IP stored:', serverIp);
// return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
try {
// 获取设备ID
const deviceId = await getDeviceId();
console.log(`Using device ID: ${deviceId}`);
const deviceInfo = await getDeviceIdAndMac();
console.log(`Using device Info: ${JSON.stringify(deviceInfo)}`);
// 构建新的API地址使用POST请求
const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`;
@ -189,7 +91,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({ device_id: deviceId })
data: JSON.stringify({ device_id: deviceInfo.deviceId,mac_addr:deviceInfo.macAddr })
});
console.log('API response received:', response);
@ -208,7 +110,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
// 检查认证结果 - 只有code等于200时才算成功
if (responseData.code === '200' || responseData.code === 200) {
// 认证成功存储服务器IP
currentServerIp = serverIp;
(global as any).currentServerIp = serverIp;
console.log('Authentication successful, server IP stored:', serverIp);
return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
} else {
@ -251,10 +153,6 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
};
}
});
// 服务器IP获取
ipcMain.handle('get-current-server-ip', () => {
return currentServerIp;
});
/**3. 侦测管理平台 */
// 下载并更新客户端
@ -326,3 +224,4 @@ ipcMain.on('install-update-and-restart', () => {
}
}
});

View File

@ -1,181 +0,0 @@
// import { ipcMain,app,BrowserWindow } from 'electron';
// // 模拟grpc服务端
// import { BTGrpcClient } from '../grpc/BTGrpcClient';
// import { MockBTService } from '../grpc/MockBTService';
// // 客户端和服务端 通信
// const IS_TEST_MODE = true; // 设置为 false 时连接真实后端
// const GRPC_SERVER_PORT = 50051;
// // 先声明变量,但不立即初始化
// let btGrpcClient: BTGrpcClient | null = null;
// let mockServer: MockBTService | null = null;
// let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器
// // 初始化 gRPC 客户端
// async function initializeGrpc() {
// try {
// if (IS_TEST_MODE) {
// console.log('启动模拟 gRPC 服务器...');
// mockServer = new MockBTService();
// await mockServer.start(GRPC_SERVER_PORT);
// // 给一点时间让服务器启动
// await new Promise(resolve => setTimeout(resolve, 1000));
// }
// // 创建 BTGrpcClient 实例连接到gRPC服务
// btGrpcClient = new BTGrpcClient();
// // 注册进度回调函数,用于接收下载进度更新
// btGrpcClient.registerProgressCallback('all', (progress) => {
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-progress-update', progress);
// }
// });
// // 测试连接
// setTimeout(async () => {
// try {
// const connected = await btGrpcClient!.testConnection();
// console.log('gRPC 连接状态:', connected ? '已连接' : '未连接');
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-connection-status', {
// connected,
// isMock: IS_TEST_MODE
// });
// }
// } catch (error) {
// console.error('连接测试失败:', error);
// }
// }, 2000);
// // 启动健康检查
// startHealthCheck();
// } catch (error) {
// console.error('gRPC 初始化失败:', error);
// }
// }
// // 启动健康检查
// function startHealthCheck() {
// if (healthCheckInterval) {
// clearInterval(healthCheckInterval);
// }
// healthCheckInterval = setInterval(async () => {
// try {
// if (!btGrpcClient) return;
// const isConnected = await btGrpcClient.testConnection();
// const mainWindow = BrowserWindow.getFocusedWindow();
// if (mainWindow) {
// mainWindow.webContents.send('grpc-connection-status', {
// connected: isConnected,
// state: btGrpcClient.getConnectionState(),
// isMock: IS_TEST_MODE
// });
// }
// } catch (error) {
// console.error('健康检查失败:', error);
// }
// }, 5000); // 每5秒检查一次
// }
// // 应用启动时初始化
// app.whenReady().then(() => {
// initializeGrpc();
// });
// // 应用退出时清理
// app.on('before-quit', async () => {
// if (healthCheckInterval) {
// clearInterval(healthCheckInterval);
// }
// if (mockServer) {
// await mockServer.stop();
// }
// });
// // 添加 gRPC 相关的 IPC 处理程序
// ipcMain.handle('grpc-start-download', async (event, config) => {
// try {
// if (!btGrpcClient) {
// return {
// success: false,
// error: 'gRPC客户端未初始化'
// };
// }
// console.log('开始gRPC下载:', config);
// // gRPC客户端向服务端发起 StartDownload 调用
// const result = await btGrpcClient.startDownload({
// torrent_url: config.torrentUrl,
// item_name: config.itemName,
// item_id: config.itemId
// });
// return { success: true, data: result };
// } catch (error: any) {
// console.error('gRPC下载失败:', error);
// return {
// success: false,
// error: error.message || '未知错误',
// details: error.details
// };
// }
// });
// ipcMain.handle('grpc-stop-download', async (event, downloadId) => {
// try {
// if (!btGrpcClient) {
// return {
// success: false,
// error: 'gRPC客户端未初始化'
// };
// }
// const result = await btGrpcClient.stopDownload(downloadId);
// return { success: true, data: result };
// } catch (error: any) {
// return {
// success: false,
// error: error.message || '未知错误'
// };
// }
// });
// ipcMain.handle('grpc-check-connection', async () => {
// if (!btGrpcClient) {
// return { connected: false, error: 'gRPC客户端未初始化' };
// }
// return {
// connected: btGrpcClient.isConnected(),
// state: btGrpcClient.getConnectionState()
// };
// });
// // 重新连接 gRPC
// ipcMain.handle('grpc-reconnect', async () => {
// try {
// if (btGrpcClient) {
// btGrpcClient.reconnect();
// return { success: true, message: '正在重新连接' };
// } else {
// await initializeGrpc();
// return { success: true, message: '正在初始化连接' };
// }
// } catch (error: any) {
// return {
// success: false,
// error: error.message || '重新连接失败'
// };
// }
// });

View File

@ -0,0 +1,126 @@
import { ipcMain } from 'electron';
import request from '../utils/request';
import { getDeviceIdAndMac } from '../utils/utils';
// import { setCurrentServerIp, getCurrentServerIp } from './sharedState'; // 修改导
// gRPC 连接请求
ipcMain.handle('connect-to-grpc', async (event, { serverIp, clientUser }: { serverIp?: string; clientUser?: string }) => {
console.log('gRPC 连接请求');
// return {
// success: true,
// message: 'gRPC 连接成功',
// data: []
// };
try {
// 如果没有传 serverIp则使用已存储的 serverIp
const ip = serverIp || (global as any).currentServerIp;
if (!ip) {
return {
success: false,
error: '未找到服务器IP地址'
};
}
// 获取设备序列号
const deviceInfo = await getDeviceIdAndMac();
if (!deviceInfo.deviceId) {
return {
success: false,
error: '无法获取设备序列号'
};
}
// 构建请求参数
const requestData = {
serverIp: ip,
serverPort: 50051,
clientSn:deviceInfo.deviceId, // 使用设备序列号作为 clientSn,
clientUser: clientUser || '' // 如果没有用户名,则传空字符串
};
console.log('连接 gRPC 服务器:', requestData);
// 发送请求到后端接口
const apiUrl = `http://${ip}:3001/api/grpc/connect`;
const response = await request(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(requestData)
});
let responseData;
try {
responseData = JSON.parse((response as any).data);
} catch (parseError) {
console.error('解析 gRPC 连接响应失败:', parseError);
return { success: false, message: '服务器响应格式错误' };
}
console.log('gRPC 连接响应:', responseData);
if (responseData.code === '200' || responseData.code === 200) {
console.log('gRPC 连接成功');
return {
success: true,
message: 'gRPC 连接成功',
data: responseData
};
} else {
const errorMessage = responseData.message || responseData.msg || responseData.error || 'gRPC 连接失败';
console.log('gRPC 连接失败:', errorMessage);
return { success: false, message: errorMessage };
}
} catch (error) {
console.error('gRPC 连接请求失败:', error);
return {
success: false,
message: error instanceof Error ? error.message : '未知错误'
};
}
});
// 镜像列表请求
ipcMain.handle('get-images-list', async (event, deviceInfo: { deviceId: string; macAddr: string }) => {
// return { success: true, message: '获取镜像列表成功', data: []}
try {
if (!(global as any).currentServerIp) {
return {
success: false,
error: '服务器未连接'
};
}
const apiUrl = `http://${(global as any).currentServerIp}:8113/api/nex/v1/client/getImageList`;
const response = await request(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({
device_id: deviceInfo.deviceId,
macAddr: deviceInfo.macAddr
})
});
let responseData;
try {
responseData = JSON.parse((response as any).data);
} catch (parseError) {
console.error('解析镜像列表响应失败:', parseError);
return { success: false, message: '服务器响应格式错误' };
}
return responseData;
} catch (error) {
console.error('获取镜像列表失败:', error);
return {
success: false,
message: error instanceof Error ? error.message : '未知错误'
};
}
});

View File

@ -0,0 +1,2 @@
// 设置全局变量
(global as any).currentServerIp;

View File

@ -5,16 +5,29 @@ contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: async () => {
return await ipcRenderer.invoke('getPlatform');
},
// 【窗口】相关API
closeApp: () => ipcRenderer.send('close-app'),
minimizeApp: () => ipcRenderer.send('minimize-app'),
restoreWindow: () => ipcRenderer.send('restore-window'),
maximizeWindow: () => ipcRenderer.send('maximize-window'),
getWindowMaximized: () => ipcRenderer.invoke('get-window-maximized'),
adjustWindowForNormal:() => ipcRenderer.invoke('adjust-window-for-normal'),
// 【bt下载】相关 API
startDownload: (config: any) => ipcRenderer.invoke('start-download', config),
stopDownload: (downloadId: string) => ipcRenderer.invoke('stop-download', downloadId),
// 【配置步骤】相关API
applyWiredConfig: (config: any) => ipcRenderer.invoke('apply-wired-config', config),
connectServer: (config: any) => ipcRenderer.invoke('connect-server', config),
// 版本更新相关API
downloadAndUpdate: (url: string) => ipcRenderer.invoke('download-and-update', url),
// 服务器IP获取
// 【镜像列表】获取
getImagesList: (config: any) => ipcRenderer.invoke('get-images-list', config),
// 【服务器IP】获取
getCurrentServerIp: () => ipcRenderer.invoke('get-current-server-ip'),
// 【设备信息】获取
getDeviceInfo: () => ipcRenderer.invoke('get-device-info'),
// 【通知后端grpc连接】
connectToGrpc: (config:any) => ipcRenderer.invoke('connect-to-grpc', config),
// 事件监听
onMainProcessMessage: (callback: (data: string) => void) => {
ipcRenderer.on('main-process-message', (_, data) => callback(data));

View File

@ -6,11 +6,21 @@ const os = require('os');
const { promisify } = require('util');
const execAsync = promisify(exec);
/** ID
* TODO: mac
const fs = require('fs');
const path = require('path');
const configPath = path.join(os.homedir(), '.xspace_configData.json'); // 用户数据文件路径
/** IDMAC
* MAC
*/
export async function getDeviceId() {
export async function getDeviceIdAndMac() {
// return {
// deviceId:'125dfsdfsd',
// macAddr: "00:00:00:00:00"
// };
try {
let macAddr = '';
// 尝试多种方法获取唯一的设备标识
const methods = [
// 方法1: CPU序列号
@ -28,10 +38,27 @@ export async function getDeviceId() {
const { stdout } = await execAsync(command);
const deviceId = stdout.trim();
// 同时获取MAC地址供后续使用
if (!macAddr) {
const networkInterfaces = os.networkInterfaces();
outerLoop: for (const interfaceName of Object.keys(networkInterfaces)) {
const interfaces = networkInterfaces[interfaceName];
for (const iface of interfaces) {
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
macAddr = iface.mac.toUpperCase();
break outerLoop;
}
}
}
}
if (deviceId && deviceId !== '' && deviceId !== 'unknown' && deviceId !== '0000000000000000') {
console.log(`Device ID obtained using command: ${command}`);
console.log(`Device ID: ${deviceId}`);
return deviceId;
return {
deviceId,
macAddr: macAddr || 'NOT_FOUND'
};
}
} catch (error) {
console.log(`Method failed: ${command}, error: ${(error as Error).message}`);
@ -46,8 +73,12 @@ export async function getDeviceId() {
for (const iface of interfaces) {
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
const fallbackId = iface.mac.replace(/:/g, '').toUpperCase();
macAddr = iface.mac.toUpperCase();
console.log(`Using MAC address as fallback device ID: ${fallbackId}`);
return fallbackId;
return {
deviceId: fallbackId,
macAddr
};
}
}
}
@ -55,12 +86,18 @@ export async function getDeviceId() {
// 最后的fallback - 使用hostname
const hostname = os.hostname();
console.log(`Using hostname as final fallback device ID: ${hostname}`);
return hostname;
return {
deviceId: hostname,
macAddr: macAddr || 'NOT_FOUND'
};
} catch (error) {
console.error('Error getting device ID:', error);
console.error('Error getting device Info:', error);
// 返回一个默认的设备ID
return 'UNKNOWN_DEVICE';
return {
deviceId: 'UNKNOWN_DEVICE',
macAddr: 'ERROR'
};
}
}
@ -103,6 +140,39 @@ export function netmaskToCidr(netmask:string) {
return netmaskMap[netmask] || '24';
}
// 读取保存的用户数据
export function getConfig() {
try {
if (fs.existsSync(configPath)) {
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
}
} catch (error) {
console.error('读取配置失败:', error);
}
}
export function setConfig(config: any) {
try {
const oldConfig = getConfig();
const newConfig = { ...oldConfig, ...config };
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
return true;
} catch (error) {
console.error('保存配置失败:', error);
return false;
}
}
// export function getServerIp() {
// return getConfig().currentServerIp;
// }
// export function setServerIp(ip: string) {
// const config = getConfig();
// config.currentServerIp = ip;
// return setConfig(config);
// }
/**模拟更新客户端 */
export async function simulateUpdate(event: any, url: any) {
const focusedWindow = BrowserWindow.getFocusedWindow();

View File

@ -1,10 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Layout, message } from 'antd';
import { message } from 'antd';
import { history, useLocation, Outlet } from 'umi';
import './index.less';
const { Header, Sider, Content } = Layout;
const MainLayout: React.FC = () => {
const [username, setUsername] = useState('');
const location = useLocation();
@ -27,20 +25,27 @@ const MainLayout: React.FC = () => {
// TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面
setTimeout(() => {
// history.push('/login');
history.push('/configSteps?tab=terminalGetImage');
history.push('/configSteps?tab=serverConfig');
},1000)
// const fetchDeviceId = async () => {
// try {
// const res = await window.electronAPI.invoke('get-device-id');
// console.log('获取设备ID:', res);
// } catch (error) {
// console.error('获取设备ID失败:', error);
// }
// }
// fetchDeviceId()
}, []);
useEffect(() => {
const fetchDeviceId = async () => {
try {
const result = await window.electronAPI.getDeviceInfo();
if (result.success) {
console.log('获取设备信息:', JSON.stringify(result.deviceInfo));
} else {
console.error('获取设备信息失败:', result.error);
}
} catch (error) {
console.error('获取设备信息异常:', error);
}
};
fetchDeviceId();
}, []);
const handleMenuClick = (key: string) => {
// 使用路由导航
history.push(`/${key}`);

View File

@ -1,9 +1,10 @@
// src/pages/components/TitleBar/index.less
.title-bar {
margin: 20px 0;
padding: 0 20px;
// margin: 20px 0; // 使用margin拖动不会起作用需要使用padding
// height: 34px;
padding: 20px;
width: 100%;
height: 34px;
height: 74px;
display: flex;
justify-content: space-between;
align-items: center;

View File

@ -140,7 +140,7 @@ const NetworkConfig: React.FC = () => {
if (activeTab === 'dhcp') {
// 如果是 DHCP 模式直接IPC 处理应用有线网络配置
try {
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'dhcp' });
const res = await window.electronAPI.applyWiredConfig({ method: 'dhcp' });
console.log('网络配置返回信息成功:', res);
if(res.success){
message.success('网络配置成功');
@ -171,7 +171,7 @@ const NetworkConfig: React.FC = () => {
values.ipv6PrefixLength = parseInt(values.ipv6PrefixLength, 10);
}
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values });
const res = await window.electronAPI.applyWiredConfig({ method: 'static', ...values });
console.log('网络配置返回信息成功:', res);
if(res.success){
message.success('网络配置成功');

View File

@ -54,7 +54,7 @@ const Index = () => {
const values = await form.validateFields();
console.log('表单提交数据:', values);
const { serverIp } = values;
const result = await window.electronAPI.invoke('connect-server', { serverIp });
const result = await window.electronAPI.connectServer({ serverIp });
console.log('连接结果:', result);
if (result.success) {
// 连接成功保存服务器IP到本地存储
@ -62,7 +62,14 @@ const Index = () => {
console.log('Connected server IP saved to localStorage:', serverIp);
// TODO:跳转到版本更新
// setActiveTab("watchManagement")
setActiveTab("terminalGetImage")
// 通知后端连接 gRPC 服务
const grpcResult = await window.electronAPI.connectToGrpc({ serverIp });
if (grpcResult.success) {
// console.log('gRPC 连接成功');
setActiveTab("terminalGetImage")
} else {
message.error(grpcResult.message || 'gRPC 连接失败');
}
// await ipcRenderer.invoke('show-login-window');
} else {
message.error(result.message || '连接服务器失败');

View File

@ -1,43 +1,79 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import ButtonCom from '../../../components/ButtonCom';
import { useConfigStep } from '@/contexts/ConfigStepContext';
import styles from './index.less';
import { message } from 'antd';
import { history } from 'umi';
import TrueIcon from '@assets/true.png'
import WebSocketService from '../../../services/webSocketService';
import { getDeviceInfo, getImagesList } from '../../../services/imageService';
const Index = () => {
const { goToNextTab,setActiveTab } = useConfigStep();
const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [isWsConnected, setIsWsConnected] = useState(false);
// 模拟获取镜像数据的API调用
useEffect(() => {
const fetchImageData = async () => {
try {
// 模拟API请求
await new Promise(resolve => setTimeout(resolve, 3000));
// 模拟随机成功或失败
const isSuccess = Math.random() > 0.3;
setImageStatus(isSuccess ? 'success' : 'loading');
const checkImageList = useCallback(async () => {
try {
// 获取设备信息
const deviceInfo = await getDeviceInfo();
// 如果失败,继续轮询检查状态
if (!isSuccess) {
const checkStatus = async () => {
// 模拟WebSocket或轮询检查
await new Promise(resolve => setTimeout(resolve, 5000));
setImageStatus('success'); // 模拟收到通知
};
checkStatus();
}
} catch (error) {
message.error('获取镜像数据失败');
// 获取镜像列表
const response = await getImagesList({
deviceId: deviceInfo.deviceId,
macAddr: deviceInfo.macAddr
});
if (response.code === 200 && response.data?.data?.length > 0) {
// 有镜像数据,直接进入下一步
setImageStatus('success');
} else {
// 没有镜像数据需要监听WebSocket
await connectWebSocket();
}
};
fetchImageData();
} catch (error) {
console.error('检查镜像数据失败:', error);
message.error('获取镜像数据失败');
setImageStatus('error');
}
}, []);
const connectWebSocket = useCallback(async () => {
try {
// 连接WebSocket
const connected = await WebSocketService.connect();
setIsWsConnected(connected);
if (connected) {
// 监听镜像数据推送
const removeListener = WebSocketService.addListener('2', (data:any) => {
if (data.type === '2') {
// 有镜像数据了,更新状态
setImageStatus('success');
// 移除监听器
removeListener();
}
});
} else {
message.error('WebSocket连接失败');
}
} catch (error) {
console.error('WebSocket连接失败:', error);
message.error('WebSocket连接失败');
}
}, []);
useEffect(() => {
// 组件挂载时检查镜像数据
checkImageList();
// 组件卸载时清理
return () => {
// 可以选择是否断开WebSocket连接
};
}, [checkImageList]);
const ImageLoadingComponent = () => (
<div className={styles["image-loading-container"]}>
<div className={styles["loading-wrapper"]}>

View File

@ -1,93 +0,0 @@
// 页面头部样式
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
}
// 镜像列表样式
.image-list {
.image-detail {
.detail-item {
margin-bottom: 16px;
label {
font-weight: 600;
color: #333;
display: inline-block;
width: 100px;
}
span {
color: #666;
}
p {
margin: 8px 0 0 100px;
color: #666;
line-height: 1.6;
}
}
}
}
// 个人资料样式
.profile-page {
.profile-content {
.profile-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
.profile-info {
h3 {
margin: 0 0 4px 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
.quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.profile-content {
.profile-header {
flex-direction: column;
text-align: center;
}
.quick-actions {
justify-content: center;
}
}
}

View File

@ -1,230 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Tag, Button, Space, Modal, message } from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
EyeOutlined,
DownloadOutlined
} from '@ant-design/icons';
import './index.less';
interface ImageItem {
id: string;
name: string;
version: string;
size: string;
status: 'active' | 'inactive' | 'building';
createTime: string;
description: string;
}
const ImageList: React.FC = () => {
const [images, setImages] = useState<ImageItem[]>([]);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
const [detailVisible, setDetailVisible] = useState(false);
useEffect(() => {
loadImages();
}, []);
const loadImages = () => {
setLoading(true);
// 模拟数据加载
setTimeout(() => {
const mockData: ImageItem[] = [
{
id: '1',
name: 'Windows 10 专业版',
version: 'v1.0.0',
size: '15.2 GB',
status: 'active',
createTime: '2024-01-15 10:30:00',
description: 'Windows 10 专业版镜像,包含常用办公软件'
},
{
id: '2',
name: 'Ubuntu 22.04 LTS',
version: 'v2.1.0',
size: '8.5 GB',
status: 'active',
createTime: '2024-01-10 14:20:00',
description: 'Ubuntu 22.04 LTS 服务器版本,适用于开发环境'
},
{
id: '3',
name: 'CentOS 8',
version: 'v1.5.0',
size: '12.1 GB',
status: 'building',
createTime: '2024-01-20 09:15:00',
description: 'CentOS 8 企业级服务器操作系统'
},
{
id: '4',
name: 'macOS Monterey',
version: 'v1.2.0',
size: '18.7 GB',
status: 'inactive',
createTime: '2024-01-05 16:45:00',
description: 'macOS Monterey 开发环境镜像'
}
];
setImages(mockData);
setLoading(false);
}, 1000);
};
const getStatusTag = (status: string) => {
const statusMap = {
active: { color: 'green', text: '可用' },
inactive: { color: 'red', text: '不可用' },
building: { color: 'orange', text: '构建中' }
};
const config = statusMap[status as keyof typeof statusMap];
return <Tag color={config.color}>{config.text}</Tag>;
};
const handleViewDetail = (record: ImageItem) => {
setSelectedImage(record);
setDetailVisible(true);
};
const handleDownload = (record: ImageItem) => {
message.success(`开始下载镜像:${record.name}`);
};
const handleEdit = (record: ImageItem) => {
message.info(`编辑镜像:${record.name}`);
};
const handleDelete = (record: ImageItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除镜像 "${record.name}" 吗?`,
onOk: () => {
setImages(images.filter(img => img.id !== record.id));
message.success('删除成功');
}
});
};
const columns = [
{
title: '镜像名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '版本',
dataIndex: 'version',
key: 'version',
width: 100,
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
width: 100,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => getStatusTag(status),
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180,
},
{
title: '操作',
key: 'action',
width: 200,
render: (_: any, record: ImageItem) => (
<Space size="small">
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => handleViewDetail(record)}
title="查看详情"
/>
<Button
type="text"
icon={<DownloadOutlined />}
onClick={() => handleDownload(record)}
title="下载"
/>
</Space>
),
},
];
return (
<div className="image-list">
<Card>
<Table
columns={columns}
dataSource={images}
rowKey="id"
loading={loading}
pagination={{
total: images.length,
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
}}
/>
</Card>
<Modal
title="镜像详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailVisible(false)}>
</Button>
]}
width={600}
>
{selectedImage && (
<div className="image-detail">
<div className="detail-item">
<label></label>
<span>{selectedImage.name}</span>
</div>
<div className="detail-item">
<label></label>
<span>{selectedImage.version}</span>
</div>
<div className="detail-item">
<label></label>
<span>{selectedImage.size}</span>
</div>
<div className="detail-item">
<label></label>
<span>{getStatusTag(selectedImage.status)}</span>
</div>
<div className="detail-item">
<label></label>
<span>{selectedImage.createTime}</span>
</div>
<div className="detail-item">
<label></label>
<p>{selectedImage.description}</p>
</div>
</div>
)}
</Modal>
</div>
);
};
export default ImageList;

View File

@ -0,0 +1,169 @@
/* src/pages/imagesList/index.less */
.images-list-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
.shadow-divider {
height: 20px;
border-radius: 4px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%);
flex-shrink: 0;
}
.content-container {
flex: 1;
padding: 29px 52px;
display: flex;
flex-direction: column;
overflow: hidden;
.header {
margin-bottom: 50px;
h1 {
font-family: "PingFang SC";
font-weight: 600;
font-style: normal;
font-size: 18px;
line-height: 29px;
color: rgba(51, 51, 51, 1);
margin: 0;
}
}
.images-grid {
// 移除 flex: 1
display: grid;
grid-template-columns: repeat(auto-fill, 150px);
gap: 50px 38px; // row-gap: 50px, column-gap: 38px
padding-bottom: 70px;
overflow-y: auto; // 保持滚动功能
// 添加最小高度以确保滚动正常工作
min-height: 0; // 关键:允许网格容器收缩
.image-item {
position: relative;
width: 150px;
height: 129px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
// 添加状态图标样式
.status-icon {
position: absolute;
top: 8px;
right: 8px;
width: 15px;
height: 15px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
.status-icon-download {
font-size: 15px;
color: #1890ff; // 蓝色下载图标
}
.status-icon-completed {
font-size: 15px;
color: #52c41a; // 绿色完成图标
}
}
.image-icon {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
padding-top: 10px;
}
.image-name {
width: 100%;
height: 29px;
padding: 0 10px;
font-size: 14px;
text-align: center;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.download-overlay,
.completed-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.download-overlay {
.download-info {
width: 100%;
padding: 0 10px;
text-align: center;
.download-text {
color: white;
font-size: 14px;
margin-bottom: 8px;
}
.progress-bar {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
.progress-fill {
height: 100%;
background: #4a90e2;
border-radius: 3px;
transition: width 0.3s;
}
}
.speed-info,
.eta-info {
color: white;
font-size: 12px;
margin-bottom: 4px;
}
}
}
.completed-overlay {
.completed-text {
color: white;
font-size: 16px;
font-weight: bold;
}
}
}
}
}
}

View File

@ -1,11 +1,504 @@
import React from 'react';
// src/pages/imagesList/index.tsx
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { message } from 'antd';
import { DownloadOutlined, CheckCircleOutlined } from '@ant-design/icons'; // 引入 Ant Design 图标
import TitleBar from '../components/TitleBar';
import ImageFileIcon from '@assets/imageFileIcon.png'
import { formatDecimalSimple } from '@/utils/utils';
import WebSocketService from '../services/webSocketService';
import './index.less';
const Index = () => {
return (
<div>
</div>
);
// 镜像项类型定义
interface ImageItem {
id: string;
name: string;
torrentUrl: string;
icon: string;
}
export default Index;
// 下载状态类型定义
interface DownloadProgress {
downloadId: string;
progress: number;
downloadSpeedText: string;
etaText: string;
state: 'started' | 'progress' | 'completed' | 'error' | 'stopped';
}
const ImagesList: React.FC = () => {
// 镜像列表数据(示例数据)
const [images, setImages] = useState<ImageItem[]>([
{
id: 'ubuntu-22-04',
name: 'Ubuntu 22.04 LTS',
torrentUrl: 'https://releases.ubuntu.com/22.04/ubuntu-22.04.3-desktop-amd64.iso.torrent',
icon: ''
},
{
id: 'windows-11',
name: 'Windows 11 Pro',
torrentUrl: 'https://download.microsoft.com/windows-11.iso.torrent',
icon: ''
},
{
id: 'centos-8',
name: 'CentOS 8 Stream',
torrentUrl: 'https://centos.org/centos-8-stream.iso.torrent',
icon: ''
},
{
id: 'debian-12',
name: 'Debian 12 Bookworm',
torrentUrl: 'https://debian.org/debian-12.iso.torrent',
icon: ''
},
{
id: 'fedora-38',
name: 'Fedora 38 Workstation',
torrentUrl: 'https://fedoraproject.org/fedora-38.iso.torrent',
icon: ''
},
{
id: 'opensuse-leap-1',
name: 'openSUSE Leap 15.5',
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
icon: ''
},
{
id: 'opensuse-leap-2',
name: 'openSUSE Leap 15.5',
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
icon: ''
},
{
id: 'opensuse-leap-3',
name: 'openSUSE Leap 15.5openSUSE Leap 15.5',
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
icon: ''
}
]);
// 下载状态映射
const [downloadStatus, setDownloadStatus] = useState<Map<string, DownloadProgress>>(new Map());
// WebSocket连接状态
const [wsConnected, setWsConnected] = useState(false);
// 防抖计时器映射
const debounceTimers = useMemo(() => new Map<string, NodeJS.Timeout>(), []);
// 初始化WebSocket连接
useEffect(() => {
let removeProgressListener: (() => void) | null = null;
let removeStartListener: (() => void) | null = null;
let removeCompleteListener: (() => void) | null = null;
let removeErrorListener: (() => void) | null = null;
let removeStopListener: (() => void) | null = null;
const initWebSocket = async () => {
try {
const connected = await WebSocketService.connect();
setWsConnected(connected);
if (connected) {
// 监听下载进度更新
removeProgressListener = WebSocketService.addListener('download-progress', (data: any) => {
if (data.type === 'download-progress') {
setDownloadStatus(prev => {
const newMap = new Map(prev);
newMap.set(data.downloadId, {
downloadId: data.downloadId,
progress: data.progress,
downloadSpeedText: data.downloadSpeedText,
etaText: data.etaText,
state: 'progress'
});
return newMap;
});
}
});
// 监听下载开始
removeStartListener = WebSocketService.addListener('download-started', (data: any) => {
if (data.type === 'download-started') {
setDownloadStatus(prev => {
const newMap = new Map(prev);
newMap.set(data.downloadId, {
downloadId: data.downloadId,
progress: 0,
downloadSpeedText: '0 MB/s',
etaText: '计算中...',
state: 'started'
});
return newMap;
});
}
});
// 监听下载完成
removeCompleteListener = WebSocketService.addListener('download-completed', (data: any) => {
if (data.type === 'download-completed') {
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
progress: 100,
state: 'completed'
});
}
return newMap;
});
message.success(`${ data.itemName } 下载完成`)
}
});
// 监听下载错误
removeErrorListener = WebSocketService.addListener('download-error', (data: any) => {
if (data.type === 'download-error') {
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
state: 'error'
});
}
return newMap;
});
message.error(`下载错误: ${data.error}`);
}
});
// 监听下载停止
removeStopListener = WebSocketService.addListener('download-stopped', (data: any) => {
if (data.type === 'download-stopped') {
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
state: 'stopped'
});
}
return newMap;
});
}
});
} else {
message.error('WebSocket连接失败');
}
} catch (error) {
console.error('WebSocket初始化失败:', error);
message.error('WebSocket初始化失败');
}
};
initWebSocket();
// 清理函数
return () => {
// 注意我们不主动断开WebSocket连接让它保持连接状态
// 只是移除当前页面的监听器
if (removeProgressListener) removeProgressListener();
if (removeStartListener) removeStartListener();
if (removeCompleteListener) removeCompleteListener();
if (removeErrorListener) removeErrorListener();
if (removeStopListener) removeStopListener();
};
}, []);
// 初始化WebSocket连接
// useEffect(() => {
// let progressWS: WebSocket;
// const initWebSocket = async () => {
// try {
// // 获取保存的服务器 IP
// const serverIp = await window.electronAPI.getCurrentServerIp();
// // if (!serverIp) {
// // message.error('服务器未连接,请先配置服务器');
// // return;
// // }
// // 如果没有服务器IP则使用本地测试
// const wsUrl = serverIp
// ? `ws://${serverIp}:3002`
// : 'ws://localhost:3002'; // 本地测试用的WebSocket地址
// console.log('尝试连接WebSocket:', wsUrl);
// progressWS = new WebSocket(wsUrl);
// // 使用服务器 IP 建立 WebSocket 连接
// // progressWS = new WebSocket(`ws://${serverIp}:3002`);
// progressWS.onopen = () => {
// console.log('WebSocket连接成功');
// setWsConnected(true);
// // 发送认证
// progressWS.send(JSON.stringify({
// type: 'auth',
// clientId: 'electron-frontend'
// }));
// };
// progressWS.onmessage = (event) => {
// const data = JSON.parse(event.data);
// handleProgressMessage(data);
// };
// progressWS.onerror = (error) => {
// console.error('WebSocket错误:', error);
// setWsConnected(false);
// };
// progressWS.onclose = () => {
// console.log('WebSocket连接关闭');
// setWsConnected(false);
// // 尝试重连
// setTimeout(initWebSocket, 5000);
// };
// } catch (error) {
// console.error('初始化WebSocket失败:', error);
// message.error('连接服务器失败');
// }
// };
// initWebSocket();
// return () => {
// if (progressWS) {
// progressWS.close();
// }
// };
// }, []);
// 处理WebSocket消息
const handleProgressMessage = useCallback((data: any) => {
switch (data.type) {
case 'download-started':
setDownloadStatus(prev => {
const newMap = new Map(prev);
newMap.set(data.downloadId, {
downloadId: data.downloadId,
progress: 0,
downloadSpeedText: '0 MB/s',
etaText: '计算中...',
state: 'started'
});
return newMap;
});
break;
case 'download-progress':
setDownloadStatus(prev => {
const newMap = new Map(prev);
newMap.set(data.downloadId, {
downloadId: data.downloadId,
progress: data.progress,
downloadSpeedText: data.downloadSpeedText,
etaText: data.etaText,
state: 'progress'
});
return newMap;
});
break;
case 'download-completed':
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
progress: 100,
state: 'completed'
});
}
return newMap;
});
message.success(`下载完成: ${data.itemName}`);
break;
case 'download-error':
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
state: 'error'
});
}
return newMap;
});
message.error(`下载错误: ${data.error}`);
break;
case 'download-stopped':
setDownloadStatus(prev => {
const newMap = new Map(prev);
const status = newMap.get(data.downloadId);
if (status) {
newMap.set(data.downloadId, {
...status,
state: 'stopped'
});
}
return newMap;
});
message.info('下载已停止');
break;
}
}, []);
// 开始下载
const startDownload = useCallback((image: ImageItem) => {
// 防抖处理
if (debounceTimers.has(image.id)) {
clearTimeout(debounceTimers.get(image.id)!);
}
const timer = setTimeout(async () => {
try {
// 检查是否已经在下载
if (downloadStatus.has(image.id)) {
const status = downloadStatus.get(image.id)!;
if (status.state === 'progress' || status.state === 'started') {
message.warning('已在下载中,请勿重复点击');
return;
}
}
// 通过 Electron IPC 发送 HTTP 请求
const result = await window.electronAPI.startDownload({
torrentUrl: image.torrentUrl,
itemName: image.name,
itemId: image.id,
savePath: '/mnt/disk/tmp',
targetPath: '/mnt/disk/voi'
});
if (!result.success) {
message.error(`下载启动失败: ${result.error}`);
} else {
message.success('开始下载');
}
} catch (error) {
console.error('下载请求失败:', error);
message.error('下载请求失败');
}
}, 300); // 300ms 防抖延迟
debounceTimers.set(image.id, timer);
}, [downloadStatus, debounceTimers]);
// 停止下载
const stopDownload = useCallback(async (downloadId: string) => {
try {
const result = await window.electronAPI.stopDownload(downloadId);
if (!result.success) {
message.error(`停止下载失败: ${result.error}`);
} else {
message.success('下载已停止');
// 停止成功后清除该下载的状态
setDownloadStatus(prev => {
const newMap = new Map(prev);
newMap.delete(downloadId);
return newMap;
});
}
} catch (error) {
console.error('停止下载失败:', error);
message.error('停止下载失败');
}
}, []);
// 渲染镜像项
const renderImageItem = (image: ImageItem) => {
const status = downloadStatus.get(image.id);
const isDownloading = status && (status.state === 'started' || status.state === 'progress');
const isCompleted = status && status.state === 'completed';
return (
<div
className={`image-item ${isCompleted ? 'completed' : ''}`}
key={image.id}
onClick={() => {
if (isDownloading) {
stopDownload(image.id);
} else if (!isCompleted) {
startDownload(image);
}
}}
>
{/* 添加状态图标 */}
<div className="status-icon">
{!isDownloading && (
<div className="status-icon">
{isCompleted ? (
<CheckCircleOutlined className="status-icon-completed" />
) : (
<DownloadOutlined className="status-icon-download" />
)}
</div>
)}
</div>
<div className="image-icon">
<img src={ImageFileIcon} />
</div>
<div className="image-name" title={image.name}>
{image.name}
</div>
{isDownloading && (
<div className="download-overlay">
<div className="download-info">
<div className="download-text">...{status?.progress.toFixed(1)}%</div>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${formatDecimalSimple(status?.progress)}%` }}
/>
</div>
<div className="speed-info">{status?.downloadSpeedText}</div>
<div className="eta-info">{status?.etaText}</div>
</div>
</div>
)}
{/* {isCompleted && (
<div className="completed-overlay">
<div className="completed-text"></div>
</div>
)} */}
</div>
);
};
return (
<div className="images-list-container">
<TitleBar />
<div className="shadow-divider" />
<div className="content-container">
<div className="header">
<h1></h1>
</div>
<div className="images-grid-con">
<div className="images-grid">
{images.map(renderImageItem)}
</div>
</div>
</div>
</div>
);
};
export default ImagesList;

View File

@ -23,8 +23,27 @@ const LoginPage: React.FC = () => {
// 存储登录状态
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('username', values.username);
// 跳转到镜像列表页面
history.push('/imagesList');
// 连接 gRPC 服务
// TODO:如果设置了ip后面会跳过serverConfig页面,这localStorage还能获取ip吗?
// const serverIp = localStorage.getItem('connected-server-ip');
const serverIp = await window.electronAPI.getCurrentServerIp();
console.log('登录后连接grpc 获取到的服务器IP:', serverIp);
if (serverIp) {
const grpcResult = await window.electronAPI.connectToGrpc({
serverIp,
clientUser: values.username
});
if (grpcResult.success) {
// console.log('gRPC 连接成功');
// 跳转到镜像列表页面
history.push('/imagesList');
} else {
message.error(grpcResult.message || 'gRPC 连接失败');
}
}
} else {
message.error('用户名或密码错误!');
}

View File

@ -0,0 +1,44 @@
// 获取设备信息的接口
interface DeviceInfo {
deviceId: string;
macAddr: string;
}
// 获取镜像列表的响应结构
interface ImageListResponse {
code: number;
data: {
data: any[];
};
message: string;
}
// 获取设备信息
export const getDeviceInfo = async (): Promise<DeviceInfo> => {
try {
const result = await window.electronAPI.getDeviceInfo();
if (result.success && result.deviceInfo) {
return {
deviceId: result.deviceInfo.deviceId || '',
macAddr: result.deviceInfo.macAddr || ''
};
}
throw new Error('获取设备信息失败');
} catch (error) {
console.error('获取设备信息异常:', error);
throw error;
}
};
// 获取镜像列表
export const getImagesList = async (deviceInfo: DeviceInfo): Promise<ImageListResponse> => {
try {
// 这里使用electron的request工具发送请求
const response = await window.electronAPI.getImagesList(deviceInfo);
return response;
} catch (error) {
console.error('获取镜像列表失败:', error);
throw error;
}
};

View File

@ -0,0 +1,197 @@
// src/services/webSocketService.ts
class WebSocketService {
private static instance: WebSocketService;
private ws: WebSocket | null = null;
private listeners: Map<string, Set<(data: any) => void>> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000;
private isConnectedValue = false;
private connectionPromise: Promise<boolean> | null = null;
private constructor() {}
public static getInstance(): WebSocketService {
if (!WebSocketService.instance) {
WebSocketService.instance = new WebSocketService();
}
return WebSocketService.instance;
}
public connect(): Promise<boolean> {
// 如果已经有一个连接尝试正在进行返回该Promise
if (this.connectionPromise) {
return this.connectionPromise;
}
// 如果已经连接直接返回true
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return Promise.resolve(true);
}
// 创建新的连接Promise
this.connectionPromise = new Promise<boolean>(async (resolve) => {
// 如果WebSocket已经存在且正在连接中等待连接结果
if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
const onOpen = () => {
this.ws?.removeEventListener('open', onOpen);
this.ws?.removeEventListener('error', onError);
resolve(true);
};
const onError = () => {
this.ws?.removeEventListener('open', onOpen);
this.ws?.removeEventListener('error', onError);
resolve(false);
};
this.ws?.addEventListener('open', onOpen);
this.ws?.addEventListener('error', onError);
return;
}
try {
// 获取服务器IP地址
const serverIp = await window.electronAPI.getCurrentServerIp();
const wsUrl = serverIp
? `ws://${serverIp}:3002`
: 'ws://localhost:3002';
this.ws = new WebSocket(wsUrl);
// this.ws = new WebSocket('ws://localhost:3002'); // 本地测试用的WebSocket地址
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.isConnectedValue = true;
this.reconnectAttempts = 0;
this.sendAuth();
resolve(true);
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
this.ws.onclose = () => {
console.log('WebSocket连接关闭');
this.isConnectedValue = false;
this.handleReconnect();
resolve(false);
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
this.isConnectedValue = false;
resolve(false);
};
} catch (error) {
console.error('WebSocket连接失败:', error);
this.isConnectedValue = false;
resolve(false);
}
}).finally(() => {
// 清除连接Promise允许下一次连接尝试
this.connectionPromise = null;
});
return this.connectionPromise;
}
private sendAuth() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'auth',
clientId: 'electron-frontend'
}));
}
}
private handleMessage(data: any) {
// 通知所有监听器
const typeListeners = this.listeners.get(data.type);
if (typeListeners) {
typeListeners.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('处理WebSocket消息时出错:', error);
}
});
}
// 通知通用监听器
const allListeners = this.listeners.get('all');
if (allListeners) {
allListeners.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('处理WebSocket消息时出错:', error);
}
});
}
}
public addListener(type: string, callback: (data: any) => void): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set());
}
this.listeners.get(type)!.add(callback);
// 返回移除监听器的函数
return () => {
const typeListeners = this.listeners.get(type);
if (typeListeners) {
typeListeners.delete(callback);
}
};
}
public removeListener(type: string, callback: (data: any) => void) {
const typeListeners = this.listeners.get(type);
if (typeListeners) {
typeListeners.delete(callback);
}
}
public sendMessage(message: any) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
private handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.connect();
}, this.reconnectDelay);
}
}
public disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.listeners.clear();
this.reconnectAttempts = 0;
this.isConnectedValue = false;
this.connectionPromise = null;
}
public isConnected(): boolean {
return this.isConnectedValue && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
}
export default WebSocketService.getInstance();

View File

@ -0,0 +1,10 @@
export function formatDecimalSimple(num: number | string) {
const number = Number(num);
if (isNaN(number)) return '0';
// 先四舍五入到两位小数
const rounded = Math.round(number * 100) / 100;
// 使用toFixed(2)然后移除末尾的0
return parseFloat(rounded.toFixed(2)).toString();
}