feat(pc): 码码码
parent
865dcdae6d
commit
0c1308335b
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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下载逻辑的完整执行流程。
|
||||
|
|
@ -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 {
|
||||
}
|
||||
|
|
@ -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 : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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(',');
|
||||
|
|
@ -134,7 +34,7 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
|
|||
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', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -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 || '重新连接失败'
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
|
@ -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 : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// 设置全局变量
|
||||
(global as any).currentServerIp;
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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'); // 用户数据文件路径
|
||||
|
||||
/** 获取设备ID(芯片序列号)和MAC地址
|
||||
* 返回设备唯一标识符和原始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();
|
||||
|
|
|
|||
|
|
@ -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,18 +25,25 @@ 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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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('网络配置成功');
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
// 通知后端连接 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 || '连接服务器失败');
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
const checkImageList = useCallback(async () => {
|
||||
try {
|
||||
// 模拟API请求
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
// 模拟随机成功或失败
|
||||
const isSuccess = Math.random() > 0.3;
|
||||
setImageStatus(isSuccess ? 'success' : 'loading');
|
||||
// 获取设备信息
|
||||
const deviceInfo = await getDeviceInfo();
|
||||
|
||||
// 如果失败,继续轮询检查状态
|
||||
if (!isSuccess) {
|
||||
const checkStatus = async () => {
|
||||
// 模拟WebSocket或轮询检查
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
setImageStatus('success'); // 模拟收到通知
|
||||
};
|
||||
checkStatus();
|
||||
// 获取镜像列表
|
||||
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();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查镜像数据失败:', error);
|
||||
message.error('获取镜像数据失败');
|
||||
setImageStatus('error');
|
||||
}
|
||||
};
|
||||
|
||||
fetchImageData();
|
||||
}, []);
|
||||
|
||||
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"]}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -23,8 +23,27 @@ const LoginPage: React.FC = () => {
|
|||
// 存储登录状态
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
localStorage.setItem('username', values.username);
|
||||
|
||||
// 连接 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('用户名或密码错误!');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue