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 { 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 { BrowserWindow } from 'electron';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import request from '../utils/request';
|
import request from '../utils/request';
|
||||||
|
|
@ -8,119 +8,19 @@ const { exec } = require('child_process');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
const window = getBrowserWindowRuntime();
|
/* 1. 平台网络配置:IPC 处理应用有线网络配置 */
|
||||||
|
ipcMain.handle('apply-wired-config', async (event, config) => {
|
||||||
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)=>{
|
|
||||||
// return {
|
// return {
|
||||||
// success: true,
|
// success: true,
|
||||||
// message: '网络配置已成功应用'
|
// message: '网络配置已成功应用'
|
||||||
// };
|
// };
|
||||||
try{
|
try {
|
||||||
console.log('应用网络配置:', config);
|
console.log('应用网络配置:', config);
|
||||||
// 获取有线连接名称
|
// 获取有线连接名称
|
||||||
const connectionName = await getWiredConnectionName();
|
const connectionName = await getWiredConnectionName();
|
||||||
console.log('有线连接名称:', connectionName);
|
console.log('有线连接名称:', connectionName);
|
||||||
|
|
||||||
if(config.method==='static'){
|
if (config.method === 'static') {
|
||||||
// 使用nmcli配置静态IP,需要使用sudo权限,一次性设置所有参数
|
// 使用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}"`;
|
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(',');
|
const dnsServers = [config.primaryDns, config.secondaryDns].filter(Boolean).join(',');
|
||||||
|
|
@ -130,11 +30,11 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
|
||||||
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
|
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
|
||||||
if (config.ipv6 && config.ipv6Gateway) {
|
if (config.ipv6 && config.ipv6Gateway) {
|
||||||
const ipv6PrefixLength = config.ipv6PrefixLength &&
|
const ipv6PrefixLength = config.ipv6PrefixLength &&
|
||||||
config.ipv6PrefixLength >= 0 &&
|
config.ipv6PrefixLength >= 0 &&
|
||||||
config.ipv6PrefixLength <= 128 ?
|
config.ipv6PrefixLength <= 128 ?
|
||||||
config.ipv6PrefixLength : 64; // 默认使用64
|
config.ipv6PrefixLength : 64; // 默认使用64
|
||||||
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${ipv6PrefixLength}" ipv6.gateway "${config.ipv6Gateway}"`;
|
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地址和网关');
|
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}"`);
|
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
|
||||||
|
|
||||||
}else{
|
} else {
|
||||||
// DHCP配置,一次性设置所有参数
|
// DHCP配置,一次性设置所有参数
|
||||||
const modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""`;
|
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,
|
success: true,
|
||||||
message: '网络配置已成功应用'
|
message: '网络配置已成功应用'
|
||||||
};
|
};
|
||||||
}catch(error:unknown){
|
} catch (error: unknown) {
|
||||||
console.error('应用网络配置失败:', error);
|
console.error('应用网络配置失败:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -169,14 +69,16 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**2. 服务器配置 */
|
/**2. 服务器配置认证 */
|
||||||
ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
||||||
console.log(`Connecting to server: ${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 {
|
try {
|
||||||
// 获取设备ID
|
// 获取设备ID
|
||||||
const deviceId = await getDeviceId();
|
const deviceInfo = await getDeviceIdAndMac();
|
||||||
console.log(`Using device ID: ${deviceId}`);
|
console.log(`Using device Info: ${JSON.stringify(deviceInfo)}`);
|
||||||
|
|
||||||
// 构建新的API地址,使用POST请求
|
// 构建新的API地址,使用POST请求
|
||||||
const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`;
|
const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`;
|
||||||
|
|
@ -189,7 +91,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'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);
|
console.log('API response received:', response);
|
||||||
|
|
@ -208,7 +110,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
||||||
// 检查认证结果 - 只有code等于200时才算成功
|
// 检查认证结果 - 只有code等于200时才算成功
|
||||||
if (responseData.code === '200' || responseData.code === 200) {
|
if (responseData.code === '200' || responseData.code === 200) {
|
||||||
// 认证成功,存储服务器IP
|
// 认证成功,存储服务器IP
|
||||||
currentServerIp = serverIp;
|
(global as any).currentServerIp = serverIp;
|
||||||
console.log('Authentication successful, server IP stored:', serverIp);
|
console.log('Authentication successful, server IP stored:', serverIp);
|
||||||
return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
|
return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -251,10 +153,6 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 服务器IP获取
|
|
||||||
ipcMain.handle('get-current-server-ip', () => {
|
|
||||||
return currentServerIp;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**3. 侦测管理平台 */
|
/**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 () => {
|
getPlatform: async () => {
|
||||||
return await ipcRenderer.invoke('getPlatform');
|
return await ipcRenderer.invoke('getPlatform');
|
||||||
},
|
},
|
||||||
|
// 【窗口】相关API
|
||||||
closeApp: () => ipcRenderer.send('close-app'),
|
closeApp: () => ipcRenderer.send('close-app'),
|
||||||
minimizeApp: () => ipcRenderer.send('minimize-app'),
|
minimizeApp: () => ipcRenderer.send('minimize-app'),
|
||||||
restoreWindow: () => ipcRenderer.send('restore-window'),
|
restoreWindow: () => ipcRenderer.send('restore-window'),
|
||||||
maximizeWindow: () => ipcRenderer.send('maximize-window'),
|
maximizeWindow: () => ipcRenderer.send('maximize-window'),
|
||||||
getWindowMaximized: () => ipcRenderer.invoke('get-window-maximized'),
|
getWindowMaximized: () => ipcRenderer.invoke('get-window-maximized'),
|
||||||
adjustWindowForNormal:() => ipcRenderer.invoke('adjust-window-for-normal'),
|
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
|
// 版本更新相关API
|
||||||
downloadAndUpdate: (url: string) => ipcRenderer.invoke('download-and-update', url),
|
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'),
|
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) => {
|
onMainProcessMessage: (callback: (data: string) => void) => {
|
||||||
ipcRenderer.on('main-process-message', (_, data) => callback(data));
|
ipcRenderer.on('main-process-message', (_, data) => callback(data));
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,21 @@ const os = require('os');
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
/** 获取设备ID(芯片序列号)
|
const fs = require('fs');
|
||||||
* TODO: 增加获取mac地址的逻辑,需要传给后端
|
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 {
|
try {
|
||||||
|
let macAddr = '';
|
||||||
|
|
||||||
// 尝试多种方法获取唯一的设备标识
|
// 尝试多种方法获取唯一的设备标识
|
||||||
const methods = [
|
const methods = [
|
||||||
// 方法1: CPU序列号
|
// 方法1: CPU序列号
|
||||||
|
|
@ -28,10 +38,27 @@ export async function getDeviceId() {
|
||||||
const { stdout } = await execAsync(command);
|
const { stdout } = await execAsync(command);
|
||||||
const deviceId = stdout.trim();
|
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') {
|
if (deviceId && deviceId !== '' && deviceId !== 'unknown' && deviceId !== '0000000000000000') {
|
||||||
console.log(`Device ID obtained using command: ${command}`);
|
console.log(`Device ID obtained using command: ${command}`);
|
||||||
console.log(`Device ID: ${deviceId}`);
|
console.log(`Device ID: ${deviceId}`);
|
||||||
return deviceId;
|
return {
|
||||||
|
deviceId,
|
||||||
|
macAddr: macAddr || 'NOT_FOUND'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Method failed: ${command}, error: ${(error as Error).message}`);
|
console.log(`Method failed: ${command}, error: ${(error as Error).message}`);
|
||||||
|
|
@ -46,8 +73,12 @@ export async function getDeviceId() {
|
||||||
for (const iface of interfaces) {
|
for (const iface of interfaces) {
|
||||||
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
||||||
const fallbackId = iface.mac.replace(/:/g, '').toUpperCase();
|
const fallbackId = iface.mac.replace(/:/g, '').toUpperCase();
|
||||||
|
macAddr = iface.mac.toUpperCase();
|
||||||
console.log(`Using MAC address as fallback device ID: ${fallbackId}`);
|
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
|
// 最后的fallback - 使用hostname
|
||||||
const hostname = os.hostname();
|
const hostname = os.hostname();
|
||||||
console.log(`Using hostname as final fallback device ID: ${hostname}`);
|
console.log(`Using hostname as final fallback device ID: ${hostname}`);
|
||||||
return hostname;
|
return {
|
||||||
|
deviceId: hostname,
|
||||||
|
macAddr: macAddr || 'NOT_FOUND'
|
||||||
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting device ID:', error);
|
console.error('Error getting device Info:', error);
|
||||||
// 返回一个默认的设备ID
|
// 返回一个默认的设备ID
|
||||||
return 'UNKNOWN_DEVICE';
|
return {
|
||||||
|
deviceId: 'UNKNOWN_DEVICE',
|
||||||
|
macAddr: 'ERROR'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +140,39 @@ export function netmaskToCidr(netmask:string) {
|
||||||
return netmaskMap[netmask] || '24';
|
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) {
|
export async function simulateUpdate(event: any, url: any) {
|
||||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Layout, message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { history, useLocation, Outlet } from 'umi';
|
import { history, useLocation, Outlet } from 'umi';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
|
||||||
|
|
||||||
const MainLayout: React.FC = () => {
|
const MainLayout: React.FC = () => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -27,20 +25,27 @@ const MainLayout: React.FC = () => {
|
||||||
// TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面
|
// TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// history.push('/login');
|
// history.push('/login');
|
||||||
history.push('/configSteps?tab=terminalGetImage');
|
history.push('/configSteps?tab=serverConfig');
|
||||||
},1000)
|
},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) => {
|
const handleMenuClick = (key: string) => {
|
||||||
// 使用路由导航
|
// 使用路由导航
|
||||||
history.push(`/${key}`);
|
history.push(`/${key}`);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
// src/pages/components/TitleBar/index.less
|
// src/pages/components/TitleBar/index.less
|
||||||
.title-bar {
|
.title-bar {
|
||||||
margin: 20px 0;
|
// margin: 20px 0; // 使用margin拖动不会起作用,需要使用padding
|
||||||
padding: 0 20px;
|
// height: 34px;
|
||||||
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 34px;
|
height: 74px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ const NetworkConfig: React.FC = () => {
|
||||||
if (activeTab === 'dhcp') {
|
if (activeTab === 'dhcp') {
|
||||||
// 如果是 DHCP 模式,直接IPC 处理应用有线网络配置
|
// 如果是 DHCP 模式,直接IPC 处理应用有线网络配置
|
||||||
try {
|
try {
|
||||||
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'dhcp' });
|
const res = await window.electronAPI.applyWiredConfig({ method: 'dhcp' });
|
||||||
console.log('网络配置返回信息成功:', res);
|
console.log('网络配置返回信息成功:', res);
|
||||||
if(res.success){
|
if(res.success){
|
||||||
message.success('网络配置成功');
|
message.success('网络配置成功');
|
||||||
|
|
@ -171,7 +171,7 @@ const NetworkConfig: React.FC = () => {
|
||||||
values.ipv6PrefixLength = parseInt(values.ipv6PrefixLength, 10);
|
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);
|
console.log('网络配置返回信息成功:', res);
|
||||||
if(res.success){
|
if(res.success){
|
||||||
message.success('网络配置成功');
|
message.success('网络配置成功');
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const Index = () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
console.log('表单提交数据:', values);
|
console.log('表单提交数据:', values);
|
||||||
const { serverIp } = values;
|
const { serverIp } = values;
|
||||||
const result = await window.electronAPI.invoke('connect-server', { serverIp });
|
const result = await window.electronAPI.connectServer({ serverIp });
|
||||||
console.log('连接结果:', result);
|
console.log('连接结果:', result);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 连接成功,保存服务器IP到本地存储
|
// 连接成功,保存服务器IP到本地存储
|
||||||
|
|
@ -62,7 +62,14 @@ const Index = () => {
|
||||||
console.log('Connected server IP saved to localStorage:', serverIp);
|
console.log('Connected server IP saved to localStorage:', serverIp);
|
||||||
// TODO:跳转到版本更新
|
// TODO:跳转到版本更新
|
||||||
// setActiveTab("watchManagement")
|
// setActiveTab("watchManagement")
|
||||||
setActiveTab("terminalGetImage")
|
// 通知后端连接 gRPC 服务
|
||||||
|
const grpcResult = await window.electronAPI.connectToGrpc({ serverIp });
|
||||||
|
if (grpcResult.success) {
|
||||||
|
// console.log('gRPC 连接成功');
|
||||||
|
setActiveTab("terminalGetImage")
|
||||||
|
} else {
|
||||||
|
message.error(grpcResult.message || 'gRPC 连接失败');
|
||||||
|
}
|
||||||
// await ipcRenderer.invoke('show-login-window');
|
// await ipcRenderer.invoke('show-login-window');
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message || '连接服务器失败');
|
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 ButtonCom from '../../../components/ButtonCom';
|
||||||
import { useConfigStep } from '@/contexts/ConfigStepContext';
|
import { useConfigStep } from '@/contexts/ConfigStepContext';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { history } from 'umi';
|
import { history } from 'umi';
|
||||||
import TrueIcon from '@assets/true.png'
|
import TrueIcon from '@assets/true.png'
|
||||||
|
import WebSocketService from '../../../services/webSocketService';
|
||||||
|
import { getDeviceInfo, getImagesList } from '../../../services/imageService';
|
||||||
|
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { goToNextTab,setActiveTab } = useConfigStep();
|
const { goToNextTab,setActiveTab } = useConfigStep();
|
||||||
const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||||
|
const [isWsConnected, setIsWsConnected] = useState(false);
|
||||||
|
|
||||||
// 模拟获取镜像数据的API调用
|
const checkImageList = useCallback(async () => {
|
||||||
useEffect(() => {
|
try {
|
||||||
const fetchImageData = async () => {
|
// 获取设备信息
|
||||||
try {
|
const deviceInfo = await getDeviceInfo();
|
||||||
// 模拟API请求
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
||||||
// 模拟随机成功或失败
|
|
||||||
const isSuccess = Math.random() > 0.3;
|
|
||||||
setImageStatus(isSuccess ? 'success' : 'loading');
|
|
||||||
|
|
||||||
// 如果失败,继续轮询检查状态
|
// 获取镜像列表
|
||||||
if (!isSuccess) {
|
const response = await getImagesList({
|
||||||
const checkStatus = async () => {
|
deviceId: deviceInfo.deviceId,
|
||||||
// 模拟WebSocket或轮询检查
|
macAddr: deviceInfo.macAddr
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
});
|
||||||
setImageStatus('success'); // 模拟收到通知
|
|
||||||
};
|
if (response.code === 200 && response.data?.data?.length > 0) {
|
||||||
checkStatus();
|
// 有镜像数据,直接进入下一步
|
||||||
}
|
setImageStatus('success');
|
||||||
} catch (error) {
|
} else {
|
||||||
message.error('获取镜像数据失败');
|
// 没有镜像数据,需要监听WebSocket
|
||||||
|
await connectWebSocket();
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error('检查镜像数据失败:', error);
|
||||||
fetchImageData();
|
message.error('获取镜像数据失败');
|
||||||
|
setImageStatus('error');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const connectWebSocket = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// 连接WebSocket
|
||||||
|
const connected = await WebSocketService.connect();
|
||||||
|
setIsWsConnected(connected);
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
// 监听镜像数据推送
|
||||||
|
const removeListener = WebSocketService.addListener('2', (data:any) => {
|
||||||
|
if (data.type === '2') {
|
||||||
|
// 有镜像数据了,更新状态
|
||||||
|
setImageStatus('success');
|
||||||
|
// 移除监听器
|
||||||
|
removeListener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error('WebSocket连接失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket连接失败:', error);
|
||||||
|
message.error('WebSocket连接失败');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 组件挂载时检查镜像数据
|
||||||
|
checkImageList();
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
return () => {
|
||||||
|
// 可以选择是否断开WebSocket连接
|
||||||
|
};
|
||||||
|
}, [checkImageList]);
|
||||||
|
|
||||||
const ImageLoadingComponent = () => (
|
const ImageLoadingComponent = () => (
|
||||||
<div className={styles["image-loading-container"]}>
|
<div className={styles["image-loading-container"]}>
|
||||||
<div className={styles["loading-wrapper"]}>
|
<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 (
|
interface ImageItem {
|
||||||
<div>
|
id: string;
|
||||||
镜像列表
|
name: string;
|
||||||
</div>
|
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('isLoggedIn', 'true');
|
||||||
localStorage.setItem('username', values.username);
|
localStorage.setItem('username', values.username);
|
||||||
// 跳转到镜像列表页面
|
|
||||||
history.push('/imagesList');
|
// 连接 gRPC 服务
|
||||||
|
// TODO:如果设置了ip后面会跳过serverConfig页面,这localStorage还能获取ip吗?
|
||||||
|
// const serverIp = localStorage.getItem('connected-server-ip');
|
||||||
|
const serverIp = await window.electronAPI.getCurrentServerIp();
|
||||||
|
console.log('登录后连接grpc 获取到的服务器IP:', serverIp);
|
||||||
|
|
||||||
|
if (serverIp) {
|
||||||
|
const grpcResult = await window.electronAPI.connectToGrpc({
|
||||||
|
serverIp,
|
||||||
|
clientUser: values.username
|
||||||
|
});
|
||||||
|
|
||||||
|
if (grpcResult.success) {
|
||||||
|
// console.log('gRPC 连接成功');
|
||||||
|
// 跳转到镜像列表页面
|
||||||
|
history.push('/imagesList');
|
||||||
|
} else {
|
||||||
|
message.error(grpcResult.message || 'gRPC 连接失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error('用户名或密码错误!');
|
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