Merge remote-tracking branch 'origin/master'
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -1,194 +0,0 @@
|
|||
// // src/grpc/BTGrpcClient.ts 调用后端服务
|
||||
// import * as grpc from '@grpc/grpc-js';
|
||||
// import * as protoLoader from '@grpc/proto-loader';
|
||||
// import path from 'path';
|
||||
// import { app } from 'electron';
|
||||
|
||||
// export interface DownloadRequest {
|
||||
// torrent_url: string;
|
||||
// item_name: string;
|
||||
// item_id?: string;
|
||||
// }
|
||||
|
||||
// export interface ProgressCallback {
|
||||
// (progress: ProgressUpdate): void;
|
||||
// }
|
||||
|
||||
// export interface ProgressUpdate {
|
||||
// download_id: string;
|
||||
// progress: number;
|
||||
// download_speed: number;
|
||||
// upload_speed: number;
|
||||
// eta: number;
|
||||
// total_size: number;
|
||||
// downloaded_size: number;
|
||||
// state: string;
|
||||
// }
|
||||
|
||||
// export class BTGrpcClient {
|
||||
// private client: any;
|
||||
// private progressCallbacks: Map<string, ProgressCallback[]> = new Map();
|
||||
// private progressStream: any = null;
|
||||
|
||||
// constructor() {
|
||||
// this.initializeClient();
|
||||
// }
|
||||
|
||||
// private initializeClient() {
|
||||
// try {
|
||||
// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
|
||||
|
||||
// const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||||
// keepCase: true,
|
||||
// longs: String,
|
||||
// enums: String,
|
||||
// defaults: true,
|
||||
// oneofs: true,
|
||||
// });
|
||||
|
||||
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
||||
// const bittorrent = protoDescriptor.bittorrent as any;
|
||||
|
||||
// // 连接到后端 Agent,假设运行在 localhost:50051
|
||||
// this.client = new bittorrent.BTDownloadService(
|
||||
// 'localhost:50051',
|
||||
// grpc.credentials.createInsecure()
|
||||
// );
|
||||
|
||||
// console.log('gRPC客户端初始化成功');
|
||||
// this.setupProgressStream();
|
||||
// } catch (error) {
|
||||
// console.error('gRPC客户端初始化失败:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 设置进度流监听
|
||||
// private setupProgressStream() {
|
||||
// try {
|
||||
// this.progressStream = this.client.SubscribeProgress({});
|
||||
// // 注册数据事件监听器,接收进度更新
|
||||
// this.progressStream.on('data', (progress: ProgressUpdate) => {
|
||||
// console.log('收到进度数据:', progress); // 添加调试日志
|
||||
// // BTGrpcClient 通过IPC将进度发送给主进程
|
||||
// this.progressCallbacks.forEach((callbacks, downloadId) => {
|
||||
// if (downloadId === 'all' || downloadId === progress.download_id) {
|
||||
// callbacks.forEach(callback => callback(progress));
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 如果有针对特定下载ID的回调,也要通知
|
||||
// const specificCallbacks = this.progressCallbacks.get(progress.download_id);
|
||||
// if (specificCallbacks) {
|
||||
// specificCallbacks.forEach(callback => callback(progress));
|
||||
// }
|
||||
// });
|
||||
|
||||
// this.progressStream.on('error', (error: Error) => {
|
||||
// console.error('进度流错误:', error);
|
||||
// });
|
||||
|
||||
// this.progressStream.on('end', () => {
|
||||
// console.log('进度流结束');
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error('设置进度流失败:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 开始下载
|
||||
// startDownload(request: DownloadRequest): Promise<any> {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// // MockBTService 接收请求,创建下载任务
|
||||
// this.client.StartDownload(request, (error: grpc.ServiceError, response: any) => {
|
||||
// if (error) {
|
||||
// reject(error);
|
||||
// } else {
|
||||
// resolve(response);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 停止下载
|
||||
// stopDownload(downloadId: string): Promise<any> {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// this.client.StopDownload({ download_id: downloadId }, (error: grpc.ServiceError, response: any) => {
|
||||
// if (error) {
|
||||
// reject(error);
|
||||
// } else {
|
||||
// resolve(response);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 注册进度回调
|
||||
// registerProgressCallback(downloadId: string, callback: ProgressCallback) {
|
||||
// if (!this.progressCallbacks.has(downloadId)) {
|
||||
// this.progressCallbacks.set(downloadId, []);
|
||||
// }
|
||||
// this.progressCallbacks.get(downloadId)!.push(callback);
|
||||
// }
|
||||
|
||||
// // 移除进度回调
|
||||
// removeProgressCallback(downloadId: string, callback: ProgressCallback) {
|
||||
// const callbacks = this.progressCallbacks.get(downloadId);
|
||||
// if (callbacks) {
|
||||
// const index = callbacks.indexOf(callback);
|
||||
// if (index > -1) {
|
||||
// callbacks.splice(index, 1);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 检查连接状态
|
||||
// isConnected(): boolean {
|
||||
// return this.client && this.client.getChannel().getConnectivityState(true) === grpc.connectivityState.READY;
|
||||
// }
|
||||
|
||||
// // 重新连接
|
||||
// reconnect() {
|
||||
// try {
|
||||
// this.client.close();
|
||||
// this.initializeClient();
|
||||
// } catch (error) {
|
||||
// console.error('重新连接失败:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// getConnectionState(): string {
|
||||
// if (!this.client) return 'NOT_CREATED';
|
||||
|
||||
// const state = this.client.getChannel().getConnectivityState(false);
|
||||
// const stateNames = {
|
||||
// [grpc.connectivityState.IDLE]: 'IDLE',
|
||||
// [grpc.connectivityState.CONNECTING]: 'CONNECTING',
|
||||
// [grpc.connectivityState.READY]: 'READY',
|
||||
// [grpc.connectivityState.TRANSIENT_FAILURE]: 'TRANSIENT_FAILURE',
|
||||
// [grpc.connectivityState.SHUTDOWN]: 'SHUTDOWN'
|
||||
// } as const;
|
||||
|
||||
// // 使用类型断言确保 state 是合法的 key
|
||||
// return stateNames[state as keyof typeof stateNames] || 'UNKNOWN';
|
||||
// }
|
||||
|
||||
// // 添加测试方法
|
||||
// async testConnection(): Promise<boolean> {
|
||||
// try {
|
||||
// // 尝试调用一个简单的方法来测试连接
|
||||
// await new Promise((resolve, reject) => {
|
||||
// this.client.ListDownloads({}, (error: any, response: any) => {
|
||||
// if (error && error.code !== grpc.status.UNIMPLEMENTED) {
|
||||
// reject(error);
|
||||
// } else {
|
||||
// resolve(response);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// console.error('连接测试失败:', error);
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
// // 本地测试用的 gRPC 服务器,用于模拟 BT 下载
|
||||
// // src/grpc/MockBTService.ts
|
||||
// import * as grpc from '@grpc/grpc-js';
|
||||
// import * as protoLoader from '@grpc/proto-loader';
|
||||
// import path from 'path';
|
||||
|
||||
// // 进度更新接口
|
||||
// interface ProgressUpdate {
|
||||
// download_id: string;
|
||||
// progress: number;
|
||||
// download_speed: number;
|
||||
// item_name?: string; // 可选的文件名字段
|
||||
// upload_speed: number;
|
||||
// eta: number;
|
||||
// total_size: number;
|
||||
// downloaded_size: number;
|
||||
// state: string;
|
||||
// }
|
||||
|
||||
// // 模拟的下载任务
|
||||
// interface MockDownloadTask {
|
||||
// id: string;
|
||||
// itemName: string;
|
||||
// torrentUrl: string;
|
||||
// progress: number;
|
||||
// totalSize: number;
|
||||
// downloadedSize: number;
|
||||
// downloadSpeed: number;
|
||||
// state: 'downloading' | 'completed' | 'error' | 'paused';
|
||||
// startTime: number;
|
||||
// }
|
||||
|
||||
// export class MockBTService {
|
||||
// private server: grpc.Server;
|
||||
// private activeDownloads: Map<string, MockDownloadTask> = new Map();
|
||||
// private progressIntervals: Map<string, NodeJS.Timeout> = new Map();
|
||||
// // 存储所有的进度回调函数,发送信息
|
||||
// private progressCallbacks: ((progress: ProgressUpdate) => void)[] = [];
|
||||
|
||||
// constructor() {
|
||||
// this.server = new grpc.Server();
|
||||
// this.setupService();
|
||||
// }
|
||||
|
||||
// private setupService() {
|
||||
// // 设置gRPC服务,加载 bittorrent.proto 定义,实现所有gRPC服务方法
|
||||
// const PROTO_PATH = path.join(__dirname, 'protos', 'bittorrent.proto');
|
||||
|
||||
// const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||||
// keepCase: true,
|
||||
// longs: String,
|
||||
// enums: String,
|
||||
// defaults: true,
|
||||
// oneofs: true,
|
||||
// });
|
||||
|
||||
// const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
||||
// const bittorrent = protoDescriptor.bittorrent as any;
|
||||
|
||||
// // 实现 gRPC 服务方法
|
||||
// this.server.addService(bittorrent.BTDownloadService.service, {
|
||||
// StartDownload: this.startDownload.bind(this),
|
||||
// StopDownload: this.stopDownload.bind(this),
|
||||
// GetDownloadStatus: this.getDownloadStatus.bind(this),
|
||||
// ListDownloads: this.listDownloads.bind(this),
|
||||
// SubscribeProgress: this.subscribeProgress.bind(this),
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 开始下载
|
||||
// // 在 startDownload 方法中,可以添加根据文件名设置不同大小的逻辑
|
||||
// private startDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
// const { torrent_url, item_name, item_id } = call.request;
|
||||
// const downloadId = item_id || `download-${Date.now()}`;
|
||||
|
||||
// console.log(`模拟开始下载: ${item_name}, ID: ${downloadId}`);
|
||||
|
||||
// // 根据文件名或ID设置不同大小的文件
|
||||
// let fileSize = 1024 * 1024 * 100; // 默认100MB
|
||||
|
||||
// if (item_name.includes('large') || item_name.includes('大文件')) {
|
||||
// fileSize = 4 * 1024 * 1024 * 1024; // 4GB
|
||||
// } else if (item_name.includes('medium') || item_name.includes('中等')) {
|
||||
// fileSize = 1024 * 1024 * 1024; // 1GB
|
||||
// } else if (item_name.includes('small') || item_name.includes('小文件')) {
|
||||
// fileSize = 100 * 1024 * 1024; // 100MB
|
||||
// }
|
||||
|
||||
// // 创建模拟下载任务
|
||||
// const task: MockDownloadTask = {
|
||||
// id: downloadId,
|
||||
// itemName: item_name,
|
||||
// torrentUrl: torrent_url,
|
||||
// progress: 0,
|
||||
// totalSize: fileSize,
|
||||
// downloadedSize: 0,
|
||||
// downloadSpeed: 1024 * 1024 * 2, // 2MB/s
|
||||
// state: 'downloading',
|
||||
// startTime: Date.now(),
|
||||
// };
|
||||
|
||||
// this.activeDownloads.set(downloadId, task);
|
||||
// console.log(`已创建下载任务: ${downloadId}, 大小: ${fileSize} bytes`);
|
||||
|
||||
// // 启动进度模拟
|
||||
// this.startProgressSimulation(downloadId);
|
||||
|
||||
// callback(null, {
|
||||
// success: true,
|
||||
// message: '下载已开始',
|
||||
// download_id: downloadId,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 停止下载
|
||||
// private stopDownload(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
// const { download_id } = call.request;
|
||||
|
||||
// if (this.activeDownloads.has(download_id)) {
|
||||
// const task = this.activeDownloads.get(download_id)!;
|
||||
// task.state = 'paused';
|
||||
|
||||
// // 清除进度定时器
|
||||
// if (this.progressIntervals.has(download_id)) {
|
||||
// clearInterval(this.progressIntervals.get(download_id));
|
||||
// this.progressIntervals.delete(download_id);
|
||||
// }
|
||||
|
||||
// console.log(`模拟停止下载: ${download_id}`);
|
||||
// callback(null, { success: true, message: '下载已停止' });
|
||||
// } else {
|
||||
// callback({
|
||||
// code: grpc.status.NOT_FOUND,
|
||||
// message: `下载任务不存在: ${download_id}`
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 获取下载状态
|
||||
// private getDownloadStatus(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
// const { download_id } = call.request;
|
||||
|
||||
// if (this.activeDownloads.has(download_id)) {
|
||||
// const task = this.activeDownloads.get(download_id)!;
|
||||
// callback(null, {
|
||||
// download_id: task.id,
|
||||
// progress: task.progress,
|
||||
// download_speed: task.downloadSpeed,
|
||||
// state: task.state,
|
||||
// total_size: task.totalSize,
|
||||
// downloaded_size: task.downloadedSize,
|
||||
// });
|
||||
// } else {
|
||||
// callback({
|
||||
// code: grpc.status.NOT_FOUND,
|
||||
// message: `下载任务不存在: ${download_id}`
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 列出所有下载
|
||||
// private listDownloads(call: grpc.ServerUnaryCall<any, any>, callback: grpc.sendUnaryData<any>) {
|
||||
// const downloads = Array.from(this.activeDownloads.values()).map(task => ({
|
||||
// download_id: task.id,
|
||||
// item_name: task.itemName,
|
||||
// progress: task.progress,
|
||||
// download_speed: task.downloadSpeed,
|
||||
// state: task.state,
|
||||
// total_size: task.totalSize,
|
||||
// downloaded_size: task.downloadedSize,
|
||||
// }));
|
||||
|
||||
// callback(null, { downloads });
|
||||
// }
|
||||
|
||||
// // 订阅进度更新(流式响应)
|
||||
// private subscribeProgress(call: grpc.ServerWritableStream<any, any>) {
|
||||
// console.log('客户端订阅了进度更新');
|
||||
|
||||
// // 存储回调以便发送进度更新
|
||||
// const callback = (progress: ProgressUpdate) => {
|
||||
// try {
|
||||
// call.write(progress);
|
||||
// } catch (error) {
|
||||
// console.error('发送进度更新失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// this.progressCallbacks.push(callback);
|
||||
|
||||
// // 当客户端断开连接时清理
|
||||
// call.on('cancelled', () => {
|
||||
// console.log('客户端取消了进度订阅');
|
||||
// const index = this.progressCallbacks.indexOf(callback);
|
||||
// if (index > -1) {
|
||||
// this.progressCallbacks.splice(index, 1);
|
||||
// }
|
||||
// });
|
||||
|
||||
// call.on('error', (error) => {
|
||||
// console.error('进度流错误:', error);
|
||||
// const index = this.progressCallbacks.indexOf(callback);
|
||||
// if (index > -1) {
|
||||
// this.progressCallbacks.splice(index, 1);
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 处理客户端断开连接
|
||||
// call.on('end', () => {
|
||||
// console.log('客户端断开连接');
|
||||
// const index = this.progressCallbacks.indexOf(callback);
|
||||
// if (index > -1) {
|
||||
// this.progressCallbacks.splice(index, 1);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 启动进度模拟
|
||||
// // 在 MockBTService.ts 中修改 startProgressSimulation 方法
|
||||
// private startProgressSimulation(downloadId: string) {
|
||||
// const interval = setInterval(() => {
|
||||
// if (this.activeDownloads.has(downloadId)) {
|
||||
// const task = this.activeDownloads.get(downloadId)!;
|
||||
|
||||
// if (task.state === 'downloading' && task.progress < 100) {
|
||||
// // // 更新进度,但确保不超过100%
|
||||
// // task.progress += Math.random() * 2;
|
||||
// // 使用固定的进度增加而不是随机值,使下载更加稳定
|
||||
// task.progress += 1.5; // 每秒增加1.5%的进度
|
||||
|
||||
// // 确保进度不超过100%
|
||||
// if (task.progress >= 100) {
|
||||
// task.progress = 100;
|
||||
// task.state = 'completed';
|
||||
// task.downloadedSize = task.totalSize; // 确保已完成时已下载大小等于总大小
|
||||
// console.log(`下载完成: ${downloadId}`);
|
||||
|
||||
// // 清除定时器
|
||||
// clearInterval(interval);
|
||||
// this.progressIntervals.delete(downloadId);
|
||||
// } else {
|
||||
// // 根据进度计算已下载大小
|
||||
// task.downloadedSize = (task.totalSize * task.progress) / 100;
|
||||
// }
|
||||
|
||||
// // 计算剩余时间(秒)
|
||||
// let eta = 0;
|
||||
// if (task.progress < 100) {
|
||||
// // 基于当前速度计算剩余时间
|
||||
// const progressPerSecond = 1.5; // 每秒进度百分比
|
||||
// eta = Math.round(((100 - task.progress) / progressPerSecond));
|
||||
// }
|
||||
|
||||
// // 发送进度更新
|
||||
// this.sendProgressUpdate({
|
||||
// download_id: task.id,
|
||||
// item_name: task.itemName,
|
||||
// progress: task.progress,
|
||||
// download_speed: task.downloadSpeed,
|
||||
// upload_speed: 1024 * 512, // 512KB/s 上传速度
|
||||
// eta: task.progress >= 100 ? 0 : Math.round(((100 - task.progress) / 2) * 1000),
|
||||
// total_size: task.totalSize,
|
||||
// downloaded_size: task.downloadedSize,
|
||||
// state: task.state,
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// clearInterval(interval);
|
||||
// this.progressIntervals.delete(downloadId);
|
||||
// }
|
||||
// }, 1000); // 每秒更新一次进度
|
||||
|
||||
// this.progressIntervals.set(downloadId, interval);
|
||||
// }
|
||||
|
||||
// // 发送进度更新给所有订阅者
|
||||
// private sendProgressUpdate(progress: ProgressUpdate) {
|
||||
// console.log('发送进度更新:', progress);
|
||||
// // 通过已注册的回调函数传递给 BTGrpcClient
|
||||
// this.progressCallbacks.forEach((callback, index) => {
|
||||
// try {
|
||||
// callback(progress);
|
||||
// } catch (error) {
|
||||
// console.error(`发送进度更新给回调 ${index} 失败:`, error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 绑定端口并启动服务器
|
||||
// start(port: number = 50051): Promise<void> {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// this.server.bindAsync(
|
||||
// `0.0.0.0:${port}`,
|
||||
// grpc.ServerCredentials.createInsecure(),
|
||||
// (error, port) => {
|
||||
// if (error) {
|
||||
// reject(error);
|
||||
// } else {
|
||||
// this.server.start();
|
||||
// console.log(`Mock gRPC 服务器运行在端口 ${port}`);
|
||||
// resolve();
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 停止服务器
|
||||
// stop(): Promise<void> {
|
||||
// return new Promise((resolve) => {
|
||||
// // 清理所有定时器
|
||||
// this.progressIntervals.forEach(interval => clearInterval(interval));
|
||||
// this.progressIntervals.clear();
|
||||
// this.activeDownloads.clear();
|
||||
// this.progressCallbacks = [];
|
||||
|
||||
// this.server.tryShutdown(() => {
|
||||
// console.log('Mock gRPC 服务器已停止');
|
||||
// resolve();
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
梳理整个gRPC下载逻辑和模拟下载的执行流程。
|
||||
|
||||
## 1. 整体架构概览
|
||||
|
||||
整个gRPC下载系统分为以下几个主要部分:
|
||||
|
||||
1. **前端页面** (`grpc.tsx`) - 用户交互界面
|
||||
2. **预加载脚本** (`preload.ts`) - 桥接前端和主进程
|
||||
3. **主进程** (`platform.ts`) - 管理gRPC客户端和服务端
|
||||
4. **gRPC客户端** (`BTGrpcClient.ts`) - 调用后端服务
|
||||
5. **gRPC服务端** (`MockBTService.ts`) - 模拟下载服务
|
||||
6. **协议定义** (`bittorrent.proto`) - gRPC接口定义
|
||||
|
||||
## 2. 启动流程
|
||||
|
||||
### 2.1 应用启动时初始化gRPC
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[应用启动] --> B[app.whenReady()]
|
||||
B --> C[initializeGrpc()]
|
||||
C --> D{IS_TEST_MODE}
|
||||
D -->|true| E[创建MockBTService实例]
|
||||
D -->|false| F[连接真实后端]
|
||||
E --> G[启动Mock gRPC服务器]
|
||||
G --> H[创建BTGrpcClient实例]
|
||||
H --> I[注册进度回调]
|
||||
I --> J[启动健康检查]
|
||||
```
|
||||
|
||||
在 `platform.ts` 中:
|
||||
- 应用启动时调用 `initializeGrpc()` 函数
|
||||
- 根据 `IS_TEST_MODE` 决定是启动模拟服务还是连接真实后端
|
||||
- 创建 `MockBTService` 实例并启动服务器
|
||||
- 创建 `BTGrpcClient` 实例连接到gRPC服务
|
||||
- 注册进度回调函数,用于接收下载进度更新
|
||||
|
||||
### 2.2 gRPC服务端启动 (MockBTService)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as MockBTService
|
||||
participant S as gRPC Server
|
||||
participant C as BTGrpcClient
|
||||
|
||||
M->>S: 创建gRPC服务器实例
|
||||
M->>S: 加载proto定义
|
||||
M->>S: 实现服务方法
|
||||
M->>S: 绑定端口并启动
|
||||
S-->>M: 服务器启动成功
|
||||
C->>S: 连接服务器
|
||||
S-->>C: 连接建立
|
||||
```
|
||||
|
||||
在 `MockBTService.ts` 中:
|
||||
1. 构造函数调用 [setupService()](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L43-L65) 设置gRPC服务
|
||||
2. 加载 `bittorrent.proto` 定义
|
||||
3. 实现所有gRPC服务方法:
|
||||
- `StartDownload` - 开始下载
|
||||
- `StopDownload` - 停止下载
|
||||
- `GetDownloadStatus` - 获取下载状态
|
||||
- `ListDownloads` - 列出所有下载
|
||||
- `SubscribeProgress` - 订阅进度更新
|
||||
4. 通过 `server.bindAsync()` 绑定端口并启动服务
|
||||
|
||||
## 3. 下载流程详解
|
||||
|
||||
### 3.1 用户触发下载
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户点击下载按钮] --> B[handleStartDownload()]
|
||||
B --> C[调用window.electronAPI.grpcStartDownload()]
|
||||
C --> D[IPC调用grpc-start-download]
|
||||
D --> E[主进程处理]
|
||||
E --> F[调用btGrpcClient.startDownload()]
|
||||
F --> G[gRPC调用StartDownload()]
|
||||
G --> H[MockBTService处理]
|
||||
H --> I[创建下载任务]
|
||||
I --> J[启动进度模拟]
|
||||
J --> K[返回下载ID]
|
||||
K --> L[前端接收响应]
|
||||
```
|
||||
|
||||
详细步骤:
|
||||
1. 用户点击"开始gRPC下载测试"按钮
|
||||
2. 调用 `handleStartDownload()` 函数
|
||||
3. 通过 `window.electronAPI.grpcStartDownload()` 发起IPC调用
|
||||
4. 主进程 `platform.ts` 接收 `grpc-start-download` 请求
|
||||
5. 调用 `btGrpcClient.startDownload()` 方法
|
||||
6. gRPC客户端向服务端发起 `StartDownload` 调用
|
||||
7. [MockBTService](file://d:\project\vdi\pc-fe\src\main\grpc\MockBTService.ts#L32-L305) 接收请求,创建下载任务
|
||||
8. 启动进度模拟定时器
|
||||
9. 返回下载ID给前端
|
||||
|
||||
### 3.2 进度更新机制
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as MockBTService
|
||||
participant I as Interval
|
||||
participant B as BTGrpcClient
|
||||
participant P as Platform主进程
|
||||
participant F as 前端
|
||||
|
||||
I->>M: 定时触发
|
||||
M->>M: 更新任务进度
|
||||
M->>M: 构造进度数据
|
||||
M->>B: 发送进度更新
|
||||
B->>P: 调用回调函数
|
||||
P->>F: IPC发送grpc-progress-update
|
||||
F->>F: 更新UI显示
|
||||
```
|
||||
|
||||
详细步骤:
|
||||
1. `startProgressSimulation()` 创建定时器
|
||||
2. 定时器每秒触发一次,更新下载进度
|
||||
3. 构造 `ProgressUpdate` 对象
|
||||
4. 调用 `sendProgressUpdate()` 发送进度更新
|
||||
5. 通过已注册的回调函数传递给 `BTGrpcClient`
|
||||
6. `BTGrpcClient` 通过IPC将进度发送给主进程
|
||||
7. 主进程通过 `webContents.send()` 将进度发送给前端
|
||||
8. 前端接收并更新UI
|
||||
|
||||
## 4. 关键代码解析
|
||||
|
||||
### 4.1 前端监听进度更新
|
||||
|
||||
```typescript
|
||||
// grpc.tsx
|
||||
useEffect(() => {
|
||||
// 设置进度监听
|
||||
const handleProgress = (progress: DownloadProgress) => {
|
||||
console.log('收到进度更新:', progress);
|
||||
setDownloads(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(progress.download_id, progress);
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
|
||||
window.electronAPI.onGrpcProgress(handleProgress);
|
||||
|
||||
return () => {
|
||||
window.electronAPI.removeAllGrpcProgressListeners();
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 4.2 主进程转发进度更新
|
||||
|
||||
```typescript
|
||||
// platform.ts
|
||||
btGrpcClient.registerProgressCallback('all', (progress) => {
|
||||
const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('grpc-progress-update', progress);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4.3 Mock服务模拟进度
|
||||
|
||||
```typescript
|
||||
// MockBTService.ts
|
||||
private startProgressSimulation(downloadId: string) {
|
||||
const interval = setInterval(() => {
|
||||
if (this.activeDownloads.has(downloadId)) {
|
||||
const task = this.activeDownloads.get(downloadId)!;
|
||||
|
||||
if (task.state === 'downloading' && task.progress < 100) {
|
||||
// 更新进度
|
||||
task.progress += Math.random() * 2;
|
||||
task.downloadedSize = (task.totalSize * task.progress) / 100;
|
||||
|
||||
// 发送进度更新
|
||||
this.sendProgressUpdate({
|
||||
download_id: task.id,
|
||||
item_name: task.itemName,
|
||||
progress: task.progress,
|
||||
download_speed: task.downloadSpeed,
|
||||
upload_speed: 1024 * 512,
|
||||
eta: Math.round(((100 - task.progress) / 2) * 1000),
|
||||
total_size: task.totalSize,
|
||||
downloaded_size: task.downloadedSize,
|
||||
state: task.state,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.progressIntervals.set(downloadId, interval);
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 数据流总结
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[前端UI] --> B[preload.ts]
|
||||
B --> C[主进程platform.ts]
|
||||
C --> D[BTGrpcClient.ts]
|
||||
D --> E[MockBTService.ts]
|
||||
E --> F[定时模拟进度]
|
||||
F --> G[发送进度更新]
|
||||
G --> H[主进程转发]
|
||||
H --> I[前端接收更新]
|
||||
I --> A
|
||||
```
|
||||
|
||||
整个流程形成了一个闭环:
|
||||
1. 用户操作触发前端请求
|
||||
2. 请求通过IPC传递到主进程
|
||||
3. 主进程通过gRPC调用后端服务
|
||||
4. Mock服务模拟下载过程
|
||||
5. 进度更新通过相同路径返回前端
|
||||
6. 前端更新UI显示最新进度
|
||||
|
||||
这就是整个gRPC下载逻辑的完整执行流程。
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// bittorrent.proto gRPC接口定义
|
||||
syntax = "proto3";
|
||||
|
||||
package bittorrent;
|
||||
|
||||
service BTDownloadService {
|
||||
rpc StartDownload (DownloadRequest) returns (DownloadResponse) {}
|
||||
rpc StopDownload (StopRequest) returns (StopResponse) {}
|
||||
rpc GetDownloadStatus (StatusRequest) returns (StatusResponse) {}
|
||||
rpc ListDownloads (ListRequest) returns (ListResponse) {}
|
||||
rpc SubscribeProgress (SubscribeRequest) returns (stream ProgressUpdate) {}
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string torrent_url = 1;
|
||||
string item_name = 2;
|
||||
string item_id = 3;
|
||||
}
|
||||
|
||||
message DownloadResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
string download_id = 3;
|
||||
}
|
||||
|
||||
message StopRequest {
|
||||
string download_id = 1;
|
||||
}
|
||||
|
||||
message StopResponse {
|
||||
bool success = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message ProgressUpdate {
|
||||
string download_id = 1;
|
||||
double progress = 2;
|
||||
double download_speed = 3;
|
||||
double upload_speed = 4;
|
||||
int64 eta = 5;
|
||||
int64 total_size = 6;
|
||||
int64 downloaded_size = 7;
|
||||
string state = 8;
|
||||
}
|
||||
|
||||
message StatusRequest {
|
||||
string download_id = 1;
|
||||
}
|
||||
|
||||
message StatusResponse {
|
||||
string download_id = 1;
|
||||
double progress = 2;
|
||||
double download_speed = 3;
|
||||
string state = 4;
|
||||
int64 total_size = 5;
|
||||
int64 downloaded_size = 6;
|
||||
}
|
||||
|
||||
message ListRequest {
|
||||
}
|
||||
|
||||
message ListResponse {
|
||||
repeated DownloadInfo downloads = 1;
|
||||
}
|
||||
|
||||
message DownloadInfo {
|
||||
string download_id = 1;
|
||||
string item_name = 2;
|
||||
double progress = 3;
|
||||
double download_speed = 4;
|
||||
string state = 5;
|
||||
int64 total_size = 6;
|
||||
int64 downloaded_size = 7;
|
||||
}
|
||||
|
||||
message SubscribeRequest {
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// bt下载
|
||||
import { ipcMain } from 'electron';
|
||||
import request from '../utils/request';
|
||||
import { currentServerIp } from './sharedState'; // 修改导入
|
||||
// 添加下载相关 IPC 处理
|
||||
ipcMain.handle('start-download', async (event, downloadConfig) => {
|
||||
// return {success:true,message:"开始下载"}
|
||||
try {
|
||||
console.log('开始下载:', downloadConfig);
|
||||
|
||||
// 检查服务器 IP 是否已设置
|
||||
if (!currentServerIp) {
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器未连接,请先配置服务器'
|
||||
};
|
||||
}
|
||||
|
||||
// 使用保存的服务器 IP 构建 API 地址
|
||||
const apiUrl = `http://${currentServerIp}:3001/api/downloads`;
|
||||
|
||||
// 发送 HTTP 请求到后端 API
|
||||
const response = await request(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(downloadConfig)
|
||||
});
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse((response as any).data);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse authentication response:', parseError);
|
||||
return { success: false, message: '服务器响应格式错误' };
|
||||
}
|
||||
// const result = await response.json();
|
||||
// 检查结果 - 只有code等于200时才算成功
|
||||
if (responseData.code === '200' || responseData.code === 200) {
|
||||
console.log('开始下载成功:', responseData);
|
||||
return { success: true, message: `开始下载成功`, data: responseData };
|
||||
} else {
|
||||
// 认证失败,返回服务器提供的错误信息
|
||||
const errorMessage = responseData.message || responseData.msg || responseData.error || '开始下载成功';
|
||||
console.log('Authentication failed:', errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载请求失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('stop-download', async (event, downloadId) => {
|
||||
// return {success:true,message:"停止下载成功"}
|
||||
try {
|
||||
console.log('停止下载:', downloadId);
|
||||
|
||||
// 检查服务器 IP 是否已设置
|
||||
if (!currentServerIp) {
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器未连接,请先配置服务器'
|
||||
};
|
||||
}
|
||||
|
||||
// 使用保存的服务器 IP 构建 API 地址
|
||||
const apiUrl = `http://${currentServerIp}:3001/api/downloads/${downloadId}`;
|
||||
|
||||
// 发送 HTTP 请求到后端 API
|
||||
const response = await request(apiUrl, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
// const result = await response.json();
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse((response as any).data);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse authentication response:', parseError);
|
||||
return { success: false, message: '服务器响应格式错误' };
|
||||
}
|
||||
// const result = await response.json();
|
||||
// 检查结果 - 只有code等于200时才算成功
|
||||
if (responseData.code === '200' || responseData.code === 200) {
|
||||
console.log('停止下载成功:', responseData);
|
||||
return { success: true, message: `停止下载成功`, data: responseData };
|
||||
} else {
|
||||
// 认证失败,返回服务器提供的错误信息
|
||||
const errorMessage = responseData.message || responseData.msg || responseData.error || '停止下载失败';
|
||||
console.log('Authentication failed:', errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止下载失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// 通用
|
||||
import { ipcMain,app } from 'electron';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { getDeviceIdAndMac } from '../utils/utils';
|
||||
|
||||
// 服务器IP获取
|
||||
ipcMain.handle('get-current-server-ip', () => {
|
||||
return (global as any).currentServerIp;
|
||||
});
|
||||
|
||||
// 终端信息获取
|
||||
ipcMain.handle('get-device-info', async () => {
|
||||
// return { success: true, deviceId: "fsfs" };
|
||||
try {
|
||||
const deviceInfo = await getDeviceIdAndMac();
|
||||
console.log(`Using device Info: ${deviceInfo}`);
|
||||
return { success: true, deviceInfo };
|
||||
} catch (error) {
|
||||
console.error('获取设备信息失败:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '未知错误' };
|
||||
}
|
||||
});
|
||||
|
||||
// 添加处理窗口调整
|
||||
ipcMain.handle('adjust-window-for-normal', async (event) => {
|
||||
try {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window) {
|
||||
// 调整窗口大小和配置
|
||||
window.setKiosk(false); // 退出全屏模式
|
||||
window.setMinimumSize(1200, 800);
|
||||
window.setSize(1200, 800);
|
||||
window.setResizable(true);
|
||||
window.setMaximizable(true);
|
||||
window.setMinimizable(true);
|
||||
// 保持无边框和隐藏标题栏的设置
|
||||
window.setFullScreen(false);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('调整窗口失败:', error);
|
||||
return { success: false, };
|
||||
}
|
||||
});
|
||||
|
||||
/**拖动窗口 */
|
||||
ipcMain.handle('drag-window', (event) => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
// 通知渲染进程开始拖拽
|
||||
focusedWindow.webContents.send('start-drag');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听渲染进程发送的消息
|
||||
ipcMain.handle('getPlatform', () => {
|
||||
return `hi, i'm from ${process.platform}`;
|
||||
});
|
||||
|
||||
// 窗口控制:最小化,退出全屏,关闭,
|
||||
// 获取窗口最大化状态
|
||||
ipcMain.handle('get-window-maximized', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
return window?.isMaximized() || false;
|
||||
});
|
||||
|
||||
ipcMain.on('close-app', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 最小化
|
||||
ipcMain.on('minimize-app', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.minimize();
|
||||
}
|
||||
// window?.minimize();
|
||||
});
|
||||
|
||||
// 退出全屏
|
||||
ipcMain.on('restore-window', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.unmaximize();
|
||||
}
|
||||
// if (window) {
|
||||
// window.setFullScreen(false);
|
||||
// }
|
||||
});
|
||||
|
||||
// 设置全屏
|
||||
ipcMain.on('maximize-window', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.maximize();
|
||||
}
|
||||
})
|
||||
|
||||
// 监听窗口状态变化并通知渲染进程
|
||||
ipcMain.on('register-window-state-listeners', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window) {
|
||||
window.on('maximize', () => {
|
||||
event.sender.send('window-maximized');
|
||||
});
|
||||
|
||||
window.on('unmaximize', () => {
|
||||
event.sender.send('window-unmaximized');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ipcMain,app } from 'electron';
|
||||
import { getDeviceId, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils';
|
||||
import { getDeviceIdAndMac, getWiredConnectionName, netmaskToCidr,simulateUpdate,performRealUpdate } from '../utils/utils';
|
||||
import { BrowserWindow } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import request from '../utils/request';
|
||||
|
|
@ -8,151 +8,51 @@ const { exec } = require('child_process');
|
|||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const window = getBrowserWindowRuntime();
|
||||
|
||||
let currentServerIp: string | undefined;
|
||||
|
||||
// 添加处理窗口调整
|
||||
ipcMain.handle('adjust-window-for-normal', async (event) => {
|
||||
try {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window) {
|
||||
// 调整窗口大小和配置
|
||||
window.setKiosk(false); // 退出全屏模式
|
||||
window.setMinimumSize(1200, 800);
|
||||
window.setSize(1200, 800);
|
||||
window.setResizable(true);
|
||||
window.setMaximizable(true);
|
||||
window.setMinimizable(true);
|
||||
// 保持无边框和隐藏标题栏的设置
|
||||
window.setFullScreen(false);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('调整窗口失败:', error);
|
||||
return { success: false, };
|
||||
}
|
||||
});
|
||||
|
||||
/**拖动窗口 */
|
||||
ipcMain.handle('drag-window', (event) => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
// 通知渲染进程开始拖拽
|
||||
focusedWindow.webContents.send('start-drag');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听渲染进程发送的消息
|
||||
ipcMain.handle('getPlatform', () => {
|
||||
return `hi, i'm from ${process.platform}`;
|
||||
});
|
||||
|
||||
// 窗口控制:最小化,退出全屏,关闭,
|
||||
// 获取窗口最大化状态
|
||||
ipcMain.handle('get-window-maximized', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
return window?.isMaximized() || false;
|
||||
});
|
||||
|
||||
ipcMain.on('close-app', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 最小化
|
||||
ipcMain.on('minimize-app', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.minimize();
|
||||
}
|
||||
// window?.minimize();
|
||||
});
|
||||
|
||||
// 退出全屏
|
||||
ipcMain.on('restore-window', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.unmaximize();
|
||||
}
|
||||
// if (window) {
|
||||
// window.setFullScreen(false);
|
||||
// }
|
||||
});
|
||||
|
||||
// 设置全屏
|
||||
ipcMain.on('maximize-window', () => {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.maximize();
|
||||
}
|
||||
})
|
||||
|
||||
// 监听窗口状态变化并通知渲染进程
|
||||
ipcMain.on('register-window-state-listeners', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
if (window) {
|
||||
window.on('maximize', () => {
|
||||
event.sender.send('window-maximized');
|
||||
});
|
||||
|
||||
window.on('unmaximize', () => {
|
||||
event.sender.send('window-unmaximized');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('get-device-id',async()=>{
|
||||
const deviceId = await getDeviceId();
|
||||
console.log(`Using device ID: ${deviceId}`);
|
||||
// TODO:传给后端
|
||||
})
|
||||
|
||||
/* 1. 平台网络配置:IPC 处理应用有线网络配置 */
|
||||
ipcMain.handle('apply-wired-config',async(event,config)=>{
|
||||
/* 1. 平台网络配置:IPC 处理应用有线网络配置 */
|
||||
ipcMain.handle('apply-wired-config', async (event, config) => {
|
||||
// return {
|
||||
// success: true,
|
||||
// message: '网络配置已成功应用'
|
||||
// };
|
||||
try{
|
||||
try {
|
||||
console.log('应用网络配置:', config);
|
||||
// 获取有线连接名称
|
||||
const connectionName = await getWiredConnectionName();
|
||||
console.log('有线连接名称:', connectionName);
|
||||
|
||||
if(config.method==='static'){
|
||||
|
||||
if (config.method === 'static') {
|
||||
// 使用nmcli配置静态IP,需要使用sudo权限,一次性设置所有参数
|
||||
let modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method manual ipv4.addresses "${config.ipv4}/${netmaskToCidr(config.subnetMask)}" ipv4.gateway "${config.ipv4Gateway}"`;
|
||||
const dnsServers = [config.primaryDns, config.secondaryDns].filter(Boolean).join(',');
|
||||
modifyCmd += ` ipv4.dns "${dnsServers}"`;
|
||||
|
||||
|
||||
// 添加 IPv6 配置(如果存在 ipv6Gateway)????ipv6和长度需要吗?ui只写了ipv6网关
|
||||
// ipv6PrefixLength 是 IPv6 地址的前缀长度,类似于 IPv4 中的子网掩码。????
|
||||
if (config.ipv6 && config.ipv6Gateway) {
|
||||
const ipv6PrefixLength = config.ipv6PrefixLength &&
|
||||
config.ipv6PrefixLength >= 0 &&
|
||||
config.ipv6PrefixLength <= 128 ?
|
||||
config.ipv6PrefixLength : 64; // 默认使用64
|
||||
const ipv6PrefixLength = config.ipv6PrefixLength &&
|
||||
config.ipv6PrefixLength >= 0 &&
|
||||
config.ipv6PrefixLength <= 128 ?
|
||||
config.ipv6PrefixLength : 64; // 默认使用64
|
||||
modifyCmd += ` ipv6.method manual ipv6.addresses "${config.ipv6}/${ipv6PrefixLength}" ipv6.gateway "${config.ipv6Gateway}"`;
|
||||
}else if (config.ipv6 || config.ipv6Gateway) {
|
||||
} else if (config.ipv6 || config.ipv6Gateway) {
|
||||
console.warn('IPv6配置不完整:需要同时提供IPv6地址和网关');
|
||||
}
|
||||
|
||||
// 执行配置命令
|
||||
console.log('执行命令:', modifyCmd.replace('unis@123', '***'));
|
||||
await execAsync(modifyCmd);
|
||||
|
||||
|
||||
// 重新激活连接
|
||||
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
|
||||
|
||||
}else{
|
||||
|
||||
} else {
|
||||
// DHCP配置,一次性设置所有参数
|
||||
const modifyCmd = `echo "unis@123" | sudo -S nmcli connection modify "${connectionName}" ipv4.method auto ipv4.addresses "" ipv4.gateway "" ipv4.dns ""`;
|
||||
|
||||
|
||||
// 执行配置命令
|
||||
console.log('执行命令:', modifyCmd.replace('unis@123', '***'));
|
||||
await execAsync(modifyCmd);
|
||||
|
||||
|
||||
// 重新激活连接
|
||||
await execAsync(`echo "unis@123" | sudo -S nmcli connection up "${connectionName}"`);
|
||||
}
|
||||
|
|
@ -160,7 +60,7 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
|
|||
success: true,
|
||||
message: '网络配置已成功应用'
|
||||
};
|
||||
}catch(error:unknown){
|
||||
} catch (error: unknown) {
|
||||
console.error('应用网络配置失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
|
|
@ -169,14 +69,16 @@ ipcMain.handle('apply-wired-config',async(event,config)=>{
|
|||
}
|
||||
})
|
||||
|
||||
/**2. 服务器配置 */
|
||||
/**2. 服务器配置认证 */
|
||||
ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
||||
console.log(`Connecting to server: ${serverIp}`);
|
||||
|
||||
// (global as any).currentServerIp = serverIp;
|
||||
// console.log('Authentication successful, server IP stored:', serverIp);
|
||||
// return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
|
||||
try {
|
||||
// 获取设备ID
|
||||
const deviceId = await getDeviceId();
|
||||
console.log(`Using device ID: ${deviceId}`);
|
||||
const deviceInfo = await getDeviceIdAndMac();
|
||||
console.log(`Using device Info: ${JSON.stringify(deviceInfo)}`);
|
||||
|
||||
// 构建新的API地址,使用POST请求
|
||||
const apiUrl = `http://${serverIp}:8113/api/nex/v1/client/authentication`;
|
||||
|
|
@ -189,7 +91,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
|||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({ device_id: deviceId })
|
||||
data: JSON.stringify({ device_id: deviceInfo.deviceId,mac_addr:deviceInfo.macAddr })
|
||||
});
|
||||
|
||||
console.log('API response received:', response);
|
||||
|
|
@ -208,7 +110,7 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
|||
// 检查认证结果 - 只有code等于200时才算成功
|
||||
if (responseData.code === '200' || responseData.code === 200) {
|
||||
// 认证成功,存储服务器IP
|
||||
currentServerIp = serverIp;
|
||||
(global as any).currentServerIp = serverIp;
|
||||
console.log('Authentication successful, server IP stored:', serverIp);
|
||||
return { success: true, message: `已连接到服务器 ${serverIp}`, serverIp: serverIp };
|
||||
} else {
|
||||
|
|
@ -251,10 +153,6 @@ ipcMain.handle('connect-server', async (event, { serverIp }) => {
|
|||
};
|
||||
}
|
||||
});
|
||||
// 服务器IP获取
|
||||
ipcMain.handle('get-current-server-ip', () => {
|
||||
return currentServerIp;
|
||||
});
|
||||
|
||||
/**3. 侦测管理平台 */
|
||||
// 下载并更新客户端
|
||||
|
|
@ -325,4 +223,5 @@ ipcMain.on('install-update-and-restart', () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
// import { ipcMain,app,BrowserWindow } from 'electron';
|
||||
|
||||
// // 模拟grpc服务端
|
||||
// import { BTGrpcClient } from '../grpc/BTGrpcClient';
|
||||
// import { MockBTService } from '../grpc/MockBTService';
|
||||
|
||||
// // 客户端和服务端 通信
|
||||
// const IS_TEST_MODE = true; // 设置为 false 时连接真实后端
|
||||
// const GRPC_SERVER_PORT = 50051;
|
||||
// // 先声明变量,但不立即初始化
|
||||
// let btGrpcClient: BTGrpcClient | null = null;
|
||||
// let mockServer: MockBTService | null = null;
|
||||
// let healthCheckInterval: NodeJS.Timeout | null = null; // 声明一个变量来存储定时器
|
||||
|
||||
|
||||
// // 初始化 gRPC 客户端
|
||||
// async function initializeGrpc() {
|
||||
// try {
|
||||
// if (IS_TEST_MODE) {
|
||||
// console.log('启动模拟 gRPC 服务器...');
|
||||
// mockServer = new MockBTService();
|
||||
// await mockServer.start(GRPC_SERVER_PORT);
|
||||
|
||||
// // 给一点时间让服务器启动
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// }
|
||||
|
||||
// // 创建 BTGrpcClient 实例连接到gRPC服务
|
||||
// btGrpcClient = new BTGrpcClient();
|
||||
|
||||
// // 注册进度回调函数,用于接收下载进度更新
|
||||
// btGrpcClient.registerProgressCallback('all', (progress) => {
|
||||
// const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
// if (mainWindow) {
|
||||
// mainWindow.webContents.send('grpc-progress-update', progress);
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 测试连接
|
||||
// setTimeout(async () => {
|
||||
// try {
|
||||
// const connected = await btGrpcClient!.testConnection();
|
||||
// console.log('gRPC 连接状态:', connected ? '已连接' : '未连接');
|
||||
|
||||
// const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
// if (mainWindow) {
|
||||
// mainWindow.webContents.send('grpc-connection-status', {
|
||||
// connected,
|
||||
// isMock: IS_TEST_MODE
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('连接测试失败:', error);
|
||||
// }
|
||||
// }, 2000);
|
||||
|
||||
// // 启动健康检查
|
||||
// startHealthCheck();
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('gRPC 初始化失败:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 启动健康检查
|
||||
// function startHealthCheck() {
|
||||
// if (healthCheckInterval) {
|
||||
// clearInterval(healthCheckInterval);
|
||||
// }
|
||||
|
||||
// healthCheckInterval = setInterval(async () => {
|
||||
// try {
|
||||
// if (!btGrpcClient) return;
|
||||
|
||||
// const isConnected = await btGrpcClient.testConnection();
|
||||
// const mainWindow = BrowserWindow.getFocusedWindow();
|
||||
// if (mainWindow) {
|
||||
// mainWindow.webContents.send('grpc-connection-status', {
|
||||
// connected: isConnected,
|
||||
// state: btGrpcClient.getConnectionState(),
|
||||
// isMock: IS_TEST_MODE
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('健康检查失败:', error);
|
||||
// }
|
||||
// }, 5000); // 每5秒检查一次
|
||||
// }
|
||||
|
||||
|
||||
// // 应用启动时初始化
|
||||
// app.whenReady().then(() => {
|
||||
// initializeGrpc();
|
||||
// });
|
||||
|
||||
// // 应用退出时清理
|
||||
// app.on('before-quit', async () => {
|
||||
// if (healthCheckInterval) {
|
||||
// clearInterval(healthCheckInterval);
|
||||
// }
|
||||
|
||||
// if (mockServer) {
|
||||
// await mockServer.stop();
|
||||
// }
|
||||
// });
|
||||
|
||||
// // 添加 gRPC 相关的 IPC 处理程序
|
||||
// ipcMain.handle('grpc-start-download', async (event, config) => {
|
||||
// try {
|
||||
// if (!btGrpcClient) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'gRPC客户端未初始化'
|
||||
// };
|
||||
// }
|
||||
|
||||
// console.log('开始gRPC下载:', config);
|
||||
// // gRPC客户端向服务端发起 StartDownload 调用
|
||||
// const result = await btGrpcClient.startDownload({
|
||||
// torrent_url: config.torrentUrl,
|
||||
// item_name: config.itemName,
|
||||
// item_id: config.itemId
|
||||
// });
|
||||
// return { success: true, data: result };
|
||||
// } catch (error: any) {
|
||||
// console.error('gRPC下载失败:', error);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: error.message || '未知错误',
|
||||
// details: error.details
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
||||
// ipcMain.handle('grpc-stop-download', async (event, downloadId) => {
|
||||
// try {
|
||||
// if (!btGrpcClient) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'gRPC客户端未初始化'
|
||||
// };
|
||||
// }
|
||||
|
||||
// const result = await btGrpcClient.stopDownload(downloadId);
|
||||
// return { success: true, data: result };
|
||||
// } catch (error: any) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: error.message || '未知错误'
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
||||
// ipcMain.handle('grpc-check-connection', async () => {
|
||||
// if (!btGrpcClient) {
|
||||
// return { connected: false, error: 'gRPC客户端未初始化' };
|
||||
// }
|
||||
|
||||
// return {
|
||||
// connected: btGrpcClient.isConnected(),
|
||||
// state: btGrpcClient.getConnectionState()
|
||||
// };
|
||||
// });
|
||||
|
||||
// // 重新连接 gRPC
|
||||
// ipcMain.handle('grpc-reconnect', async () => {
|
||||
// try {
|
||||
// if (btGrpcClient) {
|
||||
// btGrpcClient.reconnect();
|
||||
// return { success: true, message: '正在重新连接' };
|
||||
// } else {
|
||||
// await initializeGrpc();
|
||||
// return { success: true, message: '正在初始化连接' };
|
||||
// }
|
||||
// } catch (error: any) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: error.message || '重新连接失败'
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { ipcMain } from 'electron';
|
||||
import request from '../utils/request';
|
||||
import { getDeviceIdAndMac } from '../utils/utils';
|
||||
// import { setCurrentServerIp, getCurrentServerIp } from './sharedState'; // 修改导
|
||||
|
||||
// gRPC 连接请求
|
||||
ipcMain.handle('connect-to-grpc', async (event, { serverIp, clientUser }: { serverIp?: string; clientUser?: string }) => {
|
||||
console.log('gRPC 连接请求');
|
||||
// return {
|
||||
// success: true,
|
||||
// message: 'gRPC 连接成功',
|
||||
// data: []
|
||||
// };
|
||||
try {
|
||||
// 如果没有传 serverIp,则使用已存储的 serverIp
|
||||
const ip = serverIp || (global as any).currentServerIp;
|
||||
|
||||
if (!ip) {
|
||||
return {
|
||||
success: false,
|
||||
error: '未找到服务器IP地址'
|
||||
};
|
||||
}
|
||||
|
||||
// 获取设备序列号
|
||||
const deviceInfo = await getDeviceIdAndMac();
|
||||
|
||||
if (!deviceInfo.deviceId) {
|
||||
return {
|
||||
success: false,
|
||||
error: '无法获取设备序列号'
|
||||
};
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
const requestData = {
|
||||
serverIp: ip,
|
||||
serverPort: 50051,
|
||||
clientSn:deviceInfo.deviceId, // 使用设备序列号作为 clientSn,
|
||||
clientUser: clientUser || '' // 如果没有用户名,则传空字符串
|
||||
};
|
||||
|
||||
console.log('连接 gRPC 服务器:', requestData);
|
||||
|
||||
// 发送请求到后端接口
|
||||
const apiUrl = `http://${ip}:3001/api/grpc/connect`;
|
||||
const response = await request(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse((response as any).data);
|
||||
} catch (parseError) {
|
||||
console.error('解析 gRPC 连接响应失败:', parseError);
|
||||
return { success: false, message: '服务器响应格式错误' };
|
||||
}
|
||||
|
||||
console.log('gRPC 连接响应:', responseData);
|
||||
|
||||
if (responseData.code === '200' || responseData.code === 200) {
|
||||
console.log('gRPC 连接成功');
|
||||
return {
|
||||
success: true,
|
||||
message: 'gRPC 连接成功',
|
||||
data: responseData
|
||||
};
|
||||
} else {
|
||||
const errorMessage = responseData.message || responseData.msg || responseData.error || 'gRPC 连接失败';
|
||||
console.log('gRPC 连接失败:', errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('gRPC 连接请求失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 镜像列表请求
|
||||
ipcMain.handle('get-images-list', async (event, deviceInfo: { deviceId: string; macAddr: string }) => {
|
||||
// return { success: true, message: '获取镜像列表成功', data: []}
|
||||
try {
|
||||
if (!(global as any).currentServerIp) {
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器未连接'
|
||||
};
|
||||
}
|
||||
|
||||
const apiUrl = `http://${(global as any).currentServerIp}:8113/api/nex/v1/client/getImageList`;
|
||||
|
||||
const response = await request(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({
|
||||
device_id: deviceInfo.deviceId,
|
||||
macAddr: deviceInfo.macAddr
|
||||
})
|
||||
});
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = JSON.parse((response as any).data);
|
||||
} catch (parseError) {
|
||||
console.error('解析镜像列表响应失败:', parseError);
|
||||
return { success: false, message: '服务器响应格式错误' };
|
||||
}
|
||||
|
||||
return responseData;
|
||||
} catch (error) {
|
||||
console.error('获取镜像列表失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// 设置全局变量
|
||||
(global as any).currentServerIp;
|
||||
|
|
@ -5,16 +5,29 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
getPlatform: async () => {
|
||||
return await ipcRenderer.invoke('getPlatform');
|
||||
},
|
||||
// 【窗口】相关API
|
||||
closeApp: () => ipcRenderer.send('close-app'),
|
||||
minimizeApp: () => ipcRenderer.send('minimize-app'),
|
||||
restoreWindow: () => ipcRenderer.send('restore-window'),
|
||||
maximizeWindow: () => ipcRenderer.send('maximize-window'),
|
||||
getWindowMaximized: () => ipcRenderer.invoke('get-window-maximized'),
|
||||
adjustWindowForNormal:() => ipcRenderer.invoke('adjust-window-for-normal'),
|
||||
// 【bt下载】相关 API
|
||||
startDownload: (config: any) => ipcRenderer.invoke('start-download', config),
|
||||
stopDownload: (downloadId: string) => ipcRenderer.invoke('stop-download', downloadId),
|
||||
// 【配置步骤】相关API
|
||||
applyWiredConfig: (config: any) => ipcRenderer.invoke('apply-wired-config', config),
|
||||
connectServer: (config: any) => ipcRenderer.invoke('connect-server', config),
|
||||
// 版本更新相关API
|
||||
downloadAndUpdate: (url: string) => ipcRenderer.invoke('download-and-update', url),
|
||||
// 服务器IP获取
|
||||
// 【镜像列表】获取
|
||||
getImagesList: (config: any) => ipcRenderer.invoke('get-images-list', config),
|
||||
// 【服务器IP】获取
|
||||
getCurrentServerIp: () => ipcRenderer.invoke('get-current-server-ip'),
|
||||
// 【设备信息】获取
|
||||
getDeviceInfo: () => ipcRenderer.invoke('get-device-info'),
|
||||
// 【通知后端grpc连接】
|
||||
connectToGrpc: (config:any) => ipcRenderer.invoke('connect-to-grpc', config),
|
||||
// 事件监听
|
||||
onMainProcessMessage: (callback: (data: string) => void) => {
|
||||
ipcRenderer.on('main-process-message', (_, data) => callback(data));
|
||||
|
|
|
|||
|
|
@ -6,11 +6,21 @@ const os = require('os');
|
|||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/** 获取设备ID(芯片序列号)
|
||||
* TODO: 增加获取mac地址的逻辑,需要传给后端
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const configPath = path.join(os.homedir(), '.xspace_configData.json'); // 用户数据文件路径
|
||||
|
||||
/** 获取设备ID(芯片序列号)和MAC地址
|
||||
* 返回设备唯一标识符和原始MAC地址
|
||||
*/
|
||||
export async function getDeviceId() {
|
||||
export async function getDeviceIdAndMac() {
|
||||
// return {
|
||||
// deviceId:'125dfsdfsd',
|
||||
// macAddr: "00:00:00:00:00"
|
||||
// };
|
||||
try {
|
||||
let macAddr = '';
|
||||
|
||||
// 尝试多种方法获取唯一的设备标识
|
||||
const methods = [
|
||||
// 方法1: CPU序列号
|
||||
|
|
@ -28,10 +38,27 @@ export async function getDeviceId() {
|
|||
const { stdout } = await execAsync(command);
|
||||
const deviceId = stdout.trim();
|
||||
|
||||
// 同时获取MAC地址供后续使用
|
||||
if (!macAddr) {
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
outerLoop: for (const interfaceName of Object.keys(networkInterfaces)) {
|
||||
const interfaces = networkInterfaces[interfaceName];
|
||||
for (const iface of interfaces) {
|
||||
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
||||
macAddr = iface.mac.toUpperCase();
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceId && deviceId !== '' && deviceId !== 'unknown' && deviceId !== '0000000000000000') {
|
||||
console.log(`Device ID obtained using command: ${command}`);
|
||||
console.log(`Device ID: ${deviceId}`);
|
||||
return deviceId;
|
||||
return {
|
||||
deviceId,
|
||||
macAddr: macAddr || 'NOT_FOUND'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Method failed: ${command}, error: ${(error as Error).message}`);
|
||||
|
|
@ -46,8 +73,12 @@ export async function getDeviceId() {
|
|||
for (const iface of interfaces) {
|
||||
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
||||
const fallbackId = iface.mac.replace(/:/g, '').toUpperCase();
|
||||
macAddr = iface.mac.toUpperCase();
|
||||
console.log(`Using MAC address as fallback device ID: ${fallbackId}`);
|
||||
return fallbackId;
|
||||
return {
|
||||
deviceId: fallbackId,
|
||||
macAddr
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,12 +86,18 @@ export async function getDeviceId() {
|
|||
// 最后的fallback - 使用hostname
|
||||
const hostname = os.hostname();
|
||||
console.log(`Using hostname as final fallback device ID: ${hostname}`);
|
||||
return hostname;
|
||||
return {
|
||||
deviceId: hostname,
|
||||
macAddr: macAddr || 'NOT_FOUND'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting device ID:', error);
|
||||
console.error('Error getting device Info:', error);
|
||||
// 返回一个默认的设备ID
|
||||
return 'UNKNOWN_DEVICE';
|
||||
return {
|
||||
deviceId: 'UNKNOWN_DEVICE',
|
||||
macAddr: 'ERROR'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +140,39 @@ export function netmaskToCidr(netmask:string) {
|
|||
return netmaskMap[netmask] || '24';
|
||||
}
|
||||
|
||||
// 读取保存的用户数据
|
||||
export function getConfig() {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取配置失败:', error);
|
||||
}
|
||||
}
|
||||
export function setConfig(config: any) {
|
||||
try {
|
||||
const oldConfig = getConfig();
|
||||
const newConfig = { ...oldConfig, ...config };
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// export function getServerIp() {
|
||||
// return getConfig().currentServerIp;
|
||||
// }
|
||||
|
||||
// export function setServerIp(ip: string) {
|
||||
// const config = getConfig();
|
||||
// config.currentServerIp = ip;
|
||||
// return setConfig(config);
|
||||
// }
|
||||
|
||||
|
||||
/**模拟更新客户端 */
|
||||
export async function simulateUpdate(event: any, url: any) {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, message } from 'antd';
|
||||
import { message } from 'antd';
|
||||
import { history, useLocation, Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const location = useLocation();
|
||||
|
|
@ -27,19 +25,26 @@ const MainLayout: React.FC = () => {
|
|||
// TODO: 第一次来:判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定:到配置ip/DHCP页面
|
||||
setTimeout(() => {
|
||||
// history.push('/login');
|
||||
history.push('/configSteps?tab=terminalGetImage');
|
||||
history.push('/configSteps?tab=serverConfig');
|
||||
},1000)
|
||||
// const fetchDeviceId = async () => {
|
||||
// try {
|
||||
// const res = await window.electronAPI.invoke('get-device-id');
|
||||
// console.log('获取设备ID:', res);
|
||||
// } catch (error) {
|
||||
// console.error('获取设备ID失败:', error);
|
||||
// }
|
||||
// }
|
||||
// fetchDeviceId()
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDeviceId = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.getDeviceInfo();
|
||||
if (result.success) {
|
||||
console.log('获取设备信息:', JSON.stringify(result.deviceInfo));
|
||||
} else {
|
||||
console.error('获取设备信息失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备信息异常:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDeviceId();
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
// 使用路由导航
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// src/pages/components/TitleBar/index.less
|
||||
.title-bar {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
// margin: 20px 0; // 使用margin拖动不会起作用,需要使用padding
|
||||
// height: 34px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
height: 74px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ const NetworkConfig: React.FC = () => {
|
|||
if (activeTab === 'dhcp') {
|
||||
// 如果是 DHCP 模式,直接IPC 处理应用有线网络配置
|
||||
try {
|
||||
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'dhcp' });
|
||||
const res = await window.electronAPI.applyWiredConfig({ method: 'dhcp' });
|
||||
console.log('网络配置返回信息成功:', res);
|
||||
if(res.success){
|
||||
message.success('网络配置成功');
|
||||
|
|
@ -171,7 +171,7 @@ const NetworkConfig: React.FC = () => {
|
|||
values.ipv6PrefixLength = parseInt(values.ipv6PrefixLength, 10);
|
||||
}
|
||||
|
||||
const res = await window.electronAPI.invoke('apply-wired-config',{ method: 'static', ...values });
|
||||
const res = await window.electronAPI.applyWiredConfig({ method: 'static', ...values });
|
||||
console.log('网络配置返回信息成功:', res);
|
||||
if(res.success){
|
||||
message.success('网络配置成功');
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const Index = () => {
|
|||
const values = await form.validateFields();
|
||||
console.log('表单提交数据:', values);
|
||||
const { serverIp } = values;
|
||||
const result = await window.electronAPI.invoke('connect-server', { serverIp });
|
||||
const result = await window.electronAPI.connectServer({ serverIp });
|
||||
console.log('连接结果:', result);
|
||||
if (result.success) {
|
||||
// 连接成功,保存服务器IP到本地存储
|
||||
|
|
@ -62,7 +62,14 @@ const Index = () => {
|
|||
console.log('Connected server IP saved to localStorage:', serverIp);
|
||||
// TODO:跳转到版本更新
|
||||
// setActiveTab("watchManagement")
|
||||
setActiveTab("terminalGetImage")
|
||||
// 通知后端连接 gRPC 服务
|
||||
const grpcResult = await window.electronAPI.connectToGrpc({ serverIp });
|
||||
if (grpcResult.success) {
|
||||
// console.log('gRPC 连接成功');
|
||||
setActiveTab("terminalGetImage")
|
||||
} else {
|
||||
message.error(grpcResult.message || 'gRPC 连接失败');
|
||||
}
|
||||
// await ipcRenderer.invoke('show-login-window');
|
||||
} else {
|
||||
message.error(result.message || '连接服务器失败');
|
||||
|
|
|
|||
|
|
@ -1,43 +1,79 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import ButtonCom from '../../../components/ButtonCom';
|
||||
import { useConfigStep } from '@/contexts/ConfigStepContext';
|
||||
import styles from './index.less';
|
||||
import { message } from 'antd';
|
||||
import { history } from 'umi';
|
||||
import TrueIcon from '@assets/true.png'
|
||||
import WebSocketService from '../../../services/webSocketService';
|
||||
import { getDeviceInfo, getImagesList } from '../../../services/imageService';
|
||||
|
||||
|
||||
const Index = () => {
|
||||
const { goToNextTab,setActiveTab } = useConfigStep();
|
||||
const [imageStatus, setImageStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [isWsConnected, setIsWsConnected] = useState(false);
|
||||
|
||||
const checkImageList = useCallback(async () => {
|
||||
try {
|
||||
// 获取设备信息
|
||||
const deviceInfo = await getDeviceInfo();
|
||||
|
||||
// 获取镜像列表
|
||||
const response = await getImagesList({
|
||||
deviceId: deviceInfo.deviceId,
|
||||
macAddr: deviceInfo.macAddr
|
||||
});
|
||||
|
||||
// 模拟获取镜像数据的API调用
|
||||
useEffect(() => {
|
||||
const fetchImageData = async () => {
|
||||
try {
|
||||
// 模拟API请求
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
// 模拟随机成功或失败
|
||||
const isSuccess = Math.random() > 0.3;
|
||||
setImageStatus(isSuccess ? 'success' : 'loading');
|
||||
|
||||
// 如果失败,继续轮询检查状态
|
||||
if (!isSuccess) {
|
||||
const checkStatus = async () => {
|
||||
// 模拟WebSocket或轮询检查
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
setImageStatus('success'); // 模拟收到通知
|
||||
};
|
||||
checkStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取镜像数据失败');
|
||||
if (response.code === 200 && response.data?.data?.length > 0) {
|
||||
// 有镜像数据,直接进入下一步
|
||||
setImageStatus('success');
|
||||
} else {
|
||||
// 没有镜像数据,需要监听WebSocket
|
||||
await connectWebSocket();
|
||||
}
|
||||
};
|
||||
|
||||
fetchImageData();
|
||||
} catch (error) {
|
||||
console.error('检查镜像数据失败:', error);
|
||||
message.error('获取镜像数据失败');
|
||||
setImageStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const connectWebSocket = useCallback(async () => {
|
||||
try {
|
||||
// 连接WebSocket
|
||||
const connected = await WebSocketService.connect();
|
||||
setIsWsConnected(connected);
|
||||
|
||||
if (connected) {
|
||||
// 监听镜像数据推送
|
||||
const removeListener = WebSocketService.addListener('2', (data:any) => {
|
||||
if (data.type === '2') {
|
||||
// 有镜像数据了,更新状态
|
||||
setImageStatus('success');
|
||||
// 移除监听器
|
||||
removeListener();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
message.error('WebSocket连接失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error);
|
||||
message.error('WebSocket连接失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 组件挂载时检查镜像数据
|
||||
checkImageList();
|
||||
|
||||
// 组件卸载时清理
|
||||
return () => {
|
||||
// 可以选择是否断开WebSocket连接
|
||||
};
|
||||
}, [checkImageList]);
|
||||
|
||||
const ImageLoadingComponent = () => (
|
||||
<div className={styles["image-loading-container"]}>
|
||||
<div className={styles["loading-wrapper"]}>
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 镜像列表样式
|
||||
.image-list {
|
||||
.image-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0 100px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 个人资料样式
|
||||
.profile-page {
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.profile-info {
|
||||
h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Table, Tag, Button, Space, Modal, message } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import './index.less';
|
||||
|
||||
interface ImageItem {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
size: string;
|
||||
status: 'active' | 'inactive' | 'building';
|
||||
createTime: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ImageList: React.FC = () => {
|
||||
const [images, setImages] = useState<ImageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadImages();
|
||||
}, []);
|
||||
|
||||
const loadImages = () => {
|
||||
setLoading(true);
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockData: ImageItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Windows 10 专业版',
|
||||
version: 'v1.0.0',
|
||||
size: '15.2 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
description: 'Windows 10 专业版镜像,包含常用办公软件'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Ubuntu 22.04 LTS',
|
||||
version: 'v2.1.0',
|
||||
size: '8.5 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-10 14:20:00',
|
||||
description: 'Ubuntu 22.04 LTS 服务器版本,适用于开发环境'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'CentOS 8',
|
||||
version: 'v1.5.0',
|
||||
size: '12.1 GB',
|
||||
status: 'building',
|
||||
createTime: '2024-01-20 09:15:00',
|
||||
description: 'CentOS 8 企业级服务器操作系统'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'macOS Monterey',
|
||||
version: 'v1.2.0',
|
||||
size: '18.7 GB',
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-05 16:45:00',
|
||||
description: 'macOS Monterey 开发环境镜像'
|
||||
}
|
||||
];
|
||||
setImages(mockData);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap = {
|
||||
active: { color: 'green', text: '可用' },
|
||||
inactive: { color: 'red', text: '不可用' },
|
||||
building: { color: 'orange', text: '构建中' }
|
||||
};
|
||||
const config = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const handleViewDetail = (record: ImageItem) => {
|
||||
setSelectedImage(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDownload = (record: ImageItem) => {
|
||||
message.success(`开始下载镜像:${record.name}`);
|
||||
};
|
||||
|
||||
const handleEdit = (record: ImageItem) => {
|
||||
message.info(`编辑镜像:${record.name}`);
|
||||
};
|
||||
|
||||
const handleDelete = (record: ImageItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
setImages(images.filter(img => img.id !== record.id));
|
||||
message.success('删除成功');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '镜像名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render: (_: any, record: ImageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
title="查看详情"
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleDownload(record)}
|
||||
title="下载"
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={images}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: images.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="镜像详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={600}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="image-detail">
|
||||
<div className="detail-item">
|
||||
<label>镜像名称:</label>
|
||||
<span>{selectedImage.name}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>版本:</label>
|
||||
<span>{selectedImage.version}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>大小:</label>
|
||||
<span>{selectedImage.size}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>状态:</label>
|
||||
<span>{getStatusTag(selectedImage.status)}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{selectedImage.createTime}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>描述:</label>
|
||||
<p>{selectedImage.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageList;
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/* src/pages/imagesList/index.less */
|
||||
.images-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.shadow-divider {
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
padding: 29px 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
margin-bottom: 50px;
|
||||
|
||||
h1 {
|
||||
font-family: "PingFang SC";
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: 18px;
|
||||
line-height: 29px;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.images-grid {
|
||||
// 移除 flex: 1
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 150px);
|
||||
gap: 50px 38px; // row-gap: 50px, column-gap: 38px
|
||||
padding-bottom: 70px;
|
||||
overflow-y: auto; // 保持滚动功能
|
||||
// 添加最小高度以确保滚动正常工作
|
||||
min-height: 0; // 关键:允许网格容器收缩
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 129px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// 添加状态图标样式
|
||||
.status-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.status-icon-download {
|
||||
font-size: 15px;
|
||||
color: #1890ff; // 蓝色下载图标
|
||||
}
|
||||
|
||||
.status-icon-completed {
|
||||
font-size: 15px;
|
||||
color: #52c41a; // 绿色完成图标
|
||||
}
|
||||
}
|
||||
|
||||
.image-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.image-name {
|
||||
width: 100%;
|
||||
height: 29px;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.download-overlay,
|
||||
.completed-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.download-overlay {
|
||||
.download-info {
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
text-align: center;
|
||||
|
||||
.download-text {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #4a90e2;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.speed-info,
|
||||
.eta-info {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.completed-overlay {
|
||||
.completed-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,504 @@
|
|||
import React from 'react';
|
||||
// src/pages/imagesList/index.tsx
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { DownloadOutlined, CheckCircleOutlined } from '@ant-design/icons'; // 引入 Ant Design 图标
|
||||
import TitleBar from '../components/TitleBar';
|
||||
import ImageFileIcon from '@assets/imageFileIcon.png'
|
||||
import { formatDecimalSimple } from '@/utils/utils';
|
||||
import WebSocketService from '../services/webSocketService';
|
||||
import './index.less';
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<div>
|
||||
镜像列表
|
||||
</div>
|
||||
);
|
||||
// 镜像项类型定义
|
||||
interface ImageItem {
|
||||
id: string;
|
||||
name: string;
|
||||
torrentUrl: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export default Index;
|
||||
// 下载状态类型定义
|
||||
interface DownloadProgress {
|
||||
downloadId: string;
|
||||
progress: number;
|
||||
downloadSpeedText: string;
|
||||
etaText: string;
|
||||
state: 'started' | 'progress' | 'completed' | 'error' | 'stopped';
|
||||
}
|
||||
|
||||
const ImagesList: React.FC = () => {
|
||||
// 镜像列表数据(示例数据)
|
||||
const [images, setImages] = useState<ImageItem[]>([
|
||||
{
|
||||
id: 'ubuntu-22-04',
|
||||
name: 'Ubuntu 22.04 LTS',
|
||||
torrentUrl: 'https://releases.ubuntu.com/22.04/ubuntu-22.04.3-desktop-amd64.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'windows-11',
|
||||
name: 'Windows 11 Pro',
|
||||
torrentUrl: 'https://download.microsoft.com/windows-11.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'centos-8',
|
||||
name: 'CentOS 8 Stream',
|
||||
torrentUrl: 'https://centos.org/centos-8-stream.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'debian-12',
|
||||
name: 'Debian 12 Bookworm',
|
||||
torrentUrl: 'https://debian.org/debian-12.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'fedora-38',
|
||||
name: 'Fedora 38 Workstation',
|
||||
torrentUrl: 'https://fedoraproject.org/fedora-38.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'opensuse-leap-1',
|
||||
name: 'openSUSE Leap 15.5',
|
||||
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'opensuse-leap-2',
|
||||
name: 'openSUSE Leap 15.5',
|
||||
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
id: 'opensuse-leap-3',
|
||||
name: 'openSUSE Leap 15.5openSUSE Leap 15.5',
|
||||
torrentUrl: 'https://opensuse.org/opensuse-leap-15.5.iso.torrent',
|
||||
icon: ''
|
||||
}
|
||||
]);
|
||||
|
||||
// 下载状态映射
|
||||
const [downloadStatus, setDownloadStatus] = useState<Map<string, DownloadProgress>>(new Map());
|
||||
|
||||
// WebSocket连接状态
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
|
||||
// 防抖计时器映射
|
||||
const debounceTimers = useMemo(() => new Map<string, NodeJS.Timeout>(), []);
|
||||
|
||||
// 初始化WebSocket连接
|
||||
useEffect(() => {
|
||||
let removeProgressListener: (() => void) | null = null;
|
||||
let removeStartListener: (() => void) | null = null;
|
||||
let removeCompleteListener: (() => void) | null = null;
|
||||
let removeErrorListener: (() => void) | null = null;
|
||||
let removeStopListener: (() => void) | null = null;
|
||||
|
||||
const initWebSocket = async () => {
|
||||
try {
|
||||
const connected = await WebSocketService.connect();
|
||||
setWsConnected(connected);
|
||||
|
||||
if (connected) {
|
||||
// 监听下载进度更新
|
||||
removeProgressListener = WebSocketService.addListener('download-progress', (data: any) => {
|
||||
if (data.type === 'download-progress') {
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(data.downloadId, {
|
||||
downloadId: data.downloadId,
|
||||
progress: data.progress,
|
||||
downloadSpeedText: data.downloadSpeedText,
|
||||
etaText: data.etaText,
|
||||
state: 'progress'
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监听下载开始
|
||||
removeStartListener = WebSocketService.addListener('download-started', (data: any) => {
|
||||
if (data.type === 'download-started') {
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(data.downloadId, {
|
||||
downloadId: data.downloadId,
|
||||
progress: 0,
|
||||
downloadSpeedText: '0 MB/s',
|
||||
etaText: '计算中...',
|
||||
state: 'started'
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监听下载完成
|
||||
removeCompleteListener = WebSocketService.addListener('download-completed', (data: any) => {
|
||||
if (data.type === 'download-completed') {
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
progress: 100,
|
||||
state: 'completed'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
message.success(`${ data.itemName } 下载完成`)
|
||||
}
|
||||
});
|
||||
|
||||
// 监听下载错误
|
||||
removeErrorListener = WebSocketService.addListener('download-error', (data: any) => {
|
||||
if (data.type === 'download-error') {
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
state: 'error'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
message.error(`下载错误: ${data.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听下载停止
|
||||
removeStopListener = WebSocketService.addListener('download-stopped', (data: any) => {
|
||||
if (data.type === 'download-stopped') {
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
state: 'stopped'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
message.error('WebSocket连接失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket初始化失败:', error);
|
||||
message.error('WebSocket初始化失败');
|
||||
}
|
||||
};
|
||||
|
||||
initWebSocket();
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 注意:我们不主动断开WebSocket连接,让它保持连接状态
|
||||
// 只是移除当前页面的监听器
|
||||
if (removeProgressListener) removeProgressListener();
|
||||
if (removeStartListener) removeStartListener();
|
||||
if (removeCompleteListener) removeCompleteListener();
|
||||
if (removeErrorListener) removeErrorListener();
|
||||
if (removeStopListener) removeStopListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 初始化WebSocket连接
|
||||
// useEffect(() => {
|
||||
// let progressWS: WebSocket;
|
||||
|
||||
// const initWebSocket = async () => {
|
||||
// try {
|
||||
// // 获取保存的服务器 IP
|
||||
// const serverIp = await window.electronAPI.getCurrentServerIp();
|
||||
|
||||
// // if (!serverIp) {
|
||||
// // message.error('服务器未连接,请先配置服务器');
|
||||
// // return;
|
||||
// // }
|
||||
// // 如果没有服务器IP,则使用本地测试
|
||||
// const wsUrl = serverIp
|
||||
// ? `ws://${serverIp}:3002`
|
||||
// : 'ws://localhost:3002'; // 本地测试用的WebSocket地址
|
||||
|
||||
// console.log('尝试连接WebSocket:', wsUrl);
|
||||
|
||||
// progressWS = new WebSocket(wsUrl);
|
||||
// // 使用服务器 IP 建立 WebSocket 连接
|
||||
// // progressWS = new WebSocket(`ws://${serverIp}:3002`);
|
||||
|
||||
// progressWS.onopen = () => {
|
||||
// console.log('WebSocket连接成功');
|
||||
// setWsConnected(true);
|
||||
// // 发送认证
|
||||
// progressWS.send(JSON.stringify({
|
||||
// type: 'auth',
|
||||
// clientId: 'electron-frontend'
|
||||
// }));
|
||||
// };
|
||||
|
||||
// progressWS.onmessage = (event) => {
|
||||
// const data = JSON.parse(event.data);
|
||||
// handleProgressMessage(data);
|
||||
// };
|
||||
|
||||
// progressWS.onerror = (error) => {
|
||||
// console.error('WebSocket错误:', error);
|
||||
// setWsConnected(false);
|
||||
// };
|
||||
|
||||
// progressWS.onclose = () => {
|
||||
// console.log('WebSocket连接关闭');
|
||||
// setWsConnected(false);
|
||||
// // 尝试重连
|
||||
// setTimeout(initWebSocket, 5000);
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error('初始化WebSocket失败:', error);
|
||||
// message.error('连接服务器失败');
|
||||
// }
|
||||
// };
|
||||
|
||||
// initWebSocket();
|
||||
|
||||
// return () => {
|
||||
// if (progressWS) {
|
||||
// progressWS.close();
|
||||
// }
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
// 处理WebSocket消息
|
||||
const handleProgressMessage = useCallback((data: any) => {
|
||||
switch (data.type) {
|
||||
case 'download-started':
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(data.downloadId, {
|
||||
downloadId: data.downloadId,
|
||||
progress: 0,
|
||||
downloadSpeedText: '0 MB/s',
|
||||
etaText: '计算中...',
|
||||
state: 'started'
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'download-progress':
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(data.downloadId, {
|
||||
downloadId: data.downloadId,
|
||||
progress: data.progress,
|
||||
downloadSpeedText: data.downloadSpeedText,
|
||||
etaText: data.etaText,
|
||||
state: 'progress'
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
break;
|
||||
|
||||
case 'download-completed':
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
progress: 100,
|
||||
state: 'completed'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
message.success(`下载完成: ${data.itemName}`);
|
||||
break;
|
||||
|
||||
case 'download-error':
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
state: 'error'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
message.error(`下载错误: ${data.error}`);
|
||||
break;
|
||||
|
||||
case 'download-stopped':
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const status = newMap.get(data.downloadId);
|
||||
if (status) {
|
||||
newMap.set(data.downloadId, {
|
||||
...status,
|
||||
state: 'stopped'
|
||||
});
|
||||
}
|
||||
return newMap;
|
||||
});
|
||||
message.info('下载已停止');
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 开始下载
|
||||
const startDownload = useCallback((image: ImageItem) => {
|
||||
// 防抖处理
|
||||
if (debounceTimers.has(image.id)) {
|
||||
clearTimeout(debounceTimers.get(image.id)!);
|
||||
}
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
// 检查是否已经在下载
|
||||
if (downloadStatus.has(image.id)) {
|
||||
const status = downloadStatus.get(image.id)!;
|
||||
if (status.state === 'progress' || status.state === 'started') {
|
||||
message.warning('已在下载中,请勿重复点击');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 通过 Electron IPC 发送 HTTP 请求
|
||||
const result = await window.electronAPI.startDownload({
|
||||
torrentUrl: image.torrentUrl,
|
||||
itemName: image.name,
|
||||
itemId: image.id,
|
||||
savePath: '/mnt/disk/tmp',
|
||||
targetPath: '/mnt/disk/voi'
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
message.error(`下载启动失败: ${result.error}`);
|
||||
} else {
|
||||
message.success('开始下载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('下载请求失败:', error);
|
||||
message.error('下载请求失败');
|
||||
}
|
||||
}, 300); // 300ms 防抖延迟
|
||||
|
||||
debounceTimers.set(image.id, timer);
|
||||
}, [downloadStatus, debounceTimers]);
|
||||
|
||||
// 停止下载
|
||||
const stopDownload = useCallback(async (downloadId: string) => {
|
||||
try {
|
||||
const result = await window.electronAPI.stopDownload(downloadId);
|
||||
if (!result.success) {
|
||||
message.error(`停止下载失败: ${result.error}`);
|
||||
} else {
|
||||
message.success('下载已停止');
|
||||
// 停止成功后清除该下载的状态
|
||||
setDownloadStatus(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.delete(downloadId);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止下载失败:', error);
|
||||
message.error('停止下载失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 渲染镜像项
|
||||
const renderImageItem = (image: ImageItem) => {
|
||||
const status = downloadStatus.get(image.id);
|
||||
const isDownloading = status && (status.state === 'started' || status.state === 'progress');
|
||||
const isCompleted = status && status.state === 'completed';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`image-item ${isCompleted ? 'completed' : ''}`}
|
||||
key={image.id}
|
||||
onClick={() => {
|
||||
if (isDownloading) {
|
||||
stopDownload(image.id);
|
||||
} else if (!isCompleted) {
|
||||
startDownload(image);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 添加状态图标 */}
|
||||
<div className="status-icon">
|
||||
{!isDownloading && (
|
||||
<div className="status-icon">
|
||||
{isCompleted ? (
|
||||
<CheckCircleOutlined className="status-icon-completed" />
|
||||
) : (
|
||||
<DownloadOutlined className="status-icon-download" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="image-icon">
|
||||
<img src={ImageFileIcon} />
|
||||
</div>
|
||||
<div className="image-name" title={image.name}>
|
||||
{image.name}
|
||||
</div>
|
||||
|
||||
{isDownloading && (
|
||||
<div className="download-overlay">
|
||||
<div className="download-info">
|
||||
<div className="download-text">下载中...{status?.progress.toFixed(1)}%</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${formatDecimalSimple(status?.progress)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="speed-info">{status?.downloadSpeedText}</div>
|
||||
<div className="eta-info">剩余:{status?.etaText}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* {isCompleted && (
|
||||
<div className="completed-overlay">
|
||||
<div className="completed-text">下载完成</div>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="images-list-container">
|
||||
<TitleBar />
|
||||
|
||||
<div className="shadow-divider" />
|
||||
|
||||
<div className="content-container">
|
||||
<div className="header">
|
||||
<h1>全部文件</h1>
|
||||
</div>
|
||||
|
||||
<div className="images-grid-con">
|
||||
<div className="images-grid">
|
||||
{images.map(renderImageItem)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImagesList;
|
||||
|
|
@ -23,8 +23,27 @@ const LoginPage: React.FC = () => {
|
|||
// 存储登录状态
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
localStorage.setItem('username', values.username);
|
||||
// 跳转到镜像列表页面
|
||||
history.push('/imagesList');
|
||||
|
||||
// 连接 gRPC 服务
|
||||
// TODO:如果设置了ip后面会跳过serverConfig页面,这localStorage还能获取ip吗?
|
||||
// const serverIp = localStorage.getItem('connected-server-ip');
|
||||
const serverIp = await window.electronAPI.getCurrentServerIp();
|
||||
console.log('登录后连接grpc 获取到的服务器IP:', serverIp);
|
||||
|
||||
if (serverIp) {
|
||||
const grpcResult = await window.electronAPI.connectToGrpc({
|
||||
serverIp,
|
||||
clientUser: values.username
|
||||
});
|
||||
|
||||
if (grpcResult.success) {
|
||||
// console.log('gRPC 连接成功');
|
||||
// 跳转到镜像列表页面
|
||||
history.push('/imagesList');
|
||||
} else {
|
||||
message.error(grpcResult.message || 'gRPC 连接失败');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error('用户名或密码错误!');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -19,13 +19,17 @@ export default defineConfig({
|
|||
path: '/login',
|
||||
component: '@/pages/login',
|
||||
},
|
||||
{
|
||||
path: '/vncClient',
|
||||
component: '@/pages/vncClient',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '@/pages/components/Layout/index',
|
||||
routes: [
|
||||
{
|
||||
path: '/images',
|
||||
component: '@/pages/images',
|
||||
component: '@/pages/imagePage',
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
|
|
@ -39,17 +43,26 @@ export default defineConfig({
|
|||
path: '/terminal',
|
||||
component: '@/pages/terminal',
|
||||
},
|
||||
{
|
||||
path: '/network',
|
||||
component: '@/pages/network',
|
||||
},
|
||||
{
|
||||
path: '/storage',
|
||||
component: '@/pages/storage',
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
],
|
||||
npmClient: 'pnpm',
|
||||
proxy: {
|
||||
'/api/nex/v1/': {
|
||||
target: 'http://10.100.51.85:8113',
|
||||
target: 'http://192.168.2.224:8113',
|
||||
// changeOrigin: true,
|
||||
},
|
||||
'/api/files': {
|
||||
target: 'http://10.100.51.85:8113',
|
||||
target: 'http://10.100.51.86:8113',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,108 @@
|
|||
export default {
|
||||
|
||||
'POST /api/nex/v1/desktopImages/query': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
for (let i = 1; i <= page_size; i++) {
|
||||
const id = (page_num - 1) * page_size + i;
|
||||
data.push({
|
||||
id: id,
|
||||
image_name: `桌面镜像${id}`,
|
||||
parent_image: Math.floor(Math.random() * 5) + 1,
|
||||
cpu: '4核',
|
||||
memory: '8GB',
|
||||
image_version: `v1.0.${Math.floor(Math.random() * 10)}`,
|
||||
os_version: id % 3 === 0 ? 'Windows 11' : id % 3 === 1 ? 'Windows 10' : 'Ubuntu 22.04',
|
||||
image_status: Math.random() > 0.2 ? 1 : 2,
|
||||
create_time: new Date(Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
description: `这是桌面镜像${id}的描述信息`,
|
||||
desktopType: id % 2 === 0 ? 'standard' : 'custom',
|
||||
version: `1.0.${Math.floor(Math.random() * 10)}`,
|
||||
size: `${Math.floor(Math.random() * 50) + 20}GB`,
|
||||
status: Math.random() > 0.1 ? 'active' : Math.random() > 0.5 ? 'inactive' : 'building',
|
||||
file_name: `desktop_image_${id}.qcow2`,
|
||||
file_type: id % 2 === 0 ? '中型虚拟机' : '小型虚拟机',
|
||||
file_size: `${Math.floor(Math.random() * 10) + 1}TB`,
|
||||
desc: `这是测试的桌面镜像${id}`,
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
code: '200',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
total: 50,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
res.send(result);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
'POST /api/nex/v1/virtualImages/query': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
for (let i = 1; i <= page_size; i++) {
|
||||
const id = (page_num - 1) * page_size + i;
|
||||
data.push({
|
||||
id: id,
|
||||
image_name: `工具${id}`,
|
||||
description: `这是工具${id}的描述信息`,
|
||||
image_system_id: '1',
|
||||
image_system_name: '系统镜像1',
|
||||
os_version: 'Windows 10',
|
||||
storage_path: '/path/to/image',
|
||||
network_module: 'Ethernet',
|
||||
image_status: 'active',
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
code: '200',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
total: 100,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
res.send(result);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
'POST /api/nex/v1/tool/select/page': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
for (let i = 1; i <= page_size; i++) {
|
||||
const id = (page_num - 1) * page_size + i;
|
||||
data.push({
|
||||
id: id,
|
||||
tool_name: `工具${id}`,
|
||||
tool_type: Math.random() > 0.5 ? 'system' : Math.random() > 0.5 ? 'virtual' : 'desktop',
|
||||
file_size: `${Math.floor(Math.random() * 1000) + 100}MB`,
|
||||
version: `1.0.${Math.floor(Math.random() * 10)}`,
|
||||
create_time: new Date().toISOString(),
|
||||
description: `这是工具${id}的描述信息`,
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
code: '200',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
total: 100,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
data: data,
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
res.send(result);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
'POST /api/nex/v1/queryimagesList': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
|
|
@ -90,4 +194,45 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 'POST /api/nex/v1/network/select/page': (req: any, res: any) => {
|
||||
// const { page_size, page_num } = req.body;
|
||||
// const data = [];
|
||||
// const networkTypes = ['NAT', 'Isolated', 'Bridge'];
|
||||
|
||||
// for (let i = 1; i <= page_size; i++) {
|
||||
// const id = (page_num - 1) * page_size + i;
|
||||
// const networkType = networkTypes[Math.floor(Math.random() * networkTypes.length)];
|
||||
|
||||
// data.push({
|
||||
// id: id,
|
||||
// network_name: `网络${id}`,
|
||||
// bridge_name: `br${id}`,
|
||||
// network_type: networkType,
|
||||
// ip_range: networkType !== 'Bridge' ? `192.168.${id}.0/24` : null,
|
||||
// gateway: networkType !== 'Bridge' ? `192.168.${id}.1` : null,
|
||||
// subnet_mask: networkType !== 'Bridge' ? '255.255.255.0' : null,
|
||||
// dhcp_start: networkType !== 'Bridge' ? `192.168.${id}.100` : null,
|
||||
// dhcp_end: networkType !== 'Bridge' ? `192.168.${id}.200` : null,
|
||||
// dhcp_enabled: networkType !== 'Bridge' ? Math.random() > 0.3 : null,
|
||||
// auto_start: Math.random() > 0.5,
|
||||
// create_time: new Date(Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
// description: `这是网络${id}的描述信息`,
|
||||
// });
|
||||
// }
|
||||
|
||||
// const result = {
|
||||
// code: '200',
|
||||
// message: '操作成功',
|
||||
// data: {
|
||||
// total: 50,
|
||||
// page_num: page_num,
|
||||
// page_size: page_size,
|
||||
// data: data,
|
||||
// },
|
||||
// };
|
||||
// setTimeout(() => {
|
||||
// res.send(result);
|
||||
// }, 300);
|
||||
// },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export default {
|
||||
'POST /api/v1/terminal/query/devicelist': (req: any, res: any) => {
|
||||
'POST /api/nex/v1/device/select/page': (req: any, res: any) => {
|
||||
const { page_size, page_num } = req.body;
|
||||
const data = [];
|
||||
|
||||
|
|
@ -18,16 +18,11 @@ export default {
|
|||
});
|
||||
}
|
||||
const result = {
|
||||
error_code: '0000000000',
|
||||
code: '200',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
paging: {
|
||||
total: 520,
|
||||
total_num: 520,
|
||||
page_num: page_num,
|
||||
page_size: page_size,
|
||||
},
|
||||
data: data,
|
||||
total: data.length,
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
|
|
@ -70,7 +65,10 @@ export default {
|
|||
error_code: '0000000000',
|
||||
message: '操作成功',
|
||||
data: {
|
||||
data: data,
|
||||
data: {
|
||||
data: data,
|
||||
total: data.length,
|
||||
},
|
||||
},
|
||||
};
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@
|
|||
"dependencies": {
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-components": "^2.4.4",
|
||||
"@novnc/novnc": "^1.6.0",
|
||||
"@umijs/max": "^4.4.11",
|
||||
"antd": "^5.4.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"moment": "^2.30.1",
|
||||
"novnc": "^1.2.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -54,5 +54,37 @@ export const PRIOPRITY_OPTIONS = [
|
|||
{ value: 3, label: '三级' },
|
||||
];
|
||||
|
||||
export const DHCP_STATUS = {
|
||||
1: '启用',
|
||||
0: '禁用',
|
||||
} as const;
|
||||
|
||||
export const NETWORK_STATUS = {
|
||||
1: '活跃',
|
||||
0: '非活跃',
|
||||
} as const;
|
||||
|
||||
|
||||
|
||||
export const NETWORK_TYPE = {
|
||||
nat: 'NAT(网络地址转换)',
|
||||
bridge: 'Bridge(桥接)',
|
||||
isolated: 'Isolated(隔离)',
|
||||
} as const;
|
||||
|
||||
export const NETWORK_TYPE_LIST = [
|
||||
{ value: "nat", label: 'NAT(网络地址转换)' },
|
||||
{ value: 'bridge', label: 'Bridge(桥接)' },
|
||||
{ value: 'isolated', label: 'Isolated(隔离)' },
|
||||
];
|
||||
|
||||
export const NETWORK_STATUS_LIST = [
|
||||
{ value: 1, label: '活跃' },
|
||||
{ value: 0, label: '非活跃' },
|
||||
];
|
||||
|
||||
export const DEFAULT_PASSWORD="a123456"
|
||||
export const DEFAULT_BLICK_TAB="黑名单"
|
||||
export const CPU_TOTAL=16
|
||||
export const CPU_CORE_TOTAL=8
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ const MainLayout: React.FC = () => {
|
|||
if (path.startsWith('/terminal')) return 'terminal';
|
||||
if (path.startsWith('/images')) return 'images';
|
||||
if (path.startsWith('/profile')) return 'profile';
|
||||
if (path.startsWith('/network')) return 'network';
|
||||
if (path.startsWith('/storage')) return 'storage';
|
||||
return 'images'; // 默认选中镜像列表
|
||||
};
|
||||
|
||||
|
|
@ -102,6 +104,12 @@ const MainLayout: React.FC = () => {
|
|||
</Menu.Item>
|
||||
<Menu.Item key="images" icon={<AppstoreOutlined />}>
|
||||
镜像列表
|
||||
</Menu.Item>
|
||||
<Menu.Item key="network" icon={<AppstoreOutlined />}>
|
||||
网络
|
||||
</Menu.Item>
|
||||
<Menu.Item key="storage" icon={<AppstoreOutlined />}>
|
||||
存储池
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
||||
我的
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import { Col, Form, Input, InputNumber, Modal, Row, Select } from 'antd';
|
||||
// import { InputNumber } from 'antd/lib';
|
||||
import React from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface EditModalProps {
|
||||
visible: boolean;
|
||||
detialData: any;
|
||||
onCancel: () => void;
|
||||
onOk: (values: any) => void;
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({
|
||||
detialData,
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleOk = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
onOk(values);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('表单验证失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑信息"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleOk}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
{/* 名称字段 */}
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{/* CPU 字段 */}
|
||||
<Form.Item label="CPU">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={['cpu', 'size']}
|
||||
rules={[{ required: true, message: '请输入CPU大小' }]}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
addonAfter={<span>颗</span>}
|
||||
style={{ width: '190px' }}
|
||||
placeholder="CPU大小"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={['cpu', 'cores']}
|
||||
rules={[{ required: true, message: '请输入CPU内核数' }]}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
addonAfter={<span>核</span>}
|
||||
style={{ width: '190px' }}
|
||||
placeholder="CPU内核数"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
|
||||
{/* 内存字段 */}
|
||||
<Form.Item
|
||||
name="memory_total"
|
||||
label="内存"
|
||||
rules={[{ required: true, message: '请输入内存' }]}
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
addonAfter={<span>CB</span>}
|
||||
style={{ width: '393.33px' }}
|
||||
placeholder="内存"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 存储卷字段 */}
|
||||
<Form.Item
|
||||
name="storageVolume"
|
||||
label="存储卷"
|
||||
rules={[{ required: true, message: '请输入存储卷信息' }]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
{/* 描述字段 */}
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type { TableProps } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useTableParams = (
|
||||
initialParams: DESK.DeskTableParams = {
|
||||
pagination: { current: 1, pageSize: 10 },
|
||||
filters: {},// 表格的搜索对象
|
||||
sort: {},
|
||||
search: {}, // 添加搜索参数对象
|
||||
},
|
||||
) => {
|
||||
const [tableParams, setTableParams] =
|
||||
useState<DESK.DeskTableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
page_num: pagination?.current,
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (sort?.field) {
|
||||
apiParams.orderby = sort.field;
|
||||
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理表格搜索参数
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索参数
|
||||
Object.entries(search || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('getApiParams apiParams', apiParams);
|
||||
return apiParams;
|
||||
}, [tableParams]);
|
||||
|
||||
// 统一的更新方法,可以处理所有参数类型
|
||||
const updateParams = useCallback(
|
||||
(
|
||||
newParams: Partial<DESK.DeskTableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
const shouldResetPage =
|
||||
options?.resetPage ??
|
||||
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
|
||||
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...newParams,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
...newParams.pagination,
|
||||
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理表格的分页、排序、过滤等变化
|
||||
* @param pagination 分页信息
|
||||
* @param filters 过滤条件 这里面直接将filters为空的条件过滤掉
|
||||
* @param sorter 排序信息
|
||||
* @param extra 其他信息
|
||||
* @returns void
|
||||
* */
|
||||
const handleTableChange = useCallback<
|
||||
NonNullable<TableProps<DESK.DeskItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter) => {
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (key === 'image_type') {
|
||||
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
|
||||
filteredFilters[key] = Number(value[0]);
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
filteredFilters[key] = value[0];
|
||||
} else if (value !== undefined && value !== null) {
|
||||
if (!Array.isArray(value) && value !== '') {
|
||||
filteredFilters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const newParams: Partial<DESK.DeskTableParams> = {
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters: filteredFilters,
|
||||
};
|
||||
|
||||
if (!Array.isArray(sorter)) {
|
||||
newParams.sort = {
|
||||
field: sorter.field as string,
|
||||
order:
|
||||
sorter.order === 'ascend' || sorter.order === 'descend'
|
||||
? sorter.order
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
return {
|
||||
tableParams,
|
||||
getApiParams,
|
||||
updateParams, // 统一的更新方法
|
||||
handleTableChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTableParams;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 页面头部样式
|
||||
.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 {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.search-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.images-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { CODE } from '@/constants/images.constants';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import EditModal from './editModal';
|
||||
import useTableParams from './hook/hook';
|
||||
import './index.less';
|
||||
import { getDesktopImagesList,deleteDesktopImages } from '@/services/imagePage';
|
||||
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
key: string;
|
||||
width: number;
|
||||
render?: any;
|
||||
fixed?: 'left' | 'right';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// 在组件顶部添加防抖函数(支持取消)
|
||||
|
||||
// 增强版防抖函数,使用泛型明确函数类型
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number,
|
||||
immediate = false,
|
||||
) => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const Index: React.FC<DESK.ImagesProps> = (props) => {
|
||||
const { activeTabKey } = props;
|
||||
const [dataSource, setDataSource] = useState<DESK.DeskItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<any>(null);
|
||||
const [visible, setDetailVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
|
||||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||||
useTableParams({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
search: {}, // 初始化搜索参数
|
||||
});
|
||||
|
||||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||||
const tableParamsRef = useRef(tableParams);
|
||||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||||
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
if (activeTabKey === '3') {
|
||||
loadImages();
|
||||
}
|
||||
}, [
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
activeTabKey,
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'file_name',
|
||||
title: '系统镜像',
|
||||
dataIndex: 'file_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'file_type',
|
||||
title: '虚拟机规格',
|
||||
dataIndex: 'file_type',
|
||||
width: 120,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'os_version',
|
||||
title: '操作系统',
|
||||
dataIndex: 'os_version',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'desc',
|
||||
// title: '模板存放路径',
|
||||
title: '描述',
|
||||
dataIndex: 'desc',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: IMAGES.ImageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="编辑"
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" type="link" title="删除">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadImages = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 将搜索文本合并到API参数中
|
||||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
|
||||
const imagesRes = await getDesktopImagesList(apiParams);
|
||||
if (imagesRes.code === CODE) {
|
||||
setDataSource(imagesRes.data?.data || []);
|
||||
setLoading(false);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: imagesRes.data?.page_num || 1,
|
||||
total: imagesRes.data?.total || 0,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(imagesRes.message || '获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (record: DESK.DeskItem) => {
|
||||
setSelectedImage(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record: DESK.DeskItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
deleteDesktopImages({ id: record.id }).then((res) => {
|
||||
if (res.code === CODE) {
|
||||
message.success('删除成功');
|
||||
loadImages();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{columnConfigs
|
||||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||||
.map((config) => (
|
||||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns[config.key]}
|
||||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||||
>
|
||||
{config.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = columnConfigs
|
||||
.map((config) => {
|
||||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.filter((column) => !column.hidden) as TableColumn[];
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadImages();
|
||||
};
|
||||
|
||||
// 自定义分页配置
|
||||
const paginationConfig = {
|
||||
...tableParams.pagination,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
image_name: searchValue,
|
||||
},
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
// 防抖版本(500ms延迟,不立即执行)
|
||||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||||
// 立即执行版本(用于清空时立即搜索)
|
||||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchText(value);
|
||||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
immediateSearch('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常输入时使用防抖
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 直接执行搜索
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<div>{/* */}</div>
|
||||
<div className="search-input">
|
||||
<Input.Search
|
||||
placeholder="请输入名称"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleEnterSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="images-list-container">
|
||||
<div className="images-list-table">
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{visible && (
|
||||
<EditModal
|
||||
visible={visible}
|
||||
detialData={selectedImage}
|
||||
onCancel={() => {
|
||||
setDetailVisible(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
setDetailVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
.imagePage {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
// background-color: #fafafa;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
// padding: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.emptyTip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 覆盖Ant Design Tabs组件的默认样式 */
|
||||
.ant-tabs {
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ant-tabs-nav {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.ant-tabs-content {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import ToolTable from '@/pages/imagePage/tool';
|
||||
import Images from '@/pages/images';
|
||||
import type { TabsProps } from 'antd';
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import DeskImage from './deskImage';
|
||||
import styles from './index.less';
|
||||
import VirtualImages from './virtualImages';
|
||||
|
||||
|
||||
const ImagePage = () => {
|
||||
// 设置默认选中的Tab为系统镜像(索引为0)
|
||||
const [activeTabKey, setActiveTabKey] = useState<string>('1');
|
||||
// 处理Tab切换事件
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTabKey(key);
|
||||
};
|
||||
|
||||
// Tabs的items配置
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: '系统镜像',
|
||||
children: (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 系统镜像内容区域 */}
|
||||
<div className={styles.emptyTip}>
|
||||
<Images activeTabKey={activeTabKey} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '虚拟机镜像',
|
||||
children: (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 虚拟机镜像内容区域 */}
|
||||
<div className={styles.emptyTip}>
|
||||
<VirtualImages activeTabKey={activeTabKey} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '桌面镜像',
|
||||
children: (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 桌面镜像内容区域 */}
|
||||
<div className={styles.emptyTip}>
|
||||
<DeskImage activeTabKey={activeTabKey} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: '工具',
|
||||
children: (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 桌面镜像内容区域 */}
|
||||
<div className={styles.emptyTip}>
|
||||
<ToolTable activeTabKey={activeTabKey} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.imagePage}>
|
||||
<div className={styles.content}>
|
||||
<Tabs
|
||||
activeKey={activeTabKey}
|
||||
onChange={handleTabChange}
|
||||
items={items}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImagePage;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type { TableProps } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useTableParams = (
|
||||
initialParams: TOOL.TableParams = {
|
||||
pagination: { current: 1, pageSize: 10 },
|
||||
filters: {},// 表格的搜索对象
|
||||
sort: {},
|
||||
search: {}, // 添加搜索参数对象
|
||||
},
|
||||
) => {
|
||||
const [tableParams, setTableParams] =
|
||||
useState<TOOL.TableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
page_num: pagination?.current,
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (sort?.field) {
|
||||
apiParams.orderby = sort.field;
|
||||
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理表格搜索参数
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索参数
|
||||
Object.entries(search || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('getApiParams apiParams', apiParams);
|
||||
return apiParams;
|
||||
}, [tableParams]);
|
||||
|
||||
// 统一的更新方法,可以处理所有参数类型
|
||||
const updateParams = useCallback(
|
||||
(
|
||||
newParams: Partial<TOOL.TableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
const shouldResetPage =
|
||||
options?.resetPage ??
|
||||
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
|
||||
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...newParams,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
...newParams.pagination,
|
||||
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理表格的分页、排序、过滤等变化
|
||||
* @param pagination 分页信息
|
||||
* @param filters 过滤条件 这里面直接将filters为空的条件过滤掉
|
||||
* @param sorter 排序信息
|
||||
* @param extra 其他信息
|
||||
* @returns void
|
||||
* */
|
||||
const handleTableChange = useCallback<
|
||||
NonNullable<TableProps<TOOL.ToolItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter) => {
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (key === 'image_type') {
|
||||
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
|
||||
filteredFilters[key] = Number(value[0]);
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
filteredFilters[key] = value[0];
|
||||
} else if (value !== undefined && value !== null) {
|
||||
if (!Array.isArray(value) && value !== '') {
|
||||
filteredFilters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const newParams: Partial<TOOL.TableParams> = {
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters: filteredFilters,
|
||||
};
|
||||
|
||||
if (!Array.isArray(sorter)) {
|
||||
newParams.sort = {
|
||||
field: sorter.field as string,
|
||||
order:
|
||||
sorter.order === 'ascend' || sorter.order === 'descend'
|
||||
? sorter.order
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
return {
|
||||
tableParams,
|
||||
getApiParams,
|
||||
updateParams, // 统一的更新方法
|
||||
handleTableChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTableParams;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 页面头部样式
|
||||
.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 {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.search-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.images-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,523 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { CODE } from '@/constants/images.constants';
|
||||
import { deleteTool, getToollList } from '@/services/imagePage';
|
||||
import { SettingOutlined,PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import useTableParams from './hook/hook';
|
||||
import './index.less';
|
||||
import UploadFileModal from './uploadFileModal';
|
||||
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
key: string;
|
||||
width: number;
|
||||
render?: any;
|
||||
fixed?: 'left' | 'right';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// 在组件顶部添加防抖函数(支持取消)
|
||||
|
||||
// 增强版防抖函数,使用泛型明确函数类型
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number,
|
||||
immediate = false,
|
||||
) => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const Index: React.FC<DESK.ImagesProps> = (props) => {
|
||||
const { activeTabKey } = props;
|
||||
const [dataSource, setDataSource] = useState<TOOL.ToolItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<any>(null);
|
||||
const [importModalVisible, setImportModalVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
|
||||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||||
useTableParams({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
search: {}, // 初始化搜索参数
|
||||
});
|
||||
|
||||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||||
const tableParamsRef = useRef(tableParams);
|
||||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||||
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
if (activeTabKey === '4') {
|
||||
loadDataSource();
|
||||
}
|
||||
}, [
|
||||
activeTabKey,
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'tool_name',
|
||||
// title: '镜像名称',
|
||||
title: '文件名',
|
||||
dataIndex: 'tool_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'tool_type',
|
||||
title: '文件类型',
|
||||
dataIndex: 'tool_type',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'file_size',
|
||||
// title: '镜像版本',
|
||||
title: '文件大小',
|
||||
dataIndex: 'file_size',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'version',
|
||||
// title: '模板存放路径',
|
||||
title: '版本',
|
||||
dataIndex: 'version',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'create_time',
|
||||
title: '上传时间',
|
||||
dataIndex: 'create_time',
|
||||
width: 160,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
|
||||
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: TOOL.ToolItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="编辑"
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" type="link" title="删除">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadDataSource = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 将搜索文本合并到API参数中
|
||||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
|
||||
const imagesRes = await getToollList(apiParams);
|
||||
if (imagesRes.code === CODE) {
|
||||
setDataSource(imagesRes.data?.data || []);
|
||||
setLoading(false);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: imagesRes.data?.page_num || 1,
|
||||
total: imagesRes.data?.total || 0,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(imagesRes.message || '获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (record: TOOL.ToolItem) => {
|
||||
setSelectedImage(record);
|
||||
setImportModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record: TOOL.ToolItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
deleteTool({ id: record.id }).then((res) => {
|
||||
if (res.code === CODE) {
|
||||
message.success('删除成功');
|
||||
loadDataSource();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{columnConfigs
|
||||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||||
.map((config) => (
|
||||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns[config.key]}
|
||||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||||
>
|
||||
{config.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = columnConfigs
|
||||
.map((config) => {
|
||||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.filter((column) => !column.hidden) as TableColumn[];
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
// 自定义分页配置
|
||||
const paginationConfig = {
|
||||
...tableParams.pagination,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
image_name: searchValue,
|
||||
},
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
// 防抖版本(500ms延迟,不立即执行)
|
||||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||||
// 立即执行版本(用于清空时立即搜索)
|
||||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchText(value);
|
||||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
immediateSearch('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常输入时使用防抖
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 直接执行搜索
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
const onSubmitBack = () => {
|
||||
setImportModalVisible(false);
|
||||
setSelectedImage(null);
|
||||
handleRefresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}>上传</Button>
|
||||
<div className="search-input">
|
||||
<Input.Search
|
||||
placeholder="请输入名称"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleEnterSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="images-list-container">
|
||||
<div className="images-list-table">
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{importModalVisible && (
|
||||
<UploadFileModal
|
||||
visible={importModalVisible}
|
||||
onCancel={() => {
|
||||
setImportModalVisible(false);
|
||||
setSelectedImage(null);
|
||||
}}
|
||||
onSubmit={onSubmitBack}
|
||||
isEditing={selectedImage ? true : false}
|
||||
initialValues={selectedImage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/* 上传区域样式 */
|
||||
.ant-upload-drag-icon {
|
||||
color: #1890ff;
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 上传文本样式 */
|
||||
.ant-upload-text {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 提示文本样式 */
|
||||
.ant-upload-hint {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 表单标签样式 */
|
||||
.ant-form-item-label > label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 操作按钮区域样式 */
|
||||
.ant-form-item-control-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 按钮间距样式 */
|
||||
.ant-space-item {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 上传区域悬停效果 */
|
||||
.ant-upload-wrapper.ant-upload-drag {
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 40px 0;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.ant-upload-wrapper.ant-upload-drag:hover {
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 上传状态样式 */
|
||||
.ant-upload-list-item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 错误提示样式 */
|
||||
.ant-form-item-explain-error {
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 描述输入框样式 */
|
||||
.ant-input-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* 上传进度容器样式 */
|
||||
.upload-progress-container {
|
||||
margin-top: 16px;
|
||||
background-color: #fafafa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.upload-progress-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.upload-actions .ant-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.ant-progress {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 禁用状态下的上传区域 */
|
||||
.ant-upload-disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 多文件上传列表样式 */
|
||||
.ant-upload-list {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 上传文件项样式 */
|
||||
.ant-upload-list-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-form-item-label,
|
||||
.ant-form-item-control-wrapper {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.upload-progress-container {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
import { ERROR_CODE } from '@/constants/constants';
|
||||
import { addTool, updateTool } from '@/services/imagePage';
|
||||
import {
|
||||
CloseOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Progress,
|
||||
Select,
|
||||
Space,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './index.less';
|
||||
|
||||
const { Dragger } = Upload;
|
||||
|
||||
type UploadFile = any; // 简化类型定义,实际项目中应使用正确的类型
|
||||
|
||||
export interface UploadFileModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: any) => void;
|
||||
initialValues?: any; // 编辑时的初始值
|
||||
isEditing?: boolean; // 是否处于编辑状态
|
||||
}
|
||||
|
||||
// 文件类型选项
|
||||
const FILE_TYPE_OPTIONS = [
|
||||
{ value: 'system', label: '系统文件' },
|
||||
{ value: 'virtual', label: '虚拟机文件' },
|
||||
{ value: 'desktop', label: '桌面文件' },
|
||||
];
|
||||
|
||||
// 分片大小 (5MB)
|
||||
const CHUNK_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
const UploadFileModal: React.FC<UploadFileModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialValues = {},
|
||||
isEditing = false,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<Record<string, number>>(
|
||||
{},
|
||||
);
|
||||
const [uploadCancelToken, setUploadCancelToken] =
|
||||
useState<AbortController | null>(null);
|
||||
|
||||
// 当visible变化时,重置表单
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// 设置初始值
|
||||
const { description } = initialValues || {};
|
||||
if (initialValues) {
|
||||
form.setFieldsValue({ description: description });
|
||||
}
|
||||
} else {
|
||||
// 关闭时重置状态
|
||||
form.resetFields();
|
||||
setFileList([]);
|
||||
setIsUploading(false);
|
||||
setUploadProgress({});
|
||||
}
|
||||
}, [visible, initialValues, isEditing, form]);
|
||||
|
||||
// 生成文件ID
|
||||
const generateFileId = () => {
|
||||
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
};
|
||||
|
||||
// 计算文件MD5 (简化版)
|
||||
const calculateFileMD5 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
// 实际项目中应使用 crypto 库计算真实的MD5
|
||||
// 这里使用简化版,仅用于演示
|
||||
const fileId = generateFileId();
|
||||
resolve(fileId);
|
||||
});
|
||||
};
|
||||
|
||||
// 分片上传函数
|
||||
const uploadChunk = async (
|
||||
file: File,
|
||||
fileId: string,
|
||||
chunkIndex: number,
|
||||
totalChunks: number,
|
||||
chunkSize: number,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const start = chunkIndex * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
const chunkMD5 = await calculateFileMD5(chunk);
|
||||
console.log('chunkMD5====', chunkMD5);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file_id', fileId);
|
||||
formData.append('file_name', file.name);
|
||||
formData.append('file_size', file.size.toString());
|
||||
formData.append('shard_index', chunkIndex.toString());
|
||||
formData.append('shard_total', totalChunks.toString());
|
||||
formData.append('chunk_size', chunkSize.toString());
|
||||
formData.append('chunk_md5', chunkMD5);
|
||||
formData.append('chunk', chunk);
|
||||
|
||||
// 创建新的取消控制器
|
||||
const controller = new AbortController();
|
||||
setUploadCancelToken(controller);
|
||||
|
||||
const response = await fetch('/api/v1/images/file/chunk/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: controller.signal,
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
// 更新进度
|
||||
const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100);
|
||||
setUploadProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: progress,
|
||||
}));
|
||||
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('分片上传失败:', error);
|
||||
message.error(`文件 ${file.name} 分片上传失败`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理单个文件的分片上传
|
||||
const handleFileChunkUpload = async (file: File): Promise<string | null> => {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
const fileId = generateFileId();
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
|
||||
setUploadProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: 0,
|
||||
}));
|
||||
|
||||
// 按顺序上传分片
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
// 检查是否已取消上传
|
||||
if (!uploadCancelToken) {
|
||||
message.info(`文件 ${file.name} 上传已取消`);
|
||||
setIsUploading(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
const success = await uploadChunk(
|
||||
file,
|
||||
fileId,
|
||||
i,
|
||||
totalChunks,
|
||||
CHUNK_SIZE,
|
||||
);
|
||||
if (!success) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有分片上传完成后,模拟文件URL
|
||||
const fileUrl = `/uploads/${fileId}/${file.name}`;
|
||||
return fileUrl;
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error);
|
||||
message.error(`文件 ${file.name} 上传失败`);
|
||||
return null;
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理文件上传变化
|
||||
const handleUploadChange = ({
|
||||
fileList: newFileList,
|
||||
file,
|
||||
}: {
|
||||
fileList: UploadFile[];
|
||||
file?: UploadFile;
|
||||
}) => {
|
||||
setFileList(newFileList);
|
||||
|
||||
// 上传前处理
|
||||
if (file && file.status === 'uploading') {
|
||||
setIsUploading(true);
|
||||
setUploadProgress({});
|
||||
}
|
||||
|
||||
// 上传完成处理
|
||||
if (file && file.status === 'done') {
|
||||
handleFileChunkUpload(file.originFileObj);
|
||||
}
|
||||
|
||||
// 上传错误处理
|
||||
if (file && file.status === 'error') {
|
||||
setIsUploading(false);
|
||||
message.error('文件上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 取消上传
|
||||
const handleCancelUpload = () => {
|
||||
if (uploadCancelToken) {
|
||||
uploadCancelToken.abort();
|
||||
setUploadCancelToken(null);
|
||||
}
|
||||
setIsUploading(false);
|
||||
setUploadProgress({});
|
||||
message.info('上传已取消');
|
||||
};
|
||||
|
||||
// 重新上传失败的文件
|
||||
const handleReUpload = (file: UploadFile) => {
|
||||
if (file.originFileObj) {
|
||||
handleFileChunkUpload(file.originFileObj);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除已上传的文件
|
||||
const handleDeleteFile = () => {
|
||||
setFileList([]);
|
||||
setUploadProgress({});
|
||||
message.success('文件已删除');
|
||||
};
|
||||
|
||||
// 新建
|
||||
const handleCreate = (payload: any) => {
|
||||
addTool(payload).then((res: any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('保存成功');
|
||||
onSubmit(payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdata = (payload: any) => {
|
||||
updateTool(payload).then((res: any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('保存成功');
|
||||
onSubmit(payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
const values = await form.validateFields();
|
||||
console.log('form表单字段=======', values);
|
||||
if (isEditing) {
|
||||
handleUpdata({ ...values, id: initialValues?.id });
|
||||
} else {
|
||||
// 上传文件
|
||||
// 处理文件
|
||||
if (!isEditing) {
|
||||
if (fileList.length === 0 || fileList[0].status !== 'done') {
|
||||
message.error('请先上传文件');
|
||||
return;
|
||||
}
|
||||
values.fileUrl = `/uploads/${generateFileId()}/${fileList[0].name}`;
|
||||
values.fileName = fileList[0].name;
|
||||
handleCreate(values);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 上传前处理
|
||||
const beforeUpload = (file: File) => {
|
||||
// 允许多文件上传
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEditing ? '编辑' : '上传工具'}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 4 }}
|
||||
wrapperCol={{ span: 20 }}
|
||||
initialValues={initialValues}
|
||||
>
|
||||
{/* 文件类型 - 编辑时隐藏 */}
|
||||
{!isEditing && (
|
||||
<Form.Item
|
||||
name="fileType"
|
||||
label="文件类型"
|
||||
rules={[{ required: true, message: '请选择文件类型' }]}
|
||||
>
|
||||
<Select options={FILE_TYPE_OPTIONS} placeholder="请选择文件类型" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 文件上传 - 编辑时隐藏 */}
|
||||
{!isEditing && (
|
||||
<Form.Item
|
||||
name="file"
|
||||
label="文件"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={({ fileList }) => fileList}
|
||||
rules={[{ required: true, message: '请上传文件' }]}
|
||||
>
|
||||
<>
|
||||
<Dragger
|
||||
accept=".zip,.rar,.7z,.tar,.gz"
|
||||
fileList={fileList}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleUploadChange}
|
||||
multiple={false}
|
||||
disabled={isUploading}
|
||||
customRequest={({ file, onSuccess }) => {
|
||||
// 自定义上传处理
|
||||
setTimeout(() => {
|
||||
if (onSuccess) {
|
||||
onSuccess(undefined, file);
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
点击或拖拽压缩包文件到此处上传
|
||||
</p>
|
||||
<p className="ant-upload-hint">
|
||||
仅支持 .zip, .rar, .7z, .tar, .gz 格式的压缩包
|
||||
</p>
|
||||
</Dragger>
|
||||
{/* 显示上传进度和操作按钮 */}{' '}
|
||||
{Object.keys(uploadProgress).length > 0 &&
|
||||
fileList.length > 0 && (
|
||||
<div className="upload-progress-container">
|
||||
{Object.entries(uploadProgress).map(
|
||||
([fileId, progress]) => (
|
||||
<div key={fileId} className="upload-progress-item">
|
||||
<div className="progress-info">
|
||||
<span className="file-name">
|
||||
{fileList[0].name}
|
||||
</span>
|
||||
{isUploading && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={handleCancelUpload}
|
||||
className="cancel-button"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Progress percent={progress} status="active" />
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 上传完成后的删除按钮和上传失败后的重新上传按钮 */}
|
||||
{fileList.length > 0 && (
|
||||
<div className="upload-actions">
|
||||
{fileList[0].status === 'done' && (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDeleteFile}
|
||||
>
|
||||
删除文件
|
||||
</Button>
|
||||
)}
|
||||
{fileList[0].status === 'error' && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => handleReUpload(fileList[0])}
|
||||
>
|
||||
重新上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="描述"
|
||||
rules={[{ required: false }]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请输入描述信息" />
|
||||
</Form.Item>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Form.Item wrapperCol={{ offset: 19, span: 5 }}>
|
||||
<Space size="middle">
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={isUploading}>
|
||||
{isEditing ? '保存' : '提交'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFileModal;
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { ERROR_CODE } from '@/constants/constants';
|
||||
import {
|
||||
addVirtualImage,
|
||||
getVirtualImageDetail,
|
||||
updateVirtualImage,
|
||||
} from '@/services/imagePage/index';
|
||||
import {
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
message,
|
||||
} from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface EditModalProps {
|
||||
visible: boolean;
|
||||
detialData: any;
|
||||
storagePathList: any[];
|
||||
networkList: any[];
|
||||
systemList: any[];
|
||||
onCancel: () => void;
|
||||
onOk: (values: any) => void;
|
||||
}
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({
|
||||
visible,
|
||||
detialData,
|
||||
storagePathList,
|
||||
networkList,
|
||||
systemList,
|
||||
onCancel,
|
||||
onOk,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (detialData?.id) {
|
||||
const params = { id: detialData.id };
|
||||
getVirtualImageDetail(params).then((res: any) => {
|
||||
console.log('res=======', res);
|
||||
const { code, data } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
const { cpu_total, cpu_core_total } = data;
|
||||
const initialValues = {
|
||||
...data,
|
||||
cpu: { size: cpu_total, core: cpu_core_total },
|
||||
};
|
||||
form.setFieldsValue(initialValues);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [visible, form, detialData]);
|
||||
|
||||
const onAddVirtualImage = (payload: any) => {
|
||||
addVirtualImage(payload).then((res: any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('保存成功');
|
||||
onOk(payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onEditVirtualImage = (payload: any) => {
|
||||
updateVirtualImage(payload).then((res: any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('保存成功');
|
||||
onOk(payload);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
console.log('表单字段值:values', values);
|
||||
const { cpu } = values || {};
|
||||
const { size, core } = cpu || {};
|
||||
const obj: any = { ...values };
|
||||
delete obj.cpu;
|
||||
const payload = {
|
||||
cpu_total: size,
|
||||
cpu_core_total: core,
|
||||
...obj,
|
||||
};
|
||||
if (detialData.id) {
|
||||
onEditVirtualImage({ id: detialData.id, ...payload });
|
||||
} else {
|
||||
onAddVirtualImage(payload);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('表单验证失败:', error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="编辑信息"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={handleOk}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
{/* 名称字段 */}
|
||||
<Form.Item
|
||||
name="image_name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="image_system_id"
|
||||
label="系统镜像"
|
||||
rules={[{ required: true, message: '请选择系统镜像' }]}
|
||||
>
|
||||
<Select placeholder="请选择系统镜像">
|
||||
{systemList.map((item: any) => {
|
||||
const { name, value } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* CPU 字段 */}
|
||||
<Form.Item
|
||||
label="CPU"
|
||||
rules={[{ required: true, message: '请输入CPU数量' }]}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={['cpu', 'size']}
|
||||
rules={[{ required: true, message: '请输入CPU数量' }]}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
style={{ width: '190px' }}
|
||||
placeholder="CPU数量"
|
||||
addonAfter={<span>颗</span>}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name={['cpu', 'cores']}
|
||||
rules={[{ required: true, message: '请输入CPU内核数' }]}
|
||||
noStyle
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
style={{ width: '190px' }}
|
||||
placeholder="CPU内核数"
|
||||
addonAfter={<span>核</span>}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
|
||||
{/* 内存字段 */}
|
||||
<Form.Item
|
||||
name="memory_total"
|
||||
label="内存"
|
||||
rules={[{ required: true, message: '请输入内存' }]}
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
addonAfter={<span>CB</span>}
|
||||
style={{ width: '393.33px' }}
|
||||
placeholder="内存"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="system_total"
|
||||
label="系统盘"
|
||||
rules={[{ required: true, message: '请输入系统盘内存' }]}
|
||||
>
|
||||
<InputNumber
|
||||
precision={0}
|
||||
addonAfter={<span>CB</span>}
|
||||
style={{ width: '393.33px' }}
|
||||
placeholder="系统盘内存"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 存储卷字段 */}
|
||||
<Form.Item
|
||||
name="storage_path"
|
||||
label="存储卷"
|
||||
rules={[{ required: true, message: '请选择存储卷' }]}
|
||||
>
|
||||
<Select placeholder="请选择存储卷">
|
||||
{storagePathList.map((item: any) => {
|
||||
const { path } = item || {};
|
||||
return (
|
||||
<Option key={path} value={path}>
|
||||
{path}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="network_module"
|
||||
label="网络模板"
|
||||
rules={[{ required: true, message: '请选择网络模板' }]}
|
||||
>
|
||||
<Select placeholder="请选择网络模板">
|
||||
{networkList.map((item: any) => {
|
||||
const { name, value } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 描述字段 */}
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type { TableProps } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useTableParams = (
|
||||
initialParams: DESK.VirtualTableParams = {
|
||||
pagination: { current: 1, pageSize: 10 },
|
||||
filters: {},// 表格的搜索对象
|
||||
sort: {},
|
||||
search: {}, // 添加搜索参数对象
|
||||
},
|
||||
) => {
|
||||
const [tableParams, setTableParams] =
|
||||
useState<DESK.VirtualTableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
page_num: pagination?.current,
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (sort?.field) {
|
||||
apiParams.orderby = sort.field;
|
||||
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理表格搜索参数
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索参数
|
||||
Object.entries(search || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('getApiParams apiParams', apiParams);
|
||||
return apiParams;
|
||||
}, [tableParams]);
|
||||
|
||||
// 统一的更新方法,可以处理所有参数类型
|
||||
const updateParams = useCallback(
|
||||
(
|
||||
newParams: Partial<DESK.VirtualTableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
const shouldResetPage =
|
||||
options?.resetPage ??
|
||||
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
|
||||
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...newParams,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
...newParams.pagination,
|
||||
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理表格的分页、排序、过滤等变化
|
||||
* @param pagination 分页信息
|
||||
* @param filters 过滤条件 这里面直接将filters为空的条件过滤掉
|
||||
* @param sorter 排序信息
|
||||
* @param extra 其他信息
|
||||
* @returns void
|
||||
* */
|
||||
const handleTableChange = useCallback<
|
||||
NonNullable<TableProps<DESK.VirtualItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter) => {
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
// if (key === 'image_type') {
|
||||
// if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
|
||||
// filteredFilters[key] = Number(value[0]);
|
||||
// }
|
||||
// } else {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
filteredFilters[key] = value[0];
|
||||
} else if (value !== undefined && value !== null) {
|
||||
if (!Array.isArray(value) && value !== '') {
|
||||
filteredFilters[key] = value;
|
||||
}
|
||||
}
|
||||
// }
|
||||
});
|
||||
const newParams: Partial<DESK.VirtualTableParams> = {
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters: filteredFilters,
|
||||
};
|
||||
|
||||
if (!Array.isArray(sorter)) {
|
||||
newParams.sort = {
|
||||
field: sorter.field as string,
|
||||
order:
|
||||
sorter.order === 'ascend' || sorter.order === 'descend'
|
||||
? sorter.order
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
return {
|
||||
tableParams,
|
||||
getApiParams,
|
||||
updateParams, // 统一的更新方法
|
||||
handleTableChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTableParams;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 页面头部样式
|
||||
.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 {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.search-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.images-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,691 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable eqeqeq */
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
import { ERROR_CODE } from '@/constants/constants';
|
||||
import { CODE } from '@/constants/images.constants';
|
||||
import {
|
||||
cloneVirtualImage,
|
||||
deleteVirtualImage,
|
||||
getNetworkList,
|
||||
getStorageList,
|
||||
getVirtualImagesList,
|
||||
operateVirtualImage,
|
||||
} from '@/services/imagePage/index';
|
||||
import { getImagesList } from '@/services/images';
|
||||
import { DownOutlined, SettingOutlined,PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import CreatModal from './creatModal';
|
||||
import useTableParams from './hook/hook';
|
||||
import './index.less';
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'right' | 'center';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
key: string;
|
||||
width: number;
|
||||
render?: any;
|
||||
fixed?: 'left' | 'right';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// 在组件顶部添加防抖函数(支持取消)
|
||||
// 增强版防抖函数
|
||||
// 增强版防抖函数,使用泛型明确函数类型
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number,
|
||||
immediate = false,
|
||||
) => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const Index: React.FC<DESK.ImagesProps> = (props) => {
|
||||
const { activeTabKey } = props;
|
||||
const [dataSource, setDataSource] = useState<DESK.VirtualItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
const [visible, setVisible] = useState(false); // 新增状态管理
|
||||
const [detialData, setDetialData] = useState<DESK.VirtualItem | null>(null); // 新增状态管理
|
||||
const [storagePathList, setStoragePathList] = useState<any>([
|
||||
{ path: 'cvar/lib/libvirt/images' },
|
||||
]);
|
||||
const [networkList, setNetworkList] = useState<any>([
|
||||
{ name: 'default', value: 'default' },
|
||||
{ name: '桥接', value: 'qiaojie' },
|
||||
]);
|
||||
|
||||
const [systemList, setSystemList] = useState<any>([
|
||||
{ name: 'CentOS 7', value: 'CentOS 7' },
|
||||
{ name: 'CentOS 8', value: 'CentOS 8' },
|
||||
]);
|
||||
|
||||
// 存储清理函数的引用,用于在组件卸载时执行清理
|
||||
const cleanupFunctions = useRef<Array<() => void>>([]);
|
||||
|
||||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||||
useTableParams({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
search: {}, // 初始化搜索参数
|
||||
});
|
||||
|
||||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||||
const tableParamsRef = useRef(tableParams);
|
||||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 组件卸载时执行所有清理函数
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupFunctions.current.forEach((cleanup) => cleanup());
|
||||
cleanupFunctions.current = [];
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onGetNetworkList();
|
||||
onGetStorageList();
|
||||
onGetImagesList();
|
||||
}, []);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
if (activeTabKey === '2') {
|
||||
loadDataSource();
|
||||
}
|
||||
}, [
|
||||
activeTabKey,
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'image_name',
|
||||
title: '名称',
|
||||
dataIndex: 'image_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'os_version',
|
||||
title: '操作系统',
|
||||
dataIndex: 'os_version',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'image_status',
|
||||
title: '状态',
|
||||
dataIndex: 'image_status',
|
||||
width: 120,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'image_system_name',
|
||||
title: '系统镜像',
|
||||
dataIndex: 'image_system_name',
|
||||
width: 120,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: DESK.VirtualItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="控制台"
|
||||
onClick={() => handleOpenVnc(record)}
|
||||
>
|
||||
控制台
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleClone(record);
|
||||
}}
|
||||
size="small"
|
||||
type="link"
|
||||
title="克隆为模板"
|
||||
>
|
||||
克隆为模板
|
||||
</Button>
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
content={
|
||||
<div>
|
||||
<div>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
handleOperate(record, 'start');
|
||||
}}
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
handleOperate(record, 'close');
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
handleOperate(record, 'restart');
|
||||
}}
|
||||
>
|
||||
重启
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
handelOpenModal(record);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" title="删除">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<a onClick={(e) => e.preventDefault()}>
|
||||
更多
|
||||
<DownOutlined style={{ fontSize: '0.7rem' }} />
|
||||
</a>
|
||||
</Popover>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadDataSource = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 将搜索文本合并到API参数中
|
||||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
|
||||
const result = await getVirtualImagesList(apiParams);
|
||||
if (result.code == CODE) {
|
||||
setDataSource(result.data?.data || []);
|
||||
setLoading(false);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: result.data?.page_num || 1,
|
||||
total: result.data?.total || 0,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(result.message || '获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取镜像列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
//虚拟镜像克隆为桌面镜像
|
||||
const handleClone = (record: DESK.VirtualItem) => {
|
||||
cloneVirtualImage({ id: record.id }).then((res) => {
|
||||
if (res.code == CODE) {
|
||||
message.success('克隆成功');
|
||||
loadDataSource();
|
||||
} else {
|
||||
message.error(res.message || '克隆失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onGetImagesList = () => {
|
||||
const payload = {
|
||||
page_num: 1,
|
||||
page_size: 5000,
|
||||
};
|
||||
getImagesList(payload).then((res) => {
|
||||
const { code, data } = res || {};
|
||||
const { data: list } = data || {};
|
||||
if (code === ERROR_CODE) {
|
||||
setSystemList(list);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onGetStorageList = () => {
|
||||
const params = { id: detialData?.id };
|
||||
getStorageList(params).then((res) => {
|
||||
const { code, data } = res || {};
|
||||
const { data: list } = data || {};
|
||||
if (code === ERROR_CODE) {
|
||||
setStoragePathList(list);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onGetNetworkList = () => {
|
||||
const params = { id: detialData?.id };
|
||||
getNetworkList(params).then((res) => {
|
||||
const { code, data } = res || {};
|
||||
const { data: list } = data || {};
|
||||
if (code === ERROR_CODE) {
|
||||
setNetworkList(list);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: DESK.VirtualItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
deleteVirtualImage({ id: record.id }).then((res) => {
|
||||
if (res.code == CODE) {
|
||||
message.success('删除成功');
|
||||
loadDataSource();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 处理打开VNC客户端 - 在新窗口中打开
|
||||
const handleOpenVnc = (record: DESK.VirtualItem) => {
|
||||
try {
|
||||
// 构建远程桌面页面的URL
|
||||
const vncUrl = 'ws://10.100.51.118:8000/api/v1/ws/vnc/win10u';
|
||||
const remotePageUrl = `/vncClient?imageId=${
|
||||
record.id
|
||||
}&imageName=${encodeURIComponent(
|
||||
'NEXSPACE远程桌面',
|
||||
)}&vncUrl=${encodeURIComponent(vncUrl)}`;
|
||||
|
||||
// 在新窗口中打开远程桌面
|
||||
const vncWindow = window.open(
|
||||
remotePageUrl,
|
||||
'_blank',
|
||||
'width=1200,height=800,menubar=no,toolbar=no,location=no,status=no',
|
||||
);
|
||||
|
||||
// 检查窗口是否成功打开
|
||||
if (!vncWindow) {
|
||||
message.error('无法打开新窗口,可能被浏览器阻止');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('远程桌面连接失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{columnConfigs
|
||||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||||
.map((config) => (
|
||||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns[config.key]}
|
||||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||||
>
|
||||
{config.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = columnConfigs
|
||||
.map((config) => {
|
||||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.filter((column) => !column.hidden) as TableColumn[];
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
// 自定义分页配置
|
||||
const paginationConfig = {
|
||||
...tableParams.pagination,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
image_name: searchValue,
|
||||
},
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
// 防抖版本(500ms延迟,不立即执行)
|
||||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||||
// 立即执行版本(用于清空时立即搜索)
|
||||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchText(value);
|
||||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
immediateSearch('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常输入时使用防抖
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 直接执行搜索
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
// 新增弹窗
|
||||
const handelOpenModal = (record?: DESK.VirtualItem) => {
|
||||
setDetialData(record || null);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 编辑、新建保存成功回调
|
||||
const onOk = () => {
|
||||
setDetialData(null);
|
||||
setVisible(false);
|
||||
handleRefresh();
|
||||
};
|
||||
|
||||
// 虚拟镜像关闭、开启、重启
|
||||
const handleOperate = (record: DESK.VirtualItem, type: string) => {
|
||||
operateVirtualImage({
|
||||
id: record.id,
|
||||
operate_type: type,
|
||||
}).then((res: any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('操作成功');
|
||||
handleRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handelOpenModal()}>新建</Button>
|
||||
</div>
|
||||
<div className="search-input">
|
||||
<Input.Search
|
||||
placeholder="请输入名称"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleEnterSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="images-list-container">
|
||||
<div className="images-list-table">
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{visible && (
|
||||
<CreatModal
|
||||
onCancel={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
onOk={onOk}
|
||||
detialData={detialData}
|
||||
visible={visible}
|
||||
storagePathList={storagePathList}
|
||||
networkList={networkList}
|
||||
systemList={systemList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
@ -15,10 +15,11 @@
|
|||
|
||||
// 镜像列表样式
|
||||
.image-list {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
// padding: 16px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
import {
|
||||
CODE,
|
||||
IMAGES_TYPE_MAP,
|
||||
STATUS_MAP,
|
||||
} from '@/constants/images.constants';
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { CODE, STATUS_MAP } from '@/constants/images.constants';
|
||||
import { delImagesAPI, getImagesList } from '@/services/images';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
SettingOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
|
|
@ -32,6 +29,10 @@ import './index.less';
|
|||
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
||||
// interface ImagesProps {
|
||||
// activeTabKey?: string;
|
||||
// }
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
|
|
@ -40,6 +41,7 @@ type ColumnConfig = {
|
|||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
|
|
@ -66,11 +68,11 @@ const debounce = (func: Function, delay: number, immediate = false) => {
|
|||
let timer: NodeJS.Timeout | null = null;
|
||||
const debounced = (...args: any[]) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
|
|
@ -78,18 +80,19 @@ const debounce = (func: Function, delay: number, immediate = false) => {
|
|||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const ImageList: React.FC = () => {
|
||||
const ImageList: React.FC<DESK.ImagesProps> = (props) => {
|
||||
const { activeTabKey } = props;
|
||||
const [images, setImages] = useState<IMAGES.ImageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<IMAGES.ImageItem | null>(
|
||||
|
|
@ -115,219 +118,6 @@ const ImageList: React.FC = () => {
|
|||
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
loadImages();
|
||||
}, [
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'image_name',
|
||||
title: '镜像名称',
|
||||
dataIndex: 'image_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'image_file_name',
|
||||
title: '镜像文件',
|
||||
dataIndex: 'image_file_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: 'image_type',
|
||||
// title: '桌面类型',
|
||||
// dataIndex: 'image_type',
|
||||
// width: 120,
|
||||
// render: (text: number) => {
|
||||
// const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||
// return text ? IMAGES_TYPE_MAP[key] : '--';
|
||||
// },
|
||||
// defaultVisible: true,
|
||||
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||
// <Menu
|
||||
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
|
||||
// onClick={({ key }) => {
|
||||
// setSelectedKeys(key === '全部' ? [] : [key]);
|
||||
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
|
||||
// }}
|
||||
// items={[
|
||||
// { key: '全部', label: '全部' },
|
||||
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
|
||||
// key,
|
||||
// label: value,
|
||||
// })),
|
||||
// ]}
|
||||
// />
|
||||
// ),
|
||||
// filterMultiple: false,
|
||||
// defaultFilteredValue: ['全部'],
|
||||
// },
|
||||
{
|
||||
key: 'storage_path',
|
||||
title: '模板存放路径',
|
||||
dataIndex: 'storage_path',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bt_path',
|
||||
title: 'BT路径',
|
||||
dataIndex: 'bt_path',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'image_version',
|
||||
title: '镜像版本',
|
||||
dataIndex: 'image_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'os_version',
|
||||
title: '操作系统',
|
||||
dataIndex: 'os_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'image_status',
|
||||
title: '镜像状态',
|
||||
dataIndex: 'image_status',
|
||||
width: 90,
|
||||
render: (text: number) => (text ? getStatusTag(text) : '--'),
|
||||
defaultVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'create_time',
|
||||
title: '上传时间',
|
||||
dataIndex: 'create_time',
|
||||
width: 160,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
|
||||
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 90,
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: IMAGES.ImageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
title="查看详情"
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" icon={<DeleteOutlined />} title="删除" danger />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadImages = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -359,6 +149,243 @@ const ImageList: React.FC = () => {
|
|||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
if (activeTabKey === '1') {
|
||||
loadImages();
|
||||
}
|
||||
}, [
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
activeTabKey,
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'image_name',
|
||||
// title: '镜像名称',
|
||||
title: '名称',
|
||||
dataIndex: 'image_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'os_version',
|
||||
title: '操作系统',
|
||||
dataIndex: 'os_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'image_version',
|
||||
// title: '镜像版本',
|
||||
title: '版本',
|
||||
dataIndex: 'image_version',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'storage_path',
|
||||
// title: '模板存放路径',
|
||||
title: '存储位置',
|
||||
dataIndex: 'storage_path',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: 'image_file_name',
|
||||
// title: '镜像文件',
|
||||
// dataIndex: 'image_file_name',
|
||||
// width: 150,
|
||||
// defaultVisible: true,
|
||||
// alwaysVisible: true,
|
||||
// ellipsis: true,
|
||||
// render: (text: string) =>
|
||||
// text ? (
|
||||
// <Tooltip title={text} placement="topLeft">
|
||||
// {text}
|
||||
// </Tooltip>
|
||||
// ) : (
|
||||
// '--'
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// key: 'image_type',
|
||||
// title: '桌面类型',
|
||||
// dataIndex: 'image_type',
|
||||
// width: 120,
|
||||
// render: (text: number) => {
|
||||
// const key = text as keyof typeof IMAGES_TYPE_MAP;
|
||||
// return text ? IMAGES_TYPE_MAP[key] : '--';
|
||||
// },
|
||||
// defaultVisible: true,
|
||||
// filterDropdown: ({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||
// <Menu
|
||||
// selectedKeys={selectedKeys.length > 0 ? selectedKeys : ['全部']}
|
||||
// onClick={({ key }) => {
|
||||
// setSelectedKeys(key === '全部' ? [] : [key]);
|
||||
// confirm({ closeDropdown: true }); // 立即触发筛选并关闭下拉菜单
|
||||
// }}
|
||||
// items={[
|
||||
// { key: '全部', label: '全部' },
|
||||
// ...Object.entries(IMAGES_TYPE_MAP).map(([key, value]) => ({
|
||||
// key,
|
||||
// label: value,
|
||||
// })),
|
||||
// ]}
|
||||
// />
|
||||
// ),
|
||||
// filterMultiple: false,
|
||||
// defaultFilteredValue: ['全部'],
|
||||
// },
|
||||
|
||||
// {
|
||||
// key: 'bt_path',
|
||||
// title: 'BT路径',
|
||||
// dataIndex: 'bt_path',
|
||||
// width: 140,
|
||||
// defaultVisible: true,
|
||||
// ellipsis: true,
|
||||
// render: (text: string) =>
|
||||
// text ? (
|
||||
// <Tooltip title={text} placement="topLeft">
|
||||
// {text}
|
||||
// </Tooltip>
|
||||
// ) : (
|
||||
// '--'
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// key: 'image_status',
|
||||
// title: '镜像状态',
|
||||
// dataIndex: 'image_status',
|
||||
// width: 90,
|
||||
// render: (text: number) => (text ? getStatusTag(text) : '--'),
|
||||
// defaultVisible: true,
|
||||
// },
|
||||
{
|
||||
key: 'create_time',
|
||||
title: '上传时间',
|
||||
dataIndex: 'create_time',
|
||||
width: 160,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={dayjs(text).format('YYYY-MM-DD HH:mm:ss')}>
|
||||
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '--'}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: IMAGES.ImageItem) => (
|
||||
<Space size="small">
|
||||
{/* <Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
title="查看详情"
|
||||
/> */}
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="编辑"
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
{/* <Button type="text" icon={<DeleteOutlined />} title="删除" danger /> */}
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="删除"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const getStatusTag = (status: number) => {
|
||||
const config = STATUS_MAP[status as keyof typeof STATUS_MAP];
|
||||
return <Tag color={config?.color}>{config.text}</Tag>;
|
||||
|
|
@ -374,7 +401,7 @@ const ImageList: React.FC = () => {
|
|||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.image_name}" 吗?`,
|
||||
onOk: () => {
|
||||
delImagesAPI({id:record.id}).then((res) => {
|
||||
delImagesAPI({ id: record.id }).then((res) => {
|
||||
if (res.code == CODE) {
|
||||
message.success('删除成功');
|
||||
loadImages();
|
||||
|
|
@ -513,7 +540,7 @@ const ImageList: React.FC = () => {
|
|||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<Button onClick={() => setImportModalVisible(true)}>新建</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setImportModalVisible(true)}>新建</Button>
|
||||
<div className="search-input">
|
||||
<Input.Search
|
||||
placeholder="镜像名称"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
import { ERROR_CODE, NETWORK_TYPE_LIST } from '@/constants/constants';
|
||||
import { createNetwork, updateNetwork } from '@/services/network';
|
||||
import { Form, Input, Modal, Select, Switch, message } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface NetworkFormData {
|
||||
id?: number;
|
||||
network_name: string;
|
||||
bridge__name: string;
|
||||
type: 'nat' | 'isolated' | 'bridge';
|
||||
autostart: boolean;
|
||||
ip_range?: string;
|
||||
gateway?: string;
|
||||
netmask?: string;
|
||||
dhcp_start?: string;
|
||||
dhcp_end?: string;
|
||||
dhcp_enabled: boolean;
|
||||
}
|
||||
|
||||
interface NetworkEditModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (values: NetworkFormData) => void;
|
||||
initialValues?: NetworkFormData;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const NetworkEditModal: React.FC<NetworkEditModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
initialValues,
|
||||
title = '新增网络',
|
||||
}) => {
|
||||
const [form] = Form.useForm<NetworkFormData>();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && initialValues) {
|
||||
form.setFieldsValue(initialValues);
|
||||
} else if (visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, initialValues, form]);
|
||||
|
||||
//新增网络
|
||||
const onCreateNetwork = (payload: any) => {
|
||||
createNetwork(payload).then((res: any) => {
|
||||
console.log('res=======onCreateNetwork', res);
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('网络新建成功');
|
||||
onOk(payload);
|
||||
form.resetFields();
|
||||
} else {
|
||||
message.error(res?.data || res?.message || '网络新建失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//编辑网络
|
||||
const onUpdateNetwork = (payload: any) => {
|
||||
updateNetwork(payload).then((res: any) => {
|
||||
console.log('res=======onCreateNetwork', res);
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
message.success('网络修改成功');
|
||||
onOk(payload);
|
||||
form.resetFields();
|
||||
} else {
|
||||
message.error(res?.data || res?.message || '网修改建失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const { autostart, dhcp_enabled } = values || {};
|
||||
const params: any = { ...values };
|
||||
params.autostart = autostart ? 1 : 0;
|
||||
params.dhcp_enabled = dhcp_enabled ? 1 : 0;
|
||||
if (initialValues?.id) {
|
||||
params.id = initialValues?.id;
|
||||
onUpdateNetwork(params);
|
||||
} else {
|
||||
onCreateNetwork(params);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const networkType = Form.useWatch('type', form);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
// unmountOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
initialValues={{
|
||||
autostart: true,
|
||||
enable_dhcp: true,
|
||||
type: 'nat',
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="网络名称"
|
||||
name="network_name"
|
||||
rules={[
|
||||
{ required: true, message: '请输入网络名称' },
|
||||
{ max: 50, message: '网络名称最多50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
disabled={initialValues?.id ? true : false}
|
||||
placeholder="请输入网络名称"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="桥接名称"
|
||||
name="bridge_name"
|
||||
rules={[
|
||||
{ required: false, message: '请输入桥接名称' },
|
||||
{ max: 30, message: '桥接名称最多30个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入桥接名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="网络类型"
|
||||
name="type"
|
||||
rules={[{ required: true, message: '请选择网络类型' }]}
|
||||
extra={
|
||||
networkType === 'nat'
|
||||
? 'NAT模式:虚拟机通过宿主机访问外网,适合大多数场景'
|
||||
: networkType === 'isolated'
|
||||
? '隔离模式:虚拟机之间可通信,但无法访问外网'
|
||||
: '桥接模式:虚拟机直接连接到物理网络,获得独立IP'
|
||||
}
|
||||
>
|
||||
<Select placeholder="请选择网络类型">
|
||||
{NETWORK_TYPE_LIST.map((item) => {
|
||||
const { value, label } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{(networkType === 'nat' || networkType === 'isolated') && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="IP范围"
|
||||
name="ip_range"
|
||||
rules={[
|
||||
{ required: true, message: '请输入IP范围' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
|
||||
message: '请输入有效的IP范围格式,如:192.168.1.0/24',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.0/24" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="网关地址"
|
||||
name="gateway"
|
||||
rules={[
|
||||
{ required: true, message: '请输入网关地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.1" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="子网掩码"
|
||||
name="netmask"
|
||||
rules={[
|
||||
{ required: true, message: '请输入子网掩码' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的子网掩码格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:255.255.255.0" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="DHCP起始地址"
|
||||
name="dhcp_start"
|
||||
rules={[
|
||||
{ required: false, message: '请输入DHCP起始地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.100" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="DHCP结束地址"
|
||||
name="dhcp_end"
|
||||
rules={[
|
||||
{ required: false, message: '请输入DHCP结束地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.200" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="启用DHCP"
|
||||
name="enable_dhcp"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item label="开机自启动" name="autostart" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkEditModal;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type { TableProps } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useTableParams = (
|
||||
initialParams: NETWORK.TableParams = {
|
||||
pagination: { current: 1, pageSize: 10 },
|
||||
filters: {},// 表格的搜索对象
|
||||
sort: {},
|
||||
search: {}, // 添加搜索参数对象
|
||||
},
|
||||
) => {
|
||||
const [tableParams, setTableParams] =
|
||||
useState<NETWORK.TableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
page_num: pagination?.current,
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (sort?.field) {
|
||||
apiParams.orderby = sort.field;
|
||||
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理表格搜索参数
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索参数
|
||||
Object.entries(search || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('getApiParams apiParams', apiParams);
|
||||
return apiParams;
|
||||
}, [tableParams]);
|
||||
|
||||
// 统一的更新方法,可以处理所有参数类型
|
||||
const updateParams = useCallback(
|
||||
(
|
||||
newParams: Partial<NETWORK.TableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
const shouldResetPage =
|
||||
options?.resetPage ??
|
||||
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
|
||||
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...newParams,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
...newParams.pagination,
|
||||
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理表格的分页、排序、过滤等变化
|
||||
* @param pagination 分页信息
|
||||
* @param filters 过滤条件 这里面直接将filters为空的条件过滤掉
|
||||
* @param sorter 排序信息
|
||||
* @param extra 其他信息
|
||||
* @returns void
|
||||
* */
|
||||
const handleTableChange = useCallback<
|
||||
NonNullable<TableProps<NETWORK.NetworkItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter) => {
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (key === 'image_type') {
|
||||
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
|
||||
filteredFilters[key] = Number(value[0]);
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
filteredFilters[key] = value[0];
|
||||
} else if (value !== undefined && value !== null) {
|
||||
if (!Array.isArray(value) && value !== '') {
|
||||
filteredFilters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const newParams: Partial<NETWORK.TableParams> = {
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters: filteredFilters,
|
||||
};
|
||||
|
||||
if (!Array.isArray(sorter)) {
|
||||
newParams.sort = {
|
||||
field: sorter.field as string,
|
||||
order:
|
||||
sorter.order === 'ascend' || sorter.order === 'descend'
|
||||
? sorter.order
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
return {
|
||||
tableParams,
|
||||
getApiParams,
|
||||
updateParams, // 统一的更新方法
|
||||
handleTableChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTableParams;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 页面头部样式
|
||||
.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 {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.search-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.images-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,613 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
import {
|
||||
DHCP_STATUS,
|
||||
NETWORK_STATUS,
|
||||
NETWORK_TYPE,
|
||||
NETWORK_TYPE_LIST,
|
||||
NETWORK_STATUS_LIST
|
||||
} from '@/constants/constants';
|
||||
import { CODE } from '@/constants/images.constants';
|
||||
import { getNetworkList,deleteNetwork } from '@/services/network';
|
||||
import { PlusOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import NetworkEditModal from './components/NetworkEditModal';
|
||||
import useTableParams from './hook/hook';
|
||||
import './index.less';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
key: string;
|
||||
width: number;
|
||||
render?: any;
|
||||
fixed?: 'left' | 'right';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// 在组件顶部添加防抖函数(支持取消)
|
||||
|
||||
// 增强版防抖函数,使用泛型明确函数类型
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number,
|
||||
immediate = false,
|
||||
) => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const [dataSource, setDataSource] = useState<NETWORK.NetworkItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<any>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
const [networkType, setNetworkType] = useState<string>('不限'); // 网络类型
|
||||
const [status, setStatus] = useState<string>('不限'); // 网络类型
|
||||
|
||||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||||
useTableParams({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
search: {}, // 初始化搜索参数
|
||||
});
|
||||
|
||||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||||
const tableParamsRef = useRef(tableParams);
|
||||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||||
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
loadDataSource();
|
||||
}, [
|
||||
status,
|
||||
networkType,
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'network_name',
|
||||
title: '网络名称',
|
||||
dataIndex: 'network_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: '网络类型',
|
||||
dataIndex: 'type',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) => {
|
||||
const key = text ? (text as keyof typeof NETWORK_TYPE) : '';
|
||||
return (
|
||||
<Tooltip title={key ? NETWORK_TYPE[key] : '--'}>
|
||||
{key ? NETWORK_TYPE[key] : '--'}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'bridge_name',
|
||||
title: '桥接',
|
||||
dataIndex: 'bridge_name',
|
||||
width: 150,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'ip_range',
|
||||
// title: '模板存放路径',
|
||||
title: 'IP范围',
|
||||
dataIndex: 'ip_range',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'dhcp_enable',
|
||||
title: 'DHCP',
|
||||
dataIndex: 'dhcp_enable',
|
||||
width: 120,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: number) => {
|
||||
const key = text ? (text as keyof typeof DHCP_STATUS) : '';
|
||||
return text ? (
|
||||
<Tag color={text === 1 ? 'green' : 'red'}>
|
||||
{' '}
|
||||
{key ? DHCP_STATUS[key] : '--'}
|
||||
</Tag>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
//NETWORK_STATUS
|
||||
render: (text: number) => {
|
||||
const key = text ? (text as keyof typeof NETWORK_STATUS) : '';
|
||||
return text ? (
|
||||
<Tag color={text === 1 ? 'green' : 'red'}>
|
||||
{' '}
|
||||
{key ? NETWORK_STATUS[key] : '--'}
|
||||
</Tag>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: NETWORK.NetworkItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="编辑"
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个网络吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" type="link" title="删除">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadDataSource = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 将搜索文本合并到API参数中
|
||||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
if(networkType!=="不限"){
|
||||
apiParams.type=networkType;
|
||||
}
|
||||
if(status!=="不限"){
|
||||
apiParams.status=status;
|
||||
}
|
||||
const imagesRes = await getNetworkList(apiParams);
|
||||
if (imagesRes.code === CODE) {
|
||||
setDataSource(imagesRes.data?.data || []);
|
||||
setLoading(false);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: imagesRes.data?.page_num || 1,
|
||||
total: imagesRes.data?.total || 0,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(imagesRes.message || '获取网络列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取网络列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedNetwork(null);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: NETWORK.NetworkItem) => {
|
||||
setSelectedNetwork(record);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record: NETWORK.NetworkItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除网络 "${record.network_name}" 吗?`,
|
||||
onOk: () => {
|
||||
deleteNetwork({ id: record.id }).then((res) => {
|
||||
if (res.code === CODE) {
|
||||
message.success('删除成功');
|
||||
loadDataSource();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditModalOk = (values: any) => {
|
||||
// 这里可以调用新增或编辑网络的API
|
||||
if (selectedNetwork) {
|
||||
// 编辑模式
|
||||
message.success('编辑网络成功');
|
||||
} else {
|
||||
// 新增模式
|
||||
message.success('新增网络成功');
|
||||
}
|
||||
setEditModalVisible(false);
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
const handleEditModalCancel = () => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedNetwork(null);
|
||||
};
|
||||
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{columnConfigs
|
||||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||||
.map((config) => (
|
||||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns[config.key]}
|
||||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||||
>
|
||||
{config.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = columnConfigs
|
||||
.map((config) => {
|
||||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.filter((column) => !column.hidden) as TableColumn[];
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
// 自定义分页配置
|
||||
const paginationConfig = {
|
||||
...tableParams.pagination,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
network_name: searchValue,
|
||||
},
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
// 防抖版本(500ms延迟,不立即执行)
|
||||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||||
// 立即执行版本(用于清空时立即搜索)
|
||||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchText(value);
|
||||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
immediateSearch('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常输入时使用防抖
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 直接执行搜索
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
//类型查询
|
||||
const handleTypeChange = (value: string) => {
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
// 直接执行搜索
|
||||
setNetworkType(value);
|
||||
};
|
||||
|
||||
// 状态查询
|
||||
const handleStatusChange = (value: string) => {
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
// 直接执行搜索
|
||||
setStatus(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<div className="search-input">
|
||||
{/* status */}
|
||||
<span>网络类型</span>
|
||||
<Select
|
||||
// defaultValue={'不限'}
|
||||
value={networkType}
|
||||
onChange={handleTypeChange}
|
||||
style={{ width: '180px' }}
|
||||
>
|
||||
{[{ value: '不限', label: '不限' }, ...NETWORK_TYPE_LIST].map(
|
||||
(item) => {
|
||||
const { value, label } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Select>
|
||||
<span>状态</span>
|
||||
<Select
|
||||
// defaultValue={'不限'}
|
||||
value={status}
|
||||
onChange={handleStatusChange}
|
||||
style={{ width: '180px' }}
|
||||
>
|
||||
{[{ value: '不限', label: '不限' }, ...NETWORK_STATUS_LIST].map(
|
||||
(item) => {
|
||||
const { value, label } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Select>
|
||||
<Input.Search
|
||||
placeholder="网络名称"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleEnterSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="images-list-container">
|
||||
<div className="images-list-table">
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NetworkEditModal
|
||||
visible={editModalVisible}
|
||||
onCancel={handleEditModalCancel}
|
||||
onOk={handleEditModalOk}
|
||||
initialValues={selectedNetwork}
|
||||
title={selectedNetwork ? '编辑网络' : '新增网络'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import { NETWORK_TYPE_LIST } from '@/constants/constants';
|
||||
import { Form, Input, Modal, Select, Switch } from 'antd';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface NetworkFormData {
|
||||
network_name: string;
|
||||
bridge_name: string;
|
||||
network_type: 'NAT' | 'Isolated' | 'Bridge' | 'Open';
|
||||
auto_start: boolean;
|
||||
ip_range?: string;
|
||||
gateway_address?: string;
|
||||
subnet_mask?: string;
|
||||
dhcp_start_address?: string;
|
||||
dhcp_end_address?: string;
|
||||
enable_dhcp: boolean;
|
||||
}
|
||||
|
||||
interface NetworkEditModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onOk: (values: NetworkFormData) => void;
|
||||
initialValues?: NetworkFormData;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const NetworkEditModal: React.FC<NetworkEditModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onOk,
|
||||
initialValues,
|
||||
title = '新增网络',
|
||||
}) => {
|
||||
const [form] = Form.useForm<NetworkFormData>();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && initialValues) {
|
||||
form.setFieldsValue(initialValues);
|
||||
} else if (visible) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, initialValues, form]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
onOk(values);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const networkType = Form.useWatch('network_type', form);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
// unmountOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
initialValues={{
|
||||
auto_start: true,
|
||||
enable_dhcp: true,
|
||||
network_type: 'NAT',
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="网络名称"
|
||||
name="network_name"
|
||||
rules={[
|
||||
{ required: true, message: '请输入网络名称' },
|
||||
{ max: 50, message: '网络名称最多50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入网络名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="桥接名称"
|
||||
name="bridge_name"
|
||||
rules={[
|
||||
{ required: false, message: '请输入桥接名称' },
|
||||
{ max: 30, message: '桥接名称最多30个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入桥接名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="网络类型"
|
||||
name="network_type"
|
||||
rules={[{ required: true, message: '请选择网络类型' }]}
|
||||
extra={
|
||||
networkType === 'NAT'
|
||||
? 'NAT模式:虚拟机通过宿主机访问外网,适合大多数场景'
|
||||
: networkType === 'Isolated'
|
||||
? '隔离模式:虚拟机之间可通信,但无法访问外网'
|
||||
: '桥接模式:虚拟机直接连接到物理网络,获得独立IP'
|
||||
}
|
||||
>
|
||||
<Select placeholder="请选择网络类型">
|
||||
{NETWORK_TYPE_LIST.map((item) => {
|
||||
const { value, label } = item || {};
|
||||
return (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{(networkType === 'NAT' || networkType === 'Isolated') && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="IP范围"
|
||||
name="ip_range"
|
||||
rules={[
|
||||
{ required: true, message: '请输入IP范围' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/,
|
||||
message: '请输入有效的IP范围格式,如:192.168.1.0/24',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.0/24" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="网关地址"
|
||||
name="gateway_address"
|
||||
rules={[
|
||||
{ required: true, message: '请输入网关地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.1" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="子网掩码"
|
||||
name="subnet_mask"
|
||||
rules={[
|
||||
{ required: true, message: '请输入子网掩码' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的子网掩码格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:255.255.255.0" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="DHCP起始地址"
|
||||
name="dhcp_start_address"
|
||||
rules={[
|
||||
{ required: false, message: '请输入DHCP起始地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.100" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="DHCP结束地址"
|
||||
name="dhcp_end_address"
|
||||
rules={[
|
||||
{ required: false, message: '请输入DHCP结束地址' },
|
||||
{
|
||||
pattern: /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
|
||||
message: '请输入有效的IP地址格式',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="如:192.168.1.200" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="启用DHCP"
|
||||
name="enable_dhcp"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item label="开机自启动" name="auto_start" valuePropName="checked">
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkEditModal;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import type { TableProps } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useTableParams = (
|
||||
initialParams: STORAGE.TableParams = {
|
||||
pagination: { current: 1, pageSize: 10 },
|
||||
filters: {},// 表格的搜索对象
|
||||
sort: {},
|
||||
search: {}, // 添加搜索参数对象
|
||||
},
|
||||
) => {
|
||||
const [tableParams, setTableParams] =
|
||||
useState<STORAGE.TableParams>(initialParams);
|
||||
|
||||
const getApiParams = useCallback(() => {
|
||||
const { pagination, filters, sort, search, ...rest } = tableParams;
|
||||
const apiParams: Record<string, any> = {
|
||||
page_size: pagination?.pageSize,
|
||||
page_num: pagination?.current,
|
||||
...rest,
|
||||
};
|
||||
|
||||
if (sort?.field) {
|
||||
apiParams.orderby = sort.field;
|
||||
apiParams.order = sort.order === 'ascend' ? 'asc' : 'desc';
|
||||
}
|
||||
|
||||
// 处理表格搜索参数
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理搜索参数
|
||||
Object.entries(search || {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
apiParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('getApiParams apiParams', apiParams);
|
||||
return apiParams;
|
||||
}, [tableParams]);
|
||||
|
||||
// 统一的更新方法,可以处理所有参数类型
|
||||
const updateParams = useCallback(
|
||||
(
|
||||
newParams: Partial<STORAGE.TableParams>,
|
||||
options?: { resetPage?: boolean },
|
||||
) => {
|
||||
// console.log('updateParams', newParams);
|
||||
|
||||
setTableParams((prev) => {
|
||||
// 如果是搜索或过滤相关的更新,重置到第一页
|
||||
const shouldResetPage =
|
||||
options?.resetPage ??
|
||||
((newParams.search && Object.keys(newParams.search).length > 0) || // 有搜索值
|
||||
(newParams.filters && Object.keys(newParams.filters).length > 0)); // 有过滤值
|
||||
|
||||
return {
|
||||
...prev,
|
||||
...newParams,
|
||||
pagination: {
|
||||
...prev.pagination,
|
||||
...newParams.pagination,
|
||||
...(shouldResetPage ? { current: 1 } : {}), // 根据条件决定是否重置页码
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理表格的分页、排序、过滤等变化
|
||||
* @param pagination 分页信息
|
||||
* @param filters 过滤条件 这里面直接将filters为空的条件过滤掉
|
||||
* @param sorter 排序信息
|
||||
* @param extra 其他信息
|
||||
* @returns void
|
||||
* */
|
||||
const handleTableChange = useCallback<
|
||||
NonNullable<TableProps<STORAGE.StorageItem>['onChange']>
|
||||
>(
|
||||
(pagination, filters, sorter) => {
|
||||
// console.log('handleTableChange',pagination,filters,sorter,extra);
|
||||
|
||||
// 过滤掉空值的filters
|
||||
const filteredFilters: Record<string, any> = {};
|
||||
Object.entries(filters || {}).forEach(([key, value]) => {
|
||||
if (key === 'image_type') {
|
||||
if (Array.isArray(value) && value.length > 0 && value[0] !== '全部') {
|
||||
filteredFilters[key] = Number(value[0]);
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
filteredFilters[key] = value[0];
|
||||
} else if (value !== undefined && value !== null) {
|
||||
if (!Array.isArray(value) && value !== '') {
|
||||
filteredFilters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const newParams: Partial<STORAGE.TableParams> = {
|
||||
pagination: {
|
||||
current: pagination.current || 1,
|
||||
pageSize: pagination.pageSize || 10,
|
||||
},
|
||||
filters: filteredFilters,
|
||||
};
|
||||
|
||||
if (!Array.isArray(sorter)) {
|
||||
newParams.sort = {
|
||||
field: sorter.field as string,
|
||||
order:
|
||||
sorter.order === 'ascend' || sorter.order === 'descend'
|
||||
? sorter.order
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
// console.log('handleTableChange', newParams);
|
||||
|
||||
updateParams(newParams);
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
return {
|
||||
tableParams,
|
||||
getApiParams,
|
||||
updateParams, // 统一的更新方法
|
||||
handleTableChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTableParams;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 页面头部样式
|
||||
.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 {
|
||||
width:100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
.search-box {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.search-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.images-list-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.images-list-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// 表格适应样式
|
||||
.ant-table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-spin-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.ant-table-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ant-table-body {
|
||||
flex: 1;
|
||||
overflow: auto !important;
|
||||
|
||||
}
|
||||
}
|
||||
// 确保分页器在底部正确显示
|
||||
.ant-table-pagination {
|
||||
flex-shrink: 0;
|
||||
// 确保分页器始终可见
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { NETWORK_TYPE } from '@/constants/constants';
|
||||
import { CODE } from '@/constants/images.constants';
|
||||
import { deleteTool } from '@/services/imagePage';
|
||||
import { getNetworkList } from '@/services/network';
|
||||
import { SettingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import useTableParams from './hook/hook';
|
||||
import NetworkEditModal from './components/NetworkEditModal';
|
||||
import './index.less';
|
||||
|
||||
import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg';
|
||||
|
||||
// 列配置定义
|
||||
type ColumnConfig = {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
width: number;
|
||||
render?: (text: any, record: any, index: number) => React.ReactNode;
|
||||
fixed?: 'left' | 'right';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
defaultVisible: boolean; // 默认是否显示
|
||||
alwaysVisible?: boolean; // 始终显示的列
|
||||
ellipsis?: boolean; // 是否启用省略号
|
||||
filters?: { text: string; value: string }[];
|
||||
filterMultiple?: boolean; // 是否多选过滤
|
||||
filterDropdown?: (props: any) => React.ReactNode;
|
||||
defaultFilteredValue?: string[]; // 默认过滤值
|
||||
onFilter?: (value: string, record: any) => boolean;
|
||||
};
|
||||
|
||||
type TableColumn = {
|
||||
title: string;
|
||||
dataIndex?: string;
|
||||
key: string;
|
||||
width: number;
|
||||
render?: any;
|
||||
fixed?: 'left' | 'right';
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
// 在组件顶部添加防抖函数(支持取消)
|
||||
|
||||
// 增强版防抖函数,使用泛型明确函数类型
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
delay: number,
|
||||
immediate = false,
|
||||
) => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
if (immediate && !timer) {
|
||||
func(...args);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func(...args);
|
||||
}
|
||||
timer = null;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
};
|
||||
|
||||
const Index = () => {
|
||||
const [dataSource, setDataSource] = useState<STORAGE.StorageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<any>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [searchText, setSearchText] = useState<string>(''); // 添加本地搜索状态
|
||||
|
||||
const { tableParams, getApiParams, updateParams, handleTableChange } =
|
||||
useTableParams({
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
search: {}, // 初始化搜索参数
|
||||
});
|
||||
|
||||
// 在组件顶部添加一个 ref 来保存最新的 tableParams
|
||||
const tableParamsRef = useRef(tableParams);
|
||||
tableParamsRef.current = tableParams; // 每次渲染时更新 ref 的值
|
||||
|
||||
const [columnSettingsVisible, setColumnSettingsVisible] = useState(false);
|
||||
|
||||
// 表格参数变化 获取镜像列表
|
||||
useEffect(() => {
|
||||
loadDataSource();
|
||||
}, [
|
||||
tableParams.pagination?.current,
|
||||
tableParams.pagination?.pageSize,
|
||||
tableParams?.sortOrder,
|
||||
tableParams?.sortField,
|
||||
JSON.stringify(tableParams.filters), // 表格搜索参数
|
||||
JSON.stringify(tableParams.search), // 搜索参数依赖
|
||||
]);
|
||||
|
||||
// 定义所有列的配置
|
||||
const columnConfigs: ColumnConfig[] = [
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
width: 60,
|
||||
render: (text: any, row: any, index: number) =>
|
||||
(tableParams.pagination?.current - 1) *
|
||||
tableParams.pagination?.pageSize +
|
||||
index +
|
||||
1,
|
||||
defaultVisible: true,
|
||||
alwaysVisible: true,
|
||||
},
|
||||
{
|
||||
key: 'pool_name',
|
||||
title: '名称',
|
||||
dataIndex: 'pool_name',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) => {
|
||||
const key = text ? (text as keyof typeof NETWORK_TYPE) : '';
|
||||
return (
|
||||
<Tooltip title={key ? NETWORK_TYPE[key] : '--'}>
|
||||
{key ? NETWORK_TYPE[key] : '--'}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text}>{text}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
title: '路径',
|
||||
dataIndex: 'ip_range',
|
||||
width: 140,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? (
|
||||
<Tooltip title={text} placement="topLeft">
|
||||
{text}
|
||||
</Tooltip>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'dhcp',
|
||||
title: '总容量',
|
||||
dataIndex: 'dhcp',
|
||||
width: 160,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'dhcp',
|
||||
title: '剩余容量',
|
||||
dataIndex: 'dhcp',
|
||||
width: 160,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
key: 'network_status',
|
||||
title: '已分配容量',
|
||||
dataIndex: 'network_status',
|
||||
width: 160,
|
||||
defaultVisible: true,
|
||||
ellipsis: true,
|
||||
render: (text: string) =>
|
||||
text ? <Tooltip title={text || ''}>{text || '--'}</Tooltip> : '--',
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
fixed: 'right' as 'right',
|
||||
render: (_: any, record: STORAGE.StorageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
title="编辑"
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个网络吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button size="small" type="link" title="删除">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
defaultVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化 visibleColumns 状态
|
||||
const initialVisibleColumns = columnConfigs.reduce<Record<string, boolean>>(
|
||||
(acc, column) => {
|
||||
if (!column.alwaysVisible) {
|
||||
acc[column.key] = column.defaultVisible;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>(
|
||||
initialVisibleColumns,
|
||||
);
|
||||
|
||||
// 重置列设置
|
||||
const resetColumns = () => {
|
||||
setVisibleColumns(initialVisibleColumns);
|
||||
};
|
||||
|
||||
const loadDataSource = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 将搜索文本合并到API参数中
|
||||
const apiParams = {
|
||||
...getApiParams(),
|
||||
};
|
||||
|
||||
const imagesRes = await getNetworkList(apiParams);
|
||||
if (imagesRes.code === CODE) {
|
||||
setDataSource(imagesRes.data?.data || []);
|
||||
setLoading(false);
|
||||
|
||||
// 正确处理后端返回的分页信息
|
||||
updateParams({
|
||||
pagination: {
|
||||
...tableParams.pagination,
|
||||
current: imagesRes.data?.page_num || 1,
|
||||
total: imagesRes.data?.total || 0,
|
||||
pageSize: tableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
message.error(imagesRes.message || '获取网络列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('获取网络列表失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedNetwork(null);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (record: STORAGE.StorageItem) => {
|
||||
setSelectedNetwork(record);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record: STORAGE.StorageItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除网络 "${record.id}" 吗?`,
|
||||
onOk: () => {
|
||||
deleteTool({ id: record.id }).then((res) => {
|
||||
if (res.code === CODE) {
|
||||
message.success('删除成功');
|
||||
loadDataSource();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditModalOk = (values: any) => {
|
||||
// 这里可以调用新增或编辑网络的API
|
||||
if (selectedNetwork) {
|
||||
// 编辑模式
|
||||
message.success('编辑网络成功');
|
||||
} else {
|
||||
// 新增模式
|
||||
message.success('新增网络成功');
|
||||
}
|
||||
setEditModalVisible(false);
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
const handleEditModalCancel = () => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedNetwork(null);
|
||||
};
|
||||
|
||||
// 列设置相关函数
|
||||
const handleColumnChange = (columnKey: string, checked: boolean) => {
|
||||
setVisibleColumns((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
// 列设置内容
|
||||
const columnSettingsContent = (
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
{columnConfigs
|
||||
.filter((config) => !config.alwaysVisible) // 只显示可控制的列
|
||||
.map((config) => (
|
||||
<div key={config.key} style={{ padding: '4px 12px' }}>
|
||||
<Checkbox
|
||||
checked={visibleColumns[config.key]}
|
||||
onChange={(e) => handleColumnChange(config.key, e.target.checked)}
|
||||
>
|
||||
{config.title}
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Button type="link" onClick={resetColumns} style={{ padding: 0 }}>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据visibleColumns过滤显示的列
|
||||
const filteredColumns = columnConfigs
|
||||
.map((config) => {
|
||||
// 对于始终显示的列
|
||||
if (config.alwaysVisible) {
|
||||
return {
|
||||
...config,
|
||||
hidden: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 对于可控制显示/隐藏的列
|
||||
return {
|
||||
...config,
|
||||
...(visibleColumns[config.key] ? {} : { hidden: true }),
|
||||
};
|
||||
})
|
||||
.filter((column) => !column.hidden) as TableColumn[];
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadDataSource();
|
||||
};
|
||||
|
||||
// 自定义分页配置
|
||||
const paginationConfig = {
|
||||
...tableParams.pagination,
|
||||
showTotal: (total: number) => `共 ${total} 条记录`,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(searchValue: string) => {
|
||||
const currentTableParams = tableParamsRef.current;
|
||||
updateParams({
|
||||
search: {
|
||||
pool_name: searchValue,
|
||||
},
|
||||
pagination: {
|
||||
current: 1,
|
||||
pageSize: currentTableParams.pagination?.pageSize || 10,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
// 防抖版本(500ms延迟,不立即执行)
|
||||
const debouncedSearch = useRef(debounce(handleSearch, 500)).current;
|
||||
// 立即执行版本(用于清空时立即搜索)
|
||||
const immediateSearch = useRef(debounce(handleSearch, 0, true)).current;
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchText(value);
|
||||
// 取消所有未执行的防抖请求
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 清空时立即触发搜索
|
||||
if (value === '') {
|
||||
immediateSearch('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常输入时使用防抖
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
// 修改回车搜索处理
|
||||
const handleEnterSearch = (value: string) => {
|
||||
// 回车搜索时取消未执行的防抖
|
||||
debouncedSearch.cancel();
|
||||
immediateSearch.cancel();
|
||||
|
||||
// 直接执行搜索
|
||||
handleSearch(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<div className="search-box">
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<div className="search-input">
|
||||
<Input.Search
|
||||
placeholder="名称"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onSearch={handleEnterSearch}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
icon={<RefreshIcon style={{ width: 13, height: 13 }} />}
|
||||
></Button>
|
||||
<Popover
|
||||
content={columnSettingsContent}
|
||||
title="列设置"
|
||||
trigger="click"
|
||||
open={columnSettingsVisible}
|
||||
onOpenChange={setColumnSettingsVisible}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button icon={<SettingOutlined />}></Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="images-list-container">
|
||||
<div className="images-list-table">
|
||||
<Table
|
||||
columns={filteredColumns}
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationConfig}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
y: 'max-content', // 关键:允许内容决定高度
|
||||
}}
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NetworkEditModal
|
||||
visible={editModalVisible}
|
||||
onCancel={handleEditModalCancel}
|
||||
onOk={handleEditModalOk}
|
||||
initialValues={selectedNetwork}
|
||||
title={selectedNetwork ? '编辑网络' : '新增网络'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import {
|
||||
DEFAULT_BLICK_TAB,
|
||||
DEVICE_TYPE_MAP,
|
||||
// DEVICE_TYPE_MAP,
|
||||
ERROR_CODE,
|
||||
} from '@/constants/constants';
|
||||
import CustomTree from '@/pages/components/customTree';
|
||||
import CreatGroup from '@/pages/userList/mod/group';
|
||||
import { deleteDevice, getTerminalList } from '@/services/terminal';
|
||||
import {
|
||||
deleteDevice,
|
||||
getTerminalList,
|
||||
getTerminalPower,
|
||||
} from '@/services/terminal';
|
||||
import { deleteUserGroup, getGroupTree } from '@/services/userList';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
|
|
@ -207,7 +211,8 @@ const UserListPage: React.FC = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
title: '序列号',
|
||||
// title: '序列号',
|
||||
title: '终端标识',
|
||||
dataIndex: 'device_id',
|
||||
key: 'device_id',
|
||||
width: 250,
|
||||
|
|
@ -232,26 +237,27 @@ const UserListPage: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '类型',
|
||||
// dataIndex: 'device_type',
|
||||
// key: 'device_type',
|
||||
// width: 150,
|
||||
// align: 'center',
|
||||
// ellipsis: true,
|
||||
// render: (text: number) => {
|
||||
// const key = text ? (text as keyof typeof DEVICE_TYPE_MAP) : '';
|
||||
|
||||
// return (
|
||||
// <Tooltip title={key ? DEVICE_TYPE_MAP[key] : '--'}>
|
||||
// {key ? DEVICE_TYPE_MAP[key] : '--'}
|
||||
// </Tooltip>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'device_type',
|
||||
key: 'device_type',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
ellipsis: true,
|
||||
render: (text: number) => {
|
||||
const key = text as keyof typeof DEVICE_TYPE_MAP;
|
||||
return (
|
||||
<Tooltip title={DEVICE_TYPE_MAP[key] || '--'}>
|
||||
{DEVICE_TYPE_MAP[key] || '--'}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '型号',
|
||||
dataIndex: 'model',
|
||||
key: 'model',
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
ellipsis: true,
|
||||
|
|
@ -263,6 +269,36 @@ const UserListPage: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '桌面镜像',
|
||||
dataIndex: 'images_name',
|
||||
key: 'images_name',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
ellipsis: true,
|
||||
render: (text) => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '型号',
|
||||
// dataIndex: 'model',
|
||||
// key: 'model',
|
||||
// width: 150,
|
||||
// align: 'center',
|
||||
// ellipsis: true,
|
||||
// render: (text) => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Tooltip title={text || ''}>{text || '--'}</Tooltip>
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip_addr',
|
||||
|
|
@ -293,54 +329,65 @@ const UserListPage: React.FC = () => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
align: 'center',
|
||||
width: 200,
|
||||
render: (text) => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title={text || ''}>{text || '--'}</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// title: '备注',
|
||||
// dataIndex: 'description',
|
||||
// key: 'description',
|
||||
// ellipsis: true,
|
||||
// align: 'center',
|
||||
// width: 200,
|
||||
// render: (text) => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Tooltip title={text || ''}>{text || '--'}</Tooltip>
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
width: 230,
|
||||
width: 170,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setBindUserData({
|
||||
recordData: record,
|
||||
visible: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
绑定用户
|
||||
<Button size="small" type="link" onClick={() => {onOrgChange('stop',record)}}>
|
||||
关机
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setBindImageDta({ recordData: record, visible: true });
|
||||
}}
|
||||
>
|
||||
绑定镜像
|
||||
|
||||
<Button size="small" type="link" onClick={() => {onOrgChange('start',record)}}>
|
||||
开机
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
content={
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Button
|
||||
type="link"
|
||||
// size="small"
|
||||
onClick={() => {
|
||||
setBindUserData({
|
||||
recordData: record,
|
||||
visible: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
绑定用户
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
// size="small"
|
||||
onClick={() => {
|
||||
setBindImageDta({ recordData: record, visible: true });
|
||||
}}
|
||||
>
|
||||
绑定镜像
|
||||
</Button>
|
||||
<Button type="link" onClick={() => handleEditInfo(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title=""
|
||||
description="删除操作不可逆,请确认是否删除?"
|
||||
|
|
@ -351,11 +398,6 @@ const UserListPage: React.FC = () => {
|
|||
>
|
||||
<Button type="link">删除</Button>
|
||||
</Popconfirm>
|
||||
<div>
|
||||
<Button type="link" onClick={() => handleEditInfo(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -369,6 +411,27 @@ const UserListPage: React.FC = () => {
|
|||
},
|
||||
];
|
||||
|
||||
// 终端开机、关机
|
||||
const onOrgChange = (value: string, record: any) => {
|
||||
getTerminalPower({
|
||||
id: record.id,
|
||||
status: value,
|
||||
}).then((res:any) => {
|
||||
const { code } = res || {};
|
||||
if (code === ERROR_CODE) {
|
||||
const text = value === 'start'?"开机成功":"关机成功"
|
||||
message.success(text);
|
||||
getDataSource();
|
||||
} else {
|
||||
if (value === 'start') {
|
||||
message.error('开机失败');
|
||||
} else {
|
||||
message.error('关机失败');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onOrgSelect = (selectedKeys: React.Key[], e: any) => {
|
||||
const { node } = e || {};
|
||||
if (selectedKeys.length > 0) {
|
||||
|
|
@ -388,9 +451,10 @@ const UserListPage: React.FC = () => {
|
|||
setPageSize(size);
|
||||
};
|
||||
|
||||
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys as any);
|
||||
};
|
||||
// const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
|
||||
// setSelectedRowKeys(newSelectedRowKeys as any);
|
||||
// };
|
||||
|
||||
const onDeleteGroup = async () => {
|
||||
if (selectedOrg) {
|
||||
const params: any = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'umi';
|
||||
import VncClient from './mod/index';
|
||||
|
||||
/**
|
||||
* VNC远程桌面页面组件
|
||||
* 用于在新窗口中显示VNC远程桌面
|
||||
*/
|
||||
const VncRemotePage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
// 从URL参数中获取镜像ID和其他信息
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
console.log("location=======",location);
|
||||
console.log("location=======searchParams.get('vncUrl')",searchParams.get('vncUrl'))
|
||||
// const imageId = searchParams.get('imageId') || '';
|
||||
const imageName = searchParams.get('imageName') || 'NEXSPACE远程桌面';
|
||||
const vncUrl = searchParams.get('vncUrl') || '';
|
||||
const password = searchParams.get('password') || '';
|
||||
|
||||
// 设置页面标题
|
||||
useEffect(() => {
|
||||
document.title = `${imageName} - 远程控制台`;
|
||||
|
||||
// 添加关闭窗口时的清理逻辑
|
||||
const handleBeforeUnload = () => {
|
||||
// 确保在窗口关闭前通知VNC客户端进行清理
|
||||
console.log('Performing cleanup before window unload');
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [imageName]);
|
||||
|
||||
// 验证并修复VNC URL格式
|
||||
const validateAndFixVncUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
|
||||
// 检查URL格式,修复常见问题
|
||||
if (url.startsWith('ws:http')) {
|
||||
// 修复错误格式 ws:http// -> ws://
|
||||
return url.replace('ws:http//', 'ws://');
|
||||
} else if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
||||
// 如果没有协议前缀,添加ws://
|
||||
return `ws://${url}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
// 生成默认的VNC URL(如果没有提供)
|
||||
// const defaultVncUrl = imageId ? `ws://0.0.0.0:5091/vnc/0.0.0.0:5901` : '';
|
||||
const defaultVncUrl = `ws://0.0.0.0:5091/vnc/0.0.0.0:5901`;
|
||||
const finalVncUrl = validateAndFixVncUrl(vncUrl) || defaultVncUrl;
|
||||
|
||||
console.log('最终使用的VNC URL:', finalVncUrl);
|
||||
|
||||
// 当收到父窗口的消息时处理
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data === 'close-vnc-window') {
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#000',
|
||||
}}
|
||||
>
|
||||
<VncClient
|
||||
onClose={() => {
|
||||
// 确保在关闭前有适当的清理时间
|
||||
setTimeout(() => window.close(), 100);
|
||||
}}
|
||||
vncUrl={finalVncUrl}
|
||||
password={password}
|
||||
viewOnly={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VncRemotePage;
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
// VNC 远程桌面组件样式
|
||||
|
||||
.vnc-remote-desktop {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
// 全屏模式
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// 连接状态样式
|
||||
&.connecting {
|
||||
border-color: #ff9800;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
border-color: #f44336;
|
||||
}
|
||||
}
|
||||
|
||||
// 控制栏样式
|
||||
.vnc-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background-color: #2a2a2a;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// 状态指示器
|
||||
.vnc-status-indicator {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
|
||||
&.connecting {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
&.connected {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
background-color: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
}
|
||||
}
|
||||
|
||||
// 操作按钮组
|
||||
.vnc-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
:where(
|
||||
.css-dev-only-do-not-override-1vjf2v5
|
||||
).ant-btn-variant-outlined:disabled,
|
||||
:where(.css-dev-only-do-not-override-1vjf2v5).ant-btn-variant-dashed:disabled,
|
||||
:where(
|
||||
.css-dev-only-do-not-override-1vjf2v5
|
||||
).ant-btn-variant-outlined.ant-btn-disabled,
|
||||
:where(
|
||||
.css-dev-only-do-not-override-1vjf2v5
|
||||
).ant-btn-variant-dashed.ant-btn-disabled {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// gap: 8px;
|
||||
}
|
||||
|
||||
// 仅查看模式指示器
|
||||
.view-only-indicator {
|
||||
color: #ffeb3b;
|
||||
font-size: 12px;
|
||||
background-color: rgba(255, 235, 59, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
.vnc-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #555;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #333;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.disconnect {
|
||||
background-color: #f44336;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.vnc-content {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 加载覆盖层
|
||||
.vnc-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top: 4px solid #fff;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误覆盖层
|
||||
.vnc-error-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
color: #ffcdcd;
|
||||
max-width: 80%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.reconnect-button {
|
||||
padding: 10px 20px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
}
|
||||
|
||||
// VNC 画布
|
||||
.vnc-canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
cursor: default;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #2196f3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 未连接时的占位符
|
||||
.vnc-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.vnc-controls {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.vnc-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题增强
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vnc-remote-desktop {
|
||||
background-color: #0d1117;
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
.vnc-controls {
|
||||
background-color: #161b22;
|
||||
border-bottom-color: #30363d;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.vnc-button {
|
||||
background-color: #21262d;
|
||||
}
|
||||
|
||||
.vnc-button:hover:not(:disabled) {
|
||||
background-color: #30363d;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
// import { RFB } from '@novnc/novnc/core/rfb';
|
||||
import RFB from '@/public/novnc/core/rfb';
|
||||
|
||||
import './index.less';
|
||||
|
||||
interface VncRemoteDesktopProps {
|
||||
vncUrl: string;
|
||||
password?: string;
|
||||
onConnected?: () => void;
|
||||
onDisconnected?: () => void;
|
||||
onError?: (error: string) => void;
|
||||
loadingText?: string;
|
||||
className?: string;
|
||||
isFullscreen?: boolean;
|
||||
viewOnly?: boolean;
|
||||
autoScale?: boolean;
|
||||
maxRetries?: number;
|
||||
retryInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装的noVNC远程桌面控制组件
|
||||
* 使用项目中已安装的@novnc/novnc包
|
||||
*/
|
||||
const VncRemoteDesktop: React.FC<VncRemoteDesktopProps> = ({
|
||||
vncUrl,
|
||||
password,
|
||||
onConnected,
|
||||
onDisconnected,
|
||||
onError,
|
||||
loadingText = '正在连接远程桌面...',
|
||||
className = '',
|
||||
viewOnly = false,
|
||||
autoScale = true,
|
||||
maxRetries = 3,
|
||||
retryInterval = 2000,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const rfbRef = useRef<RFB | null>(null);
|
||||
const retryCountRef = useRef(0);
|
||||
const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
'disconnected' | 'connecting' | 'connected'
|
||||
>('disconnected');
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
// 监听浏览器窗口关闭事件
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
event.preventDefault();
|
||||
disconnect();
|
||||
return '';
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 清除重试定时器
|
||||
const clearRetryTimeout = () => {
|
||||
if (retryTimeoutRef.current) {
|
||||
clearTimeout(retryTimeoutRef.current);
|
||||
retryTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 连接到VNC服务器
|
||||
const connect = (resetRetry = false) => {
|
||||
if (!vncUrl || !canvasRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的重试定时器
|
||||
clearRetryTimeout();
|
||||
|
||||
// 重置重试计数(如果需要)
|
||||
if (resetRetry) {
|
||||
retryCountRef.current = 0;
|
||||
}
|
||||
|
||||
// 断开已有连接
|
||||
if (rfbRef.current) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
setConnectionStatus('connecting');
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
// 验证URL格式
|
||||
if (!vncUrl.startsWith('ws://') && !vncUrl.startsWith('wss://')) {
|
||||
throw new Error('无效的VNC URL格式,请使用ws://或wss://开头');
|
||||
}
|
||||
|
||||
console.log('WebSocket URL=========', vncUrl);
|
||||
|
||||
console.log('尝试连接到VNC服务器:', vncUrl);
|
||||
|
||||
// 创建RFB实例
|
||||
const rfb = new RFB(canvasRef.current, vncUrl, {
|
||||
credentials: password ? { password } : undefined,
|
||||
shared: true,
|
||||
wsProtocols: ['binary'],
|
||||
// focusOnClick: !viewOnly,
|
||||
dragViewport: true,
|
||||
scaleViewport: true,
|
||||
resizeSession: true,
|
||||
// viewOnly: viewOnly,
|
||||
// background: '#000000',
|
||||
});
|
||||
|
||||
// 保存RFB实例引用,用于后续操作和事件处理
|
||||
rfbRef.current = rfb;
|
||||
console.log('rfbRef.current=====保存RFB实例引用', rfbRef.current);
|
||||
|
||||
// 监听连接事件
|
||||
rfb.addEventListener('connect', () => {
|
||||
console.log('VNC连接成功');
|
||||
retryCountRef.current = 0; // 重置重试计数
|
||||
setConnectionStatus('connected');
|
||||
onConnected?.();
|
||||
});
|
||||
|
||||
// 监听断开连接事件
|
||||
rfb.addEventListener('disconnect', () => {
|
||||
console.log('VNC连接断开');
|
||||
setConnectionStatus('disconnected');
|
||||
onDisconnected?.();
|
||||
console.log('rfbRef.current=====监听断开连接事件', rfbRef.current);
|
||||
});
|
||||
|
||||
// 监听安全失败事件
|
||||
rfb.addEventListener('securityfailure', (e: any) => {
|
||||
console.error('VNC安全连接失败:', e.detail);
|
||||
setErrorMessage('安全连接失败: ' + e.detail);
|
||||
setConnectionStatus('disconnected');
|
||||
onError?.('安全连接失败: ' + e.detail);
|
||||
console.log('rfbRef.current=====监听安全失败事件', rfbRef.current);
|
||||
rfbRef.current = null;
|
||||
});
|
||||
|
||||
// 监听凭证请求事件
|
||||
rfb.addEventListener('credentialsrequired', () => {
|
||||
console.log('需要凭证');
|
||||
if (password && rfbRef.current) {
|
||||
rfbRef.current.sendCredentials({ password });
|
||||
}
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
rfb.addEventListener('error', (e: any) => {
|
||||
const errorDetail = e.detail || {};
|
||||
const errorMsg = errorDetail.message || errorDetail || '未知错误';
|
||||
console.error('VNC错误:', errorMsg);
|
||||
|
||||
// 提供更具体的错误信息
|
||||
let userFriendlyError = '';
|
||||
if (errorMsg.toString().includes('WebSocket')) {
|
||||
userFriendlyError = `WebSocket连接失败: ${errorMsg}\n可能的原因: 服务器不可达、网络问题或防火墙阻止`;
|
||||
} else if (errorMsg.toString().includes('401')) {
|
||||
userFriendlyError = '认证失败: 用户名或密码错误';
|
||||
} else if (errorMsg.toString().includes('403')) {
|
||||
userFriendlyError = '权限不足: 您没有访问该远程桌面的权限';
|
||||
} else if (errorMsg.toString().includes('timeout')) {
|
||||
userFriendlyError = '连接超时: 服务器响应超时,请检查网络连接';
|
||||
} else {
|
||||
userFriendlyError = `连接错误: ${errorMsg}`;
|
||||
}
|
||||
|
||||
setErrorMessage(userFriendlyError);
|
||||
setConnectionStatus('disconnected');
|
||||
onError?.(userFriendlyError);
|
||||
|
||||
// 自动重试连接(如果未达到最大重试次数)
|
||||
if (retryCountRef.current < maxRetries) {
|
||||
retryCountRef.current++;
|
||||
console.log(`尝试重新连接 (${retryCountRef.current}/${maxRetries})`);
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
connect();
|
||||
}, retryInterval);
|
||||
}
|
||||
});
|
||||
console.log('RFB连接配置完成');
|
||||
} catch (error) {
|
||||
console.error('创建VNC连接失败:', error);
|
||||
const errorMsg = error instanceof Error ? error.message : '创建连接失败';
|
||||
setErrorMessage(errorMsg);
|
||||
setConnectionStatus('disconnected');
|
||||
onError?.(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 断开连接
|
||||
const disconnect = () => {
|
||||
// 清除重试定时器
|
||||
clearRetryTimeout();
|
||||
|
||||
console.log('rfbRef.current=====断开连接', rfbRef);
|
||||
console.log('rfbRef.current=====断开连接', rfbRef.current);
|
||||
|
||||
if (rfbRef.current) {
|
||||
try {
|
||||
// 确保完全断开连接并释放所有资源
|
||||
rfbRef.current.disconnect();
|
||||
} catch (error) {
|
||||
console.error('Error during disconnect/cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置重试计数
|
||||
retryCountRef.current = 0;
|
||||
};
|
||||
|
||||
// 连接
|
||||
const reconnect = () => {
|
||||
setVisible(true);
|
||||
connect(true); // 重置重试计数后连接
|
||||
};
|
||||
|
||||
// 当vncUrl或password变化时重新连接
|
||||
useEffect(() => {
|
||||
if (vncUrl && visible) {
|
||||
connect(true); // 重置重试计数后连接
|
||||
}
|
||||
|
||||
// 组件卸载时断开连接并清理资源
|
||||
return () => {
|
||||
disconnect();
|
||||
clearRetryTimeout();
|
||||
};
|
||||
}, [visible, vncUrl, password, maxRetries, retryInterval]);
|
||||
|
||||
// 当autoScale或viewOnly属性变化时更新RFB实例
|
||||
useEffect(() => {
|
||||
if (rfbRef.current) {
|
||||
rfbRef.current.viewOnly = viewOnly;
|
||||
rfbRef.current.focusOnClick = !viewOnly;
|
||||
rfbRef.current.dragViewport = !viewOnly;
|
||||
rfbRef.current.scaleViewport = autoScale;
|
||||
rfbRef.current.resizeSession = autoScale;
|
||||
}
|
||||
}, [viewOnly, autoScale]);
|
||||
|
||||
const menuItems: MenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: <div>挂载优化工具</div>,
|
||||
// label: (
|
||||
// <Button
|
||||
// onClick={() => {}}
|
||||
// disabled={connectionStatus === 'connecting'}
|
||||
// title="挂载优化工具"
|
||||
// >
|
||||
// 挂载优化工具
|
||||
// </Button>
|
||||
// ),
|
||||
},
|
||||
{
|
||||
key: '2', // 注意 key 应该唯一
|
||||
label: <div>挂载应用软件盘</div>,
|
||||
// label: (
|
||||
// <Button
|
||||
// onClick={() => {}}
|
||||
// disabled={connectionStatus === 'connecting'}
|
||||
// title="挂载应用软件盘"
|
||||
// >
|
||||
// 挂载应用软件盘
|
||||
// </Button>
|
||||
// ),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: <div>挂载应用软件盘</div>,
|
||||
// label: (
|
||||
// <Button
|
||||
// onClick={() => {}}
|
||||
// disabled={connectionStatus === 'connecting'}
|
||||
// title="挂载应用软件盘"
|
||||
// >
|
||||
// 挂载应用软件盘
|
||||
// </Button>
|
||||
// ),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`vnc-remote-desktop ${className} ${connectionStatus}`}>
|
||||
{/* 控制栏 */}
|
||||
<div className="vnc-controls">
|
||||
<span className={`vnc-status-indicator ${connectionStatus}`}>
|
||||
{connectionStatus === 'connecting' && '连接中...'}
|
||||
{connectionStatus === 'connected' && '已连接'}
|
||||
{connectionStatus === 'disconnected' && '已断开'}
|
||||
</span>
|
||||
|
||||
<div className="vnc-actions">
|
||||
{viewOnly && <span className="view-only-indicator">仅查看模式</span>}
|
||||
{!(connectionStatus === 'connected') ? (
|
||||
<Button
|
||||
onClick={reconnect}
|
||||
disabled={connectionStatus === 'connecting'}
|
||||
title="重新连接"
|
||||
>
|
||||
连接
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
disabled={!(connectionStatus === 'connected')}
|
||||
title="关机"
|
||||
>
|
||||
关机
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
disabled={!(connectionStatus === 'connected')}
|
||||
title="重启"
|
||||
>
|
||||
重启
|
||||
</Button>
|
||||
<Dropdown
|
||||
menu={menuItems}
|
||||
disabled={!(connectionStatus === 'connected')}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
disabled={!(connectionStatus === 'connected')}
|
||||
title="安装模板工具"
|
||||
>
|
||||
安装模板工具
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<Button
|
||||
onClick={disconnect}
|
||||
disabled={!(connectionStatus === 'connected')}
|
||||
title="断开连接"
|
||||
type="default"
|
||||
>
|
||||
断开连接
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 远程桌面内容 */}
|
||||
<div className="vnc-content">
|
||||
{/* 加载状态 */}
|
||||
{connectionStatus === 'connecting' && (
|
||||
<div className="vnc-loading-overlay">
|
||||
<div className="loading-spinner"></div>
|
||||
<div>{loadingText}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误状态 */}
|
||||
{errorMessage && connectionStatus === 'disconnected' && (
|
||||
<div className="vnc-error-overlay">
|
||||
<div className="error-icon">❌</div>
|
||||
<div className="error-message">{errorMessage}</div>
|
||||
<Button className="reconnect-button" onClick={reconnect}>
|
||||
重新连接
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* VNC画布,使用canvas远程桌面控制页面无法展示,不要使用 */}
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="vnc-canvas"
|
||||
tabIndex={viewOnly ? -1 : 0}
|
||||
style={{
|
||||
display: connectionStatus === 'connected' ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 未连接时的占位符 */}
|
||||
{connectionStatus === 'disconnected' && !errorMessage && (
|
||||
<div className="vnc-placeholder">
|
||||
<div className="placeholder-icon">🖥️</div>
|
||||
<div>等待连接...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VncRemoteDesktop;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# README
|
||||
|
||||
`noVnc` 从1.5版本及以上移除了core文件夹,且EMS支持有问题
|
||||
|
||||
解决方案:
|
||||
使用1.4版本或下载1.6版本源码使用
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* noVNC: HTML5 VNC client
|
||||
* Copyright (C) 2019 The noVNC authors
|
||||
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||
*
|
||||
* See README.md for usage and integration instructions.
|
||||
*/
|
||||
|
||||
// Fallback for all uncought errors
|
||||
function handleError(event, err) {
|
||||
try {
|
||||
const msg = document.getElementById('noVNC_fallback_errormsg');
|
||||
|
||||
// Work around Firefox bug:
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
|
||||
if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show the initial error
|
||||
if (msg.hasChildNodes()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.classList.add('noVNC_message');
|
||||
div.appendChild(document.createTextNode(event.message));
|
||||
msg.appendChild(div);
|
||||
|
||||
if (event.filename) {
|
||||
div = document.createElement("div");
|
||||
div.className = 'noVNC_location';
|
||||
let text = event.filename;
|
||||
if (event.lineno !== undefined) {
|
||||
text += ":" + event.lineno;
|
||||
if (event.colno !== undefined) {
|
||||
text += ":" + event.colno;
|
||||
}
|
||||
}
|
||||
div.appendChild(document.createTextNode(text));
|
||||
msg.appendChild(div);
|
||||
}
|
||||
|
||||
if (err && err.stack) {
|
||||
div = document.createElement("div");
|
||||
div.className = 'noVNC_stack';
|
||||
div.appendChild(document.createTextNode(err.stack));
|
||||
msg.appendChild(div);
|
||||
}
|
||||
|
||||
document.getElementById('noVNC_fallback_error')
|
||||
.classList.add("noVNC_open");
|
||||
|
||||
} catch (exc) {
|
||||
document.write("noVNC encountered an error.");
|
||||
}
|
||||
|
||||
// Try to disable keyboard interaction, best effort
|
||||
try {
|
||||
// Remove focus from the currently focused element in order to
|
||||
// prevent keyboard interaction from continuing
|
||||
if (document.activeElement) { document.activeElement.blur(); }
|
||||
|
||||
// Don't let any element be focusable when showing the error
|
||||
let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
|
||||
document.querySelectorAll(keyboardFocusable).forEach((elem) => {
|
||||
elem.setAttribute("tabindex", "-1");
|
||||
});
|
||||
} catch (exc) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Don't return true since this would prevent the error
|
||||
// from being printed to the browser console.
|
||||
return false;
|
||||
}
|
||||
|
||||
window.addEventListener('error', evt => handleError(evt, evt.error));
|
||||
window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="alt.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="18.205425"
|
||||
inkscape:cy="17.531398"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<g
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text5290">
|
||||
<path
|
||||
d="m 9.9560547,1042.3329 -2.9394531,0 -0.4638672,1.3281 -1.8896485,0 2.7001953,-7.29 2.241211,0 2.7001958,7.29 -1.889649,0 -0.4589843,-1.3281 z m -2.4707031,-1.3526 1.9970703,0 -0.9960938,-2.9003 -1.0009765,2.9003 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5340" />
|
||||
<path
|
||||
d="m 13.188477,1036.0634 1.748046,0 0,7.5976 -1.748046,0 0,-7.5976 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5342" />
|
||||
<path
|
||||
d="m 18.535156,1036.6395 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151367,0.5176 0.151368,0.1318 0.600586,0.1318 l 0.898438,0 0,1.25 -1.499024,0 q -1.035156,0 -1.469726,-0.4297 -0.429688,-0.4345 -0.429688,-1.4697 l 0,-2.3193 -0.86914,0 0,-1.25 0.86914,0 0,-1.5528 1.748047,0 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5344" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="clipboard.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="15.366606"
|
||||
inkscape:cy="16.42981"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 9,6 6,6 C 5.4459889,6 5,6.4459889 5,7 l 0,13 c 0,0.554011 0.4459889,1 1,1 l 13,0 c 0.554011,0 1,-0.445989 1,-1 L 20,7 C 20,6.4459889 19.554011,6 19,6 l -3,0"
|
||||
transform="translate(0,1027.3622)"
|
||||
id="rect6083"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cssssssssc" />
|
||||
<rect
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect6085"
|
||||
width="7"
|
||||
height="4"
|
||||
x="9"
|
||||
y="1031.3622"
|
||||
ry="1.00002" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
|
||||
d="m 8.5071212,1038.8622 7.9999998,0"
|
||||
id="path6087"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
|
||||
d="m 8.5071212,1041.8622 3.9999998,0"
|
||||
id="path6089"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.50196081"
|
||||
d="m 8.5071212,1044.8622 5.9999998,0"
|
||||
id="path6091"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="connect.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="37.14834"
|
||||
inkscape:cy="1.9525926"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<g
|
||||
id="g5103"
|
||||
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,-729.15757,315.8823)">
|
||||
<path
|
||||
sodipodi:nodetypes="cssssc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect5096"
|
||||
d="m 11,1040.3622 -5,0 c -1.108,0 -2,-0.892 -2,-2 l 0,-4 c 0,-1.108 0.892,-2 2,-2 l 5,0"
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 14,1032.3622 5,0 c 1.108,0 2,0.892 2,2 l 0,4 c 0,1.108 -0.892,2 -2,2 l -5,0"
|
||||
id="path5099"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cssssc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path5101"
|
||||
d="m 9,1036.3622 7,0"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ctrl.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="18.205425"
|
||||
inkscape:cy="17.531398"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<g
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text5290">
|
||||
<path
|
||||
d="m 9.1210938,1043.1898 q -0.5175782,0.2686 -1.0791016,0.4053 -0.5615235,0.1367 -1.171875,0.1367 -1.8212891,0 -2.8857422,-1.0156 -1.0644531,-1.0205 -1.0644531,-2.7637 0,-1.748 1.0644531,-2.7637 1.0644531,-1.0205 2.8857422,-1.0205 0.6103515,0 1.171875,0.1368 0.5615234,0.1367 1.0791016,0.4052 l 0,1.5088 q -0.522461,-0.3564 -1.0302735,-0.5224 -0.5078125,-0.1661 -1.0693359,-0.1661 -1.0058594,0 -1.5820313,0.6446 -0.5761719,0.6445 -0.5761719,1.7773 0,1.1279 0.5761719,1.7725 0.5761719,0.6445 1.5820313,0.6445 0.5615234,0 1.0693359,-0.166 0.5078125,-0.166 1.0302735,-0.5225 l 0,1.5088 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5370" />
|
||||
<path
|
||||
d="m 12.514648,1036.5687 0,1.5528 1.801758,0 0,1.25 -1.801758,0 0,2.3193 q 0,0.3809 0.151368,0.5176 0.151367,0.1318 0.600586,0.1318 l 0.898437,0 0,1.25 -1.499023,0 q -1.035157,0 -1.469727,-0.4297 -0.429687,-0.4345 -0.429687,-1.4697 l 0,-2.3193 -0.8691411,0 0,-1.25 0.8691411,0 0,-1.5528 1.748046,0 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5372" />
|
||||
<path
|
||||
d="m 19.453125,1039.6107 q -0.229492,-0.1074 -0.458984,-0.1562 -0.22461,-0.054 -0.454102,-0.054 -0.673828,0 -1.040039,0.4345 -0.361328,0.4297 -0.361328,1.2354 l 0,2.5195 -1.748047,0 0,-5.4687 1.748047,0 0,0.8984 q 0.336914,-0.5371 0.771484,-0.7813 0.439453,-0.249 1.049805,-0.249 0.08789,0 0.19043,0.01 0.102539,0 0.297851,0.029 l 0.0049,1.582 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5374" />
|
||||
<path
|
||||
d="m 20.332031,1035.9926 1.748047,0 0,7.5976 -1.748047,0 0,-7.5976 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5376" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ctrlaltdel.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="11.135667"
|
||||
inkscape:cy="16.407428"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect5253"
|
||||
width="5"
|
||||
height="5.0000172"
|
||||
x="16"
|
||||
y="1031.3622"
|
||||
ry="1.0000174" />
|
||||
<rect
|
||||
y="1043.3622"
|
||||
x="4"
|
||||
height="5.0000172"
|
||||
width="5"
|
||||
id="rect5255"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
ry="1.0000174" />
|
||||
<rect
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect5257"
|
||||
width="5"
|
||||
height="5.0000172"
|
||||
x="13"
|
||||
y="1043.3622"
|
||||
ry="1.0000174" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="disconnect.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="25.05707"
|
||||
inkscape:cy="11.594858"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<g
|
||||
id="g5171"
|
||||
transform="translate(-24.062499,-6.15775e-4)">
|
||||
<path
|
||||
id="path5110"
|
||||
transform="translate(0,1027.3622)"
|
||||
d="m 39.744141,3.4960938 c -0.769923,0 -1.539607,0.2915468 -2.121094,0.8730468 l -2.566406,2.5664063 1.414062,1.4140625 2.566406,-2.5664063 c 0.403974,-0.404 1.010089,-0.404 1.414063,0 l 2.828125,2.828125 c 0.40398,0.4039 0.403907,1.0101621 0,1.4140629 l -2.566406,2.566406 1.414062,1.414062 2.566406,-2.566406 c 1.163041,-1.1629 1.162968,-3.0791874 0,-4.2421874 L 41.865234,4.3691406 C 41.283747,3.7876406 40.514063,3.4960937 39.744141,3.4960938 Z M 39.017578,9.015625 a 1.0001,1.0001 0 0 0 -0.6875,0.3027344 l -0.445312,0.4453125 1.414062,1.4140621 0.445313,-0.445312 A 1.0001,1.0001 0 0 0 39.017578,9.015625 Z m -6.363281,0.7070312 a 1.0001,1.0001 0 0 0 -0.6875,0.3027348 L 28.431641,13.5625 c -1.163042,1.163 -1.16297,3.079187 0,4.242188 l 2.828125,2.828124 c 1.162974,1.163101 3.079213,1.163101 4.242187,0 l 3.535156,-3.535156 a 1.0001,1.0001 0 1 0 -1.414062,-1.414062 l -3.535156,3.535156 c -0.403974,0.404 -1.010089,0.404 -1.414063,0 l -2.828125,-2.828125 c -0.403981,-0.404 -0.403908,-1.010162 0,-1.414063 l 3.535156,-3.537109 A 1.0001,1.0001 0 0 0 32.654297,9.7226562 Z m 3.109375,2.1621098 -2.382813,2.384765 a 1.0001,1.0001 0 1 0 1.414063,1.414063 l 2.382812,-2.384766 -1.414062,-1.414062 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
|
||||
y="752.29541"
|
||||
x="-712.31262"
|
||||
height="18.000017"
|
||||
width="3"
|
||||
id="rect5116"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="drag.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="9.8789407"
|
||||
inkscape:cy="9.5008608"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 7.039733,1049.3037 c -0.4309106,-0.1233 -0.7932634,-0.4631 -0.9705434,-0.9103 -0.04922,-0.1241 -0.057118,-0.2988 -0.071321,-1.5771 l -0.015972,-1.4375 -0.328125,-0.082 c -0.7668138,-0.1927 -1.1897046,-0.4275 -1.7031253,-0.9457 -0.4586773,-0.4629 -0.6804297,-0.8433 -0.867034,-1.4875 -0.067215,-0.232 -0.068001,-0.2642 -0.078682,-3.2188 -0.012078,-3.341 -0.020337,-3.2012 0.2099452,-3.5555 0.2246623,-0.3458 0.5798271,-0.5892 0.9667343,-0.6626 0.092506,-0.017 0.531898,-0.032 0.9764271,-0.032 l 0.8082347,0 1.157e-4,1.336 c 1.125e-4,1.2779 0.00281,1.3403 0.062214,1.4378 0.091785,0.1505 0.2357707,0.226 0.4314082,0.2261 0.285389,2e-4 0.454884,-0.1352 0.5058962,-0.4042 0.019355,-0.102 0.031616,-0.982 0.031616,-2.269 0,-1.9756 0.00357,-2.1138 0.059205,-2.2926 0.1645475,-0.5287 0.6307616,-0.9246 1.19078,-1.0113 0.8000572,-0.1238 1.5711277,0.4446 1.6860387,1.2429 0.01732,0.1203 0.03177,0.8248 0.03211,1.5657 6.19e-4,1.3449 7.22e-4,1.347 0.07093,1.4499 0.108355,0.1587 0.255268,0.2248 0.46917,0.2108 0.204069,-0.013 0.316116,-0.08 0.413642,-0.2453 0.06028,-0.1024 0.06307,-0.1778 0.07862,-2.1218 0.01462,-1.8283 0.02124,-2.0285 0.07121,-2.1549 0.260673,-0.659 0.934894,-1.0527 1.621129,-0.9465 0.640523,0.099 1.152269,0.6104 1.243187,1.2421 0.01827,0.1269 0.03175,0.9943 0.03211,2.0657 l 6.19e-4,1.8469 0.07031,0.103 c 0.108355,0.1587 0.255267,0.2248 0.46917,0.2108 0.204069,-0.013 0.316115,-0.08 0.413642,-0.2453 0.05951,-0.1011 0.06329,-0.1786 0.07907,-1.6218 0.01469,-1.3438 0.02277,-1.5314 0.07121,-1.6549 0.257975,-0.6576 0.934425,-1.0527 1.620676,-0.9465 0.640522,0.099 1.152269,0.6104 1.243186,1.2421 0.0186,0.1292 0.03179,1.0759 0.03222,2.3125 7.15e-4,2.0335 0.0025,2.0966 0.06283,2.1956 0.09178,0.1505 0.235771,0.226 0.431409,0.2261 0.285388,2e-4 0.454884,-0.1352 0.505897,-0.4042 0.01874,-0.099 0.03161,-0.8192 0.03161,-1.769 0,-1.4848 0.0043,-1.6163 0.0592,-1.7926 0.164548,-0.5287 0.630762,-0.9246 1.19078,-1.0113 0.800057,-0.1238 1.571128,0.4446 1.686039,1.2429 0.04318,0.2999 0.04372,9.1764 5.78e-4,9.4531 -0.04431,0.2841 -0.217814,0.6241 -0.420069,0.8232 -0.320102,0.315 -0.63307,0.4268 -1.194973,0.4268 l -0.35281,0 -2.51e-4,1.2734 c -1.25e-4,0.7046 -0.01439,1.3642 -0.03191,1.4766 -0.06665,0.4274 -0.372966,0.8704 -0.740031,1.0702 -0.349999,0.1905 0.01748,0.18 -6.242199,0.1776 -5.3622439,0 -5.7320152,-0.01 -5.9121592,-0.057 l 1.4e-5,0 z"
|
||||
id="path4379"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="error.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="14.00357"
|
||||
inkscape:cy="12.443398"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 7 3 C 4.7839905 3 3 4.7839905 3 7 L 3 18 C 3 20.21601 4.7839905 22 7 22 L 18 22 C 20.21601 22 22 20.21601 22 18 L 22 7 C 22 4.7839905 20.21601 3 18 3 L 7 3 z M 7.6992188 6 A 1.6916875 1.6924297 0 0 1 8.9121094 6.5117188 L 12.5 10.101562 L 16.087891 6.5117188 A 1.6916875 1.6924297 0 0 1 17.251953 6 A 1.6916875 1.6924297 0 0 1 18.480469 8.90625 L 14.892578 12.496094 L 18.480469 16.085938 A 1.6916875 1.6924297 0 1 1 16.087891 18.478516 L 12.5 14.888672 L 8.9121094 18.478516 A 1.6916875 1.6924297 0 1 1 6.5214844 16.085938 L 10.109375 12.496094 L 6.5214844 8.90625 A 1.6916875 1.6924297 0 0 1 7.6992188 6 z "
|
||||
transform="translate(0,1027.3622)"
|
||||
id="rect4135" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="esc.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="18.205425"
|
||||
inkscape:cy="17.531398"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="text5290"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<g
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:48px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text5290">
|
||||
<path
|
||||
d="m 3.9331055,1036.1464 5.0732422,0 0,1.4209 -3.1933594,0 0,1.3574 3.0029297,0 0,1.4209 -3.0029297,0 0,1.6699 3.3007812,0 0,1.4209 -5.180664,0 0,-7.29 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5314" />
|
||||
<path
|
||||
d="m 14.963379,1038.1385 0,1.3282 q -0.561524,-0.2344 -1.083984,-0.3516 -0.522461,-0.1172 -0.986329,-0.1172 -0.498046,0 -0.742187,0.127 -0.239258,0.122 -0.239258,0.3808 0,0.21 0.180664,0.3223 0.185547,0.1123 0.65918,0.166 l 0.307617,0.044 q 1.342773,0.1709 1.806641,0.5615 0.463867,0.3906 0.463867,1.2256 0,0.874 -0.644531,1.3134 -0.644532,0.4395 -1.923829,0.4395 -0.541992,0 -1.123046,-0.088 -0.576172,-0.083 -1.186524,-0.2539 l 0,-1.3281 q 0.522461,0.2539 1.069336,0.3808 0.551758,0.127 1.118164,0.127 0.512695,0 0.771485,-0.1416 0.258789,-0.1416 0.258789,-0.4199 0,-0.2344 -0.180664,-0.3467 -0.175782,-0.1172 -0.708008,-0.1807 l -0.307617,-0.039 q -1.166993,-0.1465 -1.635743,-0.542 -0.46875,-0.3955 -0.46875,-1.2012 0,-0.8691 0.595703,-1.2891 0.595704,-0.4199 1.826172,-0.4199 0.483399,0 1.015625,0.073 0.532227,0.073 1.157227,0.2294 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5316" />
|
||||
<path
|
||||
d="m 21.066895,1038.1385 0,1.4258 q -0.356446,-0.2441 -0.717774,-0.3613 -0.356445,-0.1172 -0.742187,-0.1172 -0.732422,0 -1.142579,0.4297 -0.405273,0.4248 -0.405273,1.1914 0,0.7666 0.405273,1.1963 0.410157,0.4248 1.142579,0.4248 0.410156,0 0.776367,-0.1221 0.371094,-0.122 0.683594,-0.3613 l 0,1.4307 q -0.410157,0.1513 -0.834961,0.2246 -0.419922,0.078 -0.844727,0.078 -1.479492,0 -2.314453,-0.7568 -0.834961,-0.7618 -0.834961,-2.1143 0,-1.3525 0.834961,-2.1094 0.834961,-0.7617 2.314453,-0.7617 0.429688,0 0.844727,0.078 0.419921,0.073 0.834961,0.2246 z"
|
||||
style="font-size:10px;fill:#ffffff;fill-opacity:1"
|
||||
id="path5318" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="9"
|
||||
height="10"
|
||||
viewBox="0 0 9 10"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="expander.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="9.8737281"
|
||||
inkscape:cy="6.4583132"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-object-midpoints="false"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1042.3622)">
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 2.0800781,1042.3633 A 2.0002,2.0002 0 0 0 0,1044.3613 l 0,6 a 2.0002,2.0002 0 0 0 3.0292969,1.7168 l 5,-3 a 2.0002,2.0002 0 0 0 0,-3.4316 l -5,-3 a 2.0002,2.0002 0 0 0 -0.9492188,-0.2832 z"
|
||||
id="path4138"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="fullscreen.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="16.400723"
|
||||
inkscape:cy="15.083758"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<rect
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect5006"
|
||||
width="17"
|
||||
height="17.000017"
|
||||
x="4"
|
||||
y="1031.3622"
|
||||
ry="3.0000174" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 7.5,1044.8622 4,0 -1.5,-1.5 1.5,-1.5 -1,-1 -1.5,1.5 -1.5,-1.5 0,4 z"
|
||||
id="path5017"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path5025"
|
||||
d="m 17.5,1034.8622 -4,0 1.5,1.5 -1.5,1.5 1,1 1.5,-1.5 1.5,1.5 0,-4 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="5"
|
||||
height="6"
|
||||
viewBox="0 0 5 6"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="handle.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="1.3551778"
|
||||
inkscape:cy="8.7800329"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1046.3622)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 4.0000803,1049.3622 -3,-2 0,4 z"
|
||||
id="path4247"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,172 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="15"
|
||||
height="50"
|
||||
viewBox="0 0 15 50"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="handle_bg.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="-10.001409"
|
||||
inkscape:cy="24.512566"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1002.3622)">
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4249"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="9.5"
|
||||
y="1008.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1013.8622"
|
||||
x="9.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4255"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1008.8622"
|
||||
x="4.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4261"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4263"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="4.5"
|
||||
y="1013.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1039.8622"
|
||||
x="9.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4265"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4267"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="9.5"
|
||||
y="1044.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4269"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="4.5"
|
||||
y="1039.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1044.8622"
|
||||
x="4.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4271"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4273"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="9.5"
|
||||
y="1018.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1018.8622"
|
||||
x="4.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4275"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4277"
|
||||
width="1"
|
||||
height="1.0000174"
|
||||
x="9.5"
|
||||
y="1034.8622"
|
||||
ry="1.7382812e-05" />
|
||||
<rect
|
||||
ry="1.7382812e-05"
|
||||
y="1034.8622"
|
||||
x="4.5"
|
||||
height="1.0000174"
|
||||
width="1"
|
||||
id="rect4279"
|
||||
style="opacity:0.25;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -0,0 +1,42 @@
|
|||
BROWSER_SIZES := 16 24 32 48 64
|
||||
#ANDROID_SIZES := 72 96 144 192
|
||||
# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
|
||||
# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
|
||||
ANDROID_SIZES := 96 144 192
|
||||
WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
|
||||
|
||||
#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
|
||||
IOS_2X_SIZES := 40 58 80 120 152 167
|
||||
IOS_3X_SIZES := 60 87 120 180
|
||||
ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
|
||||
|
||||
ALL_ICONS := \
|
||||
$(ALL_IOS_SIZES:%=novnc-ios-%.png) \
|
||||
novnc.ico
|
||||
|
||||
all: $(ALL_ICONS)
|
||||
|
||||
# Our testing shows that the ICO file need to be sorted in largest to
|
||||
# smallest to get the apporpriate behviour
|
||||
WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
|
||||
WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
|
||||
.INTERMEDIATE: $(WEB_BASE_ICONS)
|
||||
|
||||
novnc.ico: $(WEB_BASE_ICONS)
|
||||
convert $(WEB_BASE_ICONS) "$@"
|
||||
|
||||
# General conversion
|
||||
novnc-%.png: novnc-icon.svg
|
||||
convert -depth 8 -background transparent \
|
||||
-size $*x$* "$(lastword $^)" "$@"
|
||||
|
||||
# iOS icons use their own SVG
|
||||
novnc-ios-%.png: novnc-ios-icon.svg
|
||||
convert -depth 8 -background transparent \
|
||||
-size $*x$* "$(lastword $^)" "$@"
|
||||
|
||||
# The smallest sizes are generated using a different SVG
|
||||
novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
|
||||
|
||||
clean:
|
||||
rm -f *.png
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="novnc-icon-sm.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="45.254834"
|
||||
inkscape:cx="9.722703"
|
||||
inkscape:cy="5.5311896"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4169" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1036.3621)">
|
||||
<rect
|
||||
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4167"
|
||||
width="16"
|
||||
height="15.999992"
|
||||
x="0"
|
||||
y="1036.3622"
|
||||
ry="2.6666584" />
|
||||
<path
|
||||
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 2.6666667,1036.3621 C 1.1893373,1036.3621 0,1037.5515 0,1039.0288 l 0,10.6666 c 0,1.4774 1.1893373,2.6667 2.6666667,2.6667 l 4,0 C 11.837333,1052.3621 16,1046.7128 16,1039.6955 l 0,-0.6667 c 0,-1.4773 -1.189337,-2.6667 -2.666667,-2.6667 l -10.6666663,0 z"
|
||||
id="rect4173"
|
||||
inkscape:connector-curvature="0" />
|
||||
<g
|
||||
id="g4381">
|
||||
<g
|
||||
transform="translate(0.25,0.25)"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
id="g4365">
|
||||
<g
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
id="g4367">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4369"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 C 2.011349,1040.3621 2,1040.3741 2,1040.3981 l 0,2.964 -1,0 0,-4 z"
|
||||
sodipodi:nodetypes="scsccsssscccs" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4371"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss" />
|
||||
</g>
|
||||
<g
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
id="g4373">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4375"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4377"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
|
||||
sodipodi:nodetypes="ccccccccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4379"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
|
||||
sodipodi:nodetypes="cssssccscsscscc" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g4356">
|
||||
<g
|
||||
id="g4347">
|
||||
<path
|
||||
sodipodi:nodetypes="scsccsssscccs"
|
||||
d="m 4.3289754,1039.3621 c 0.1846149,0 0.3419956,0.071 0.4716623,0.2121 C 4.933546,1039.7121 5,1039.8793 5,1040.0759 l 0,3.2862 -1,0 0,-2.964 c 0,-0.024 -0.011592,-0.036 -0.034038,-0.036 l -1.931924,0 c -0.022689,0 -0.034038,0.012 -0.034038,0.036 l 0,2.964 -1,0 0,-4 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4143"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss"
|
||||
d="m 6.6710244,1039.3621 2.6579513,0 c 0.184775,0 0.3419957,0.071 0.471662,0.2121 C 9.933546,1039.7121 10,1039.8793 10,1040.0759 l 0,2.5724 c 0,0.1966 -0.066454,0.3655 -0.1993623,0.5069 -0.1296663,0.1379 -0.286887,0.2069 -0.471662,0.2069 l -2.6579513,0 c -0.184775,0 -0.3436164,-0.069 -0.4765247,-0.2069 C 6.0648334,1043.0138 6,1042.8449 6,1042.6483 l 0,-2.5724 c 0,-0.1966 0.064833,-0.3638 0.1944997,-0.5017 0.1329083,-0.1414 0.2917497,-0.2121 0.4765247,-0.2121 z m 2.2949386,1 -1.931926,0 C 7.011344,1040.3621 7,1040.3741 7,1040.3981 l 0,1.928 c 0,0.024 0.011347,0.036 0.034037,0.036 l 1.931926,0 c 0.02269,0 0.034037,-0.012 0.034037,-0.036 l 0,-1.928 c 0,-0.024 -0.011347,-0.036 -0.034037,-0.036 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4145"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g4351">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
d="m 3,1047.1121 1,-2.75 1,0 -1.5,4 -1,0 -1.5,-4 1,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4147"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccccc"
|
||||
d="m 9,1046.8621 0,-2.5 1,0 0,4 -1,0 -2,-2.5 0,2.5 -1,0 0,-4 1,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4149"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="cssssccscsscscc"
|
||||
d="m 15,1045.3621 -2.96596,0 c -0.02269,0 -0.03404,0.012 -0.03404,0.036 l 0,1.928 c 0,0.024 0.01135,0.036 0.03404,0.036 l 2.96596,0 0,1 -3.324113,0 c -0.188017,0 -0.348479,-0.068 -0.481388,-0.2037 C 11.064833,1048.0192 11,1047.8511 11,1047.6542 l 0,-2.5842 c 0,-0.1969 0.06483,-0.3633 0.194499,-0.4991 0.132909,-0.1392 0.293371,-0.2088 0.481388,-0.2088 l 3.324113,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4151"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,163 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48.000001"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="novnc-icon.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.313708"
|
||||
inkscape:cx="27.187245"
|
||||
inkscape:cy="17.700974"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4169" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1004.3621)">
|
||||
<rect
|
||||
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4167"
|
||||
width="48"
|
||||
height="48"
|
||||
x="0"
|
||||
y="1004.3621"
|
||||
ry="7.9999785" />
|
||||
<path
|
||||
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 8,1004.3621 c -4.4319881,0 -8,3.568 -8,8 l 0,32 c 0,4.432 3.5680119,8 8,8 l 12,0 c 15.512,0 28,-16.948 28,-38 l 0,-2 c 0,-4.432 -3.568012,-8 -8,-8 l -32,0 z"
|
||||
id="rect4173"
|
||||
inkscape:connector-curvature="0" />
|
||||
<g
|
||||
id="g4300"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
transform="translate(0.5,0.5)">
|
||||
<g
|
||||
id="g4302"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none">
|
||||
<path
|
||||
sodipodi:nodetypes="scsccsssscccs"
|
||||
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4304"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss"
|
||||
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4306"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g
|
||||
id="g4308"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4310"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccccc"
|
||||
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4312"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
sodipodi:nodetypes="cssssccscsscscc"
|
||||
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4314"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g4291"
|
||||
style="stroke:none">
|
||||
<g
|
||||
id="g4282"
|
||||
style="stroke:none">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4143"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
|
||||
sodipodi:nodetypes="scsccsssscccs" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4145"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss" />
|
||||
</g>
|
||||
<g
|
||||
id="g4286"
|
||||
style="stroke:none">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4147"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4149"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
|
||||
sodipodi:nodetypes="ccccccccccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4151"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
|
||||
sodipodi:nodetypes="cssssccscsscscc" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48.000001"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="novnc-ios-icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.313708"
|
||||
inkscape:cx="27.356195"
|
||||
inkscape:cy="17.810253"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4169" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1004.3621)">
|
||||
<rect
|
||||
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4167"
|
||||
width="48"
|
||||
height="48"
|
||||
x="0"
|
||||
y="1004.3621"
|
||||
inkscape:label="background" />
|
||||
<path
|
||||
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 0,1004.3621 v 48 h 20 c 15.512,0 28,-16.948 28,-38 v -10 z"
|
||||
id="rect4173"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:label="darker_grey_plate" />
|
||||
<g
|
||||
id="g4300"
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke:none"
|
||||
transform="translate(0.5,0.5)"
|
||||
inkscape:label="shadows">
|
||||
<g
|
||||
id="g4302"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
inkscape:label="no">
|
||||
<path
|
||||
sodipodi:nodetypes="scsccsssscccs"
|
||||
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4304"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="n" />
|
||||
<path
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss"
|
||||
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4306"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="o" />
|
||||
</g>
|
||||
<g
|
||||
id="g4308"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
inkscape:label="VNC">
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4310"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="V" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccccccccc"
|
||||
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4312"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="N" />
|
||||
<path
|
||||
sodipodi:nodetypes="cssssccscsscscc"
|
||||
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4314"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:label="C" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g4291"
|
||||
style="stroke:none"
|
||||
inkscape:label="noVNC">
|
||||
<g
|
||||
id="g4282"
|
||||
style="stroke:none"
|
||||
inkscape:label="no">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4143"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
|
||||
sodipodi:nodetypes="scsccsssscccs"
|
||||
inkscape:label="n" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4145"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
|
||||
sodipodi:nodetypes="sscsscsscsscssssssssss"
|
||||
inkscape:label="o" />
|
||||
</g>
|
||||
<g
|
||||
id="g4286"
|
||||
style="stroke:none"
|
||||
inkscape:label="VNC">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4147"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
|
||||
sodipodi:nodetypes="cccccccc"
|
||||
inkscape:label="V" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4149"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
|
||||
sodipodi:nodetypes="ccccccccccc"
|
||||
inkscape:label="N" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4151"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
|
||||
sodipodi:nodetypes="cssssccscsscscc"
|
||||
inkscape:label="C" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="info.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="15.720838"
|
||||
inkscape:cy="8.9111233"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 12.5 3 A 9.5 9.4999914 0 0 0 3 12.5 A 9.5 9.4999914 0 0 0 12.5 22 A 9.5 9.4999914 0 0 0 22 12.5 A 9.5 9.4999914 0 0 0 12.5 3 z M 12.5 5 A 1.5 1.5000087 0 0 1 14 6.5 A 1.5 1.5000087 0 0 1 12.5 8 A 1.5 1.5000087 0 0 1 11 6.5 A 1.5 1.5000087 0 0 1 12.5 5 z M 10.521484 8.9785156 L 12.521484 8.9785156 A 1.50015 1.50015 0 0 1 14.021484 10.478516 L 14.021484 15.972656 A 1.50015 1.50015 0 0 1 14.498047 18.894531 C 14.498047 18.894531 13.74301 19.228309 12.789062 18.912109 C 12.312092 18.754109 11.776235 18.366625 11.458984 17.828125 C 11.141734 17.289525 11.021484 16.668469 11.021484 15.980469 L 11.021484 11.980469 L 10.521484 11.980469 A 1.50015 1.50015 0 1 1 10.521484 8.9804688 L 10.521484 8.9785156 z "
|
||||
transform="translate(0,1027.3622)"
|
||||
id="path4136" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="keyboard.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/keyboard.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#717171"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="31.285341"
|
||||
inkscape:cy="8.8028469"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:snap-smooth-nodes="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 7,3 C 4.8012876,3 3,4.8013 3,7 3,11.166667 3,15.333333 3,19.5 3,20.8764 4.1236413,22 5.5,22 l 14,0 C 20.876358,22 22,20.8764 22,19.5 22,15.333333 22,11.166667 22,7 22,4.8013 20.198712,3 18,3 Z m 0,2 11,0 c 1.125307,0 2,0.8747 2,2 L 20,12 5,12 5,7 C 5,5.8747 5.8746931,5 7,5 Z M 6.5,14 C 6.777,14 7,14.223 7,14.5 7,14.777 6.777,15 6.5,15 6.223,15 6,14.777 6,14.5 6,14.223 6.223,14 6.5,14 Z m 2,0 C 8.777,14 9,14.223 9,14.5 9,14.777 8.777,15 8.5,15 8.223,15 8,14.777 8,14.5 8,14.223 8.223,14 8.5,14 Z m 2,0 C 10.777,14 11,14.223 11,14.5 11,14.777 10.777,15 10.5,15 10.223,15 10,14.777 10,14.5 10,14.223 10.223,14 10.5,14 Z m 2,0 C 12.777,14 13,14.223 13,14.5 13,14.777 12.777,15 12.5,15 12.223,15 12,14.777 12,14.5 12,14.223 12.223,14 12.5,14 Z m 2,0 C 14.777,14 15,14.223 15,14.5 15,14.777 14.777,15 14.5,15 14.223,15 14,14.777 14,14.5 14,14.223 14.223,14 14.5,14 Z m 2,0 C 16.777,14 17,14.223 17,14.5 17,14.777 16.777,15 16.5,15 16.223,15 16,14.777 16,14.5 16,14.223 16.223,14 16.5,14 Z m 2,0 C 18.777,14 19,14.223 19,14.5 19,14.777 18.777,15 18.5,15 18.223,15 18,14.777 18,14.5 18,14.223 18.223,14 18.5,14 Z m -13,2 C 5.777,16 6,16.223 6,16.5 6,16.777 5.777,17 5.5,17 5.223,17 5,16.777 5,16.5 5,16.223 5.223,16 5.5,16 Z m 2,0 C 7.777,16 8,16.223 8,16.5 8,16.777 7.777,17 7.5,17 7.223,17 7,16.777 7,16.5 7,16.223 7.223,16 7.5,16 Z m 2,0 C 9.777,16 10,16.223 10,16.5 10,16.777 9.777,17 9.5,17 9.223,17 9,16.777 9,16.5 9,16.223 9.223,16 9.5,16 Z m 2,0 C 11.777,16 12,16.223 12,16.5 12,16.777 11.777,17 11.5,17 11.223,17 11,16.777 11,16.5 11,16.223 11.223,16 11.5,16 Z m 2,0 C 13.777,16 14,16.223 14,16.5 14,16.777 13.777,17 13.5,17 13.223,17 13,16.777 13,16.5 13,16.223 13.223,16 13.5,16 Z m 2,0 C 15.777,16 16,16.223 16,16.5 16,16.777 15.777,17 15.5,17 15.223,17 15,16.777 15,16.5 15,16.223 15.223,16 15.5,16 Z m 2,0 C 17.777,16 18,16.223 18,16.5 18,16.777 17.777,17 17.5,17 17.223,17 17,16.777 17,16.5 17,16.223 17.223,16 17.5,16 Z m 2,0 C 19.777,16 20,16.223 20,16.5 20,16.777 19.777,17 19.5,17 19.223,17 19,16.777 19,16.5 19,16.223 19.223,16 19.5,16 Z M 6,18 c 0.554,0 1,0.446 1,1 0,0.554 -0.446,1 -1,1 -0.554,0 -1,-0.446 -1,-1 0,-0.554 0.446,-1 1,-1 z m 2.8261719,0 7.3476561,0 C 16.631643,18 17,18.368372 17,18.826172 l 0,0.347656 C 17,19.631628 16.631643,20 16.173828,20 L 8.8261719,20 C 8.3683573,20 8,19.631628 8,19.173828 L 8,18.826172 C 8,18.368372 8.3683573,18 8.8261719,18 Z m 10.1113281,0 0.125,0 C 19.581551,18 20,18.4184 20,18.9375 l 0,0.125 C 20,19.5816 19.581551,20 19.0625,20 l -0.125,0 C 18.418449,20 18,19.5816 18,19.0625 l 0,-0.125 C 18,18.4184 18.418449,18 18.9375,18 Z"
|
||||
transform="translate(0,1027.3622)"
|
||||
id="rect4160"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sccssccsssssccssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="m 12.499929,1033.8622 -2,2 1.500071,0 0,2 1,0 0,-2 1.499929,0 z"
|
||||
id="path4150"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="power.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="9.3159849"
|
||||
inkscape:cy="13.436208"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 9 6.8183594 C 6.3418164 8.1213032 4.5 10.849161 4.5 14 C 4.5 18.4065 8.0935666 22 12.5 22 C 16.906433 22 20.5 18.4065 20.5 14 C 20.5 10.849161 18.658184 8.1213032 16 6.8183594 L 16 9.125 C 17.514327 10.211757 18.5 11.984508 18.5 14 C 18.5 17.3256 15.825553 20 12.5 20 C 9.1744469 20 6.5 17.3256 6.5 14 C 6.5 11.984508 7.4856727 10.211757 9 9.125 L 9 6.8183594 z "
|
||||
transform="translate(0,1027.3622)"
|
||||
id="path6140" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 12.5,1031.8836 0,6.4786"
|
||||
id="path6142"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="settings.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="14.69683"
|
||||
inkscape:cy="8.8039511"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 11 3 L 11 5.1601562 A 7.5 7.5 0 0 0 8.3671875 6.2460938 L 6.84375 4.7226562 L 4.7226562 6.84375 L 6.2480469 8.3691406 A 7.5 7.5 0 0 0 5.1523438 11 L 3 11 L 3 14 L 5.1601562 14 A 7.5 7.5 0 0 0 6.2460938 16.632812 L 4.7226562 18.15625 L 6.84375 20.277344 L 8.3691406 18.751953 A 7.5 7.5 0 0 0 11 19.847656 L 11 22 L 14 22 L 14 19.839844 A 7.5 7.5 0 0 0 16.632812 18.753906 L 18.15625 20.277344 L 20.277344 18.15625 L 18.751953 16.630859 A 7.5 7.5 0 0 0 19.847656 14 L 22 14 L 22 11 L 19.839844 11 A 7.5 7.5 0 0 0 18.753906 8.3671875 L 20.277344 6.84375 L 18.15625 4.7226562 L 16.630859 6.2480469 A 7.5 7.5 0 0 0 14 5.1523438 L 14 3 L 11 3 z M 12.5 10 A 2.5 2.5 0 0 1 15 12.5 A 2.5 2.5 0 0 1 12.5 15 A 2.5 2.5 0 0 1 10 12.5 A 2.5 2.5 0 0 1 12.5 10 z "
|
||||
transform="translate(0,1027.3622)"
|
||||
id="rect4967" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="tab.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="11.67335"
|
||||
inkscape:cy="17.881696"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 3,1031.3622 0,8 2,0 0,-4 0,-4 -2,0 z m 2,4 4,4 0,-3 13,0 0,-2 -13,0 0,-3 -4,4 z"
|
||||
id="rect5194"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
id="path5211"
|
||||
d="m 22,1048.3622 0,-8 -2,0 0,4 0,4 2,0 z m -2,-4 -4,-4 0,3 -13,0 0,2 13,0 0,3 4,-4 z"
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="extrakeys.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="15.234555"
|
||||
inkscape:cy="9.9710826"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="m 8,1031.3622 c -2.1987124,0 -4,1.8013 -4,4 l 0,8.9996 c 0,2.1987 1.8012876,4 4,4 l 9,0 c 2.198712,0 4,-1.8013 4,-4 l 0,-8.9996 c 0,-2.1987 -1.801288,-4 -4,-4 z m 0,2 9,0 c 1.125307,0 2,0.8747 2,2 l 0,7.0005 c 0,1.1253 -0.874693,2 -2,2 l -9,0 c -1.1253069,0 -2,-0.8747 -2,-2 l 0,-7.0005 c 0,-1.1253 0.8746931,-2 2,-2 z"
|
||||
id="rect5006"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ssssssssssssssssss" />
|
||||
<g
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'DejaVu Sans';-inkscape-font-specification:'Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text4167"
|
||||
transform="matrix(0.96021948,0,0,0.96021948,0.18921715,41.80659)">
|
||||
<path
|
||||
d="m 14.292969,1040.6791 -2.939453,0 -0.463868,1.3281 -1.889648,0 2.700195,-7.29 2.241211,0 2.700196,7.29 -1.889649,0 -0.458984,-1.3281 z m -2.470703,-1.3526 1.99707,0 -0.996094,-2.9004 -1.000976,2.9004 z"
|
||||
id="path4172"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="warning.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="16.457343"
|
||||
inkscape:cy="12.179552"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
showguides="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:snap-nodes="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4136" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1027.3622)">
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 12.513672 3.0019531 C 11.751609 2.9919531 11.052563 3.4242687 10.710938 4.1054688 L 3.2109375 19.105469 C 2.5461937 20.435369 3.5132277 21.9999 5 22 L 20 22 C 21.486772 21.9999 22.453806 20.435369 21.789062 19.105469 L 14.289062 4.1054688 C 13.951849 3.4330688 13.265888 3.0066531 12.513672 3.0019531 z M 12.478516 6.9804688 A 1.50015 1.50015 0 0 1 14 8.5 L 14 14.5 A 1.50015 1.50015 0 1 1 11 14.5 L 11 8.5 A 1.50015 1.50015 0 0 1 12.478516 6.9804688 z M 12.5 17 A 1.5 1.5 0 0 1 14 18.5 A 1.5 1.5 0 0 1 12.5 20 A 1.5 1.5 0 0 1 11 18.5 A 1.5 1.5 0 0 1 12.5 17 z "
|
||||
transform="translate(0,1027.3622)"
|
||||
id="path4208" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
sodipodi:docname="windows.svg"
|
||||
inkscape:export-filename="/home/ossman/devel/noVNC/images/drag.png"
|
||||
inkscape:version="0.92.4 (unknown)"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="-293 384 25 25"
|
||||
xml:space="preserve"
|
||||
width="25"
|
||||
height="25"><metadata
|
||||
id="metadata21"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs19" /><sodipodi:namedview
|
||||
pagecolor="#959595"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1136"
|
||||
id="namedview17"
|
||||
showgrid="true"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="3.926913"
|
||||
inkscape:cy="13.255959"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2"><inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid818" /></sodipodi:namedview>
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
|
||||
<path
|
||||
style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
|
||||
d="M 21 4 L 11 5.1757812 L 11 12 L 21 12 L 21 4 z M 10 5.2949219 L 4 6 L 4 12 L 10 12 L 10 5.2949219 z "
|
||||
transform="translate(-293,384)"
|
||||
id="path853" /><path
|
||||
id="path858"
|
||||
d="m -272,405 -10,-1.17578 V 397 h 10 z M -283,403.70508 -289,403 v -6 h 6 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" /></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |