Add files via upload
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* 终端相关样式
|
||||
*/
|
||||
|
||||
/* CSS变量 */
|
||||
:root {
|
||||
--terminal-bg: #f7f7f7;
|
||||
--terminal-panel-bg: #ffffff;
|
||||
--terminal-border: #e5e5e5;
|
||||
--terminal-text: #333333;
|
||||
--terminal-text-light: #666666;
|
||||
--terminal-hover-bg: #f0f0f0;
|
||||
--terminal-button-bg: #444444;
|
||||
--terminal-button-text: #ffffff;
|
||||
--terminal-button-hover: #333333;
|
||||
--terminal-tab-bg: #555555;
|
||||
--terminal-tab-active: #1a1a1a;
|
||||
--terminal-tab-text: #cccccc;
|
||||
--terminal-shadow: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
[data-theme="dark"] {
|
||||
--terminal-bg: #1e293b;
|
||||
--terminal-panel-bg: #334155;
|
||||
--terminal-border: #475569;
|
||||
--terminal-text: #f1f5f9;
|
||||
--terminal-text-light: #94a3b8;
|
||||
--terminal-hover-bg: #475569;
|
||||
--terminal-button-bg: #e2e8f0;
|
||||
--terminal-button-text: #0f172a;
|
||||
--terminal-button-hover: #f1f5f9;
|
||||
--terminal-tab-bg: #64748b;
|
||||
--terminal-tab-active: #e2e8f0;
|
||||
--terminal-tab-text: #f1f5f9;
|
||||
--terminal-shadow: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 终端容器 */
|
||||
.terminal-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 10px var(--terminal-shadow);
|
||||
}
|
||||
|
||||
/* 会话管理面板 */
|
||||
.sessions-card {
|
||||
flex: 0 0 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--terminal-panel-bg);
|
||||
border-right: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.sessions-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.sessions-header h2 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.sessions-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sessions-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.sessions-section {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.sessions-section h3 {
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--terminal-text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.sessions-list {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--terminal-bg);
|
||||
box-shadow: 0 1px 3px var(--terminal-shadow);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background-color: var(--terminal-hover-bg);
|
||||
box-shadow: 0 1px 5px var(--terminal-shadow);
|
||||
}
|
||||
|
||||
.session-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.session-item-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
.session-item-details {
|
||||
font-size: 0.8rem;
|
||||
color: var(--terminal-text-light);
|
||||
}
|
||||
|
||||
.session-item-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.session-item-action {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--terminal-text-light);
|
||||
cursor: pointer;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.session-item-action:hover {
|
||||
background-color: var(--terminal-hover-bg);
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
/* 会话组样式 */
|
||||
.session-group-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.session-group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background-color: var(--terminal-hover-bg);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-group-content {
|
||||
padding: 5px 0 5px 15px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.session-group-content.active {
|
||||
max-height: 500px;
|
||||
padding: 10px 0 10px 15px;
|
||||
}
|
||||
|
||||
.session-empty {
|
||||
font-size: 0.8rem;
|
||||
color: var(--terminal-text-light);
|
||||
text-align: center;
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
/* 终端卡片 */
|
||||
.terminal-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: var(--terminal-panel-bg);
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.terminal-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background-color: var(--terminal-button-bg);
|
||||
border-bottom: 1px solid var(--terminal-border);
|
||||
}
|
||||
|
||||
.terminal-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px; /* 防止滚动条挡住内容 */
|
||||
margin: 0;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.terminal-tab {
|
||||
padding: 5px 12px;
|
||||
background-color: var(--terminal-tab-bg);
|
||||
color: var(--terminal-tab-text);
|
||||
border-radius: 5px 5px 0 0;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.terminal-tab.active {
|
||||
background-color: var(--terminal-tab-active);
|
||||
color: var(--terminal-text);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-tab.active {
|
||||
background-color: var(--terminal-tab-active);
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-tab {
|
||||
background-color: var(--terminal-tab-bg);
|
||||
color: var(--terminal-tab-text);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-tab:hover {
|
||||
background-color: #94a3b8;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.terminal-tab-close {
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.terminal-tab:hover .terminal-tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.terminal-windows {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-instance {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.terminal-instance.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* XTerm 相关样式 */
|
||||
.xterm {
|
||||
padding: 5px;
|
||||
height: 100%;
|
||||
font-feature-settings: "liga" 0;
|
||||
}
|
||||
|
||||
.xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
background-color: #1a1a1a;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #444 #222;
|
||||
}
|
||||
|
||||
.xterm-viewport::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.xterm-viewport::-webkit-scrollbar-track {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.xterm-screen {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.xterm-rows {
|
||||
font-variant-ligatures: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 会话状态指示灯样式 */
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #22c55e; /* 绿色 - 活跃 */
|
||||
box-shadow: 0 0 5px #22c55e;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background-color: #f59e0b; /* 黄色 - 连接中 */
|
||||
box-shadow: 0 0 5px #f59e0b;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: #ef4444; /* 红色 - 断开 */
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #f97316; /* 橙色 - 错误 */
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 5px rgba(34, 197, 94, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* 设备连接对话框 */
|
||||
#device-connect-modal .modal-content {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
/* 创建新终端相关样式 */
|
||||
#reconnect-terminal, #connect-device {
|
||||
background-color: var(--terminal-button-bg) !important;
|
||||
color: var(--terminal-button-text) !important;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#reconnect-terminal:hover, #connect-device:hover {
|
||||
background-color: var(--terminal-button-hover) !important;
|
||||
color: var(--terminal-button-text) !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下终端按钮的特殊处理 - 使用最高优先级 */
|
||||
[data-theme="dark"] #reconnect-terminal,
|
||||
[data-theme="dark"] #connect-device {
|
||||
background-color: #e2e8f0 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #reconnect-terminal:hover,
|
||||
[data-theme="dark"] #connect-device:hover {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 确保终端工具栏按钮在暗色主题下正确显示 */
|
||||
[data-theme="dark"] .terminal-actions .btn {
|
||||
background-color: #e2e8f0 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .terminal-actions .btn:hover {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* 暗色主题下会话管理面板按钮的特殊处理 */
|
||||
[data-theme="dark"] .sessions-actions .btn {
|
||||
background-color: #e2e8f0 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sessions-actions .btn:hover {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sessions-actions .btn-sm {
|
||||
background-color: #e2e8f0 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #cbd5e1 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sessions-actions .btn-sm:hover {
|
||||
background-color: #f1f5f9 !important;
|
||||
color: #0f172a !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* 适配移动设备 */
|
||||
@media (max-width: 768px) {
|
||||
.terminal-layout {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.sessions-card {
|
||||
flex: none;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.terminal-card {
|
||||
flex: none;
|
||||
height: calc(100vh - 400px);
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,818 @@
|
||||
/**
|
||||
* 仪表盘功能模块
|
||||
* 提供主题切换、数据统计、系统状态监控等功能
|
||||
*/
|
||||
|
||||
class DashboardManager {
|
||||
constructor() {
|
||||
this.refreshInterval = null;
|
||||
this.timeInterval = null;
|
||||
this.activities = [];
|
||||
this.lastDataSnapshot = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化仪表盘
|
||||
*/
|
||||
init() {
|
||||
console.log('初始化仪表盘管理器...');
|
||||
|
||||
// 初始化主题切换器
|
||||
this.initializeThemeSwitcher();
|
||||
|
||||
// 更新当前时间
|
||||
this.updateCurrentTime();
|
||||
this.timeInterval = setInterval(() => this.updateCurrentTime(), 1000);
|
||||
|
||||
// 加载仪表盘数据
|
||||
this.loadDashboardData();
|
||||
|
||||
// 绑定仪表盘事件
|
||||
this.bindEvents();
|
||||
|
||||
// 添加活动记录
|
||||
this.addActivity('仪表盘初始化成功', 'success');
|
||||
|
||||
// 设置自动刷新(30秒)
|
||||
this.refreshInterval = setInterval(() => this.loadDashboardData(), 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题切换器初始化
|
||||
*/
|
||||
initializeThemeSwitcher() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (!themeToggle) {
|
||||
console.warn('主题切换器元素未找到');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取保存的主题或使用默认主题
|
||||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||||
console.log('当前主题:', currentTheme);
|
||||
|
||||
// 应用主题
|
||||
this.applyTheme(currentTheme);
|
||||
|
||||
// 设置切换器状态
|
||||
themeToggle.checked = currentTheme === 'dark';
|
||||
|
||||
// 监听主题切换事件
|
||||
themeToggle.addEventListener('change', (event) => {
|
||||
const newTheme = event.target.checked ? 'dark' : 'light';
|
||||
console.log('切换主题到:', newTheme);
|
||||
|
||||
this.applyTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// 添加活动记录
|
||||
this.addActivity(`切换到${newTheme === 'dark' ? '暗色' : '明亮'}主题`, 'info');
|
||||
});
|
||||
|
||||
console.log('主题切换器初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
*/
|
||||
applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
||||
// 更新元标签以支持系统主题
|
||||
let themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (!themeColorMeta) {
|
||||
themeColorMeta = document.createElement('meta');
|
||||
themeColorMeta.name = 'theme-color';
|
||||
document.head.appendChild(themeColorMeta);
|
||||
}
|
||||
|
||||
// 根据主题设置元标签颜色
|
||||
if (theme === 'dark') {
|
||||
themeColorMeta.content = '#1e293b';
|
||||
} else {
|
||||
themeColorMeta.content = '#ffffff';
|
||||
}
|
||||
|
||||
console.log(`主题 ${theme} 已应用`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前时间
|
||||
*/
|
||||
updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
const currentTimeElement = document.getElementById('current-time');
|
||||
if (currentTimeElement) {
|
||||
currentTimeElement.textContent = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载仪表盘数据
|
||||
*/
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
console.log('开始加载仪表盘数据...');
|
||||
|
||||
// 显示加载状态
|
||||
this.setLoadingState(true);
|
||||
|
||||
// 并行加载各种数据
|
||||
const [devices, topology, credentials, activeConnections, terminalSessions] = await Promise.all([
|
||||
this.fetchWithFallback('/api/devices', []),
|
||||
this.fetchWithFallback('/api/topology', { nodes: {}, links: [] }),
|
||||
this.fetchWithFallback('/api/credentials', []),
|
||||
this.getActiveSessions(),
|
||||
this.getTerminalSessions()
|
||||
]);
|
||||
|
||||
console.log('数据加载完成:', {
|
||||
devices: devices.length,
|
||||
topology: Object.keys(topology.nodes || {}).length,
|
||||
credentials: credentials.length,
|
||||
activeConnections: activeConnections.length,
|
||||
terminalSessions: terminalSessions.length
|
||||
});
|
||||
|
||||
// 检测数据变化
|
||||
const currentSnapshot = {
|
||||
devicesCount: devices.length,
|
||||
topologyCount: Object.keys(topology.nodes || {}).length,
|
||||
credentialsCount: credentials.length,
|
||||
connectionsCount: activeConnections.length,
|
||||
sessionsCount: terminalSessions.length
|
||||
};
|
||||
|
||||
// 如果有数据变化,记录活动
|
||||
if (this.lastDataSnapshot) {
|
||||
this.detectDataChanges(this.lastDataSnapshot, currentSnapshot);
|
||||
}
|
||||
|
||||
this.lastDataSnapshot = currentSnapshot;
|
||||
|
||||
// 更新统计数据
|
||||
this.updateDashboardStats(devices, topology, credentials, activeConnections, terminalSessions);
|
||||
|
||||
// 更新设备概览
|
||||
this.updateDevicesOverview(devices);
|
||||
|
||||
// 检查系统状态
|
||||
await this.checkSystemStatus();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
this.addActivity('加载仪表盘数据失败', 'error');
|
||||
} finally {
|
||||
this.setLoadingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测数据变化
|
||||
*/
|
||||
detectDataChanges(oldSnapshot, newSnapshot) {
|
||||
if (newSnapshot.devicesCount !== oldSnapshot.devicesCount) {
|
||||
const change = newSnapshot.devicesCount - oldSnapshot.devicesCount;
|
||||
this.addActivity(`设备数量${change > 0 ? '增加' : '减少'}${Math.abs(change)}个`, change > 0 ? 'success' : 'warning');
|
||||
}
|
||||
|
||||
if (newSnapshot.topologyCount !== oldSnapshot.topologyCount) {
|
||||
const change = newSnapshot.topologyCount - oldSnapshot.topologyCount;
|
||||
this.addActivity(`拓扑节点${change > 0 ? '增加' : '减少'}${Math.abs(change)}个`, 'info');
|
||||
}
|
||||
|
||||
if (newSnapshot.credentialsCount !== oldSnapshot.credentialsCount) {
|
||||
const change = newSnapshot.credentialsCount - oldSnapshot.credentialsCount;
|
||||
this.addActivity(`凭据${change > 0 ? '增加' : '减少'}${Math.abs(change)}个`, 'info');
|
||||
}
|
||||
|
||||
if (newSnapshot.connectionsCount !== oldSnapshot.connectionsCount) {
|
||||
const change = newSnapshot.connectionsCount - oldSnapshot.connectionsCount;
|
||||
this.addActivity(`活跃连接${change > 0 ? '增加' : '减少'}${Math.abs(change)}个`, change > 0 ? 'success' : 'info');
|
||||
}
|
||||
|
||||
if (newSnapshot.sessionsCount !== oldSnapshot.sessionsCount) {
|
||||
const change = newSnapshot.sessionsCount - oldSnapshot.sessionsCount;
|
||||
this.addActivity(`终端会话${change > 0 ? '增加' : '减少'}${Math.abs(change)}个`, change > 0 ? 'success' : 'info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃会话数
|
||||
*/
|
||||
async getActiveSessions() {
|
||||
try {
|
||||
// 首先尝试从后端API获取活跃连接
|
||||
const response = await fetch('/api/connections/active');
|
||||
if (response.ok) {
|
||||
const connections = await response.json();
|
||||
return connections || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法从API获取活跃连接:', error);
|
||||
}
|
||||
|
||||
// 从全局会话管理器获取活跃会话
|
||||
if (window.sessionManager && window.sessionManager.activeSessions) {
|
||||
return Object.keys(window.sessionManager.activeSessions);
|
||||
}
|
||||
|
||||
// 从终端管理器获取
|
||||
if (window.terminalManager && window.terminalManager.terminals) {
|
||||
return Object.keys(window.terminalManager.terminals);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取真正的终端会话数
|
||||
*/
|
||||
async getTerminalSessions() {
|
||||
try {
|
||||
// 尝试从终端管理器获取
|
||||
if (window.terminalManager && window.terminalManager.terminals) {
|
||||
return Object.keys(window.terminalManager.terminals);
|
||||
}
|
||||
|
||||
// 尝试从会话管理器获取
|
||||
if (window.sessionManager && window.sessionManager.activeSessions) {
|
||||
return Object.keys(window.sessionManager.activeSessions);
|
||||
}
|
||||
|
||||
// 尝试从DOM获取终端标签页数量
|
||||
const terminalTabs = document.querySelectorAll('.terminal-tab');
|
||||
if (terminalTabs.length > 0) {
|
||||
return Array.from(terminalTabs).map((tab, index) => `terminal-${index}`);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.warn('获取终端会话失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带回退的网络请求
|
||||
*/
|
||||
async fetchWithFallback(url, fallback) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log(`API ${url} 请求成功:`, data);
|
||||
return data;
|
||||
} else {
|
||||
console.warn(`API ${url} 请求失败 (${response.status}):`, response.statusText);
|
||||
return fallback;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`API ${url} 请求异常:`, error);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置加载状态
|
||||
*/
|
||||
setLoadingState(loading) {
|
||||
const refreshBtn = document.getElementById('refresh-dashboard');
|
||||
if (refreshBtn) {
|
||||
if (loading) {
|
||||
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 加载中';
|
||||
refreshBtn.disabled = true;
|
||||
} else {
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync"></i> 刷新';
|
||||
refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新仪表盘统计数据
|
||||
*/
|
||||
updateDashboardStats(devices, topology, credentials, activeConnections, terminalSessions) {
|
||||
// 设备数量
|
||||
const devicesCount = devices.length;
|
||||
this.updateStatCard('dashboard-devices-count', devicesCount);
|
||||
this.updateStatChange('dashboard-devices-change', `+${devicesCount}`);
|
||||
|
||||
// 活跃连接数(会话数)
|
||||
const connectionsCount = activeConnections.length;
|
||||
this.updateStatCard('dashboard-connections-count', connectionsCount);
|
||||
this.updateStatChange('dashboard-connections-change', connectionsCount > 0 ? `+${connectionsCount}` : '0');
|
||||
|
||||
// 拓扑节点数
|
||||
const topologyNodes = topology.nodes ? Object.keys(topology.nodes).length : 0;
|
||||
this.updateStatCard('dashboard-topology-nodes', topologyNodes);
|
||||
this.updateStatChange('dashboard-topology-change', `+${topologyNodes}`);
|
||||
|
||||
// 终端会话数(修正:之前错误地显示凭据数量)
|
||||
const sessionsCount = terminalSessions.length;
|
||||
this.updateStatCard('dashboard-sessions-count', sessionsCount);
|
||||
this.updateStatChange('dashboard-sessions-change', `+${sessionsCount}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计卡片的数值
|
||||
*/
|
||||
updateStatCard(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
const currentValue = parseInt(element.textContent) || 0;
|
||||
const targetValue = parseInt(value) || 0;
|
||||
|
||||
if (currentValue !== targetValue) {
|
||||
this.animateNumber(element, currentValue, targetValue, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计变化指示器
|
||||
*/
|
||||
updateStatChange(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
|
||||
// 根据值设置样式
|
||||
element.className = 'stat-change';
|
||||
if (value.startsWith('+') && value !== '+0') {
|
||||
element.classList.add('positive');
|
||||
} else if (value.startsWith('-')) {
|
||||
element.classList.add('negative');
|
||||
} else {
|
||||
element.classList.add('neutral');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字动画效果
|
||||
*/
|
||||
animateNumber(element, start, end, duration) {
|
||||
const startTime = performance.now();
|
||||
|
||||
const updateNumber = (currentTime) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用缓动函数
|
||||
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.round(start + (end - start) * easedProgress);
|
||||
element.textContent = current;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateNumber);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(updateNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备概览
|
||||
*/
|
||||
updateDevicesOverview(devices) {
|
||||
const devicesOverview = document.getElementById('devices-overview');
|
||||
if (!devicesOverview) return;
|
||||
|
||||
if (!devices || devices.length === 0) {
|
||||
devicesOverview.innerHTML = `
|
||||
<div class="no-devices">
|
||||
<i class="fas fa-server"></i>
|
||||
<p>暂无设备</p>
|
||||
<button class="btn btn-sm" onclick="dashboardManager.goToDevices()">
|
||||
<i class="fas fa-plus"></i> 添加设备
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示前5个设备
|
||||
const displayDevices = devices.slice(0, 5);
|
||||
const deviceItems = displayDevices.map(device => `
|
||||
<div class="device-overview-item" onclick="dashboardManager.goToDevices('${device.id}')" title="点击查看设备详情">
|
||||
<div class="device-overview-icon">
|
||||
<i class="fas fa-${this.getDeviceIcon(device.device_type)}"></i>
|
||||
</div>
|
||||
<div class="device-overview-info">
|
||||
<div class="device-overview-name">${device.name}</div>
|
||||
<div class="device-overview-details">${device.ip_address} • ${this.getVendorDisplayName(device.vendor)}</div>
|
||||
</div>
|
||||
<div class="device-overview-status ${device.status || 'unknown'}" title="状态: ${this.getStatusDisplayName(device.status)}"></div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
devicesOverview.innerHTML = deviceItems;
|
||||
|
||||
// 如果设备数量超过5个,显示"查看更多"
|
||||
if (devices.length > 5) {
|
||||
devicesOverview.innerHTML += `
|
||||
<div class="device-overview-item" onclick="dashboardManager.goToDevices()" style="cursor: pointer;" title="查看所有设备">
|
||||
<div class="device-overview-icon">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</div>
|
||||
<div class="device-overview-info">
|
||||
<div class="device-overview-name">查看更多</div>
|
||||
<div class="device-overview-details">还有 ${devices.length - 5} 个设备</div>
|
||||
</div>
|
||||
<div class="device-overview-status more-indicator"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备图标
|
||||
*/
|
||||
getDeviceIcon(deviceType) {
|
||||
const icons = {
|
||||
router: 'route',
|
||||
switch: 'network-wired',
|
||||
firewall: 'shield-alt',
|
||||
wireless_controller: 'wifi',
|
||||
access_point: 'broadcast-tower',
|
||||
load_balancer: 'balance-scale',
|
||||
server: 'server'
|
||||
};
|
||||
return icons[deviceType] || 'server';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取厂商显示名称
|
||||
*/
|
||||
getVendorDisplayName(vendor) {
|
||||
const vendorNames = {
|
||||
cisco: 'Cisco',
|
||||
huawei: '华为',
|
||||
h3c: 'H3C',
|
||||
juniper: 'Juniper',
|
||||
arista: 'Arista',
|
||||
fortinet: 'Fortinet',
|
||||
checkpoint: 'CheckPoint',
|
||||
other: '其他'
|
||||
};
|
||||
return vendorNames[vendor] || vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态显示名称
|
||||
*/
|
||||
getStatusDisplayName(status) {
|
||||
const statusNames = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
unreachable: '不可达',
|
||||
maintenance: '维护中',
|
||||
unknown: '未知'
|
||||
};
|
||||
return statusNames[status] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查系统状态
|
||||
*/
|
||||
async checkSystemStatus() {
|
||||
// 检查网络连接
|
||||
await this.checkNetworkStatus();
|
||||
|
||||
// 检查数据库状态
|
||||
await this.checkDatabaseStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网络状态
|
||||
*/
|
||||
async checkNetworkStatus() {
|
||||
const statusIcon = document.getElementById('network-status');
|
||||
const statusText = document.getElementById('network-status-text');
|
||||
|
||||
if (!statusIcon || !statusText) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/devices', {
|
||||
method: 'HEAD',
|
||||
signal: AbortSignal.timeout(5000) // 5秒超时
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
statusIcon.className = 'status-icon online';
|
||||
statusIcon.innerHTML = '<i class="fas fa-wifi"></i>';
|
||||
statusText.textContent = '连接正常';
|
||||
} else {
|
||||
statusIcon.className = 'status-icon warning';
|
||||
statusIcon.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
|
||||
statusText.textContent = '连接异常';
|
||||
}
|
||||
} catch (error) {
|
||||
statusIcon.className = 'status-icon offline';
|
||||
statusIcon.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||||
statusText.textContent = '连接失败';
|
||||
console.error('网络状态检查失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库状态
|
||||
*/
|
||||
async checkDatabaseStatus() {
|
||||
const statusIcon = document.getElementById('database-status');
|
||||
const statusText = document.getElementById('database-status-text');
|
||||
|
||||
if (!statusIcon || !statusText) return;
|
||||
|
||||
try {
|
||||
// 通过尝试加载设备列表来检查数据库状态
|
||||
const response = await fetch('/api/devices', {
|
||||
signal: AbortSignal.timeout(3000) // 3秒超时
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
statusIcon.className = 'status-icon online';
|
||||
statusIcon.innerHTML = '<i class="fas fa-check-circle"></i>';
|
||||
statusText.textContent = '正常';
|
||||
} else {
|
||||
statusIcon.className = 'status-icon warning';
|
||||
statusIcon.innerHTML = '<i class="fas fa-exclamation-triangle"></i>';
|
||||
statusText.textContent = '异常';
|
||||
}
|
||||
} catch (error) {
|
||||
statusIcon.className = 'status-icon offline';
|
||||
statusIcon.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||||
statusText.textContent = '错误';
|
||||
console.error('数据库状态检查失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
bindEvents() {
|
||||
// 刷新仪表盘按钮
|
||||
const refreshBtn = document.getElementById('refresh-dashboard');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
this.loadDashboardData();
|
||||
this.addActivity('手动刷新仪表盘数据', 'info');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加活动记录
|
||||
*/
|
||||
addActivity(text, type = 'info') {
|
||||
const activitiesList = document.getElementById('recent-activities');
|
||||
if (!activitiesList) return;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// 添加到内部数组
|
||||
this.activities.unshift({ text, type, time: now });
|
||||
|
||||
// 限制活动数量
|
||||
if (this.activities.length > 50) {
|
||||
this.activities = this.activities.slice(0, 50);
|
||||
}
|
||||
|
||||
const activityItem = document.createElement('div');
|
||||
activityItem.className = 'activity-item';
|
||||
activityItem.innerHTML = `
|
||||
<div class="activity-icon ${type}">
|
||||
<i class="fas fa-${this.getActivityIcon(type)}"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">${text}</div>
|
||||
<div class="activity-time">${this.getRelativeTime(now)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 插入到列表顶部
|
||||
activitiesList.insertBefore(activityItem, activitiesList.firstChild);
|
||||
|
||||
// 限制显示的活动数量(最多保留10条显示)
|
||||
const displayedActivities = activitiesList.querySelectorAll('.activity-item');
|
||||
if (displayedActivities.length > 10) {
|
||||
activitiesList.removeChild(displayedActivities[displayedActivities.length - 1]);
|
||||
}
|
||||
|
||||
// 定期更新活动时间
|
||||
setTimeout(() => this.updateActivityTimes(), 60000);
|
||||
|
||||
console.log(`活动记录: ${text} (${type})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活动图标
|
||||
*/
|
||||
getActivityIcon(type) {
|
||||
const icons = {
|
||||
info: 'info-circle',
|
||||
success: 'check-circle',
|
||||
warning: 'exclamation-triangle',
|
||||
error: 'times-circle'
|
||||
};
|
||||
return icons[type] || 'info-circle';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间
|
||||
*/
|
||||
getRelativeTime(date) {
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (seconds < 60) return '刚刚';
|
||||
if (minutes < 60) return `${minutes}分钟前`;
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
if (days < 7) return `${days}天前`;
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新活动时间显示
|
||||
*/
|
||||
updateActivityTimes() {
|
||||
const timeElements = document.querySelectorAll('.activity-time');
|
||||
timeElements.forEach((element, index) => {
|
||||
if (this.activities[index]) {
|
||||
element.textContent = this.getRelativeTime(this.activities[index].time);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到设备页面
|
||||
*/
|
||||
goToDevices(deviceId = null) {
|
||||
const devicesLink = document.querySelector('.nav-link[data-page="devices"]');
|
||||
if (devicesLink) {
|
||||
devicesLink.click();
|
||||
|
||||
// 如果指定了设备ID,在设备页面高亮该设备
|
||||
if (deviceId) {
|
||||
setTimeout(() => {
|
||||
const deviceElement = document.getElementById(`device-${deviceId}`);
|
||||
if (deviceElement) {
|
||||
deviceElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
deviceElement.style.backgroundColor = 'var(--primary-color)';
|
||||
deviceElement.style.color = 'white';
|
||||
setTimeout(() => {
|
||||
deviceElement.style.backgroundColor = '';
|
||||
deviceElement.style.color = '';
|
||||
}, 2000);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
this.addActivity('跳转到设备管理页面', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新系统状态
|
||||
*/
|
||||
refreshSystemStatus() {
|
||||
this.checkSystemStatus();
|
||||
this.addActivity('刷新系统状态', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新设备概览
|
||||
*/
|
||||
refreshDevicesOverview() {
|
||||
this.loadDashboardData();
|
||||
this.addActivity('刷新设备概览', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空活动记录
|
||||
*/
|
||||
clearActivities() {
|
||||
this.activities = [];
|
||||
const activitiesList = document.getElementById('recent-activities');
|
||||
if (activitiesList) {
|
||||
activitiesList.innerHTML = `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-text">活动记录已清空</div>
|
||||
<div class="activity-time">刚刚</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
console.log('活动记录已清空');
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁函数
|
||||
*/
|
||||
destroy() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
if (this.timeInterval) {
|
||||
clearInterval(this.timeInterval);
|
||||
this.timeInterval = null;
|
||||
}
|
||||
console.log('仪表盘管理器已销毁');
|
||||
}
|
||||
}
|
||||
|
||||
// 全局仪表盘管理器实例
|
||||
let dashboardManager = null;
|
||||
|
||||
// 初始化仪表盘的全局函数
|
||||
function initializeDashboard() {
|
||||
if (!dashboardManager) {
|
||||
dashboardManager = new DashboardManager();
|
||||
console.log('仪表盘管理器初始化完成');
|
||||
}
|
||||
return dashboardManager;
|
||||
}
|
||||
|
||||
// 全局快捷函数
|
||||
function goToDevices(deviceId = null) {
|
||||
if (dashboardManager) {
|
||||
dashboardManager.goToDevices(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSystemStatus() {
|
||||
if (dashboardManager) {
|
||||
dashboardManager.refreshSystemStatus();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDevicesOverview() {
|
||||
if (dashboardManager) {
|
||||
dashboardManager.refreshDevicesOverview();
|
||||
}
|
||||
}
|
||||
|
||||
function clearActivities() {
|
||||
if (dashboardManager) {
|
||||
dashboardManager.clearActivities();
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露到全局作用域
|
||||
window.DashboardManager = DashboardManager;
|
||||
window.initializeDashboard = initializeDashboard;
|
||||
window.dashboardManager = dashboardManager;
|
||||
window.goToDevices = goToDevices;
|
||||
window.refreshSystemStatus = refreshSystemStatus;
|
||||
window.refreshDevicesOverview = refreshDevicesOverview;
|
||||
window.clearActivities = clearActivities;
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (dashboardManager) {
|
||||
dashboardManager.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (dashboardManager) {
|
||||
if (document.hidden) {
|
||||
console.log('页面隐藏,暂停定时器');
|
||||
if (dashboardManager.refreshInterval) {
|
||||
clearInterval(dashboardManager.refreshInterval);
|
||||
dashboardManager.refreshInterval = null;
|
||||
}
|
||||
} else {
|
||||
console.log('页面可见,恢复定时器');
|
||||
if (!dashboardManager.refreshInterval) {
|
||||
dashboardManager.refreshInterval = setInterval(() => dashboardManager.loadDashboardData(), 30000);
|
||||
}
|
||||
// 立即刷新一次数据
|
||||
dashboardManager.loadDashboardData();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('仪表盘模块已加载');
|
||||
@@ -0,0 +1,807 @@
|
||||
/**
|
||||
* 会话管理器模块
|
||||
*
|
||||
* 负责管理设备连接会话,包括保存会话配置、加载会话和会话组管理
|
||||
*/
|
||||
|
||||
class SessionManager {
|
||||
constructor() {
|
||||
this.activeSessions = {}; // 当前活跃会话
|
||||
this.savedSessions = {}; // 保存的会话配置
|
||||
this.sessionGroups = {}; // 会话组
|
||||
|
||||
// 从本地存储加载保存的会话配置
|
||||
this.loadFromStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将会话配置保存到本地存储
|
||||
*/
|
||||
saveToStorage() {
|
||||
localStorage.setItem('netbrain_saved_sessions', JSON.stringify(this.savedSessions));
|
||||
localStorage.setItem('netbrain_session_groups', JSON.stringify(this.sessionGroups));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载会话配置
|
||||
*/
|
||||
loadFromStorage() {
|
||||
try {
|
||||
const savedSessions = localStorage.getItem('netbrain_saved_sessions');
|
||||
const sessionGroups = localStorage.getItem('netbrain_session_groups');
|
||||
|
||||
if (savedSessions) {
|
||||
this.savedSessions = JSON.parse(savedSessions);
|
||||
}
|
||||
|
||||
if (sessionGroups) {
|
||||
this.sessionGroups = JSON.parse(sessionGroups);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载会话配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册活跃会话
|
||||
* @param {string} sessionId 会话ID
|
||||
* @param {Object} sessionInfo 会话信息对象
|
||||
*/
|
||||
registerActiveSession(sessionId, sessionInfo) {
|
||||
// 检查是否已存在相同的会话
|
||||
// 防止重复注册同一个设备和凭据的会话
|
||||
if (sessionInfo.deviceId && sessionInfo.credentialId) {
|
||||
const existingSessionIds = Object.keys(this.activeSessions);
|
||||
|
||||
for (const existingId of existingSessionIds) {
|
||||
const existingSession = this.activeSessions[existingId];
|
||||
|
||||
// 如果发现相同设备和凭据的活跃会话,则移除旧会话
|
||||
if (existingSession.deviceId === sessionInfo.deviceId &&
|
||||
existingSession.credentialId === sessionInfo.credentialId &&
|
||||
existingId !== sessionId) {
|
||||
|
||||
console.log(`发现重复会话,正在移除旧会话 ${existingId}`);
|
||||
delete this.activeSessions[existingId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册新会话
|
||||
this.activeSessions[sessionId] = {
|
||||
...sessionInfo,
|
||||
startTime: sessionInfo.startTime || new Date(),
|
||||
status: sessionInfo.status || 'active'
|
||||
};
|
||||
|
||||
// 输出调试信息
|
||||
console.log(`注册会话: ${sessionId}, 当前活跃会话数: ${Object.keys(this.activeSessions).length}`);
|
||||
|
||||
this.notifySessionListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销活跃会话
|
||||
* @param {string} sessionId 会话ID
|
||||
*/
|
||||
unregisterActiveSession(sessionId) {
|
||||
if (this.activeSessions[sessionId]) {
|
||||
delete this.activeSessions[sessionId];
|
||||
this.notifySessionListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话状态
|
||||
* @param {string} sessionId 会话ID
|
||||
* @param {string} status 状态
|
||||
*/
|
||||
updateSessionStatus(sessionId, status) {
|
||||
if (this.activeSessions[sessionId]) {
|
||||
this.activeSessions[sessionId].status = status;
|
||||
this.notifySessionListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存会话配置
|
||||
* @param {string} name 会话名称
|
||||
* @param {Object} config 会话配置
|
||||
*/
|
||||
saveSession(name, config) {
|
||||
this.savedSessions[name] = {
|
||||
...config,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveToStorage();
|
||||
this.notifySessionListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除保存的会话配置
|
||||
* @param {string} name 会话名称
|
||||
*/
|
||||
deleteSession(name) {
|
||||
if (this.savedSessions[name]) {
|
||||
delete this.savedSessions[name];
|
||||
|
||||
// 同时从会话组中移除
|
||||
for (const groupName in this.sessionGroups) {
|
||||
const group = this.sessionGroups[groupName];
|
||||
const index = group.sessions.indexOf(name);
|
||||
if (index !== -1) {
|
||||
group.sessions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.saveToStorage();
|
||||
this.notifySessionListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话组
|
||||
* @param {string} groupName 组名称
|
||||
* @param {Array} sessions 会话列表
|
||||
*/
|
||||
createSessionGroup(groupName, sessions = []) {
|
||||
this.sessionGroups[groupName] = {
|
||||
name: groupName,
|
||||
sessions: sessions,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveToStorage();
|
||||
this.notifySessionListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话组
|
||||
* @param {string} groupName 组名称
|
||||
* @param {Array} sessions 会话列表
|
||||
*/
|
||||
updateSessionGroup(groupName, sessions) {
|
||||
if (this.sessionGroups[groupName]) {
|
||||
this.sessionGroups[groupName].sessions = sessions;
|
||||
this.saveToStorage();
|
||||
this.notifySessionListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话组
|
||||
* @param {string} groupName 组名称
|
||||
*/
|
||||
deleteSessionGroup(groupName) {
|
||||
if (this.sessionGroups[groupName]) {
|
||||
delete this.sessionGroups[groupName];
|
||||
this.saveToStorage();
|
||||
this.notifySessionListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有活跃会话
|
||||
* @returns {Object} 活跃会话对象
|
||||
*/
|
||||
getActiveSessions() {
|
||||
return this.activeSessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有保存的会话配置
|
||||
* @returns {Object} 保存的会话配置
|
||||
*/
|
||||
getSavedSessions() {
|
||||
return this.savedSessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有会话组
|
||||
* @returns {Object} 会话组对象
|
||||
*/
|
||||
getSessionGroups() {
|
||||
return this.sessionGroups;
|
||||
}
|
||||
|
||||
// 会话变更监听器列表
|
||||
sessionListeners = [];
|
||||
|
||||
/**
|
||||
* 添加会话变更监听器
|
||||
* @param {Function} listener 监听器函数
|
||||
*/
|
||||
addSessionListener(listener) {
|
||||
this.sessionListeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除会话变更监听器
|
||||
* @param {Function} listener 监听器函数
|
||||
*/
|
||||
removeSessionListener(listener) {
|
||||
const index = this.sessionListeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
this.sessionListeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有会话监听器
|
||||
*/
|
||||
notifySessionListeners() {
|
||||
const data = {
|
||||
activeSessions: this.activeSessions,
|
||||
savedSessions: this.savedSessions,
|
||||
sessionGroups: this.sessionGroups
|
||||
};
|
||||
|
||||
this.sessionListeners.forEach(listener => {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
console.error('会话监听器执行错误:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到设备
|
||||
* @param {string} deviceId 设备ID
|
||||
* @param {string} credentialId 凭据ID
|
||||
* @param {string} name 会话名称
|
||||
* @returns {Object} 连接信息
|
||||
*/
|
||||
async connectToDevice(deviceId, credentialId, name) {
|
||||
// 会话ID使用设备ID和凭据ID组合
|
||||
const sessionId = `${deviceId}_${credentialId}_${Date.now()}`;
|
||||
|
||||
// 创建会话记录
|
||||
const sessionInfo = {
|
||||
id: sessionId,
|
||||
name: name,
|
||||
deviceId: deviceId,
|
||||
credentialId: credentialId,
|
||||
startTime: new Date(),
|
||||
status: 'connecting'
|
||||
};
|
||||
|
||||
// 注册活跃会话
|
||||
this.registerActiveSession(sessionId, sessionInfo);
|
||||
|
||||
return sessionInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话管理UI类
|
||||
*
|
||||
* 处理会话管理相关的UI交互
|
||||
*/
|
||||
class SessionManagerUI {
|
||||
constructor() {
|
||||
// 获取DOM元素
|
||||
this.activeSessionsList = document.getElementById('active-sessions-list');
|
||||
this.savedSessionsList = document.getElementById('saved-sessions-list');
|
||||
this.sessionGroupsList = document.getElementById('session-groups-list');
|
||||
|
||||
// 获取会话操作按钮
|
||||
this.newSessionBtn = document.getElementById('new-session-btn');
|
||||
this.saveSessionBtn = document.getElementById('save-session-btn');
|
||||
this.manageGroupsBtn = document.getElementById('manage-groups-btn');
|
||||
|
||||
// 绑定按钮事件
|
||||
if (this.newSessionBtn) {
|
||||
this.newSessionBtn.addEventListener('click', () => {
|
||||
// 显示连接对话框
|
||||
window.terminalManager.showConnectDialog();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.saveSessionBtn) {
|
||||
this.saveSessionBtn.addEventListener('click', () => {
|
||||
this.showSaveSessionDialog();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.manageGroupsBtn) {
|
||||
this.manageGroupsBtn.addEventListener('click', () => {
|
||||
this.showManageGroupsDialog();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定保存会话表单提交事件
|
||||
const saveSessionForm = document.getElementById('save-session-form');
|
||||
if (saveSessionForm) {
|
||||
saveSessionForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveSession();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定创建组按钮事件
|
||||
const createGroupBtn = document.getElementById('create-group-btn');
|
||||
if (createGroupBtn) {
|
||||
createGroupBtn.addEventListener('click', () => {
|
||||
this.createSessionGroup();
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定对话框关闭按钮事件
|
||||
document.querySelectorAll('.modal .close, .modal .close-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 添加会话变更监听器
|
||||
if (window.sessionManager) {
|
||||
window.sessionManager.addSessionListener(this.updateUI.bind(this));
|
||||
}
|
||||
|
||||
// 初始化UI
|
||||
this.updateUI({
|
||||
activeSessions: window.sessionManager?.getActiveSessions() || {},
|
||||
savedSessions: window.sessionManager?.getSavedSessions() || {},
|
||||
sessionGroups: window.sessionManager?.getSessionGroups() || {}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
updateUI(data) {
|
||||
const { activeSessions, savedSessions, sessionGroups } = data;
|
||||
|
||||
// 更新活跃会话列表
|
||||
this.updateActiveSessionsList(activeSessions);
|
||||
|
||||
// 更新保存的会话列表
|
||||
this.updateSavedSessionsList(savedSessions);
|
||||
|
||||
// 更新会话组列表
|
||||
this.updateSessionGroupsList(sessionGroups, savedSessions);
|
||||
}
|
||||
|
||||
// 更新活跃会话列表
|
||||
updateActiveSessionsList(activeSessions) {
|
||||
if (!this.activeSessionsList) return;
|
||||
|
||||
this.activeSessionsList.innerHTML = '';
|
||||
|
||||
const sessionIds = Object.keys(activeSessions);
|
||||
|
||||
if (sessionIds.length === 0) {
|
||||
this.activeSessionsList.innerHTML = '<div class="session-empty">暂无活跃会话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用Set避免重复会话
|
||||
const processedSessions = new Set();
|
||||
|
||||
sessionIds.forEach(sessionId => {
|
||||
// 避免重复显示会话
|
||||
if (processedSessions.has(sessionId)) {
|
||||
console.warn(`会话ID ${sessionId} 已存在,避免重复显示`);
|
||||
return;
|
||||
}
|
||||
|
||||
processedSessions.add(sessionId);
|
||||
|
||||
const session = activeSessions[sessionId];
|
||||
const sessionItem = document.createElement('div');
|
||||
sessionItem.className = 'session-item';
|
||||
sessionItem.dataset.sessionId = sessionId; // 添加会话ID作为数据属性
|
||||
|
||||
// 状态指示灯
|
||||
let statusClass = '';
|
||||
|
||||
switch (session.status) {
|
||||
case 'active':
|
||||
statusClass = 'status-active';
|
||||
break;
|
||||
case 'connecting':
|
||||
statusClass = 'status-connecting';
|
||||
break;
|
||||
case 'disconnected':
|
||||
statusClass = 'status-disconnected';
|
||||
break;
|
||||
case 'error':
|
||||
statusClass = 'status-error';
|
||||
break;
|
||||
default:
|
||||
statusClass = 'status-connecting';
|
||||
break;
|
||||
}
|
||||
|
||||
sessionItem.innerHTML = `
|
||||
<div class="session-item-content">
|
||||
<div class="session-item-title">
|
||||
<span class="status-indicator ${statusClass}"></span>
|
||||
${session.name}
|
||||
</div>
|
||||
<div class="session-item-details">
|
||||
${new Date(session.startTime).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-item-actions">
|
||||
<button class="session-item-action save-session" title="保存会话">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
<button class="session-item-action close-session" title="关闭会话" data-session-id="${sessionId}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 保存会话按钮
|
||||
sessionItem.querySelector('.save-session').addEventListener('click', () => {
|
||||
this.showSaveSessionDialog(session);
|
||||
});
|
||||
|
||||
// 关闭会话按钮 - 修复关闭逻辑
|
||||
sessionItem.querySelector('.close-session').addEventListener('click', (event) => {
|
||||
const clickedSessionId = event.currentTarget.dataset.sessionId;
|
||||
console.log(`关闭会话: ${clickedSessionId}`);
|
||||
|
||||
const termInfo = window.terminalManager.getTerminalById(clickedSessionId);
|
||||
if (termInfo) {
|
||||
window.terminalManager.closeTerminal(clickedSessionId);
|
||||
} else {
|
||||
// 如果没有找到对应的终端,直接注销会话
|
||||
window.sessionManager.unregisterActiveSession(clickedSessionId);
|
||||
// 手动更新UI
|
||||
this.updateActiveSessionsList(window.sessionManager.getActiveSessions());
|
||||
}
|
||||
});
|
||||
|
||||
this.activeSessionsList.appendChild(sessionItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新保存的会话列表
|
||||
updateSavedSessionsList(savedSessions) {
|
||||
if (!this.savedSessionsList) return;
|
||||
|
||||
this.savedSessionsList.innerHTML = '';
|
||||
|
||||
const sessionNames = Object.keys(savedSessions);
|
||||
|
||||
if (sessionNames.length === 0) {
|
||||
this.savedSessionsList.innerHTML = '<div class="session-empty">暂无保存的会话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
sessionNames.forEach(sessionName => {
|
||||
const session = savedSessions[sessionName];
|
||||
const sessionItem = document.createElement('div');
|
||||
sessionItem.className = 'session-item';
|
||||
|
||||
sessionItem.innerHTML = `
|
||||
<div class="session-item-content">
|
||||
<div class="session-item-title">${sessionName}</div>
|
||||
<div class="session-item-details">
|
||||
${session.description || ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-item-actions">
|
||||
<button class="session-item-action connect-session" title="连接会话">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
<button class="session-item-action delete-session" title="删除会话">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 连接会话按钮
|
||||
sessionItem.querySelector('.connect-session').addEventListener('click', () => {
|
||||
if (session.deviceId && session.credentialId) {
|
||||
// 创建连接
|
||||
window.terminalManager.createTerminal(
|
||||
sessionName,
|
||||
session.deviceId,
|
||||
session.credentialId
|
||||
);
|
||||
} else {
|
||||
alert('无法连接会话:缺少设备ID或凭据ID');
|
||||
}
|
||||
});
|
||||
|
||||
// 删除会话按钮
|
||||
sessionItem.querySelector('.delete-session').addEventListener('click', () => {
|
||||
if (confirm(`确定要删除保存的会话"${sessionName}"吗?`)) {
|
||||
window.sessionManager.deleteSession(sessionName);
|
||||
}
|
||||
});
|
||||
|
||||
this.savedSessionsList.appendChild(sessionItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新会话组列表
|
||||
updateSessionGroupsList(sessionGroups, savedSessions) {
|
||||
if (!this.sessionGroupsList) return;
|
||||
|
||||
this.sessionGroupsList.innerHTML = '';
|
||||
|
||||
const groupNames = Object.keys(sessionGroups);
|
||||
|
||||
if (groupNames.length === 0) {
|
||||
this.sessionGroupsList.innerHTML = '<div class="session-empty">暂无会话组</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
groupNames.forEach(groupName => {
|
||||
const group = sessionGroups[groupName];
|
||||
const groupItem = document.createElement('div');
|
||||
groupItem.className = 'session-group-item';
|
||||
|
||||
// 组标题
|
||||
const groupHeader = document.createElement('div');
|
||||
groupHeader.className = 'session-group-header';
|
||||
groupHeader.innerHTML = `
|
||||
<div class="session-item-title">
|
||||
<i class="fas fa-folder"></i> ${groupName}
|
||||
</div>
|
||||
<div class="session-item-actions">
|
||||
<button class="session-item-action toggle-group">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 组内容
|
||||
const groupContent = document.createElement('div');
|
||||
groupContent.className = 'session-group-content';
|
||||
|
||||
// 添加组内会话
|
||||
if (group.sessions.length === 0) {
|
||||
groupContent.innerHTML = '<div class="session-empty">此组中没有会话</div>';
|
||||
} else {
|
||||
group.sessions.forEach(sessionName => {
|
||||
if (savedSessions[sessionName]) {
|
||||
const session = savedSessions[sessionName];
|
||||
const sessionItem = document.createElement('div');
|
||||
sessionItem.className = 'session-item';
|
||||
|
||||
sessionItem.innerHTML = `
|
||||
<div class="session-item-content">
|
||||
<div class="session-item-title">${sessionName}</div>
|
||||
</div>
|
||||
<div class="session-item-actions">
|
||||
<button class="session-item-action connect-session" title="连接会话">
|
||||
<i class="fas fa-plug"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 连接会话按钮
|
||||
sessionItem.querySelector('.connect-session').addEventListener('click', () => {
|
||||
if (session.deviceId && session.credentialId) {
|
||||
// 创建连接
|
||||
window.terminalManager.createTerminal(
|
||||
sessionName,
|
||||
session.deviceId,
|
||||
session.credentialId
|
||||
);
|
||||
} else {
|
||||
alert('无法连接会话:缺少设备ID或凭据ID');
|
||||
}
|
||||
});
|
||||
|
||||
groupContent.appendChild(sessionItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换组展开/折叠
|
||||
groupHeader.querySelector('.toggle-group').addEventListener('click', () => {
|
||||
groupContent.classList.toggle('active');
|
||||
const icon = groupHeader.querySelector('.toggle-group i');
|
||||
if (groupContent.classList.contains('active')) {
|
||||
icon.className = 'fas fa-chevron-up';
|
||||
} else {
|
||||
icon.className = 'fas fa-chevron-down';
|
||||
}
|
||||
});
|
||||
|
||||
groupItem.appendChild(groupHeader);
|
||||
groupItem.appendChild(groupContent);
|
||||
this.sessionGroupsList.appendChild(groupItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示保存会话对话框
|
||||
showSaveSessionDialog(session) {
|
||||
const modal = document.getElementById('save-session-modal');
|
||||
modal.style.display = 'block';
|
||||
|
||||
// 加载活跃会话列表到下拉框
|
||||
const activeSessionSelect = document.getElementById('active-session-select');
|
||||
if (activeSessionSelect) {
|
||||
activeSessionSelect.innerHTML = '<option value="">-- 请选择会话 --</option>';
|
||||
|
||||
const activeSessions = window.sessionManager.getActiveSessions();
|
||||
for (const sessionId in activeSessions) {
|
||||
const s = activeSessions[sessionId];
|
||||
const option = document.createElement('option');
|
||||
option.value = sessionId;
|
||||
option.textContent = s.name;
|
||||
activeSessionSelect.appendChild(option);
|
||||
|
||||
// 如果有指定的会话,则选中它
|
||||
if (session && sessionId === session.id) {
|
||||
option.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载会话组列表到下拉框
|
||||
const sessionGroupSelect = document.getElementById('session-group');
|
||||
if (sessionGroupSelect) {
|
||||
sessionGroupSelect.innerHTML = '<option value="">-- 无分组 --</option>';
|
||||
|
||||
const sessionGroups = window.sessionManager.getSessionGroups();
|
||||
for (const groupName in sessionGroups) {
|
||||
const option = document.createElement('option');
|
||||
option.value = groupName;
|
||||
option.textContent = groupName;
|
||||
sessionGroupSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单默认值
|
||||
if (session) {
|
||||
const sessionNameInput = document.getElementById('session-name');
|
||||
if (sessionNameInput) {
|
||||
sessionNameInput.value = session.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示管理会话组对话框
|
||||
showManageGroupsDialog() {
|
||||
const modal = document.getElementById('manage-groups-modal');
|
||||
modal.style.display = 'block';
|
||||
|
||||
this.updateGroupsManagerUI();
|
||||
}
|
||||
|
||||
// 更新管理会话组对话框内容
|
||||
updateGroupsManagerUI() {
|
||||
const groupsList = document.getElementById('manage-groups-list');
|
||||
if (!groupsList) return;
|
||||
|
||||
groupsList.innerHTML = '';
|
||||
|
||||
const sessionGroups = window.sessionManager.getSessionGroups();
|
||||
const groupNames = Object.keys(sessionGroups);
|
||||
|
||||
if (groupNames.length === 0) {
|
||||
groupsList.innerHTML = '<div class="group-empty">暂无会话组</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
groupNames.forEach(groupName => {
|
||||
const group = sessionGroups[groupName];
|
||||
const groupItem = document.createElement('div');
|
||||
groupItem.className = 'group-item';
|
||||
groupItem.innerHTML = `
|
||||
<div class="group-item-name">${groupName}</div>
|
||||
<div class="group-item-actions">
|
||||
<button class="session-item-action edit-group" title="编辑组">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="session-item-action delete-group" title="删除组">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 编辑组按钮
|
||||
groupItem.querySelector('.edit-group').addEventListener('click', () => {
|
||||
this.showEditGroupDialog(groupName, group);
|
||||
});
|
||||
|
||||
// 删除组按钮
|
||||
groupItem.querySelector('.delete-group').addEventListener('click', () => {
|
||||
if (confirm(`确定要删除会话组"${groupName}"吗?`)) {
|
||||
window.sessionManager.deleteSessionGroup(groupName);
|
||||
this.updateGroupsManagerUI();
|
||||
}
|
||||
});
|
||||
|
||||
groupsList.appendChild(groupItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示编辑会话组对话框
|
||||
showEditGroupDialog(groupName, group) {
|
||||
// 实现编辑会话组功能
|
||||
alert(`编辑会话组功能开发中: ${groupName}`);
|
||||
}
|
||||
|
||||
// 创建会话组
|
||||
createSessionGroup() {
|
||||
const groupNameInput = document.getElementById('new-group-name');
|
||||
if (!groupNameInput) return;
|
||||
|
||||
const groupName = groupNameInput.value.trim();
|
||||
|
||||
if (!groupName) {
|
||||
alert('请输入组名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.sessionManager.getSessionGroups()[groupName]) {
|
||||
alert('已存在同名会话组');
|
||||
return;
|
||||
}
|
||||
|
||||
window.sessionManager.createSessionGroup(groupName);
|
||||
groupNameInput.value = '';
|
||||
this.updateGroupsManagerUI();
|
||||
}
|
||||
|
||||
// 保存会话
|
||||
saveSession() {
|
||||
const sessionId = document.getElementById('active-session-select')?.value;
|
||||
const sessionName = document.getElementById('session-name')?.value.trim();
|
||||
const sessionDesc = document.getElementById('session-description')?.value.trim();
|
||||
const groupName = document.getElementById('session-group')?.value;
|
||||
|
||||
if (!sessionId || !sessionName) {
|
||||
alert('请选择会话并输入会话名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSessions = window.sessionManager.getActiveSessions();
|
||||
const sessionInfo = activeSessions[sessionId];
|
||||
|
||||
if (!sessionInfo) {
|
||||
alert('所选会话不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存会话配置
|
||||
const config = {
|
||||
name: sessionName,
|
||||
description: sessionDesc,
|
||||
deviceId: sessionInfo.deviceId,
|
||||
credentialId: sessionInfo.credentialId,
|
||||
originalId: sessionId
|
||||
};
|
||||
|
||||
window.sessionManager.saveSession(sessionName, config);
|
||||
|
||||
// 如果指定了会话组,添加到组
|
||||
if (groupName) {
|
||||
const groups = window.sessionManager.getSessionGroups();
|
||||
if (groups[groupName]) {
|
||||
const sessions = [...groups[groupName].sessions];
|
||||
if (!sessions.includes(sessionName)) {
|
||||
sessions.push(sessionName);
|
||||
window.sessionManager.updateSessionGroup(groupName, sessions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
document.getElementById('save-session-modal').style.display = 'none';
|
||||
|
||||
// 重置表单
|
||||
document.getElementById('save-session-form')?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// 原本在这里自动创建实例的代码已移除,改为在页面加载时统一创建
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{constructor(){}activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()}));
|
||||
//# sourceMappingURL=xterm-addon-fit.js.map
|
||||
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(function(){return(()=>{"use strict";var e={6:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,i={}){this._terminal=e,this._regex=t,this._handler=n,this._options=i}provideLinks(e,t){const i=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(i))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:i}=e;this._options.hover(t,n,i)}},e)))}};class n{static computeLink(e,t,i,r){const o=new RegExp(t.source,(t.flags||"")+"g"),[s,a]=n._getWindowedLineStrings(e-1,i),c=s.join("");let d;const l=[];for(;d=o.exec(c);){const e=d[0];try{const t=new URL(e),n=decodeURI(t.toString());if(e!==n&&e+"/"!==n)continue}catch(e){continue}const[t,o]=n._mapStrIdx(i,a,0,d.index),[s,c]=n._mapStrIdx(i,t,o,e.length);if(-1===t||-1===o||-1===s||-1===c)continue;const p={start:{x:o+1,y:t+1},end:{x:c,y:s+1}};l.push({range:p,text:e,activate:r})}return l}static _getWindowedLineStrings(e,t){let n,i=e,r=e,o=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(o=0;(n=t.buffer.active.getLine(--i))&&o<2048&&(s=n.translateToString(!0),o+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),o=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&o<2048&&(s=n.translateToString(!0),o+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,i]}static _mapStrIdx(e,t,n,i){const r=e.buffer.active,o=r.getNullCell();let s=n;for(;i;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,o);const s=o.getChars();if(o.getWidth()&&(i-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,o),2===o.getWidth()&&(i+=1))}if(i<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=n}},t={};function n(i){var r=t[i];if(void 0!==r)return r.exports;var o=t[i]={exports:{}};return e[i](o,o.exports,n),o.exports}var i={};return(()=>{var e=i;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function o(e,t){const n=window.open();if(n){try{n.opener=null}catch(e){}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=o,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,i=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,i,this._handler,n))}dispose(){var e;null===(e=this._linkProvider)||void 0===e||e.dispose()}}})(),i})()}));
|
||||
//# sourceMappingURL=xterm-addon-web-links.js.map
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user