feat: 状态监视器添加IP地址显示
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
||||||
import { settingsService } from '../services/settings.service'; // <-- Import settingsService
|
import { settingsService } from '../services/settings.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IP 黑名单检查中间件
|
* IP 黑名单检查中间件
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as ConnectionService from '../services/connection.service';
|
import * as ConnectionService from '../services/connection.service';
|
||||||
import * as SshService from '../services/ssh.service';
|
import * as SshService from '../services/ssh.service';
|
||||||
import * as GuacamoleService from '../services/guacamole.service'; // 导入 GuacamoleService
|
import * as GuacamoleService from '../services/guacamole.service';
|
||||||
import * as ImportExportService from '../services/import-export.service';
|
import * as ImportExportService from '../services/import-export.service';
|
||||||
import * as ConnectionRepository from '../repositories/connection.repository'; // +++ 导入 ConnectionRepository +++
|
import * as ConnectionRepository from '../repositories/connection.repository';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
testUnsavedConnection,
|
testUnsavedConnection,
|
||||||
exportConnections,
|
exportConnections,
|
||||||
importConnections,
|
importConnections,
|
||||||
getRdpSessionToken, // Import the new controller function
|
getRdpSessionToken,
|
||||||
getVncSessionToken, // Import the VNC session token controller function
|
getVncSessionToken,
|
||||||
cloneConnection, // +++ Import the clone controller function +++
|
cloneConnection,
|
||||||
// updateConnectionTags, // No longer directly used by primary flow
|
|
||||||
addTagToConnections // +++ Import the new controller function for adding tag to multiple connections +++
|
addTagToConnections
|
||||||
} from './connections.controller';
|
} from './connections.controller';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Database } from 'sqlite3';
|
|||||||
import * as schemaSql from './schema';
|
import * as schemaSql from './schema';
|
||||||
import * as appearanceRepository from '../repositories/appearance.repository';
|
import * as appearanceRepository from '../repositories/appearance.repository';
|
||||||
import * as terminalThemeRepository from '../repositories/terminal-theme.repository';
|
import * as terminalThemeRepository from '../repositories/terminal-theme.repository';
|
||||||
import * as settingsRepository from '../repositories/settings.repository'; // <-- Import settings repository
|
import * as settingsRepository from '../repositories/settings.repository';
|
||||||
import { presetTerminalThemes } from '../config/preset-themes-definition';
|
import { presetTerminalThemes } from '../config/preset-themes-definition';
|
||||||
import { runDb } from './connection'; // Import runDb for init functions
|
import { runDb } from './connection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface describing a database table definition for initialization.
|
* Interface describing a database table definition for initialization.
|
||||||
@@ -12,12 +12,12 @@ import { runDb } from './connection'; // Import runDb for init functions
|
|||||||
export interface TableDefinition {
|
export interface TableDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
sql: string;
|
sql: string;
|
||||||
init?: (db: Database) => Promise<void>; // Optional initialization function
|
init?: (db: Database) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initialization Functions ---
|
|
||||||
|
|
||||||
// Remove the old initSettingsTable function, as the logic is now in the repository
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes preset terminal themes.
|
* Initializes preset terminal themes.
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ if (dataConfigResultGlobal.error && (dataConfigResultGlobal.error as NodeJS.Errn
|
|||||||
} else if (!dataConfigResultGlobal.error) {
|
} else if (!dataConfigResultGlobal.error) {
|
||||||
console.log(`[ENV Init Early] Loaded environment variables from data .env file: ${dataEnvPathGlobal}`);
|
console.log(`[ENV Init Early] Loaded environment variables from data .env file: ${dataEnvPathGlobal}`);
|
||||||
}
|
}
|
||||||
// --- 结束环境变量的早期加载 ---
|
|
||||||
|
|
||||||
import express = require('express');
|
import express = require('express');
|
||||||
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
// import fs from 'fs'; // Moved up
|
|
||||||
// import path from 'path'; // Moved up
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
// import dotenv from 'dotenv'; // Moved up
|
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import sessionFileStore from 'session-file-store';
|
import sessionFileStore from 'session-file-store';
|
||||||
import { getDbInstance } from './database/connection';
|
import { getDbInstance } from './database/connection';
|
||||||
@@ -50,18 +50,18 @@ import commandHistoryRoutes from './command-history/command-history.routes';
|
|||||||
import quickCommandsRoutes from './quick-commands/quick-commands.routes';
|
import quickCommandsRoutes from './quick-commands/quick-commands.routes';
|
||||||
import terminalThemeRoutes from './terminal-themes/terminal-theme.routes';
|
import terminalThemeRoutes from './terminal-themes/terminal-theme.routes';
|
||||||
import appearanceRoutes from './appearance/appearance.routes';
|
import appearanceRoutes from './appearance/appearance.routes';
|
||||||
import sshKeysRouter from './ssh_keys/ssh_keys.routes'; // +++ Import SSH Key routes +++
|
import sshKeysRouter from './ssh_keys/ssh_keys.routes';
|
||||||
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes'; // +++ Import Quick Command Tag routes +++
|
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes';
|
||||||
import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes'; // +++ Import SSH Suspend routes +++
|
import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes';
|
||||||
import { initializeWebSocket } from './websocket';
|
import { initializeWebSocket } from './websocket';
|
||||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
||||||
|
|
||||||
// --- 初始化通知系统 (导入即初始化单例) ---
|
|
||||||
import './services/event.service'; // 确保事件服务被加载
|
import './services/event.service';
|
||||||
import './services/notification.processor.service'; // 确保处理器被加载并监听事件
|
import './services/notification.processor.service';
|
||||||
import './services/notification.dispatcher.service'; // 确保分发器被加载并监听处理器事件
|
import './services/notification.dispatcher.service';
|
||||||
// --- 结束通知系统初始化 ---
|
|
||||||
// --- 环境变量和密钥初始化 ---
|
|
||||||
|
|
||||||
// --- 全局错误处理 ---
|
// --- 全局错误处理 ---
|
||||||
// 捕获未处理的 Promise Rejection
|
// 捕获未处理的 Promise Rejection
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export const assignTagToCommands = async (req: Request, res: Response): Promise<
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用 Service 函数处理批量分配
|
// 调用 Service 函数处理批量分配
|
||||||
console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); // +++ 添加日志 +++
|
console.log(`[Controller] assignTagToCommands: Received commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
|
||||||
await QuickCommandsService.assignTagToCommands(commandIds, tagId);
|
await QuickCommandsService.assignTagToCommands(commandIds, tagId);
|
||||||
res.status(200).json({ success: true, message: `标签 ${tagId} 已成功尝试关联到 ${commandIds.length} 个指令。` });
|
res.status(200).json({ success: true, message: `标签 ${tagId} 已成功尝试关联到 ${commandIds.length} 个指令。` });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -261,9 +261,9 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
|||||||
statusMonitorIntervalSeconds: '3',
|
statusMonitorIntervalSeconds: '3',
|
||||||
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure),
|
[SIDEBAR_CONFIG_KEY]: JSON.stringify(defaultSidebarPanesStructure),
|
||||||
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings),
|
[CAPTCHA_CONFIG_KEY]: JSON.stringify(defaultCaptchaSettings),
|
||||||
timezone: 'UTC', // 添加时区默认值
|
timezone: 'UTC', // 时区默认值
|
||||||
terminalScrollbackLimit: '5000', // 添加终端回滚行数默认值
|
terminalScrollbackLimit: '5000', // 终端回滚行数默认值
|
||||||
terminalEnableRightClickPaste: 'true', // 添加终端右键粘贴默认值
|
terminalEnableRightClickPaste: 'true', // 终端右键粘贴默认值
|
||||||
};
|
};
|
||||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||||
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
|
const sqlInsertOrIgnore = `INSERT OR IGNORE INTO settings (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import * as ConnectionRepository from '../repositories/connection.repository';
|
import * as ConnectionRepository from '../repositories/connection.repository';
|
||||||
import { encrypt, decrypt } from '../utils/crypto';
|
import { encrypt, decrypt } from '../utils/crypto';
|
||||||
import { AuditLogService } from './audit.service';
|
import { AuditLogService } from './audit.service';
|
||||||
import * as SshKeyService from './ssh_key.service'; // +++ Import SshKeyService +++
|
import * as SshKeyService from './ssh_key.service';
|
||||||
import {
|
import {
|
||||||
ConnectionBase,
|
ConnectionBase,
|
||||||
ConnectionWithTags,
|
ConnectionWithTags,
|
||||||
CreateConnectionInput,
|
CreateConnectionInput,
|
||||||
UpdateConnectionInput,
|
UpdateConnectionInput,
|
||||||
FullConnectionData
|
FullConnectionData
|
||||||
} from '../types/connection.types'; // 从集中类型文件导入
|
} from '../types/connection.types';
|
||||||
|
|
||||||
export type { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput };
|
export type { ConnectionBase, ConnectionWithTags, CreateConnectionInput, UpdateConnectionInput };
|
||||||
|
|
||||||
|
|
||||||
const auditLogService = new AuditLogService(); // 实例化 AuditLogService
|
const auditLogService = new AuditLogService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有连接(包含标签)
|
* 获取所有连接(包含标签)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
import * as ConnectionRepository from '../repositories/connection.repository';
|
import * as ConnectionRepository from '../repositories/connection.repository';
|
||||||
import * as ProxyRepository from '../repositories/proxy.repository';
|
import * as ProxyRepository from '../repositories/proxy.repository';
|
||||||
import * as TagService from '../services/tag.service'; // +++ 导入标签服务 +++
|
import * as TagService from '../services/tag.service';
|
||||||
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||||
import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto'; // For decrypting connection details
|
import { decrypt, getEncryptionKeyBuffer as getCryptoKeyBuffer } from '../utils/crypto';
|
||||||
import { getAllDecryptedSshKeys, DecryptedSshKeyDetails } from '../services/ssh_key.service'; // 静态导入, SshKeyData -> DecryptedSshKeyDetails
|
import { getAllDecryptedSshKeys, DecryptedSshKeyDetails } from '../services/ssh_key.service';
|
||||||
import archiver from 'archiver';
|
import archiver from 'archiver';
|
||||||
archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted"));
|
archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted"));
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted"));
|
|||||||
|
|
||||||
interface ImportedConnectionData {
|
interface ImportedConnectionData {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'SSH' | 'RDP' | 'VNC'; // Add type field
|
type: 'SSH' | 'RDP' | 'VNC';
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import notificationProcessorService, { ProcessedNotification } from './notification.processor.service'; // 导入导出的接口
|
import notificationProcessorService, { ProcessedNotification } from './notification.processor.service';
|
||||||
import { NotificationChannelType, NotificationChannelConfig } from '../types/notification.types';
|
import { NotificationChannelType, NotificationChannelConfig } from '../types/notification.types';
|
||||||
|
|
||||||
// 1. 定义通知发送器接口
|
// 1. 定义通知发送器接口
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as QuickCommandsRepository from '../repositories/quick-commands.repository';
|
import * as QuickCommandsRepository from '../repositories/quick-commands.repository';
|
||||||
import { QuickCommandWithTags } from '../repositories/quick-commands.repository'; // Import the type with tags
|
import { QuickCommandWithTags } from '../repositories/quick-commands.repository';
|
||||||
import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository'; // Import the new tag repository
|
import * as QuickCommandTagRepository from '../repositories/quick-command-tag.repository';
|
||||||
|
|
||||||
// 定义排序类型
|
// 定义排序类型
|
||||||
export type QuickCommandSortBy = 'name' | 'usage_count';
|
export type QuickCommandSortBy = 'name' | 'usage_count';
|
||||||
@@ -119,7 +119,7 @@ export const assignTagToCommands = async (commandIds: number[], tagId: number):
|
|||||||
|
|
||||||
// 调用 Repository 函数执行批量关联
|
// 调用 Repository 函数执行批量关联
|
||||||
// 注意:这里需要导入 QuickCommandTagRepository
|
// 注意:这里需要导入 QuickCommandTagRepository
|
||||||
console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`); // +++ 添加日志 +++
|
console.log(`[Service] assignTagToCommands: Calling repo with commandIds: ${JSON.stringify(commandIds)}, tagId: ${tagId}`);
|
||||||
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
|
await QuickCommandTagRepository.addTagToCommands(commandIds, tagId);
|
||||||
console.log(`[Service] assignTagToCommands: Repo call finished for tag ${tagId}.`); // +++ 修改日志 +++
|
console.log(`[Service] assignTagToCommands: Repo call finished for tag ${tagId}.`); // +++ 修改日志 +++
|
||||||
// 可以在这里添加额外的业务逻辑,例如发送事件通知等
|
// 可以在这里添加额外的业务逻辑,例如发送事件通知等
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS = 3; // 默认状态监控间隔
|
|||||||
const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键
|
const IP_BLACKLIST_ENABLED_KEY = 'ipBlacklistEnabled'; // IP 黑名单启用设置键
|
||||||
const SHOW_CONNECTION_TAGS_KEY = 'showConnectionTags'; // 连接标签显示设置键
|
const SHOW_CONNECTION_TAGS_KEY = 'showConnectionTags'; // 连接标签显示设置键
|
||||||
const SHOW_QUICK_COMMAND_TAGS_KEY = 'showQuickCommandTags'; // 快捷指令标签显示设置键
|
const SHOW_QUICK_COMMAND_TAGS_KEY = 'showQuickCommandTags'; // 快捷指令标签显示设置键
|
||||||
|
const SHOW_STATUS_MONITOR_IP_ADDRESS_KEY = 'showStatusMonitorIpAddress'; // 状态监视器IP显示设置键
|
||||||
|
|
||||||
export const settingsService = {
|
export const settingsService = {
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +128,7 @@ export const settingsService = {
|
|||||||
// 出错时返回默认值 true (安全起见,默认启用)
|
// 出错时返回默认值 true (安全起见,默认启用)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取焦点切换顺序
|
* 获取焦点切换顺序
|
||||||
@@ -190,7 +191,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${FOCUS_SEQUENCE_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${FOCUS_SEQUENCE_KEY}:`, error);
|
||||||
throw new Error('Failed to save focus switcher sequence.');
|
throw new Error('Failed to save focus switcher sequence.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取导航栏可见性设置
|
* 获取导航栏可见性设置
|
||||||
@@ -208,7 +209,7 @@ export const settingsService = {
|
|||||||
// 出错时返回默认值 true
|
// 出错时返回默认值 true
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置导航栏可见性
|
* 设置导航栏可见性
|
||||||
@@ -225,7 +226,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${NAV_BAR_VISIBLE_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${NAV_BAR_VISIBLE_KEY}:`, error);
|
||||||
throw new Error('Failed to save nav bar visibility setting.');
|
throw new Error('Failed to save nav bar visibility setting.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取布局树设置
|
* 获取布局树设置
|
||||||
@@ -241,7 +242,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error getting layout tree setting (key: ${LAYOUT_TREE_KEY}):`, error);
|
console.error(`[Service] Error getting layout tree setting (key: ${LAYOUT_TREE_KEY}):`, error);
|
||||||
return null; // 出错时返回 null
|
return null; // 出错时返回 null
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置布局树
|
* 设置布局树
|
||||||
@@ -265,7 +266,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${LAYOUT_TREE_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${LAYOUT_TREE_KEY}:`, error);
|
||||||
throw new Error('Failed to save layout tree setting.');
|
throw new Error('Failed to save layout tree setting.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取终端选中自动复制设置
|
* 获取终端选中自动复制设置
|
||||||
@@ -283,7 +284,7 @@ export const settingsService = {
|
|||||||
// 出错时返回默认值 false
|
// 出错时返回默认值 false
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置终端选中自动复制
|
* 设置终端选中自动复制
|
||||||
@@ -300,7 +301,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${AUTO_COPY_ON_SELECT_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${AUTO_COPY_ON_SELECT_KEY}:`, error);
|
||||||
throw new Error('Failed to save auto copy on select setting.');
|
throw new Error('Failed to save auto copy on select setting.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取状态监控轮询间隔 (秒)
|
* 获取状态监控轮询间隔 (秒)
|
||||||
@@ -327,7 +328,7 @@ export const settingsService = {
|
|||||||
}
|
}
|
||||||
// 返回默认值
|
// 返回默认值
|
||||||
return DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS;
|
return DEFAULT_STATUS_MONITOR_INTERVAL_SECONDS;
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置状态监控轮询间隔 (秒)
|
* 设置状态监控轮询间隔 (秒)
|
||||||
@@ -349,7 +350,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${STATUS_MONITOR_INTERVAL_SECONDS_KEY}:`, error);
|
||||||
throw new Error('Failed to save status monitor interval setting.');
|
throw new Error('Failed to save status monitor interval setting.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
// --- Sidebar Config Specific Functions ---
|
// --- Sidebar Config Specific Functions ---
|
||||||
|
|
||||||
@@ -493,9 +494,9 @@ export const settingsService = {
|
|||||||
return valueStr !== 'false';
|
return valueStr !== 'false';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Service] Error getting show connection tags setting (key: ${SHOW_CONNECTION_TAGS_KEY}):`, error);
|
console.error(`[Service] Error getting show connection tags setting (key: ${SHOW_CONNECTION_TAGS_KEY}):`, error);
|
||||||
return true; // 默认返回 true
|
return true;
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
async setShowConnectionTags(enabled: boolean): Promise<void> {
|
async setShowConnectionTags(enabled: boolean): Promise<void> {
|
||||||
console.log(`[Service] setShowConnectionTags called with: ${enabled}`);
|
console.log(`[Service] setShowConnectionTags called with: ${enabled}`);
|
||||||
@@ -508,7 +509,7 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_CONNECTION_TAGS_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_CONNECTION_TAGS_KEY}:`, error);
|
||||||
throw new Error('Failed to save show connection tags setting.');
|
throw new Error('Failed to save show connection tags setting.');
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
// --- Show Quick Command Tags ---
|
// --- Show Quick Command Tags ---
|
||||||
async getShowQuickCommandTags(): Promise<boolean> {
|
async getShowQuickCommandTags(): Promise<boolean> {
|
||||||
@@ -520,9 +521,9 @@ export const settingsService = {
|
|||||||
return valueStr !== 'false';
|
return valueStr !== 'false';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Service] Error getting show quick command tags setting (key: ${SHOW_QUICK_COMMAND_TAGS_KEY}):`, error);
|
console.error(`[Service] Error getting show quick command tags setting (key: ${SHOW_QUICK_COMMAND_TAGS_KEY}):`, error);
|
||||||
return true; // 默认返回 true
|
return true;
|
||||||
}
|
}
|
||||||
}, // *** 确保这里有逗号 ***
|
},
|
||||||
|
|
||||||
async setShowQuickCommandTags(enabled: boolean): Promise<void> {
|
async setShowQuickCommandTags(enabled: boolean): Promise<void> {
|
||||||
console.log(`[Service] setShowQuickCommandTags called with: ${enabled}`);
|
console.log(`[Service] setShowQuickCommandTags called with: ${enabled}`);
|
||||||
@@ -535,6 +536,29 @@ export const settingsService = {
|
|||||||
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, error);
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_QUICK_COMMAND_TAGS_KEY}:`, error);
|
||||||
throw new Error('Failed to save show quick command tags setting.');
|
throw new Error('Failed to save show quick command tags setting.');
|
||||||
}
|
}
|
||||||
} // <-- No comma after the last method
|
},
|
||||||
|
|
||||||
|
// --- Show Status Monitor IP Address ---
|
||||||
|
async getShowStatusMonitorIpAddress(): Promise<boolean> {
|
||||||
|
console.log(`[Service] Attempting to get setting for key: ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}`);
|
||||||
|
try {
|
||||||
|
const valueStr = await settingsRepository.getSetting(SHOW_STATUS_MONITOR_IP_ADDRESS_KEY);
|
||||||
|
// 默认显示 (true),所以只有当值为 'false' 时才返回 false
|
||||||
|
return valueStr !== 'false';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error getting show status monitor IP address setting (key: ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}):`, error);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setShowStatusMonitorIpAddress(enabled: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
const valueStr = String(enabled);
|
||||||
|
await settingsRepository.setSetting(SHOW_STATUS_MONITOR_IP_ADDRESS_KEY, valueStr);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Service] Error calling settingsRepository.setSetting for key ${SHOW_STATUS_MONITOR_IP_ADDRESS_KEY}:`, error);
|
||||||
|
throw new Error('Failed to save show status monitor IP address setting.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}; // <-- End of settingsService object definition
|
}; // <-- End of settingsService object definition
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Client, SFTPWrapper, Stats, WriteStream } from 'ssh2'; // Import WriteStream (Removed Dirent)
|
import { Client, SFTPWrapper, Stats, WriteStream } from 'ssh2';
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { ClientState, AuthenticatedWebSocket } from '../websocket/types'; // 导入统一的 ClientState 和 AuthenticatedWebSocket
|
import { ClientState, AuthenticatedWebSocket } from '../websocket/types';
|
||||||
import * as pathModule from 'path'; // +++ Import path module +++
|
import * as pathModule from 'path';
|
||||||
import * as jschardet from 'jschardet'; // +++ Import jschardet +++
|
import * as jschardet from 'jschardet';
|
||||||
import * as iconv from 'iconv-lite'; // +++ Import iconv-lite +++
|
import * as iconv from 'iconv-lite';
|
||||||
// +++ 导入新类型 +++
|
// +++ 导入新类型 +++
|
||||||
import {
|
import {
|
||||||
SftpCompressRequestPayload,
|
SftpCompressRequestPayload,
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { settingsService } from '../services/settings.service';
|
import { settingsService } from '../services/settings.service';
|
||||||
import { AuditLogService } from '../services/audit.service';
|
import { AuditLogService } from '../services/audit.service';
|
||||||
import { NotificationService } from '../services/notification.service'; // 添加导入
|
import { NotificationService } from '../services/notification.service';
|
||||||
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
import { ipBlacklistService } from '../services/ip-blacklist.service';
|
||||||
import { exportConnectionsAsEncryptedZip } from '../services/import-export.service'; // Import the new export service
|
import { exportConnectionsAsEncryptedZip } from '../services/import-export.service';
|
||||||
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types'; // <-- Import CAPTCHA types
|
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types';
|
||||||
import i18next from '../i18n'; // +++ Import i18next +++
|
import i18next from '../i18n';
|
||||||
|
|
||||||
const auditLogService = new AuditLogService();
|
const auditLogService = new AuditLogService();
|
||||||
const notificationService = new NotificationService(); // 添加实例
|
const notificationService = new NotificationService();
|
||||||
|
|
||||||
export const settingsController = {
|
export const settingsController = {
|
||||||
/**
|
/**
|
||||||
@@ -39,23 +39,24 @@ export const settingsController = {
|
|||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
|
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled',
|
||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++
|
||||||
'showPopupFileManager', // +++ 添加弹窗文件管理器设置键 +++
|
'showPopupFileManager', // +++ 弹窗文件管理器设置键 +++
|
||||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++
|
||||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++
|
||||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
'fileManagerColWidths', // +++ 文件管理器列宽键 +++
|
||||||
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
|
'commandInputSyncTarget', // +++ 命令输入同步目标键 +++
|
||||||
'timezone', // 添加时区键
|
'timezone', // 时区键
|
||||||
'rdpModalWidth', // 添加 RDP 模态框宽度键
|
'rdpModalWidth', // RDP 模态框宽度键
|
||||||
'rdpModalHeight', // 添加 RDP 模态框高度键
|
'rdpModalHeight', // RDP 模态框高度键
|
||||||
'vncModalWidth', // 添加 VNC 模态框宽度键
|
'vncModalWidth', // VNC 模态框宽度键
|
||||||
'vncModalHeight', // 添加 VNC 模态框高度键
|
'vncModalHeight', // VNC 模态框高度键
|
||||||
'ipBlacklistEnabled', // <-- 添加 IP 黑名单启用键
|
'ipBlacklistEnabled', // <-- 添加 IP 黑名单启用键
|
||||||
'layoutLocked', // +++ 添加布局锁定键 +++
|
'layoutLocked', // +++ 布局锁定键 +++
|
||||||
'terminalScrollbackLimit', // 添加终端回滚行数键
|
'terminalScrollbackLimit', // 终端回滚行数键
|
||||||
'fileManagerShowDeleteConfirmation', // 添加文件管理器删除确认键
|
'fileManagerShowDeleteConfirmation', // 文件管理器删除确认键
|
||||||
'terminalEnableRightClickPaste' // 添加终端右键粘贴键
|
'terminalEnableRightClickPaste', // 终端右键粘贴键
|
||||||
|
'showStatusMonitorIpAddress' // 添加状态监视器IP显示键 (与服务层和前端统一)
|
||||||
];
|
];
|
||||||
const filteredSettings: Record<string, string> = {};
|
const filteredSettings: Record<string, string> = {};
|
||||||
for (const key in settingsToUpdate) {
|
for (const key in settingsToUpdate) {
|
||||||
@@ -89,9 +90,7 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getFocusSwitcherSequence(req: Request, res: Response): Promise<void> {
|
async getFocusSwitcherSequence(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取焦点切换顺序的请求。');
|
|
||||||
const sequence = await settingsService.getFocusSwitcherSequence();
|
const sequence = await settingsService.getFocusSwitcherSequence();
|
||||||
console.log('[控制器] 向客户端发送焦点切换顺序:', JSON.stringify(sequence));
|
|
||||||
res.json(sequence);
|
res.json(sequence);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 获取焦点切换顺序时出错:', error);
|
console.error('[控制器] 获取焦点切换顺序时出错:', error);
|
||||||
@@ -103,7 +102,6 @@ export const settingsController = {
|
|||||||
* 设置焦点切换顺序
|
* 设置焦点切换顺序
|
||||||
*/
|
*/
|
||||||
async setFocusSwitcherSequence(req: Request, res: Response): Promise<void> {
|
async setFocusSwitcherSequence(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置焦点切换顺序的请求。');
|
|
||||||
try {
|
try {
|
||||||
// +++ 修改:获取请求体并验证其是否符合 FocusSwitcherFullConfig 结构 +++
|
// +++ 修改:获取请求体并验证其是否符合 FocusSwitcherFullConfig 结构 +++
|
||||||
const fullConfig = req.body;
|
const fullConfig = req.body;
|
||||||
@@ -121,14 +119,14 @@ export const settingsController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 使用验证后的完整配置调用 settingsService.setFocusSwitcherSequence...');
|
|
||||||
// +++ 传递验证后的 fullConfig 给服务层 +++
|
// +++ 传递验证后的 fullConfig 给服务层 +++
|
||||||
await settingsService.setFocusSwitcherSequence(fullConfig);
|
await settingsService.setFocusSwitcherSequence(fullConfig);
|
||||||
console.log('[控制器] settingsService.setFocusSwitcherSequence 成功完成。');
|
|
||||||
|
|
||||||
console.log('[控制器] 记录审计操作: FOCUS_SWITCHER_SEQUENCE_UPDATED');
|
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
|
|
||||||
|
|
||||||
res.status(200).json({ message: '焦点切换顺序已成功更新' });
|
res.status(200).json({ message: '焦点切换顺序已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置焦点切换顺序时出错:', error);
|
console.error('[控制器] 设置焦点切换顺序时出错:', error);
|
||||||
@@ -145,9 +143,7 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getNavBarVisibility(req: Request, res: Response): Promise<void> {
|
async getNavBarVisibility(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取导航栏可见性的请求。');
|
|
||||||
const isVisible = await settingsService.getNavBarVisibility();
|
const isVisible = await settingsService.getNavBarVisibility();
|
||||||
console.log(`[控制器] 向客户端发送导航栏可见性: ${isVisible}`);
|
|
||||||
res.json({ visible: isVisible });
|
res.json({ visible: isVisible });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 获取导航栏可见性时出错:', error);
|
console.error('[控制器] 获取导航栏可见性时出错:', error);
|
||||||
@@ -159,7 +155,6 @@ export const settingsController = {
|
|||||||
* 设置导航栏可见性
|
* 设置导航栏可见性
|
||||||
*/
|
*/
|
||||||
async setNavBarVisibility(req: Request, res: Response): Promise<void> {
|
async setNavBarVisibility(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置导航栏可见性的请求。');
|
|
||||||
try {
|
try {
|
||||||
const { visible } = req.body;
|
const { visible } = req.body;
|
||||||
console.log('[控制器] 请求体 visible:', visible);
|
console.log('[控制器] 请求体 visible:', visible);
|
||||||
@@ -170,12 +165,12 @@ export const settingsController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setNavBarVisibility...');
|
|
||||||
await settingsService.setNavBarVisibility(visible);
|
await settingsService.setNavBarVisibility(visible);
|
||||||
console.log('[控制器] settingsService.setNavBarVisibility 成功完成。');
|
|
||||||
|
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
|
|
||||||
res.status(200).json({ message: '导航栏可见性已成功更新' });
|
res.status(200).json({ message: '导航栏可见性已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置导航栏可见性时出错:', error);
|
console.error('[控制器] 设置导航栏可见性时出错:', error);
|
||||||
@@ -188,19 +183,18 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getLayoutTree(req: Request, res: Response): Promise<void> {
|
async getLayoutTree(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取布局树的请求。');
|
|
||||||
const layoutJson = await settingsService.getLayoutTree();
|
const layoutJson = await settingsService.getLayoutTree();
|
||||||
if (layoutJson) {
|
if (layoutJson) {
|
||||||
try {
|
try {
|
||||||
const layout = JSON.parse(layoutJson);
|
const layout = JSON.parse(layoutJson);
|
||||||
console.log('[控制器] 向客户端发送布局树。');
|
|
||||||
res.json(layout);
|
res.json(layout);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('[控制器] 从数据库解析布局树 JSON 失败:', parseError);
|
console.error('[控制器] 从数据库解析布局树 JSON 失败:', parseError);
|
||||||
res.status(500).json({ message: '获取布局树失败:存储的数据格式无效' });
|
res.status(500).json({ message: '获取布局树失败:存储的数据格式无效' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[控制器] 在设置中未找到布局树,发送 null。');
|
|
||||||
res.json(null);
|
res.json(null);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -213,7 +207,6 @@ export const settingsController = {
|
|||||||
* 设置布局树
|
* 设置布局树
|
||||||
*/
|
*/
|
||||||
async setLayoutTree(req: Request, res: Response): Promise<void> {
|
async setLayoutTree(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置布局树的请求。');
|
|
||||||
try {
|
try {
|
||||||
const layoutTree = req.body;
|
const layoutTree = req.body;
|
||||||
|
|
||||||
@@ -225,13 +218,13 @@ export const settingsController = {
|
|||||||
|
|
||||||
const layoutJson = JSON.stringify(layoutTree);
|
const layoutJson = JSON.stringify(layoutTree);
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setLayoutTree...');
|
|
||||||
await settingsService.setLayoutTree(layoutJson);
|
await settingsService.setLayoutTree(layoutJson);
|
||||||
console.log('[控制器] settingsService.setLayoutTree 成功完成。');
|
|
||||||
|
|
||||||
// auditLogService.logAction('LAYOUT_TREE_UPDATED');
|
// auditLogService.logAction('LAYOUT_TREE_UPDATED');
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
res.status(200).json({ message: '布局树已成功更新' });
|
res.status(200).json({ message: '布局树已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置布局树时出错:', error);
|
console.error('[控制器] 设置布局树时出错:', error);
|
||||||
@@ -281,9 +274,7 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getAutoCopyOnSelect(req: Request, res: Response): Promise<void> {
|
async getAutoCopyOnSelect(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取“选中时自动复制”设置的请求。');
|
|
||||||
const isEnabled = await settingsService.getAutoCopyOnSelect();
|
const isEnabled = await settingsService.getAutoCopyOnSelect();
|
||||||
console.log(`[控制器] 向客户端发送“选中时自动复制”设置: ${isEnabled}`);
|
|
||||||
res.json({ enabled: isEnabled });
|
res.json({ enabled: isEnabled });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 获取终端选中自动复制设置时出错:', error);
|
console.error('[控制器] 获取终端选中自动复制设置时出错:', error);
|
||||||
@@ -295,7 +286,6 @@ export const settingsController = {
|
|||||||
* 设置终端选中自动复制
|
* 设置终端选中自动复制
|
||||||
*/
|
*/
|
||||||
async setAutoCopyOnSelect(req: Request, res: Response): Promise<void> {
|
async setAutoCopyOnSelect(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置“选中时自动复制”设置的请求。');
|
|
||||||
try {
|
try {
|
||||||
const { enabled } = req.body;
|
const { enabled } = req.body;
|
||||||
console.log('[控制器] 请求体 enabled:', enabled);
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
@@ -306,12 +296,12 @@ export const settingsController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setAutoCopyOnSelect...');
|
|
||||||
await settingsService.setAutoCopyOnSelect(enabled);
|
await settingsService.setAutoCopyOnSelect(enabled);
|
||||||
console.log('[控制器] settingsService.setAutoCopyOnSelect 成功完成。');
|
|
||||||
|
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
|
|
||||||
res.status(200).json({ message: '终端选中自动复制设置已成功更新' });
|
res.status(200).json({ message: '终端选中自动复制设置已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置终端选中自动复制时出错:', error);
|
console.error('[控制器] 设置终端选中自动复制时出错:', error);
|
||||||
@@ -325,7 +315,6 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getSidebarConfig(req: Request, res: Response): Promise<void> {
|
async getSidebarConfig(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取侧边栏配置的请求。');
|
|
||||||
const config = await settingsService.getSidebarConfig();
|
const config = await settingsService.getSidebarConfig();
|
||||||
console.log('[控制器] 向客户端发送侧边栏配置:', config);
|
console.log('[控制器] 向客户端发送侧边栏配置:', config);
|
||||||
res.json(config);
|
res.json(config);
|
||||||
@@ -339,7 +328,6 @@ export const settingsController = {
|
|||||||
* 设置侧栏配置
|
* 设置侧栏配置
|
||||||
*/
|
*/
|
||||||
async setSidebarConfig(req: Request, res: Response): Promise<void> {
|
async setSidebarConfig(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置侧边栏配置的请求。');
|
|
||||||
try {
|
try {
|
||||||
const configDto: UpdateSidebarConfigDto = req.body;
|
const configDto: UpdateSidebarConfigDto = req.body;
|
||||||
console.log('[控制器] 请求体:', configDto);
|
console.log('[控制器] 请求体:', configDto);
|
||||||
@@ -352,12 +340,12 @@ export const settingsController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setSidebarConfig...');
|
|
||||||
await settingsService.setSidebarConfig(configDto);
|
await settingsService.setSidebarConfig(configDto);
|
||||||
console.log('[控制器] settingsService.setSidebarConfig 成功完成。');
|
|
||||||
|
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
|
|
||||||
res.status(200).json({ message: '侧栏配置已成功更新' });
|
res.status(200).json({ message: '侧栏配置已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置侧栏配置时出错:', error);
|
console.error('[控制器] 设置侧栏配置时出错:', error);
|
||||||
@@ -375,7 +363,6 @@ export const settingsController = {
|
|||||||
*/
|
*/
|
||||||
async getCaptchaConfig(req: Request, res: Response): Promise<void> {
|
async getCaptchaConfig(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取 CAPTCHA 配置的请求。');
|
|
||||||
const fullConfig = await settingsService.getCaptchaConfig();
|
const fullConfig = await settingsService.getCaptchaConfig();
|
||||||
|
|
||||||
const publicConfig = {
|
const publicConfig = {
|
||||||
@@ -397,7 +384,6 @@ async getCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
* 设置 CAPTCHA 配置
|
* 设置 CAPTCHA 配置
|
||||||
*/
|
*/
|
||||||
async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置 CAPTCHA 配置的请求。');
|
|
||||||
try {
|
try {
|
||||||
const configDto: UpdateCaptchaSettingsDto = req.body;
|
const configDto: UpdateCaptchaSettingsDto = req.body;
|
||||||
console.log('[控制器] 请求体 (DTO, 密钥已屏蔽):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' });
|
console.log('[控制器] 请求体 (DTO, 密钥已屏蔽):', { ...configDto, hcaptchaSecretKey: '***', recaptchaSecretKey: '***' });
|
||||||
@@ -409,12 +395,12 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setCaptchaConfig...');
|
|
||||||
await settingsService.setCaptchaConfig(configDto);
|
await settingsService.setCaptchaConfig(configDto);
|
||||||
console.log('[控制器] settingsService.setCaptchaConfig 成功完成。');
|
|
||||||
|
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
|
|
||||||
res.status(200).json({ message: 'CAPTCHA 配置已成功更新' });
|
res.status(200).json({ message: 'CAPTCHA 配置已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置 CAPTCHA 配置时出错:', error);
|
console.error('[控制器] 设置 CAPTCHA 配置时出错:', error);
|
||||||
@@ -430,9 +416,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
// --- Show Connection Tags ---
|
// --- Show Connection Tags ---
|
||||||
async getShowConnectionTags(req: Request, res: Response): Promise<void> {
|
async getShowConnectionTags(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取“显示连接标签”设置的请求。');
|
|
||||||
const isEnabled = await settingsService.getShowConnectionTags();
|
const isEnabled = await settingsService.getShowConnectionTags();
|
||||||
console.log(`[控制器] 向客户端发送“显示连接标签”设置: ${isEnabled}`);
|
|
||||||
res.json({ enabled: isEnabled });
|
res.json({ enabled: isEnabled });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 获取“显示连接标签”设置时出错:', error);
|
console.error('[控制器] 获取“显示连接标签”设置时出错:', error);
|
||||||
@@ -441,7 +425,6 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
}, // *** 确保这里有逗号 ***
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
async setShowConnectionTags(req: Request, res: Response): Promise<void> {
|
async setShowConnectionTags(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置“显示连接标签”设置的请求。');
|
|
||||||
try {
|
try {
|
||||||
const { enabled } = req.body;
|
const { enabled } = req.body;
|
||||||
console.log('[控制器] 请求体 enabled:', enabled);
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
@@ -452,14 +435,14 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setShowConnectionTags...');
|
|
||||||
await settingsService.setShowConnectionTags(enabled);
|
await settingsService.setShowConnectionTags(enabled);
|
||||||
console.log('[控制器] settingsService.setShowConnectionTags 成功完成。');
|
|
||||||
|
|
||||||
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
||||||
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showConnectionTags'] });
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
res.status(200).json({ message: '“显示连接标签”设置已成功更新' });
|
res.status(200).json({ message: '“显示连接标签”设置已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置“显示连接标签”时出错:', error);
|
console.error('[控制器] 设置“显示连接标签”时出错:', error);
|
||||||
@@ -470,9 +453,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
// --- Show Quick Command Tags ---
|
// --- Show Quick Command Tags ---
|
||||||
async getShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
async getShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到获取“显示快捷指令标签”设置的请求。');
|
|
||||||
const isEnabled = await settingsService.getShowQuickCommandTags();
|
const isEnabled = await settingsService.getShowQuickCommandTags();
|
||||||
console.log(`[控制器] 向客户端发送“显示快捷指令标签”设置: ${isEnabled}`);
|
|
||||||
res.json({ enabled: isEnabled });
|
res.json({ enabled: isEnabled });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 获取“显示快捷指令标签”设置时出错:', error);
|
console.error('[控制器] 获取“显示快捷指令标签”设置时出错:', error);
|
||||||
@@ -481,7 +462,6 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
}, // *** 确保这里有逗号 ***
|
}, // *** 确保这里有逗号 ***
|
||||||
|
|
||||||
async setShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
async setShowQuickCommandTags(req: Request, res: Response): Promise<void> {
|
||||||
console.log('[控制器] 收到设置“显示快捷指令标签”设置的请求。');
|
|
||||||
try {
|
try {
|
||||||
const { enabled } = req.body;
|
const { enabled } = req.body;
|
||||||
console.log('[控制器] 请求体 enabled:', enabled);
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
@@ -492,27 +472,63 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[控制器] 调用 settingsService.setShowQuickCommandTags...');
|
|
||||||
await settingsService.setShowQuickCommandTags(enabled);
|
await settingsService.setShowQuickCommandTags(enabled);
|
||||||
console.log('[控制器] settingsService.setShowQuickCommandTags 成功完成。');
|
|
||||||
|
|
||||||
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
||||||
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showQuickCommandTags'] });
|
||||||
|
|
||||||
console.log('[控制器] 发送成功响应。');
|
|
||||||
res.status(200).json({ message: '“显示快捷指令标签”设置已成功更新' });
|
res.status(200).json({ message: '“显示快捷指令标签”设置已成功更新' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 设置“显示快捷指令标签”时出错:', error);
|
console.error('[控制器] 设置“显示快捷指令标签”时出错:', error);
|
||||||
res.status(500).json({ message: '设置“显示快捷指令标签”失败', error: error.message });
|
res.status(500).json({ message: '设置“显示快捷指令标签”失败', error: error.message });
|
||||||
}
|
}
|
||||||
}, // <-- Add comma here for the new method
|
},
|
||||||
|
|
||||||
|
// --- Show Status Monitor IP Address ---
|
||||||
|
async getShowStatusMonitorIpAddress(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const isEnabled = await settingsService.getShowStatusMonitorIpAddress();
|
||||||
|
res.json({ enabled: isEnabled });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 获取“显示状态监视器IP地址”设置时出错:', error);
|
||||||
|
res.status(500).json({ message: '获取“显示状态监视器IP地址”设置失败', error: error.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async setShowStatusMonitorIpAddress(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { enabled } = req.body;
|
||||||
|
console.log('[控制器] 请求体 enabled:', enabled);
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
console.warn('[控制器] 收到无效的 enabled 格式:', enabled);
|
||||||
|
res.status(400).json({ message: '无效的请求体,"enabled" 必须是一个布尔值' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await settingsService.setShowStatusMonitorIpAddress(enabled);
|
||||||
|
|
||||||
|
|
||||||
|
auditLogService.logAction('SETTINGS_UPDATED', { updatedKeys: ['showStatusMonitorIpAddress'] });
|
||||||
|
notificationService.sendNotification('SETTINGS_UPDATED', { updatedKeys: ['showStatusMonitorIpAddress'] });
|
||||||
|
|
||||||
|
|
||||||
|
res.status(200).json({ message: '“显示状态监视器IP地址”设置已成功更新' });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[控制器] 设置“显示状态监视器IP地址”时出错:', error);
|
||||||
|
res.status(500).json({ message: '设置“显示状态监视器IP地址”失败', error: error.message });
|
||||||
|
}
|
||||||
|
}, // <-- Add comma here
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出所有连接配置为加密的 ZIP 文件
|
* 导出所有连接配置为加密的 ZIP 文件
|
||||||
*/
|
*/
|
||||||
async exportAllConnections(req: Request, res: Response): Promise<void> {
|
async exportAllConnections(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('[控制器] 收到导出所有连接的请求。');
|
|
||||||
const encryptedZipBuffer = await exportConnectionsAsEncryptedZip(true);
|
const encryptedZipBuffer = await exportConnectionsAsEncryptedZip(true);
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/zip');
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
@@ -520,7 +536,7 @@ async setCaptchaConfig(req: Request, res: Response): Promise<void> {
|
|||||||
res.send(encryptedZipBuffer);
|
res.send(encryptedZipBuffer);
|
||||||
|
|
||||||
// auditLogService.logAction('CONNECTIONS_EXPORTED', { userId: (req.user as any)?.id || 'unknown' }); // 移除审计日志
|
// auditLogService.logAction('CONNECTIONS_EXPORTED', { userId: (req.user as any)?.id || 'unknown' }); // 移除审计日志
|
||||||
console.log('[控制器] 成功发送加密的连接导出文件。');
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[控制器] 导出所有连接时出错:', error);
|
console.error('[控制器] 导出所有连接时出错:', error);
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ router.put('/show-quick-command-tags', settingsController.setShowQuickCommandTag
|
|||||||
// GET /api/v1/settings/export-connections - 导出所有连接为加密的 ZIP 文件
|
// GET /api/v1/settings/export-connections - 导出所有连接为加密的 ZIP 文件
|
||||||
router.get('/export-connections', settingsController.exportAllConnections);
|
router.get('/export-connections', settingsController.exportAllConnections);
|
||||||
|
|
||||||
|
// +++ 显示状态监视器IP地址路由 +++
|
||||||
|
// GET /api/v1/settings/show-status-monitor-ip-address - 获取设置
|
||||||
|
router.get('/show-status-monitor-ip-address', settingsController.getShowStatusMonitorIpAddress);
|
||||||
|
// PUT /api/v1/settings/show-status-monitor-ip-address - 更新设置
|
||||||
|
router.put('/show-status-monitor-ip-address', settingsController.setShowStatusMonitorIpAddress);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { sshSuspendService } from '../services/ssh-suspend.service'; // 导入单例服务
|
import { sshSuspendService } from '../services/ssh-suspend.service';
|
||||||
import { SuspendedSessionInfo } from '../types/ssh-suspend.types'; // 导入类型
|
import { SuspendedSessionInfo } from '../types/ssh-suspend.types';
|
||||||
|
|
||||||
export class SshSuspendController {
|
export class SshSuspendController {
|
||||||
// private sshSuspendService: SshSuspendService; // 不再需要,直接使用导入的单例
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// this.sshSuspendService = new SshSuspendService(); // 不再需要实例化
|
|
||||||
// 绑定方法到当前实例,以确保 'this' 上下文正确
|
|
||||||
this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this);
|
this.getSuspendedSshSessions = this.getSuspendedSshSessions.bind(this);
|
||||||
this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this);
|
this.terminateAndRemoveSession = this.terminateAndRemoveSession.bind(this);
|
||||||
this.removeSessionEntry = this.removeSessionEntry.bind(this);
|
this.removeSessionEntry = this.removeSessionEntry.bind(this);
|
||||||
this.editSessionNameHttp = this.editSessionNameHttp.bind(this); // 绑定新方法
|
this.editSessionNameHttp = this.editSessionNameHttp.bind(this);
|
||||||
this.exportSessionLog = this.exportSessionLog.bind(this); // +++ 绑定导出日志方法 +++
|
this.exportSessionLog = this.exportSessionLog.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSuspendedSshSessions(req: Request, res: Response): Promise<void> {
|
public async getSuspendedSshSessions(req: Request, res: Response): Promise<void> {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export interface UpdateCaptchaSettingsDto {
|
|||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
sidebar?: SidebarConfig;
|
sidebar?: SidebarConfig;
|
||||||
captcha?: CaptchaSettings;
|
captcha?: CaptchaSettings;
|
||||||
|
showStatusMonitorIpAddress?: boolean; // 新增:是否在状态监视器中显示IP地址
|
||||||
// 可以添加其他设置模块,例如:
|
// 可以添加其他设置模块,例如:
|
||||||
// security?: SecuritySettings;
|
// security?: SecuritySettings;
|
||||||
// general?: GeneralSettings;
|
// general?: GeneralSettings;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { initializeHeartbeat } from './websocket/heartbeat';
|
|||||||
import { initializeUpgradeHandler } from './websocket/upgrade';
|
import { initializeUpgradeHandler } from './websocket/upgrade';
|
||||||
import { initializeConnectionHandler } from './websocket/connection';
|
import { initializeConnectionHandler } from './websocket/connection';
|
||||||
import { clientStates } from './websocket/state';
|
import { clientStates } from './websocket/state';
|
||||||
import { sshSuspendService } from './services/ssh-suspend.service'; // 导入实例
|
import { sshSuspendService } from './services/ssh-suspend.service';
|
||||||
import { SftpService } from './services/sftp.service'; // +++ 导入 SftpService +++
|
import { SftpService } from './services/sftp.service';
|
||||||
import { cleanupClientConnection } from './websocket/utils';
|
import { cleanupClientConnection } from './websocket/utils';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,11 @@ import { useLayoutStore } from './stores/layout.store';
|
|||||||
import { useFocusSwitcherStore } from './stores/focusSwitcher.store';
|
import { useFocusSwitcherStore } from './stores/focusSwitcher.store';
|
||||||
import { useSessionStore } from './stores/session.store';
|
import { useSessionStore } from './stores/session.store';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// 导入通知显示组件
|
|
||||||
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
||||||
// 导入文件编辑器弹窗组件
|
|
||||||
import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
||||||
// 导入样式自定义器组件
|
|
||||||
import StyleCustomizer from './components/StyleCustomizer.vue';
|
import StyleCustomizer from './components/StyleCustomizer.vue';
|
||||||
// +++ 导入焦点切换配置器组件 +++
|
|
||||||
import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue';
|
import FocusSwitcherConfigurator from './components/FocusSwitcherConfigurator.vue';
|
||||||
// +++ 导入 RDP 模态框组件 +++
|
|
||||||
import RemoteDesktopModal from './components/RemoteDesktopModal.vue';
|
import RemoteDesktopModal from './components/RemoteDesktopModal.vue';
|
||||||
// +++ 导入 VNC 模态框组件 +++
|
|
||||||
import VncModal from './components/VncModal.vue';
|
import VncModal from './components/VncModal.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -69,7 +63,7 @@ onMounted(() => {
|
|||||||
// Use setTimeout to ensure styles are applied and elements have dimensions
|
// Use setTimeout to ensure styles are applied and elements have dimensions
|
||||||
setTimeout(updateUnderline, 100);
|
setTimeout(updateUnderline, 100);
|
||||||
|
|
||||||
// +++ 添加全局 Alt 键监听器 +++
|
// +++ 全局 Alt 键监听器 +++
|
||||||
window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++
|
window.addEventListener('keydown', handleAltKeyDown); // +++ 监听 keydown 设置状态 +++
|
||||||
window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++
|
window.addEventListener('keyup', handleGlobalKeyUp); // +++ 监听 keyup 执行切换 +++
|
||||||
|
|
||||||
@@ -86,7 +80,7 @@ onMounted(() => {
|
|||||||
layoutStore.loadHeaderVisibility();
|
layoutStore.loadHeaderVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// +++ 添加卸载钩子以移除监听器 +++
|
// +++ 卸载钩子以移除监听器 +++
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++
|
window.removeEventListener('keydown', handleAltKeyDown); // +++ 移除 keydown 监听 +++
|
||||||
window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++
|
window.removeEventListener('keyup', handleGlobalKeyUp); // +++ 移除 keyup 监听 +++
|
||||||
|
|||||||
@@ -59,8 +59,8 @@
|
|||||||
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
import { ref, reactive, computed, watch, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
import { useQuickCommandsStore, type QuickCommandFE } from '../stores/quickCommands.store';
|
||||||
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store'; // +++ Import new tag store +++
|
import { useQuickCommandTagsStore } from '../stores/quickCommandTags.store';
|
||||||
import TagInput from './TagInput.vue'; // +++ Import TagInput component (assuming it exists) +++
|
import TagInput from './TagInput.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
|
commandToEdit?: QuickCommandFE | null; // 接收要编辑的指令对象 (should include tagIds)
|
||||||
@@ -109,12 +109,12 @@ onMounted(() => {
|
|||||||
// --- Tag Creation Handling ---
|
// --- Tag Creation Handling ---
|
||||||
// Assuming TagInput emits 'create-tag' with the tag name
|
// Assuming TagInput emits 'create-tag' with the tag name
|
||||||
const handleCreateTag = async (tagName: string) => {
|
const handleCreateTag = async (tagName: string) => {
|
||||||
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] Received create-tag event for: ${tagName}`);
|
||||||
if (!tagName || tagName.trim().length === 0) return;
|
if (!tagName || tagName.trim().length === 0) return;
|
||||||
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.addTag...`);
|
||||||
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
|
const newTag = await quickCommandTagsStore.addTag(tagName.trim());
|
||||||
if (newTag && !formData.tagIds.includes(newTag.id)) {
|
if (newTag && !formData.tagIds.includes(newTag.id)) {
|
||||||
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] New tag created (ID: ${newTag.id}), adding to selection.`);
|
||||||
// Add the new tag's ID to the selected list
|
// Add the new tag's ID to the selected list
|
||||||
formData.tagIds.push(newTag.id);
|
formData.tagIds.push(newTag.id);
|
||||||
}
|
}
|
||||||
@@ -122,12 +122,12 @@ const handleCreateTag = async (tagName: string) => {
|
|||||||
|
|
||||||
// --- Tag Deletion Handling ---
|
// --- Tag Deletion Handling ---
|
||||||
const handleDeleteTag = async (tagId: number) => {
|
const handleDeleteTag = async (tagId: number) => {
|
||||||
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] Received delete-tag event for ID: ${tagId}`);
|
||||||
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
|
const tagToDelete = quickCommandTagsStore.tags.find(t => t.id === tagId);
|
||||||
if (!tagToDelete) return;
|
if (!tagToDelete) return;
|
||||||
|
|
||||||
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
if (confirm(t('tags.prompts.confirmDelete', { name: tagToDelete.name }))) {
|
||||||
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] Calling quickCommandTagsStore.deleteTag...`);
|
||||||
const success = await quickCommandTagsStore.deleteTag(tagId);
|
const success = await quickCommandTagsStore.deleteTag(tagId);
|
||||||
if (success) {
|
if (success) {
|
||||||
// If deletion is successful, TagInput's availableTags will update,
|
// If deletion is successful, TagInput's availableTags will update,
|
||||||
@@ -135,7 +135,7 @@ const handleDeleteTag = async (tagId: number) => {
|
|||||||
// We also need to remove it from the local formData.tagIds if it was selected.
|
// We also need to remove it from the local formData.tagIds if it was selected.
|
||||||
const index = formData.tagIds.indexOf(tagId);
|
const index = formData.tagIds.indexOf(tagId);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`); // +++ 添加日志 +++
|
console.log(`[QuickCmdForm] Removing deleted tag ID ${tagId} from selection.`);
|
||||||
formData.tagIds.splice(index, 1);
|
formData.tagIds.splice(index, 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -46,12 +46,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
|
import { useCommandHistoryStore, CommandHistoryEntryFE } from '../stores/commandHistory.store';
|
||||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 引入 UI 通知 store
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||||
import { useI18n } from 'vue-i18n'; // 引入 i18n
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const commandHistoryStore = useCommandHistoryStore();
|
const commandHistoryStore = useCommandHistoryStore();
|
||||||
const uiNotificationsStore = useUiNotificationsStore(); // 实例化 UI 通知 store
|
const uiNotificationsStore = useUiNotificationsStore();
|
||||||
const { t } = useI18n(); // 使用 i18n
|
const { t } = useI18n();
|
||||||
const hoveredItemId = ref<number | null>(null);
|
const hoveredItemId = ref<number | null>(null);
|
||||||
const listContainer = ref<HTMLElement | null>(null);
|
const listContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
import { ref, watch, nextTick, onMounted, onBeforeUnmount, defineExpose, computed, defineOptions } from 'vue'; // Import defineOptions
|
import { ref, watch, nextTick, onMounted, onBeforeUnmount, defineExpose, computed, defineOptions } from 'vue'; // Import defineOptions
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useSessionStore } from '../stores/session.store'; // +++ 导入 Session Store +++
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useQuickCommandsStore } from '../stores/quickCommands.store';
|
import { useQuickCommandsStore } from '../stores/quickCommands.store';
|
||||||
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
import { useCommandHistoryStore } from '../stores/commandHistory.store';
|
||||||
import QuickCommandsModal from './QuickCommandsModal.vue'; // +++ Import the modal component +++
|
import QuickCommandsModal from './QuickCommandsModal.vue';
|
||||||
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue'; // +++ Import the new modal +++
|
import SuspendedSshSessionsModal from './SuspendedSshSessionsModal.vue';
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // +++ Import File Editor Store +++
|
import { useFileEditorStore } from '../stores/fileEditor.store';
|
||||||
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
||||||
|
|
||||||
// Disable attribute inheritance as this component has multiple root nodes (div + modal)
|
|
||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, type PropType, ref, watch, defineExpose, onMounted, onBeforeUnmount, nextTick } from 'vue'; // 添加 nextTick
|
import { computed, type PropType, ref, watch, defineExpose, onMounted, onBeforeUnmount, nextTick } from 'vue'; // 添加 nextTick
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
// import { storeToRefs } from 'pinia'; // 移除 storeToRefs
|
import MonacoEditor from './MonacoEditor.vue';
|
||||||
import MonacoEditor from './MonacoEditor.vue'; // 导入 Monaco Editor 组件
|
import FileEditorTabs from './FileEditorTabs.vue';
|
||||||
import FileEditorTabs from './FileEditorTabs.vue'; // 导入标签栏组件 (路径确认无误)
|
import type { FileTab } from '../stores/fileEditor.store';
|
||||||
// import { useFileEditorStore } from '../stores/fileEditor.store'; // 移除 Store 导入
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
|
||||||
import type { FileTab } from '../stores/fileEditor.store'; // 保留类型导入
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useSessionStore } from '../stores/session.store'; // +++ 导入会话 Store +++
|
import { storeToRefs } from 'pinia';
|
||||||
import { useSettingsStore } from '../stores/settings.store'; // +++ 导入设置 Store +++
|
|
||||||
import { storeToRefs } from 'pinia'; // +++ 导入 storeToRefs +++
|
|
||||||
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
import { useWorkspaceEventEmitter } from '../composables/workspaceEvents';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -228,7 +226,7 @@ let unregisterFocusFn: (() => void) | null = null; // 保存注销函数
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 注册动作并保存返回的注销函数
|
// 注册动作并保存返回的注销函数
|
||||||
unregisterFocusFn = focusSwitcherStore.registerFocusAction('fileEditorActive', focusActiveEditor);
|
unregisterFocusFn = focusSwitcherStore.registerFocusAction('fileEditorActive', focusActiveEditor);
|
||||||
// +++ 添加键盘事件监听器 +++
|
// +++ 键盘事件监听器 +++
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const {
|
|||||||
closeOtherTabs, // 修正:移除 Global 后缀
|
closeOtherTabs, // 修正:移除 Global 后缀
|
||||||
closeTabsToTheRight, // 修正:移除 Global 后缀
|
closeTabsToTheRight, // 修正:移除 Global 后缀
|
||||||
closeTabsToTheLeft, // 修正:移除 Global 后缀
|
closeTabsToTheLeft, // 修正:移除 Global 后缀
|
||||||
changeEncoding: changeGlobalEncoding, // +++ 添加全局编码更改 action +++
|
changeEncoding: changeGlobalEncoding, // +++ 全局编码更改 action +++
|
||||||
} = fileEditorStore;
|
} = fileEditorStore;
|
||||||
|
|
||||||
// 会话 Store Actions (用于非共享模式)
|
// 会话 Store Actions (用于非共享模式)
|
||||||
@@ -58,7 +58,7 @@ const {
|
|||||||
closeOtherTabsInSession,
|
closeOtherTabsInSession,
|
||||||
closeTabsToTheRightInSession,
|
closeTabsToTheRightInSession,
|
||||||
closeTabsToTheLeftInSession,
|
closeTabsToTheLeftInSession,
|
||||||
changeEncodingInSession, // +++ 添加会话编码更改 action +++
|
changeEncodingInSession, // +++ 会话编码更改 action +++
|
||||||
} = sessionStore;
|
} = sessionStore;
|
||||||
|
|
||||||
// --- 移除本地文件状态 ---
|
// --- 移除本地文件状态 ---
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ const triggerDownloadDirectory = (item: FileListItem) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// +++ 添加压缩/解压处理函数 +++
|
// +++ 压缩/解压处理函数 +++
|
||||||
const handleCompress = (items: FileListItem[], format: CompressFormat) => {
|
const handleCompress = (items: FileListItem[], format: CompressFormat) => {
|
||||||
if (!currentSftpManager.value) {
|
if (!currentSftpManager.value) {
|
||||||
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot compress: SFTP manager not available.`);
|
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot compress: SFTP manager not available.`);
|
||||||
@@ -800,7 +800,7 @@ watch(sortDirection, () => {
|
|||||||
const saveLayoutSettings = () => {
|
const saveLayoutSettings = () => {
|
||||||
// 确保 colWidths.value 是普通对象,而不是 Proxy
|
// 确保 colWidths.value 是普通对象,而不是 Proxy
|
||||||
const widthsToSave = JSON.parse(JSON.stringify(colWidths.value));
|
const widthsToSave = JSON.parse(JSON.stringify(colWidths.value));
|
||||||
// +++ 添加日志:记录保存的值 +++
|
// +++ 日志:记录保存的值 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering saveLayoutSettings: multiplier=${rowSizeMultiplier.value}, widths=${JSON.stringify(widthsToSave)}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering saveLayoutSettings: multiplier=${rowSizeMultiplier.value}, widths=${JSON.stringify(widthsToSave)}`);
|
||||||
settingsStore.updateFileManagerLayoutSettings(rowSizeMultiplier.value, widthsToSave);
|
settingsStore.updateFileManagerLayoutSettings(rowSizeMultiplier.value, widthsToSave);
|
||||||
};
|
};
|
||||||
@@ -819,7 +819,7 @@ watchEffect(() => {
|
|||||||
const storeMultiplier = fileManagerRowSizeMultiplierNumber.value;
|
const storeMultiplier = fileManagerRowSizeMultiplierNumber.value;
|
||||||
const storeWidths = fileManagerColWidthsObject.value;
|
const storeWidths = fileManagerColWidthsObject.value;
|
||||||
|
|
||||||
// +++ 添加日志:记录从 store 获取的值 +++
|
// +++ 日志:记录从 store 获取的值 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] watchEffect triggered. Store values: multiplier=${storeMultiplier}, widths=${JSON.stringify(storeWidths)}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] watchEffect triggered. Store values: multiplier=${storeMultiplier}, widths=${JSON.stringify(storeWidths)}`);
|
||||||
|
|
||||||
// 只有当 store 加载完成并提供了有效值时才更新
|
// 只有当 store 加载完成并提供了有效值时才更新
|
||||||
@@ -829,7 +829,7 @@ watchEffect(() => {
|
|||||||
const currentWidthsString = JSON.stringify(colWidths.value);
|
const currentWidthsString = JSON.stringify(colWidths.value);
|
||||||
const storeWidthsString = JSON.stringify(storeWidths);
|
const storeWidthsString = JSON.stringify(storeWidths);
|
||||||
|
|
||||||
// +++ 添加日志:记录当前值和 store 值,以及是否更新 +++
|
// +++ 日志:记录当前值和 store 值,以及是否更新 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Multiplier=${currentMultiplier}, Store Multiplier=${storeMultiplier}. Update needed: ${storeMultiplier !== currentMultiplier}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Multiplier=${currentMultiplier}, Store Multiplier=${storeMultiplier}. Update needed: ${storeMultiplier !== currentMultiplier}`);
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Widths=${currentWidthsString}, Store Widths=${storeWidthsString}. Update needed: ${storeWidthsString !== currentWidthsString}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Comparing values: Current Widths=${currentWidthsString}, Store Widths=${storeWidthsString}. Update needed: ${storeWidthsString !== currentWidthsString}`);
|
||||||
|
|
||||||
@@ -851,7 +851,7 @@ watchEffect(() => {
|
|||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Column widths updated from store: ${JSON.stringify(updatedWidths)}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Column widths updated from store: ${JSON.stringify(updatedWidths)}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// +++ 添加日志:记录等待 store 加载 +++
|
// +++ 日志:记录等待 store 加载 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Waiting for valid layout settings from store... Store Multiplier=${storeMultiplier}, Store Widths Keys=${Object.keys(storeWidths).length}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Waiting for valid layout settings from store... Store Multiplier=${storeMultiplier}, Store Widths Keys=${Object.keys(storeWidths).length}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1103,7 +1103,7 @@ const stopResize = () => {
|
|||||||
document.body.style.cursor = '';
|
document.body.style.cursor = '';
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = '';
|
||||||
// +++ 在调整结束后保存列宽 +++
|
// +++ 在调整结束后保存列宽 +++
|
||||||
// +++ 添加日志:记录触发保存 +++
|
// +++ 日志:记录触发保存 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] stopResize triggered saveLayoutSettings.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] stopResize triggered saveLayoutSettings.`);
|
||||||
saveLayoutSettings();
|
saveLayoutSettings();
|
||||||
}
|
}
|
||||||
@@ -1240,7 +1240,7 @@ const handleWheel = (event: WheelEvent) => {
|
|||||||
// console.log(`Row size multiplier: ${rowSizeMultiplier.value}`); // 调试日志
|
// console.log(`Row size multiplier: ${rowSizeMultiplier.value}`); // 调试日志
|
||||||
// +++ 在行大小变化后保存设置 +++
|
// +++ 在行大小变化后保存设置 +++
|
||||||
if (rowSizeMultiplier.value !== oldMultiplier) {
|
if (rowSizeMultiplier.value !== oldMultiplier) {
|
||||||
// +++ 添加日志:记录触发保存 +++
|
// +++ 日志:记录触发保存 +++
|
||||||
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] handleWheel triggered saveLayoutSettings.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] handleWheel triggered saveLayoutSettings.`);
|
||||||
saveLayoutSettings();
|
saveLayoutSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, type PropType } from 'vue';
|
import { ref, type PropType } from 'vue';
|
||||||
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu'; // 导入菜单项类型
|
import type { ContextMenuItem } from '../composables/file-manager/useFileManagerContextMenu';
|
||||||
import { onUnmounted } from 'vue';
|
import { onUnmounted } from 'vue';
|
||||||
import { useDeviceDetection } from '../composables/useDeviceDetection';
|
import { useDeviceDetection } from '../composables/useDeviceDetection';
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, type Ref, nextTick } from 'vue'; // Import nextTick
|
import { ref, computed, watch, type Ref, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store'; // +++ Import settings store +++
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { storeToRefs } from 'pinia'; // +++ Import storeToRefs +++
|
import { storeToRefs } from 'pinia';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import LayoutNodeEditor from './LayoutNodeEditor.vue';
|
import LayoutNodeEditor from './LayoutNodeEditor.vue';
|
||||||
// +++ Import a switch component if available, otherwise use checkbox +++
|
|
||||||
// Assuming a simple checkbox for now
|
|
||||||
|
|
||||||
// --- Props ---
|
// --- Props ---
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ConnectionInfo } from '../stores/connections.store'; // +++ 导入 ConnectionInfo 类型 +++
|
import type { ConnectionInfo } from '../stores/connections.store';
|
||||||
import { computed, defineAsyncComponent, type PropType, type Component, ref, watch, onMounted } from 'vue';
|
import { computed, defineAsyncComponent, type PropType, type Component, ref, watch, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'; // <-- Import useI18n
|
import { useI18n } from 'vue-i18n';
|
||||||
// 添加依赖 font-awesome
|
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
import { useLayoutStore, type LayoutNode, type PaneName } from '../stores/layout.store';
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useFileEditorStore } from '../stores/fileEditor.store'; // <-- Import FileEditorStore
|
import { useFileEditorStore } from '../stores/fileEditor.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store'; // +++ Import SettingsStore +++
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useSidebarResize } from '../composables/useSidebarResize'; // +++ Import useSidebarResize +++
|
import { useSidebarResize } from '../composables/useSidebarResize';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// import { defineEmits } from 'vue'; // --- 移除 ---
|
|
||||||
|
|
||||||
// --- Props ---
|
// --- Props ---
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -267,7 +267,7 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
|
|||||||
// --- Methods ---
|
// --- Methods ---
|
||||||
// 处理 Splitpanes 大小调整事件
|
// 处理 Splitpanes 大小调整事件
|
||||||
const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string]: any }> }) => {
|
const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string]: any }> }) => {
|
||||||
// +++ 添加更详细的日志 +++
|
// +++ 更详细的日志 +++
|
||||||
// +++ Log the entire layoutNode object if ID is undefined +++
|
// +++ Log the entire layoutNode object if ID is undefined +++
|
||||||
if (props.layoutNode && typeof props.layoutNode.id === 'undefined') {
|
if (props.layoutNode && typeof props.layoutNode.id === 'undefined') {
|
||||||
console.warn(`[LayoutRenderer DEBUG] handlePaneResize triggered but props.layoutNode.id is undefined. Full layoutNode prop:`, JSON.parse(JSON.stringify(props.layoutNode)));
|
console.warn(`[LayoutRenderer DEBUG] handlePaneResize triggered but props.layoutNode.id is undefined. Full layoutNode prop:`, JSON.parse(JSON.stringify(props.layoutNode)));
|
||||||
@@ -291,7 +291,7 @@ const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string
|
|||||||
size: paneInfo.size
|
size: paneInfo.size
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// +++ 添加调用 store action 前的日志 +++
|
// +++ 调用 store action 前的日志 +++
|
||||||
// console.log(`[LayoutRenderer DEBUG] Calling layoutStore.updateNodeSizes for node ID: ${props.layoutNode.id} with sizes:`, JSON.parse(JSON.stringify(childrenSizes)));
|
// console.log(`[LayoutRenderer DEBUG] Calling layoutStore.updateNodeSizes for node ID: ${props.layoutNode.id} with sizes:`, JSON.parse(JSON.stringify(childrenSizes)));
|
||||||
// 调用 store action 来更新节点大小
|
// 调用 store action 来更新节点大小
|
||||||
layoutStore.updateNodeSizes(props.layoutNode.id, childrenSizes);
|
layoutStore.updateNodeSizes(props.layoutNode.id, childrenSizes);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount, watch, defineExpose } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, watch, defineExpose } from 'vue';
|
||||||
import * as monaco from 'monaco-editor';
|
import * as monaco from 'monaco-editor';
|
||||||
|
|
||||||
const localFontSize = ref(14); // 添加本地字体大小状态,默认 14
|
const localFontSize = ref(14); // 本地字体大小状态,默认 14
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useNotificationsStore } from '../stores/notifications.store';
|
import { useNotificationsStore } from '../stores/notifications.store';
|
||||||
import { NotificationSetting, NotificationChannelType, NotificationEvent } from '../types/server.types';
|
import { NotificationSetting, NotificationChannelType, NotificationEvent } from '../types/server.types';
|
||||||
import NotificationSettingForm from './NotificationSettingForm.vue'; // Import the form component
|
import NotificationSettingForm from './NotificationSettingForm.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
const store = useNotificationsStore();
|
const store = useNotificationsStore();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed, watchEffect } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useConnectionsStore } from '../stores/connections.store'; // +++ Import connections store +++
|
import { useConnectionsStore } from '../stores/connections.store';
|
||||||
// @ts-ignore - guacamole-common-js 缺少官方类型定义
|
// @ts-ignore - guacamole-common-js 缺少官方类型定义
|
||||||
import Guacamole from 'guacamole-common-js';
|
import Guacamole from 'guacamole-common-js';
|
||||||
import apiClient from '../utils/apiClient';
|
import apiClient from '../utils/apiClient';
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useSshKeysStore } from '../stores/sshKeys.store';
|
import { useSshKeysStore } from '../stores/sshKeys.store';
|
||||||
import SshKeyManagementModal from './SshKeyManagementModal.vue'; // Import the modal
|
import SshKeyManagementModal from './SshKeyManagementModal.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: number | null; // The selected ssh_key_id (v-model)
|
modelValue: number | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue']); // Removed 'use-direct-input' event
|
const emit = defineEmits(['update:modelValue']); // Removed 'use-direct-input' event
|
||||||
|
|||||||
@@ -40,11 +40,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted, computed, type PropType } from 'vue'; // 添加 PropType
|
import { ref, watch, onMounted, computed, type PropType } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Line } from 'vue-chartjs';
|
import { Line } from 'vue-chartjs';
|
||||||
import { useSessionStore } from '../stores/session.store'; // 注入 sessionStore
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
import { storeToRefs } from 'pinia';
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
Title,
|
Title,
|
||||||
|
|||||||
@@ -25,6 +25,19 @@
|
|||||||
|
|
||||||
<!-- 状态网格 -->
|
<!-- 状态网格 -->
|
||||||
<div v-else class="status-grid grid gap-3">
|
<div v-else class="status-grid grid gap-3">
|
||||||
|
<!-- IP 地址 (如果启用) -->
|
||||||
|
<div v-if="statusMonitorShowIpBoolean && activeSessionId && sessionIpAddress" class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||||
|
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">IP:</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="ip-address-value truncate text-left cursor-pointer hover:text-primary transition-colors"
|
||||||
|
:title="sessionIpAddress"
|
||||||
|
@click="copyIpToClipboard(sessionIpAddress)">
|
||||||
|
{{ sessionIpAddress }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- CPU 型号 -->
|
<!-- CPU 型号 -->
|
||||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
|
||||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
|
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
|
||||||
@@ -122,9 +135,17 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import StatusCharts from './StatusCharts.vue';
|
import StatusCharts from './StatusCharts.vue';
|
||||||
import { useSessionStore } from '../stores/session.store'; // 注入 sessionStore
|
import { useSessionStore } from '../stores/session.store'; // 注入 sessionStore
|
||||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store
|
||||||
|
import { useConnectionsStore } from '../stores/connections.store'; // 导入连接 store
|
||||||
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // + 导入通知 store
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
|
const settingsStore = useSettingsStore(); // 实例化设置 store
|
||||||
|
const connectionsStore = useConnectionsStore(); // 实例化连接 store
|
||||||
|
const uiNotificationsStore = useUiNotificationsStore(); // + 实例化通知 store
|
||||||
const { sessions } = storeToRefs(sessionStore); // 获取响应式的 sessions
|
const { sessions } = storeToRefs(sessionStore); // 获取响应式的 sessions
|
||||||
|
const { statusMonitorShowIpBoolean } = storeToRefs(settingsStore); // 获取 IP 显示设置
|
||||||
const isSwitchingSession = ref(false);
|
const isSwitchingSession = ref(false);
|
||||||
|
|
||||||
interface ServerStatus {
|
interface ServerStatus {
|
||||||
@@ -278,4 +299,30 @@ const swapDisplay = computed(() => {
|
|||||||
return `${formatMemorySize(used)} / ${formatMemorySize(total)} ${percent}`;
|
return `${formatMemorySize(used)} / ${formatMemorySize(total)} ${percent}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sessionIpAddress = computed(() => {
|
||||||
|
const sessionState = currentSessionState.value;
|
||||||
|
if (sessionState && sessionState.connectionId) {
|
||||||
|
// 直接从 connectionsStore 的 connections 数组中查找
|
||||||
|
const connectionIdAsNumber = parseInt(sessionState.connectionId, 10);
|
||||||
|
if (isNaN(connectionIdAsNumber)) {
|
||||||
|
return null; // 如果 connectionId 不是有效的数字,则返回 null
|
||||||
|
}
|
||||||
|
const connectionInfo = connectionsStore.connections.find(conn => conn.id === connectionIdAsNumber);
|
||||||
|
return connectionInfo?.host || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||||
|
if (!ipAddress) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(ipAddress);
|
||||||
|
uiNotificationsStore.showSuccess(t('common.copied', '已复制!')); // + 使用通知 store 显示消息
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy IP address: ', err);
|
||||||
|
uiNotificationsStore.showError(t('statusMonitor.copyIpError', '复制 IP 失败')); // + 可选:显示错误通知
|
||||||
|
// 可以在这里添加一个错误提示,如果需要的话
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
|||||||
selectTag(existingTag);
|
selectTag(existingTag);
|
||||||
} else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing
|
} else if (!existingTag && allowCreate.value) { // Only create if allowed and not existing
|
||||||
// 如果是新标签,则 emit 事件让父组件处理创建
|
// 如果是新标签,则 emit 事件让父组件处理创建
|
||||||
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`); // +++ 添加日志 +++
|
console.log(`[TagInput] Emitting create-tag for: ${trimmedInput}`);
|
||||||
emit('create-tag', trimmedInput);
|
emit('create-tag', trimmedInput);
|
||||||
// 父组件负责创建、更新 availableTags prop,然后 TagInput 会响应式更新
|
// 父组件负责创建、更新 availableTags prop,然后 TagInput 会响应式更新
|
||||||
// 父组件也负责将新创建的 tag ID 添加到 modelValue
|
// 父组件也负责将新创建的 tag ID 添加到 modelValue
|
||||||
@@ -160,9 +160,9 @@ const removeTagLocally = (tagToRemove: GenericTag) => {
|
|||||||
|
|
||||||
// 处理全局删除标签 (点击标签上的 'x' 图标) - Emit event
|
// 处理全局删除标签 (点击标签上的 'x' 图标) - Emit event
|
||||||
const handleDeleteTagGlobally = (tagToDelete: GenericTag) => {
|
const handleDeleteTagGlobally = (tagToDelete: GenericTag) => {
|
||||||
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`); // +++ 添加日志 +++
|
console.log(`[TagInput] handleDeleteTagGlobally called for tag ID: ${tagToDelete.id}, Name: ${tagToDelete.name}`);
|
||||||
// Emit event for parent to handle deletion confirmation and API call
|
// Emit event for parent to handle deletion confirmation and API call
|
||||||
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`); // +++ 添加日志 +++
|
console.log(`[TagInput] Emitting delete-tag with ID: ${tagToDelete.id}`);
|
||||||
emit('delete-tag', tagToDelete.id);
|
emit('delete-tag', tagToDelete.id);
|
||||||
// Parent should handle confirmation, call store action, and update modelValue/availableTags
|
// Parent should handle confirmation, call store action, and update modelValue/availableTags
|
||||||
// We might still want to remove it locally immediately for better UX,
|
// We might still want to remove it locally immediately for better UX,
|
||||||
|
|||||||
@@ -562,7 +562,7 @@ const clearSearch = () => {
|
|||||||
searchAddon?.clearDecorations();
|
searchAddon?.clearDecorations();
|
||||||
};
|
};
|
||||||
|
|
||||||
// +++ 添加 clear 方法 +++
|
// +++ clear 方法 +++
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
terminal?.clear();
|
terminal?.clear();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineEmits } from 'vue'; // +++ Import ref +++
|
import { ref, defineEmits } from 'vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'send-key', keySequence: string): void;
|
(e: 'send-key', keySequence: string): void;
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettingsStore } from '../../stores/settings.store';
|
import { useSettingsStore } from '../../stores/settings.store';
|
||||||
import { useAppearanceStore } from '../../stores/appearance.store'; // 导入外观 store
|
import { useAppearanceStore } from '../../stores/appearance.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useAppearanceSettings } from '../../composables/settings/useAppearanceSettings';
|
import { useAppearanceSettings } from '../../composables/settings/useAppearanceSettings';
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { settings } = storeToRefs(settingsStore); // To ensure v-if="settings" works
|
const { settings } = storeToRefs(settingsStore);
|
||||||
const appearanceStore = useAppearanceStore(); // 实例化外观 store
|
const appearanceStore = useAppearanceStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -222,7 +222,27 @@
|
|||||||
<p v-if="terminalEnableRightClickPasteMessage" :class="['text-sm', terminalEnableRightClickPasteSuccess ? 'text-success' : 'text-error']">{{ terminalEnableRightClickPasteMessage }}</p>
|
<p v-if="terminalEnableRightClickPasteMessage" :class="['text-sm', terminalEnableRightClickPasteSuccess ? 'text-success' : 'text-error']">{{ terminalEnableRightClickPasteMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<hr class="border-border/50">
|
||||||
|
<!-- Status Monitor Show IP -->
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<h3 class="text-base font-semibold text-foreground mb-3">{{ $t('settings.statusMonitorShowIp.title', '状态监视器 IP 显示') }}</h3>
|
||||||
|
<form @submit.prevent="handleUpdateStatusMonitorShowIpSetting" class="space-y-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="statusMonitorShowIp" v-model="statusMonitorShowIpEnabled"
|
||||||
|
class="h-4 w-4 rounded border-border text-primary focus:ring-primary mr-2 cursor-pointer">
|
||||||
|
<label for="statusMonitorShowIp" class="text-sm text-foreground cursor-pointer select-none">{{ $t('settings.statusMonitorShowIp.enableLabel', '在状态监视器中显示IP地址') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between pt-2">
|
||||||
|
<button type="submit"
|
||||||
|
:disabled="statusMonitorShowIpLoading"
|
||||||
|
class="px-4 py-2 bg-button text-button-text rounded-md shadow-sm hover:bg-button-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition duration-150 ease-in-out text-sm font-medium">
|
||||||
|
{{ $t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<p v-if="statusMonitorShowIpMessage" :class="['text-sm', statusMonitorShowIpSuccess ? 'text-success' : 'text-error']">{{ statusMonitorShowIpMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -283,6 +303,11 @@ const {
|
|||||||
showPopupFileManagerMessage,
|
showPopupFileManagerMessage,
|
||||||
showPopupFileManagerSuccess,
|
showPopupFileManagerSuccess,
|
||||||
handleUpdateShowPopupFileManager,
|
handleUpdateShowPopupFileManager,
|
||||||
|
statusMonitorShowIpEnabled,
|
||||||
|
statusMonitorShowIpLoading,
|
||||||
|
statusMonitorShowIpMessage,
|
||||||
|
statusMonitorShowIpSuccess,
|
||||||
|
handleUpdateStatusMonitorShowIpSetting,
|
||||||
} = useWorkspaceSettings();
|
} = useWorkspaceSettings();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue'; // 导入 ComponentPublicInstance
|
import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue';
|
||||||
import type { FileListItem } from '../../types/sftp.types'; // 修正路径
|
import type { FileListItem } from '../../types/sftp.types';
|
||||||
import { type useI18n } from 'vue-i18n'; // 导入 useI18n 以获取 t 的类型
|
import { type useI18n } from 'vue-i18n';
|
||||||
import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue'; // <-- 导入组件类型
|
import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue';
|
||||||
|
|
||||||
// 定义菜单项类型 (可以根据需要扩展)
|
// 定义菜单项类型 (可以根据需要扩展)
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
@@ -242,7 +242,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
|
|||||||
];
|
];
|
||||||
} else { // Clicked on '..'
|
} else { // Clicked on '..'
|
||||||
menu = [
|
menu = [
|
||||||
// +++ 添加粘贴 (可以粘贴到上级目录) +++
|
// +++ 粘贴 (可以粘贴到上级目录) +++
|
||||||
{ label: t('fileManager.actions.paste'), action: onPaste, disabled: !(isConnected.value && isSftpReady.value) || !hasClipboardContent },
|
{ label: t('fileManager.actions.paste'), action: onPaste, disabled: !(isConnected.value && isSftpReady.value) || !hasClipboardContent },
|
||||||
{ label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !(isConnected.value && isSftpReady.value) }
|
{ label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !(isConnected.value && isSftpReady.value) }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ref, watch } from 'vue';
|
|||||||
import { useSettingsStore } from '../../stores/settings.store';
|
import { useSettingsStore } from '../../stores/settings.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { availableLocales } from '../../i18n'; // 导入可用语言列表
|
import { availableLocales } from '../../i18n';
|
||||||
|
|
||||||
export function useSystemSettings() {
|
export function useSystemSettings() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export function useWorkspaceSettings() {
|
|||||||
terminalScrollbackLimitNumber,
|
terminalScrollbackLimitNumber,
|
||||||
fileManagerShowDeleteConfirmationBoolean,
|
fileManagerShowDeleteConfirmationBoolean,
|
||||||
terminalEnableRightClickPasteBoolean,
|
terminalEnableRightClickPasteBoolean,
|
||||||
showPopupFileManagerBoolean, // +++ Import the new getter +++
|
showPopupFileManagerBoolean,
|
||||||
|
statusMonitorShowIpBoolean,
|
||||||
} = storeToRefs(settingsStore);
|
} = storeToRefs(settingsStore);
|
||||||
|
|
||||||
// --- Popup Editor ---
|
// --- Popup Editor ---
|
||||||
@@ -286,6 +287,30 @@ export function useWorkspaceSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Status Monitor Show IP ---
|
||||||
|
const statusMonitorShowIpEnabled = ref(false);
|
||||||
|
const statusMonitorShowIpLoading = ref(false);
|
||||||
|
const statusMonitorShowIpMessage = ref('');
|
||||||
|
const statusMonitorShowIpSuccess = ref(false);
|
||||||
|
|
||||||
|
const handleUpdateStatusMonitorShowIpSetting = async () => {
|
||||||
|
statusMonitorShowIpLoading.value = true;
|
||||||
|
statusMonitorShowIpMessage.value = '';
|
||||||
|
statusMonitorShowIpSuccess.value = false;
|
||||||
|
try {
|
||||||
|
const valueToSave = statusMonitorShowIpEnabled.value ? 'true' : 'false';
|
||||||
|
await settingsStore.updateSetting('showStatusMonitorIpAddress', valueToSave);
|
||||||
|
statusMonitorShowIpMessage.value = t('common.saved');
|
||||||
|
statusMonitorShowIpSuccess.value = true;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update status monitor IP display setting:', error);
|
||||||
|
statusMonitorShowIpMessage.value = error.message || t('settings.statusMonitorShowIp.error.saveFailed', 'Failed to save status monitor IP display setting.'); // 需要添加相应的i18n键
|
||||||
|
statusMonitorShowIpSuccess.value = false;
|
||||||
|
} finally {
|
||||||
|
statusMonitorShowIpLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Watchers to sync local state with store state
|
// Watchers to sync local state with store state
|
||||||
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
|
watch(showPopupFileEditorBoolean, (newValue) => { popupEditorEnabled.value = newValue; }, { immediate: true });
|
||||||
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
|
watch(shareFileEditorTabsBoolean, (newValue) => { shareTabsEnabled.value = newValue; }, { immediate: true });
|
||||||
@@ -298,6 +323,7 @@ export function useWorkspaceSettings() {
|
|||||||
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
|
watch(fileManagerShowDeleteConfirmationBoolean, (newValue) => { fileManagerShowDeleteConfirmationLocal.value = newValue; }, { immediate: true });
|
||||||
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true });
|
watch(terminalEnableRightClickPasteBoolean, (newValue) => { terminalEnableRightClickPasteLocal.value = newValue; }, { immediate: true });
|
||||||
watch(showPopupFileManagerBoolean, (newValue) => { showPopupFileManagerLocal.value = newValue; }, { immediate: true }); // +++ Watch for popup file manager +++
|
watch(showPopupFileManagerBoolean, (newValue) => { showPopupFileManagerLocal.value = newValue; }, { immediate: true }); // +++ Watch for popup file manager +++
|
||||||
|
watch(statusMonitorShowIpBoolean, (newValue) => { statusMonitorShowIpEnabled.value = newValue; }, { immediate: true });
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -367,5 +393,11 @@ export function useWorkspaceSettings() {
|
|||||||
showPopupFileManagerMessage,
|
showPopupFileManagerMessage,
|
||||||
showPopupFileManagerSuccess,
|
showPopupFileManagerSuccess,
|
||||||
handleUpdateShowPopupFileManager,
|
handleUpdateShowPopupFileManager,
|
||||||
|
|
||||||
|
statusMonitorShowIpEnabled,
|
||||||
|
statusMonitorShowIpLoading,
|
||||||
|
statusMonitorShowIpMessage,
|
||||||
|
statusMonitorShowIpSuccess,
|
||||||
|
handleUpdateStatusMonitorShowIpSetting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue'; // 引入 reactive 和 computed
|
import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue';
|
||||||
import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types';
|
import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types';
|
||||||
import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types';
|
import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types';
|
||||||
// 导入 UI 通知 store
|
|
||||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 更正导入
|
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface WebSocketDependencies
|
* @interface WebSocketDependencies
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function useSidebarResize({
|
|||||||
const handleMouseDown = (event: MouseEvent) => {
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
console.log(`[useSidebarResize] handleMouseDown triggered for side: ${side}`, { sidebar: sidebarRef.value, handle: handleRef.value }); // +++ Add Log +++
|
console.log(`[useSidebarResize] handleMouseDown triggered for side: ${side}`, { sidebar: sidebarRef.value, handle: handleRef.value }); // +++ Add Log +++
|
||||||
if (!sidebarRef.value || !handleRef.value) {
|
if (!sidebarRef.value || !handleRef.value) {
|
||||||
console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.'); // +++ Add Log +++
|
console.log('[useSidebarResize] MouseDown ignored: sidebarRef or handleRef is null.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -816,6 +816,10 @@
|
|||||||
"terminalRightClickPasteSuccess": "Terminal right-click paste setting saved.",
|
"terminalRightClickPasteSuccess": "Terminal right-click paste setting saved.",
|
||||||
"terminalRightClickPasteError": "Failed to save terminal right-click paste setting."
|
"terminalRightClickPasteError": "Failed to save terminal right-click paste setting."
|
||||||
},
|
},
|
||||||
|
"statusMonitorShowIp": {
|
||||||
|
"title": "Status Monitor: Show IP Address",
|
||||||
|
"enableLabel": "Show IP address in status monitor"
|
||||||
|
},
|
||||||
"terminalScrollback": {
|
"terminalScrollback": {
|
||||||
"title": "Terminal Scrollback Limit",
|
"title": "Terminal Scrollback Limit",
|
||||||
"limitLabel": "Maximum Lines",
|
"limitLabel": "Maximum Lines",
|
||||||
@@ -940,6 +944,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
|
"saved": "Saved",
|
||||||
"testing": "Testing...",
|
"testing": "Testing...",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
@@ -962,7 +967,8 @@
|
|||||||
"sortDescending": "Descending",
|
"sortDescending": "Descending",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"minimize": "Minimize",
|
"minimize": "Minimize",
|
||||||
"send":"Send"
|
"send":"Send",
|
||||||
|
"copied": "Copied to clipboard"
|
||||||
},
|
},
|
||||||
"layoutConfigurator": {
|
"layoutConfigurator": {
|
||||||
"title": "Layout Configurator",
|
"title": "Layout Configurator",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"retry": "再試行",
|
"retry": "再試行",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saving": "保存中...",
|
"saving": "保存中...",
|
||||||
|
"saved": "保存済み",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"sortAscending": "昇順",
|
"sortAscending": "昇順",
|
||||||
@@ -104,7 +105,8 @@
|
|||||||
"width": "幅",
|
"width": "幅",
|
||||||
"restore": "元に戻す",
|
"restore": "元に戻す",
|
||||||
"minimize": "最小化",
|
"minimize": "最小化",
|
||||||
"send":"送信する"
|
"send":"送信する",
|
||||||
|
"copied": "クリップボードにコピーしました"
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
"actions": {
|
"actions": {
|
||||||
@@ -1075,6 +1077,10 @@
|
|||||||
"terminalRightClickPasteSuccess": "ターミナルの右クリック貼り付け設定が保存されました。",
|
"terminalRightClickPasteSuccess": "ターミナルの右クリック貼り付け設定が保存されました。",
|
||||||
"terminalRightClickPasteError": "ターミナルの右クリック貼り付け設定の保存に失敗しました。"
|
"terminalRightClickPasteError": "ターミナルの右クリック貼り付け設定の保存に失敗しました。"
|
||||||
},
|
},
|
||||||
|
"statusMonitorShowIp": {
|
||||||
|
"title": "ステータスモニターでIPアドレスを表示",
|
||||||
|
"enableLabel": "ステータスモニターにIPアドレスを表示する"
|
||||||
|
},
|
||||||
"terminalScrollback": {
|
"terminalScrollback": {
|
||||||
"title": "ターミナルスクロールバック制限",
|
"title": "ターミナルスクロールバック制限",
|
||||||
"limitLabel": "最大行数",
|
"limitLabel": "最大行数",
|
||||||
|
|||||||
@@ -810,6 +810,10 @@
|
|||||||
"terminalRightClickPasteSuccess": "终端右键粘贴设置已保存。",
|
"terminalRightClickPasteSuccess": "终端右键粘贴设置已保存。",
|
||||||
"terminalRightClickPasteError": "保存终端右键粘贴设置失败。"
|
"terminalRightClickPasteError": "保存终端右键粘贴设置失败。"
|
||||||
},
|
},
|
||||||
|
"statusMonitorShowIp": {
|
||||||
|
"title": "状态监视器显示IP地址",
|
||||||
|
"enableLabel": "在状态监视器中显示IP地址"
|
||||||
|
},
|
||||||
"terminalScrollback": {
|
"terminalScrollback": {
|
||||||
"title": "终端回滚行数",
|
"title": "终端回滚行数",
|
||||||
"limitLabel": "最大行数",
|
"limitLabel": "最大行数",
|
||||||
@@ -941,6 +945,7 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"saving": "保存中...",
|
"saving": "保存中...",
|
||||||
|
"saved": "已保存",
|
||||||
"testing": "测试中...",
|
"testing": "测试中...",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
@@ -963,7 +968,8 @@
|
|||||||
"sortDescending": "降序",
|
"sortDescending": "降序",
|
||||||
"restore": "还原",
|
"restore": "还原",
|
||||||
"minimize": "最小化",
|
"minimize": "最小化",
|
||||||
"send":"发送"
|
"send":"发送",
|
||||||
|
"copied": "已复制到剪贴板"
|
||||||
},
|
},
|
||||||
"layoutConfigurator": {
|
"layoutConfigurator": {
|
||||||
"title": "布局管理器",
|
"title": "布局管理器",
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia'; // 引入 Pinia
|
import { createPinia } from 'pinia';
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; // 引入持久化插件
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router'; // 引入我们创建的 router
|
import router from './router';
|
||||||
import i18n from './i18n'; // 引入 i18n 实例
|
import i18n from './i18n';
|
||||||
import { useAuthStore } from './stores/auth.store'; // 引入 Auth Store
|
import { useAuthStore } from './stores/auth.store';
|
||||||
import { useSettingsStore } from './stores/settings.store'; // 引入 Settings Store
|
import { useSettingsStore } from './stores/settings.store';
|
||||||
import { useAppearanceStore } from './stores/appearance.store'; // 引入 Appearance Store
|
import { useAppearanceStore } from './stores/appearance.store';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
// 导入 Font Awesome CSS
|
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
// 导入 splitpanes CSS
|
|
||||||
import 'splitpanes/dist/splitpanes.css';
|
import 'splitpanes/dist/splitpanes.css';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
import apiClient from '../utils/apiClient';
|
||||||
import router from '../router'; // 引入 router 用于重定向
|
import router from '../router';
|
||||||
import { setLocale } from '../i18n'; // 导入 setLocale
|
import { setLocale } from '../i18n';
|
||||||
|
|
||||||
// 扩展的用户信息接口,包含 2FA 状态和语言偏好
|
// 扩展的用户信息接口,包含 2FA 状态和语言偏好
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ref, computed, readonly, watch, nextTick } from 'vue'; // Import nextTick
|
import { ref, computed, readonly, watch, nextTick } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useSessionStore } from './session.store'; // 导入会话 Store
|
import { useSessionStore } from './session.store';
|
||||||
import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types'; // 移除 SftpReadFileRequestPayload, 因为 readFile 不再需要它
|
import type { SaveStatus, SftpReadFileSuccessPayload } from '../types/sftp.types';
|
||||||
import * as iconv from '@vscode/iconv-lite-umd'; // +++ 导入 iconv-lite +++
|
import * as iconv from '@vscode/iconv-lite-umd';
|
||||||
import { Buffer } from 'buffer/'; // +++ 导入 Buffer (需要安装 buffer 依赖) +++
|
import { Buffer } from 'buffer/';
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
// 文件信息,用于打开文件操作
|
// 文件信息,用于打开文件操作
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
import apiClient from '../utils/apiClient';
|
||||||
import { ref, computed, watch } from 'vue'; // Import watch
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useUiNotificationsStore } from './uiNotifications.store';
|
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||||
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store'; // +++ Import new tag store +++
|
import { useQuickCommandTagsStore, type QuickCommandTag } from './quickCommandTags.store';
|
||||||
import { useI18n } from 'vue-i18n'; // +++ Import i18n for "Untagged" +++
|
import { useI18n } from 'vue-i18n';
|
||||||
// Assuming QuickCommand type in types includes tagIds now, or define it here
|
|
||||||
// import type { QuickCommand } from '../types/quick-commands.types';
|
|
||||||
|
|
||||||
// 定义前端使用的快捷指令接口 (包含 tagIds)
|
// 定义前端使用的快捷指令接口 (包含 tagIds)
|
||||||
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
export interface QuickCommandFE { // Renamed from QuickCommand if necessary
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// packages/frontend/src/stores/session.store.ts
|
|
||||||
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
|
import { useConnectionsStore, type ConnectionInfo } from './connections.store';
|
||||||
|
|
||||||
// 从新模块导入状态
|
|
||||||
import {
|
import {
|
||||||
sessions,
|
sessions,
|
||||||
activeSessionId,
|
activeSessionId,
|
||||||
@@ -13,30 +13,30 @@ import {
|
|||||||
rdpConnectionInfo,
|
rdpConnectionInfo,
|
||||||
isVncModalOpen,
|
isVncModalOpen,
|
||||||
vncConnectionInfo,
|
vncConnectionInfo,
|
||||||
// SSH Suspend Mode State
|
|
||||||
suspendedSshSessions,
|
suspendedSshSessions,
|
||||||
isLoadingSuspendedSessions,
|
isLoadingSuspendedSessions,
|
||||||
} from './session/state';
|
} from './session/state';
|
||||||
|
|
||||||
// 从新模块导入 Getters
|
|
||||||
import {
|
import {
|
||||||
sessionTabs,
|
sessionTabs,
|
||||||
sessionTabsWithStatus,
|
sessionTabsWithStatus,
|
||||||
activeSession,
|
activeSession,
|
||||||
} from './session/getters';
|
} from './session/getters';
|
||||||
|
|
||||||
// 从新模块导入 Actions
|
|
||||||
import * as sessionActions from './session/actions/sessionActions';
|
import * as sessionActions from './session/actions/sessionActions';
|
||||||
import * as editorActions from './session/actions/editorActions';
|
import * as editorActions from './session/actions/editorActions';
|
||||||
import * as sftpManagerActions from './session/actions/sftpManagerActions';
|
import * as sftpManagerActions from './session/actions/sftpManagerActions';
|
||||||
import * as modalActions from './session/actions/modalActions';
|
import * as modalActions from './session/actions/modalActions';
|
||||||
import * as commandInputActions from './session/actions/commandInputActions';
|
import * as commandInputActions from './session/actions/commandInputActions';
|
||||||
import * as sshSuspendActions from './session/actions/sshSuspendActions'; // 导入 SSH 挂起 Actions
|
import * as sshSuspendActions from './session/actions/sshSuspendActions';
|
||||||
|
|
||||||
|
|
||||||
// 导入需要的类型 (例如 FileInfo 可能会在参数中使用)
|
|
||||||
import type { FileInfo } from './fileEditor.store';
|
import type { FileInfo } from './fileEditor.store';
|
||||||
// SftpManagerInstance 类型主要在 action 文件内部使用,但如果 store 直接暴露它,则需要导入
|
|
||||||
// import type { SftpManagerInstance } from './session/types';
|
|
||||||
|
|
||||||
|
|
||||||
export const useSessionStore = defineStore('session', () => {
|
export const useSessionStore = defineStore('session', () => {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
// packages/frontend/src/stores/session/actions/sessionActions.ts
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useConnectionsStore, type ConnectionInfo } from '../../connections.store'; // 路径: packages/frontend/src/stores/connections.store.ts
|
import { useConnectionsStore, type ConnectionInfo } from '../../connections.store';
|
||||||
import { sessions, activeSessionId } from '../state';
|
import { sessions, activeSessionId } from '../state';
|
||||||
import { generateSessionId } from '../utils';
|
import { generateSessionId } from '../utils';
|
||||||
import type { SessionState, SshTerminalInstance, StatusMonitorInstance, DockerManagerInstance, SftpManagerInstance, WsManagerInstance } from '../types';
|
import type { SessionState, SshTerminalInstance, StatusMonitorInstance, DockerManagerInstance, SftpManagerInstance, WsManagerInstance } from '../types';
|
||||||
|
|
||||||
// Composables for manager creation - 路径相对于此文件
|
|
||||||
import { createWebSocketConnectionManager } from '../../../composables/useWebSocketConnection';
|
import { createWebSocketConnectionManager } from '../../../composables/useWebSocketConnection';
|
||||||
import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal';
|
import { createSshTerminalManager, type SshTerminalDependencies } from '../../../composables/useSshTerminal';
|
||||||
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor';
|
import { createStatusMonitorManager, type StatusMonitorDependencies } from '../../../composables/useStatusMonitor';
|
||||||
import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager';
|
import { createDockerManager, type DockerManagerDependencies } from '../../../composables/useDockerManager';
|
||||||
import { registerSshSuspendHandlers } from './sshSuspendActions'; // 导入 SSH 挂起处理器注册函数
|
import { registerSshSuspendHandlers } from './sshSuspendActions';
|
||||||
// getOrCreateSftpManager 将在 sftpManagerActions.ts 中定义,并在主 store 中协调
|
|
||||||
|
|
||||||
// --- 辅助函数 (特定于此模块的 actions) ---
|
// --- 辅助函数 (特定于此模块的 actions) ---
|
||||||
const findConnectionInfo = (connectionId: number | string, connectionsStore: ReturnType<typeof useConnectionsStore>): ConnectionInfo | undefined => {
|
const findConnectionInfo = (connectionId: number | string, connectionsStore: ReturnType<typeof useConnectionsStore>): ConnectionInfo | undefined => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// packages/frontend/src/stores/session/actions/sshSuspendActions.ts
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { sessions, suspendedSshSessions, isLoadingSuspendedSessions, activeSessionId } from '../state';
|
import { sessions, suspendedSshSessions, isLoadingSuspendedSessions, activeSessionId } from '../state';
|
||||||
import type {
|
import type {
|
||||||
@@ -8,8 +8,8 @@ import type {
|
|||||||
SshSuspendResumeReqMessage,
|
SshSuspendResumeReqMessage,
|
||||||
SshSuspendTerminateReqMessage,
|
SshSuspendTerminateReqMessage,
|
||||||
SshSuspendRemoveEntryReqMessage,
|
SshSuspendRemoveEntryReqMessage,
|
||||||
// SshSuspendEditNameReqMessage, // Removed, using HTTP API
|
|
||||||
// S2C Payloads
|
|
||||||
SshMarkedForSuspendAckPayload,
|
SshMarkedForSuspendAckPayload,
|
||||||
SshUnmarkedForSuspendAckPayload,
|
SshUnmarkedForSuspendAckPayload,
|
||||||
SshSuspendListResponsePayload,
|
SshSuspendListResponsePayload,
|
||||||
@@ -17,19 +17,19 @@ import type {
|
|||||||
SshOutputCachedChunkPayload,
|
SshOutputCachedChunkPayload,
|
||||||
SshSuspendTerminatedRespPayload,
|
SshSuspendTerminatedRespPayload,
|
||||||
SshSuspendEntryRemovedRespPayload,
|
SshSuspendEntryRemovedRespPayload,
|
||||||
// SshSuspendNameEditedRespPayload, // Removed, using HTTP API
|
|
||||||
SshSuspendAutoTerminatedNotifPayload,
|
|
||||||
} from '../../../types/websocket.types'; // 路径: packages/frontend/src/types/websocket.types.ts
|
|
||||||
import type { WsManagerInstance, SessionState } from '../types'; // 路径: packages/frontend/src/stores/session/types.ts // Re-add WsManagerInstance
|
|
||||||
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions'; // 使用 openNewSession 和 closeSession
|
|
||||||
import { useConnectionsStore } from '../../connections.store'; // 用于获取连接信息
|
|
||||||
import { useUiNotificationsStore } from '../../uiNotifications.store'; // 用于显示通知
|
|
||||||
import type { SuspendedSshSession } from '../../../types/ssh-suspend.types'; // 路径: packages/frontend/src/types/ssh-suspend.types.ts
|
|
||||||
import i18n from '../../../i18n'; // 直接导入 i18n 实例
|
|
||||||
import type { ComposerTranslation } from 'vue-i18n'; // 导入 ComposerTranslation 类型
|
|
||||||
import apiClient from '../../../utils/apiClient'; // +++ 导入 apiClient +++
|
|
||||||
|
|
||||||
const t: ComposerTranslation = i18n.global.t; // 从全局 i18n 实例获取 t 函数并显式注解类型
|
SshSuspendAutoTerminatedNotifPayload,
|
||||||
|
} from '../../../types/websocket.types';
|
||||||
|
import type { WsManagerInstance, SessionState } from '../types';
|
||||||
|
import { closeSession as closeSessionAction, activateSession as activateSessionAction, openNewSession, closeSession } from './sessionActions';
|
||||||
|
import { useConnectionsStore } from '../../connections.store';
|
||||||
|
import { useUiNotificationsStore } from '../../uiNotifications.store';
|
||||||
|
import type { SuspendedSshSession } from '../../../types/ssh-suspend.types';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
import type { ComposerTranslation } from 'vue-i18n';
|
||||||
|
import apiClient from '../../../utils/apiClient';
|
||||||
|
|
||||||
|
const t: ComposerTranslation = i18n.global.t;
|
||||||
|
|
||||||
// 辅助函数:获取一个可用的 WebSocket 管理器
|
// 辅助函数:获取一个可用的 WebSocket 管理器
|
||||||
// 优先使用当前激活的会话,或者任意一个已连接的 SSH 会话
|
// 优先使用当前激活的会话,或者任意一个已连接的 SSH 会话
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// packages/frontend/src/stores/session/types.ts
|
|
||||||
|
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { FileTab as OriginalFileTab } from '../fileEditor.store'; // 路径: packages/frontend/src/stores/fileEditor.store.ts
|
import type { FileTab as OriginalFileTab } from '../fileEditor.store';
|
||||||
import type { WsConnectionStatus } from '../../composables/useWebSocketConnection'; // 路径: packages/frontend/src/composables/useWebSocketConnection.ts
|
import type { WsConnectionStatus } from '../../composables/useWebSocketConnection';
|
||||||
|
import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager';
|
||||||
// 从源模块导入 DockerManagerInstance 类型并使用别名
|
|
||||||
import type { DockerManagerInstance as OriginalDockerManagerInstance } from '../../composables/useDockerManager'; // 路径: packages/frontend/src/composables/useDockerManager.ts
|
|
||||||
|
|
||||||
// 导入工厂函数仅用于通过 ReturnType 推导实例类型
|
// 导入工厂函数仅用于通过 ReturnType 推导实例类型
|
||||||
// 这些导入仅用于类型推断,不在运行时使用
|
// 这些导入仅用于类型推断,不在运行时使用
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import apiClient from '../utils/apiClient'; // 使用统一的 apiClient
|
import apiClient from '../utils/apiClient';
|
||||||
import { ref, computed } from 'vue'; // 移除 watch
|
import { ref, computed } from 'vue';
|
||||||
import i18n, { setLocale, defaultLng, availableLocales } from '../i18n'; // Import i18n instance, setLocale, defaultLng, and availableLocales
|
import i18n, { setLocale, defaultLng, availableLocales } from '../i18n';
|
||||||
import type { PaneName } from './layout.store';
|
import type { PaneName } from './layout.store';
|
||||||
import { useAuthStore } from './auth.store';
|
import { useAuthStore } from './auth.store';
|
||||||
import type { ConnectionInfo } from './connections.store';
|
import type { ConnectionInfo } from './connections.store';
|
||||||
@@ -62,6 +62,7 @@ interface SettingsState {
|
|||||||
terminalScrollbackLimit?: string; // 终端回滚行数上限 (e.g., '5000', '0' for unlimited)
|
terminalScrollbackLimit?: string; // 终端回滚行数上限 (e.g., '5000', '0' for unlimited)
|
||||||
fileManagerShowDeleteConfirmation?: string; // 'true' or 'false' - 文件管理器删除确认提示
|
fileManagerShowDeleteConfirmation?: string; // 'true' or 'false' - 文件管理器删除确认提示
|
||||||
terminalEnableRightClickPaste?: string; // 'true' or 'false' - 终端右键粘贴
|
terminalEnableRightClickPaste?: string; // 'true' or 'false' - 终端右键粘贴
|
||||||
|
showStatusMonitorIpAddress?: string; // 'true' or 'false' - 状态监视器显示IP地址
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
if (settings.value.showPopupFileEditor === undefined) {
|
if (settings.value.showPopupFileEditor === undefined) {
|
||||||
settings.value.showPopupFileEditor = 'true';
|
settings.value.showPopupFileEditor = 'true';
|
||||||
}
|
}
|
||||||
// +++ 添加 showPopupFileManager 默认值 (改为 false) +++
|
// +++ showPopupFileManager 默认值 (改为 false) +++
|
||||||
if (settings.value.showPopupFileManager === undefined) {
|
if (settings.value.showPopupFileManager === undefined) {
|
||||||
settings.value.showPopupFileManager = 'false'; // 默认禁用弹窗文件管理器
|
settings.value.showPopupFileManager = 'false'; // 默认禁用弹窗文件管理器
|
||||||
}
|
}
|
||||||
@@ -304,6 +305,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
settings.value.terminalEnableRightClickPaste = 'true'; // 默认启用右键粘贴
|
settings.value.terminalEnableRightClickPaste = 'true'; // 默认启用右键粘贴
|
||||||
console.log(`[SettingsStore] terminalEnableRightClickPaste not found, set to default: ${settings.value.terminalEnableRightClickPaste}`);
|
console.log(`[SettingsStore] terminalEnableRightClickPaste not found, set to default: ${settings.value.terminalEnableRightClickPaste}`);
|
||||||
}
|
}
|
||||||
|
if (settings.value.showStatusMonitorIpAddress === undefined) {
|
||||||
|
settings.value.showStatusMonitorIpAddress = 'false'; // 默认禁用状态监视器显示IP
|
||||||
|
console.log(`[SettingsStore] showStatusMonitorIpAddress not found, set to default: ${settings.value.showStatusMonitorIpAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- 语言设置 ---
|
// --- 语言设置 ---
|
||||||
@@ -375,19 +380,19 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// 移除外观相关的键检查
|
// 移除外观相关的键检查
|
||||||
const allowedKeys: Array<keyof SettingsState> = [
|
const allowedKeys: Array<keyof SettingsState> = [
|
||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
|
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++
|
||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++
|
||||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++
|
||||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++
|
||||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
'fileManagerColWidths', // +++ 文件管理器列宽键 +++
|
||||||
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
|
'commandInputSyncTarget', // +++ 命令输入同步目标键 +++
|
||||||
'timezone', // 添加时区键
|
'timezone', // 时区键
|
||||||
'rdpModalWidth', // 添加 RDP 模态框宽度键
|
'rdpModalWidth', // RDP 模态框宽度键
|
||||||
'rdpModalHeight', // 添加 RDP 模态框高度键
|
'rdpModalHeight', // RDP 模态框高度键
|
||||||
'vncModalWidth', // 添加 VNC 模态框宽度键
|
'vncModalWidth', // VNC 模态框宽度键
|
||||||
'vncModalHeight', // 添加 VNC 模态框高度键
|
'vncModalHeight', // VNC 模态框高度键
|
||||||
'ipBlacklistEnabled',
|
'ipBlacklistEnabled',
|
||||||
'dashboardSortBy',
|
'dashboardSortBy',
|
||||||
'dashboardSortOrder',
|
'dashboardSortOrder',
|
||||||
@@ -396,7 +401,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
'layoutLocked',
|
'layoutLocked',
|
||||||
'terminalScrollbackLimit',
|
'terminalScrollbackLimit',
|
||||||
'fileManagerShowDeleteConfirmation',
|
'fileManagerShowDeleteConfirmation',
|
||||||
'terminalEnableRightClickPaste'
|
'terminalEnableRightClickPaste',
|
||||||
|
'showStatusMonitorIpAddress'
|
||||||
];
|
];
|
||||||
if (!allowedKeys.includes(key)) {
|
if (!allowedKeys.includes(key)) {
|
||||||
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
||||||
@@ -470,19 +476,19 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// 移除外观相关的键检查
|
// 移除外观相关的键检查
|
||||||
const allowedKeys: Array<keyof SettingsState> = [
|
const allowedKeys: Array<keyof SettingsState> = [
|
||||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||||
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ 添加 showPopupFileManager +++
|
'showPopupFileEditor', 'showPopupFileManager', 'shareFileEditorTabs', 'ipWhitelistEnabled', // +++ showPopupFileManager +++
|
||||||
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
'autoCopyOnSelect', 'dockerStatusIntervalSeconds', 'dockerDefaultExpand',
|
||||||
'statusMonitorIntervalSeconds', // +++ 添加状态监控间隔键 +++
|
'statusMonitorIntervalSeconds', // +++ 状态监控间隔键 +++
|
||||||
'workspaceSidebarPersistent', // +++ 添加侧边栏固定键 +++
|
'workspaceSidebarPersistent', // +++ 侧边栏固定键 +++
|
||||||
'sidebarPaneWidths', // +++ 添加侧边栏宽度对象键 +++
|
'sidebarPaneWidths', // +++ 侧边栏宽度对象键 +++
|
||||||
'fileManagerRowSizeMultiplier', // +++ 添加文件管理器行大小键 +++
|
'fileManagerRowSizeMultiplier', // +++ 文件管理器行大小键 +++
|
||||||
'fileManagerColWidths', // +++ 添加文件管理器列宽键 +++
|
'fileManagerColWidths', // +++ 文件管理器列宽键 +++
|
||||||
'commandInputSyncTarget', // +++ 添加命令输入同步目标键 +++
|
'commandInputSyncTarget', // +++ 命令输入同步目标键 +++
|
||||||
'timezone', // 添加时区键
|
'timezone', // 时区键
|
||||||
'rdpModalWidth', // 添加 RDP 模态框宽度键
|
'rdpModalWidth', // RDP 模态框宽度键
|
||||||
'rdpModalHeight', // 添加 RDP 模态框高度键
|
'rdpModalHeight', // RDP 模态框高度键
|
||||||
'vncModalWidth', // 添加 VNC 模态框宽度键
|
'vncModalWidth', // VNC 模态框宽度键
|
||||||
'vncModalHeight', // 添加 VNC 模态框高度键
|
'vncModalHeight', // VNC 模态框高度键
|
||||||
'ipBlacklistEnabled',
|
'ipBlacklistEnabled',
|
||||||
'dashboardSortBy',
|
'dashboardSortBy',
|
||||||
'dashboardSortOrder',
|
'dashboardSortOrder',
|
||||||
@@ -491,7 +497,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
'layoutLocked',
|
'layoutLocked',
|
||||||
'terminalScrollbackLimit',
|
'terminalScrollbackLimit',
|
||||||
'fileManagerShowDeleteConfirmation',
|
'fileManagerShowDeleteConfirmation',
|
||||||
'terminalEnableRightClickPaste'
|
'terminalEnableRightClickPaste',
|
||||||
|
'showStatusMonitorIpAddress'
|
||||||
];
|
];
|
||||||
const filteredUpdates: Partial<SettingsState> = {};
|
const filteredUpdates: Partial<SettingsState> = {};
|
||||||
let languageUpdate: string | undefined = undefined;
|
let languageUpdate: string | undefined = undefined;
|
||||||
@@ -794,6 +801,10 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
return settings.value.terminalEnableRightClickPaste !== 'false'; // Default to true
|
return settings.value.terminalEnableRightClickPaste !== 'false'; // Default to true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusMonitorShowIpBoolean = computed(() => {
|
||||||
|
return settings.value.showStatusMonitorIpAddress === 'true';
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings, // 只包含通用设置
|
settings, // 只包含通用设置
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -838,5 +849,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
terminalScrollbackLimitNumber, // Expose terminal scrollback limit getter
|
terminalScrollbackLimitNumber, // Expose terminal scrollback limit getter
|
||||||
fileManagerShowDeleteConfirmationBoolean, // Expose file manager delete confirmation getter
|
fileManagerShowDeleteConfirmationBoolean, // Expose file manager delete confirmation getter
|
||||||
terminalEnableRightClickPasteBoolean, // Expose terminal right click paste getter
|
terminalEnableRightClickPasteBoolean, // Expose terminal right click paste getter
|
||||||
|
statusMonitorShowIpBoolean, // 暴露状态监视器显示IP getter
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import router from '../router'; // 引入 router 用于可能的重定向
|
import router from '../router';
|
||||||
import { useAuthStore } from '../stores/auth.store'; // 引入 auth store 用于检查认证状态和登出
|
import { useAuthStore } from '../stores/auth.store';
|
||||||
|
|
||||||
// 创建 axios 实例
|
// 创建 axios 实例
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import AddConnectionForm from '../components/AddConnectionForm.vue'; // +++ 引入表单组件 +++
|
import AddConnectionForm from '../components/AddConnectionForm.vue';
|
||||||
import { useConnectionsStore } from '../stores/connections.store';
|
import { useConnectionsStore } from '../stores/connections.store';
|
||||||
import { useAuditLogStore } from '../stores/audit.store';
|
import { useAuditLogStore } from '../stores/audit.store';
|
||||||
import { useSessionStore } from '../stores/session.store';
|
import { useSessionStore } from '../stores/session.store';
|
||||||
import { useTagsStore } from '../stores/tags.store'; // +++ 添加标签 store +++
|
import { useTagsStore } from '../stores/tags.store';
|
||||||
import type { TagInfo } from '../stores/tags.store'; // +++ 修正标签类型导入 +++
|
import type { TagInfo } from '../stores/tags.store';
|
||||||
// Removed settings store import for sorting
|
|
||||||
import type { SortField, SortOrder } from '../stores/settings.store'; // Keep type import
|
import type { SortField, SortOrder } from '../stores/settings.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import type { ConnectionInfo } from '../stores/connections.store';
|
import type { ConnectionInfo } from '../stores/connections.store';
|
||||||
import { storeToRefs } from 'pinia'; // Keep for other stores if needed
|
import { storeToRefs } from 'pinia';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN, enUS, ja } from 'date-fns/locale';
|
import { zhCN, enUS, ja } from 'date-fns/locale';
|
||||||
import type { Locale } from 'date-fns';
|
import type { Locale } from 'date-fns';
|
||||||
@@ -21,18 +21,18 @@ const router = useRouter();
|
|||||||
const connectionsStore = useConnectionsStore();
|
const connectionsStore = useConnectionsStore();
|
||||||
const auditLogStore = useAuditLogStore();
|
const auditLogStore = useAuditLogStore();
|
||||||
const sessionStore = useSessionStore();
|
const sessionStore = useSessionStore();
|
||||||
const tagsStore = useTagsStore(); // +++ 实例化标签 store +++
|
const tagsStore = useTagsStore();
|
||||||
// Removed settings store instantiation
|
|
||||||
|
|
||||||
const { connections, isLoading: isLoadingConnections } = storeToRefs(connectionsStore);
|
const { connections, isLoading: isLoadingConnections } = storeToRefs(connectionsStore);
|
||||||
const { logs: auditLogs, isLoading: isLoadingLogs, totalLogs } = storeToRefs(auditLogStore);
|
const { logs: auditLogs, isLoading: isLoadingLogs, totalLogs } = storeToRefs(auditLogStore);
|
||||||
const { tags, isLoading: isLoadingTags } = storeToRefs(tagsStore); // +++ 获取标签数据和加载状态 +++
|
const { tags, isLoading: isLoadingTags } = storeToRefs(tagsStore);
|
||||||
// Removed refs from settings store
|
|
||||||
|
|
||||||
|
|
||||||
// Local state for sorting with localStorage persistence
|
|
||||||
const LS_SORT_BY_KEY = 'dashboard_connections_sort_by';
|
const LS_SORT_BY_KEY = 'dashboard_connections_sort_by';
|
||||||
const LS_SORT_ORDER_KEY = 'dashboard_connections_sort_order';
|
const LS_SORT_ORDER_KEY = 'dashboard_connections_sort_order';
|
||||||
const LS_FILTER_TAG_KEY = 'dashboard_connections_filter_tag'; // +++ 添加标签筛选的 localStorage key +++
|
const LS_FILTER_TAG_KEY = 'dashboard_connections_filter_tag';
|
||||||
|
|
||||||
// Initialize with localStorage values or defaults
|
// Initialize with localStorage values or defaults
|
||||||
const localSortBy = ref<SortField>(localStorage.getItem(LS_SORT_BY_KEY) as SortField || 'last_connected_at');
|
const localSortBy = ref<SortField>(localStorage.getItem(LS_SORT_BY_KEY) as SortField || 'last_connected_at');
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ const finishEditingTag = async () => {
|
|||||||
|
|
||||||
if (commandIdsToAssign.length > 0) {
|
if (commandIdsToAssign.length > 0) {
|
||||||
console.log(`[QuickCmdView] Assigning ${commandIdsToAssign.length} commands to new tag ID: ${newTag.id}`);
|
console.log(`[QuickCmdView] Assigning ${commandIdsToAssign.length} commands to new tag ID: ${newTag.id}`);
|
||||||
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`); // +++ 添加日志 +++
|
console.log(`[QuickCmdView] Command IDs to assign: ${JSON.stringify(commandIdsToAssign)}`);
|
||||||
// Call the store action to assign commands to the new tag
|
// Call the store action to assign commands to the new tag
|
||||||
const assignSuccess = await quickCommandsStore.assignCommandsToTagAction(commandIdsToAssign, newTag.id);
|
const assignSuccess = await quickCommandsStore.assignCommandsToTagAction(commandIdsToAssign, newTag.id);
|
||||||
if (assignSuccess) {
|
if (assignSuccess) {
|
||||||
|
|||||||
@@ -86,10 +86,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'; // Simplified Vue imports
|
import { onMounted, ref } from 'vue';
|
||||||
import { useAuthStore } from '../stores/auth.store';
|
import { useAuthStore } from '../stores/auth.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
import { useAppearanceStore } from '../stores/appearance.store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useVersionCheck } from '../composables/settings/useVersionCheck';
|
import { useVersionCheck } from '../composables/settings/useVersionCheck';
|
||||||
|
|||||||
Reference in New Issue
Block a user