Add files via upload

This commit is contained in:
Koreyoshi
2025-05-30 21:59:39 +08:00
committed by v6ole
parent d7b1778a85
commit 3236fa947d
88 changed files with 30606 additions and 0 deletions
+818
View File
@@ -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('仪表盘模块已加载');
+807
View File
@@ -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
+2
View File
@@ -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
+209
View File
@@ -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