feat(grpc): grpc模拟

master
chenyt 2025-08-25 15:25:23 +08:00
parent 7ac0003942
commit 587cdb0e0d
12 changed files with 1466 additions and 6 deletions

View File

@ -36,6 +36,10 @@ export default defineConfig({
path: '/',
component: '@/pages/components/Layout/index',
routes: [
{
path: '/grpc',
component: '@/pages/grpc/grpc',
},
{
path: '/welcome',
component: '@/pages/welcome',

176
pc-fe/package-lock.json generated
View File

@ -10,13 +10,17 @@
"hasInstallScript": true,
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"antd": "^5.26.6",
"axios": "^1.11.0",
"classnames": "^2.5.1",
"google-protobuf": "^4.0.0",
"umi": "^4.0.42"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.3",
"@types/google-protobuf": "^3.15.12",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/plugin-electron": "^0.2.0",
@ -3207,6 +3211,55 @@
"use-isomorphic-layout-effect": "^1.1.1"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
"version": "0.7.15",
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.5.3",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -3600,6 +3653,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@loadable/component": {
"version": "5.15.2",
"resolved": "https://registry.npmmirror.com/@loadable/component/-/component-5.15.2.tgz",
@ -4153,6 +4216,70 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@rc-component/async-validator": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
@ -4791,6 +4918,13 @@
"@types/node": "*"
}
},
"node_modules/@types/google-protobuf": {
"version": "3.15.12",
"resolved": "https://registry.npmmirror.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -12033,6 +12167,12 @@
"license": "MIT",
"peer": true
},
"node_modules/google-protobuf": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/google-protobuf/-/google-protobuf-4.0.0.tgz",
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@ -14102,6 +14242,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -14140,6 +14286,12 @@
"license": "MIT",
"peer": true
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
@ -16664,6 +16816,30 @@
"license": "ISC",
"optional": true
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",

View File

@ -17,13 +17,17 @@
"homepage": "http://10.209.8.11/users/sign_in",
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.8.0",
"antd": "^5.26.6",
"axios": "^1.11.0",
"classnames": "^2.5.1",
"google-protobuf": "^4.0.0",
"umi": "^4.0.42"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.3",
"@types/google-protobuf": "^3.15.12",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/plugin-electron": "^0.2.0",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,191 @@
import { ipcMain,app } from 'electron';
import { ipcMain,app,BrowserWindow } from 'electron';
import { getDeviceId, getWiredConnectionName,netmaskToCidr } from '../utils/utils';
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
// 模拟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; // 声明一个变量来存储定时器
const window = getBrowserWindowRuntime();
// 初始化 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 || '重新连接失败'
};
}
});
// 监听渲染进程发送的消息
ipcMain.handle('getPlatform', () => {
return `hi, i'm from ${process.platform}`;
@ -27,7 +207,6 @@ ipcMain.on('exit-kiosk', () => {
});
ipcMain.handle('get-deviceid',async()=>{
const deviceId = await getDeviceId();
console.log(`Using device ID: ${deviceId}`);

View File

@ -1,6 +1,6 @@
import { contextBridge, ipcRenderer } from 'electron';
// 页面调用的方法
// 页面调用的方法:桥接前端和主进程
contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: async () => {
return await ipcRenderer.invoke('getPlatform');
@ -8,10 +8,25 @@ contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.send('close-app'),
minimizeApp: () => ipcRenderer.send('minimize-app'),
exitKiosk: () => ipcRenderer.send('exit-kiosk'),
// 新增的 gRPC API
grpcStartDownload: (config: any) => ipcRenderer.invoke('grpc-start-download', config),
grpcStopDownload: (downloadId: string) => ipcRenderer.invoke('grpc-stop-download', downloadId),
grpcCheckConnection: () => ipcRenderer.invoke('grpc-check-connection'),
// gRPC 进度监听
onGrpcProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('grpc-progress-update', (_, progress) => callback(progress));
},
// 移除监听器
removeAllGrpcProgressListeners: () => {
ipcRenderer.removeAllListeners('grpc-progress-update');
},
// 事件监听
onMainProcessMessage: (callback: (data: string) => void) => {
ipcRenderer.on('main-process-message', (_, data) => callback(data));
},
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))

View File

@ -25,9 +25,9 @@ const MainLayout: React.FC = () => {
useEffect(() => {
// TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面
// setTimeout(() => {
// history.push('/configSteps');
// },1000)
setTimeout(() => {
history.push('/grpc');
},1000)
// const fetchDeviceId = async () => {
// try {
// const res = await window.electronAPI.invoke('get-deviceid');

View File

@ -0,0 +1,15 @@
.welcomeCon{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color:#fff;
.showTextCon{
display: flex;
flex-direction: column;
justify-self: center;
align-items: center;
}
}

View File

@ -0,0 +1,254 @@
// index.tsx 修改
import React, { useState, useEffect, useCallback } from 'react';
import styles from './grpc.less';
// import WelcomeIcon from '../../assets/welcome-icon.jpg';
// 类型定义
interface DownloadProgress {
download_id: string;
item_name?: string;
progress: number;
download_speed: number;
upload_speed: number;
eta: number;
total_size: number;
downloaded_size: number;
state: string;
}
// declare global {
// interface Window {
// electronAPI: {
// grpcStartDownload: (config: any) => Promise<any>;
// grpcStopDownload: (downloadId: string) => Promise<any>;
// grpcCheckConnection: () => Promise<{ connected: boolean }>;
// onGrpcProgress: (callback: (progress: DownloadProgress) => void) => void;
// removeAllGrpcProgressListeners: () => void;
// };
// }
// }
const Welcome: React.FC = () => {
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
useEffect(() => {
// 检查 gRPC 连接状态
const checkConnection = async () => {
try {
const result = await window.electronAPI.grpcCheckConnection();
setConnectionStatus(result.connected ? 'connected' : 'disconnected');
} catch (error) {
setConnectionStatus('disconnected');
}
};
checkConnection();
// 设置进度监听
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();
};
}, []);
// 在 React 组件中添加连接状态监听
useEffect(() => {
const handleConnectionStatus = (event: any, status: any) => {
console.log('收到gRPC连接状态更新:', status);
setConnectionStatus(status.connected ? 'connected' : 'disconnected');
console.log('gRPC连接状态:', status);
};
window.electronAPI.on('grpc-connection-status', handleConnectionStatus);
return () => {
window.electronAPI.off('grpc-connection-status', handleConnectionStatus);
};
}, []);
const handleStartDownload = useCallback(async () => {
try {
// 根据时间生成不同的文件名,便于测试不同大小的文件
const fileName = `测试文件-${Date.now()}`;
const result = await window.electronAPI.grpcStartDownload({
torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789',
itemName: fileName,
itemId: `test-${Date.now()}`
});
if (result.success) {
console.log('下载开始成功:', result.data);
} else {
console.error('下载开始失败:', result.error);
}
} catch (error) {
console.error('调用下载失败:', error);
}
}, []);
const handleRefreshDownloads = useCallback(async () => {
try {
// 这里可以调用一个获取当前下载列表的API如果有的话
console.log('当前下载任务:', downloads);
} catch (error) {
console.error('刷新下载列表失败:', error);
}
}, [downloads]);
// 添加一个测试大文件下载的函数
const handleStartLargeDownload = useCallback(async () => {
try {
// 发起IPC调用
const result = await window.electronAPI.grpcStartDownload({
torrentUrl: 'magnet:?xt=urn:btih:EXAMPLEHASH123456789',
itemName: 'large-大文件测试',
itemId: `large-test-${Date.now()}`
});
if (result.success) {
console.log('大文件下载开始成功:', result.data);
} else {
console.error('大文件下载开始失败:', result.error);
}
} catch (error) {
console.error('调用大文件下载失败:', error);
}
}, []);
const handleReconnect = useCallback(async () => {
try {
console.log('尝试重新连接 gRPC...');
const result = await window.electronAPI.invoke('grpc-reconnect');
if (result.success) {
console.log('重新连接成功:', result.message);
} else {
console.error('重新连接失败:', result.error);
}
} catch (error) {
console.error('重新连接异常:', error);
}
}, []);
const renderConnectionStatus = () => {
switch (connectionStatus) {
case 'connecting':
return <span style={{ color: 'orange' }}> ...</span>;
case 'connected':
return <span style={{ color: 'green' }}> </span>;
case 'disconnected':
return <span style={{ color: 'red' }}> </span>;
default:
return <span style={{ color: 'gray' }}> </span>;
}
};
return (
<div className={styles.welcomeCon}>
<div className={styles.showTextCon}>
{/* 连接状态显示 */}
<div style={{ marginTop: '20px', fontSize: '14px' }}>
: {renderConnectionStatus()}
</div>
{/* 下载测试按钮 */}
<div style={{ marginTop: '20px' }}>
<button
onClick={handleStartDownload}
style={{
padding: '10px 20px',
backgroundColor: connectionStatus === 'connected' ? '#1890ff' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: connectionStatus === 'connected' ? 'pointer' : 'not-allowed',
marginRight: '10px'
}}
disabled={connectionStatus !== 'connected'}
>
gRPC
</button>
<button
onClick={handleStartLargeDownload}
style={{
padding: '10px 20px',
backgroundColor: connectionStatus === 'connected' ? '#722ed1' : '#ccc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: connectionStatus === 'connected' ? 'pointer' : 'not-allowed'
}}
disabled={connectionStatus !== 'connected'}
>
</button>
<button
onClick={handleRefreshDownloads}
style={{
padding: '5px 10px',
backgroundColor: '#faad14',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
</button>
<button
onClick={handleReconnect}
style={{
padding: '5px 10px',
backgroundColor: '#52c41a',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
gRPC
</button>
</div>
{/* 显示当前下载数量 */}
<div style={{ marginTop: '10px', fontSize: '14px' }}>
: {downloads.size}
</div>
{/* 下载列表显示 */}
{Array.from(downloads.entries()).map(([id, progress]) => (
<div key={id} style={{
marginTop: '15px',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
minWidth: '300px'
}}>
<div style={{ fontWeight: 'bold' }}>{progress.download_id}</div>
<div>: {progress.item_name || '未知'}</div>
<div>: {progress.progress.toFixed(1)}%</div>
<div>: {progress.state}</div>
<div>: {(progress.download_speed / 1024 / 1024).toFixed(2)} MB/s</div>
<div>: {(progress.downloaded_size / 1024 / 1024).toFixed(2)} MB</div>
<div>: {(progress.total_size / 1024 / 1024).toFixed(2)} MB</div>
<div>: {Math.ceil(progress.eta / 60)} </div>
</div>
))}
</div>
</div>
);
};
export default Welcome;