3370 lines
151 KiB
HTML
3370 lines
151 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ title }}</title>
|
||
|
||
<!-- CSS样式 -->
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||
<link rel="stylesheet" href="{{ url_for('static', path='js/xterm/xterm.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', path='css/styles.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', path='css/terminal.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', path='css/topology.css') }}">
|
||
|
||
<!-- JavaScript依赖 -->
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<script src="{{ url_for('static', path='js/xterm/xterm.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/xterm/xterm-addon-fit.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/xterm/xterm-addon-web-links.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/session-manager.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/terminal.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/topology.js') }}"></script>
|
||
<script src="{{ url_for('static', path='js/dashboard.js') }}"></script>
|
||
|
||
<!-- 全局主题初始化脚本 -->
|
||
<script>
|
||
// 立即应用保存的主题,避免闪烁
|
||
(function() {
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||
|
||
// 页面加载完成后应用logo切换
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const loginLogo = document.getElementById('login-logo');
|
||
const headerLogo = document.getElementById('header-logo');
|
||
|
||
if (savedTheme === 'dark') {
|
||
// 暗色主题使用logod.png
|
||
if (loginLogo) {
|
||
loginLogo.src = loginLogo.src.replace('logo.png', 'logod.png');
|
||
}
|
||
if (headerLogo) {
|
||
headerLogo.src = headerLogo.src.replace('logo.png', 'logod.png');
|
||
}
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<!-- 容器包含所有页面内容,通过JS控制显示/隐藏 -->
|
||
<div id="app">
|
||
<!-- 登录页面 -->
|
||
<div id="login-page" class="page">
|
||
<div class="login-container">
|
||
<div class="login-logo">
|
||
<img id="login-logo" src="{{ url_for('static', path='images/logo.png') }}" alt="NetBrain MCP" class="logo">
|
||
</div>
|
||
<h1 class="login-title">NetBrain MCP</h1>
|
||
<p class="login-description">
|
||
通过MCP协议连接大型语言模型与网络设备
|
||
</p>
|
||
<button class="btn btn-large" id="start-btn">开始使用</button>
|
||
<div class="copyright">
|
||
© 2025 NetBrain MCP
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主页内容 -->
|
||
<div id="home-page" class="page" style="display: none;">
|
||
<header class="header">
|
||
<div class="header-container">
|
||
<div class="header-logo">
|
||
<img id="header-logo" src="{{ url_for('static', path='images/logo.png') }}" alt="NetBrain MCP">
|
||
<h1>NetBrain MCP</h1>
|
||
</div>
|
||
<nav class="nav-links">
|
||
<a href="#dashboard" class="nav-link active" data-page="dashboard"><i class="fas fa-gauge-high"></i> 仪表盘</a>
|
||
<a href="#devices" class="nav-link" data-page="devices"><i class="fas fa-server"></i> 设备</a>
|
||
<a href="#terminal" class="nav-link" data-page="terminal"><i class="fas fa-terminal"></i> 终端</a>
|
||
<a href="#topology" class="nav-link" data-page="topology"><i class="fas fa-network-wired"></i> 拓扑</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-content">
|
||
<div class="container">
|
||
<!-- 仪表盘页面 -->
|
||
<div id="dashboard-content" class="page-content">
|
||
<div class="dashboard-header">
|
||
<h1 class="page-title">
|
||
<i class="fas fa-gauge-high"></i> 仪表盘
|
||
</h1>
|
||
<div class="dashboard-controls">
|
||
<div class="theme-switcher">
|
||
<label class="switch">
|
||
<input type="checkbox" id="theme-toggle">
|
||
<span class="slider">
|
||
<i class="fas fa-sun"></i>
|
||
<i class="fas fa-moon"></i>
|
||
</span>
|
||
</label>
|
||
<span class="theme-label">主题模式</span>
|
||
</div>
|
||
<button id="refresh-dashboard" class="btn btn-sm">
|
||
<i class="fas fa-sync"></i> 刷新
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片网格 -->
|
||
<div class="stats-cards-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-icon devices">
|
||
<i class="fas fa-server"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number" id="dashboard-devices-count">0</div>
|
||
<div class="stat-label">网络设备</div>
|
||
<div class="stat-change positive" id="dashboard-devices-change">+0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-icon connections">
|
||
<i class="fas fa-link"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number" id="dashboard-connections-count">0</div>
|
||
<div class="stat-label">活跃连接</div>
|
||
<div class="stat-change neutral" id="dashboard-connections-change">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-icon topology">
|
||
<i class="fas fa-project-diagram"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number" id="dashboard-topology-nodes">0</div>
|
||
<div class="stat-label">拓扑节点</div>
|
||
<div class="stat-change positive" id="dashboard-topology-change">+0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card">
|
||
<div class="stat-icon sessions">
|
||
<i class="fas fa-terminal"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-number" id="dashboard-sessions-count">0</div>
|
||
<div class="stat-label">终端会话</div>
|
||
<div class="stat-change positive" id="dashboard-sessions-change">+0</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="dashboard-main-content">
|
||
<!-- 左侧列 -->
|
||
<div class="dashboard-left-column">
|
||
<!-- 系统状态卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<h3><i class="fas fa-heartbeat"></i> 系统状态</h3>
|
||
<div class="card-actions">
|
||
<button class="btn-icon" onclick="refreshSystemStatus()">
|
||
<i class="fas fa-sync"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="status-grid">
|
||
<div class="status-item">
|
||
<div class="status-icon online">
|
||
<i class="fas fa-check-circle"></i>
|
||
</div>
|
||
<div class="status-info">
|
||
<div class="status-label">MCP服务器</div>
|
||
<div class="status-value">运行正常</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-item">
|
||
<div class="status-icon online">
|
||
<i class="fas fa-check-circle"></i>
|
||
</div>
|
||
<div class="status-info">
|
||
<div class="status-label">Web服务</div>
|
||
<div class="status-value">运行正常</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-item">
|
||
<div class="status-icon" id="database-status">
|
||
<i class="fas fa-check-circle"></i>
|
||
</div>
|
||
<div class="status-info">
|
||
<div class="status-label">数据存储</div>
|
||
<div class="status-value" id="database-status-text">正常</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-item">
|
||
<div class="status-icon" id="network-status">
|
||
<i class="fas fa-wifi"></i>
|
||
</div>
|
||
<div class="status-info">
|
||
<div class="status-label">网络连接</div>
|
||
<div class="status-value" id="network-status-text">检查中...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧列 -->
|
||
<div class="dashboard-right-column">
|
||
<!-- 近期活动卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<h3><i class="fas fa-clock"></i> 近期活动</h3>
|
||
<div class="card-actions">
|
||
<button class="btn-icon" onclick="clearActivities()">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-content">
|
||
<div id="recent-activities" class="activities-list">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备概览卡片 -->
|
||
<div class="dashboard-card">
|
||
<div class="card-header">
|
||
<h3><i class="fas fa-network-wired"></i> 设备概览</h3>
|
||
<div class="card-actions">
|
||
<button class="btn-icon" onclick="refreshDevicesOverview()">
|
||
<i class="fas fa-sync"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-content">
|
||
<div id="devices-overview" class="devices-overview">
|
||
<div class="no-devices">
|
||
<i class="fas fa-server"></i>
|
||
<p>暂无设备</p>
|
||
<button class="btn btn-sm" onclick="goToDevices()">
|
||
<i class="fas fa-plus"></i> 添加设备
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统信息footer -->
|
||
<div class="dashboard-footer">
|
||
<div class="system-info">
|
||
<span><i class="fas fa-code"></i> NetBrain MCP v1.0</span>
|
||
<span><i class="fas fa-calendar"></i> <span id="current-time"></span></span>
|
||
<span><i class="fas fa-user"></i> 管理员</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设备管理页面 -->
|
||
<div id="devices-content" class="page-content" style="display: none;">
|
||
<h1 class="page-title">设备管理</h1>
|
||
<div class="device-management-container">
|
||
<!-- 设备管理卡片 -->
|
||
<div class="card device-card">
|
||
<div class="device-manager">
|
||
<div class="device-list-header">
|
||
<h2><i class="fas fa-server"></i> 设备列表</h2>
|
||
<button id="add-device-btn" class="btn"><i class="fas fa-plus"></i> 添加设备</button>
|
||
</div>
|
||
<div id="device-list" class="item-list">
|
||
<div class="device-list-empty">
|
||
<p>尚未添加任何设备</p>
|
||
</div>
|
||
<!-- 设备列表会通过JavaScript动态加载 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 凭据管理卡片 -->
|
||
<div class="card credential-card">
|
||
<div class="credential-manager">
|
||
<div class="credential-list-header">
|
||
<h2><i class="fas fa-key"></i> 凭据列表</h2>
|
||
<button id="add-credential-btn" class="btn"><i class="fas fa-plus"></i> 添加凭据</button>
|
||
</div>
|
||
<div id="credential-list" class="item-list">
|
||
<div class="credential-list-empty">
|
||
<p>尚未添加任何凭据</p>
|
||
</div>
|
||
<!-- 凭据列表会通过JavaScript动态加载 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 终端页面 - 多标签页终端实现 -->
|
||
<div id="terminal-content" class="page-content">
|
||
<h1 class="page-title">终端控制台</h1>
|
||
|
||
<div class="terminal-layout">
|
||
<!-- 会话管理面板 - 移到左侧 -->
|
||
<div class="sessions-card">
|
||
<div class="sessions-header">
|
||
<h2><i class="fas fa-network-wired"></i> 会话管理</h2>
|
||
<div class="sessions-actions">
|
||
<button id="new-session-btn" class="btn btn-sm"><i class="fas fa-plus"></i> 新建会话</button>
|
||
<button id="save-session-btn" class="btn btn-sm"><i class="fas fa-save"></i> 保存会话</button>
|
||
<button id="manage-groups-btn" class="btn btn-sm"><i class="fas fa-folder"></i> 管理分组</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sessions-container">
|
||
<!-- 活跃会话 -->
|
||
<div class="sessions-section">
|
||
<h3><i class="fas fa-bolt"></i> 活跃会话</h3>
|
||
<div id="active-sessions-list" class="sessions-list">
|
||
<!-- 会话项将通过JavaScript动态生成 -->
|
||
<div class="session-empty">暂无活跃会话</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存的会话 -->
|
||
<div class="sessions-section">
|
||
<h3><i class="fas fa-bookmark"></i> 保存的会话</h3>
|
||
<div id="saved-sessions-list" class="sessions-list">
|
||
<!-- 保存的会话项将通过JavaScript动态生成 -->
|
||
<div class="session-empty">暂无保存的会话</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 会话组 -->
|
||
<div class="sessions-section">
|
||
<h3><i class="fas fa-folder"></i> 会话组</h3>
|
||
<div id="session-groups-list" class="sessions-list">
|
||
<!-- 会话组项将通过JavaScript动态生成 -->
|
||
<div class="session-empty">暂无会话组</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 终端窗口 - 移到右侧 -->
|
||
<div class="terminal-card">
|
||
<div class="terminal-container">
|
||
<!-- 终端工具栏 -->
|
||
<div class="terminal-toolbar">
|
||
<div class="terminal-tabs">
|
||
<!-- 标签页将由JavaScript动态创建 -->
|
||
</div>
|
||
<div class="terminal-actions">
|
||
<button id="reconnect-terminal" class="btn btn-sm"><i class="fas fa-sync"></i> 重新连接</button>
|
||
<button id="connect-device" class="btn btn-sm"><i class="fas fa-plug"></i> 连接设备</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 终端窗口容器 -->
|
||
<div class="terminal-windows">
|
||
<!-- 终端实例将由JavaScript动态创建 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 网络拓扑页面 -->
|
||
<div id="topology-content" class="page-content" style="display: none;">
|
||
<h1 class="page-title">网络拓扑</h1>
|
||
|
||
<!-- 拓扑控制工具栏 -->
|
||
<div class="topology-toolbar">
|
||
<div class="toolbar-section">
|
||
<h3><i class="fas fa-play"></i> 发现控制</h3>
|
||
<div class="toolbar-actions">
|
||
<button id="discover-topology-btn" class="btn btn-primary">
|
||
<i class="fas fa-search"></i> 发现拓扑
|
||
</button>
|
||
<button id="refresh-topology-btn" class="btn">
|
||
<i class="fas fa-sync"></i> 刷新
|
||
</button>
|
||
<button id="clear-topology-btn" class="btn btn-outline">
|
||
<i class="fas fa-trash"></i> 清空
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar-section">
|
||
<h3><i class="fas fa-eye"></i> 视图控制</h3>
|
||
<div class="toolbar-actions">
|
||
<button id="fit-topology-btn" class="btn">
|
||
<i class="fas fa-expand"></i> 适应窗口
|
||
</button>
|
||
<button id="zoom-in-btn" class="btn">
|
||
<i class="fas fa-search-plus"></i> 放大
|
||
</button>
|
||
<button id="zoom-out-btn" class="btn">
|
||
<i class="fas fa-search-minus"></i> 缩小
|
||
</button>
|
||
<button id="center-topology-btn" class="btn">
|
||
<i class="fas fa-crosshairs"></i> 居中
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toolbar-section">
|
||
<h3><i class="fas fa-filter"></i> 过滤选项</h3>
|
||
<div class="toolbar-actions">
|
||
<select id="vendor-filter" class="form-control form-control-sm">
|
||
<option value="">所有厂商</option>
|
||
<option value="cisco">Cisco</option>
|
||
<option value="huawei">华为</option>
|
||
<option value="h3c">H3C</option>
|
||
<option value="juniper">Juniper</option>
|
||
</select>
|
||
<select id="device-type-filter" class="form-control form-control-sm">
|
||
<option value="">所有类型</option>
|
||
<option value="router">路由器</option>
|
||
<option value="switch">交换机</option>
|
||
<option value="firewall">防火墙</option>
|
||
</select>
|
||
<button id="apply-filter-btn" class="btn btn-sm">
|
||
<i class="fas fa-check"></i> 应用
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 拓扑主界面 -->
|
||
<div class="topology-main">
|
||
<!-- 拓扑可视化区域 -->
|
||
<div class="topology-visualization">
|
||
<div id="topology-canvas" class="topology-canvas">
|
||
<!-- 拓扑加载状态 -->
|
||
<div id="topology-loading" class="topology-loading">
|
||
<div class="loading-spinner">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
</div>
|
||
<div class="loading-text">正在加载拓扑数据...</div>
|
||
</div>
|
||
|
||
<!-- 拓扑空状态 -->
|
||
<div id="topology-empty" class="topology-empty" style="display: none;">
|
||
<div class="empty-icon">
|
||
<i class="fas fa-project-diagram"></i>
|
||
</div>
|
||
<div class="empty-title">暂无拓扑数据</div>
|
||
<div class="empty-description">
|
||
请先添加网络设备,然后点击"发现拓扑"按钮开始网络拓扑发现
|
||
</div>
|
||
<button id="goto-devices-btn" class="btn btn-primary">
|
||
<i class="fas fa-server"></i> 管理设备
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 拓扑SVG容器 -->
|
||
<svg id="topology-svg" width="100%" height="100%" style="display: none;">
|
||
<!-- 定义箭头标记 -->
|
||
<defs>
|
||
<marker id="arrowhead" markerWidth="10" markerHeight="7"
|
||
refX="9" refY="3.5" orient="auto">
|
||
<polygon points="0 0, 10 3.5, 0 7" fill="#666" />
|
||
</marker>
|
||
<marker id="arrowhead-selected" markerWidth="10" markerHeight="7"
|
||
refX="9" refY="3.5" orient="auto">
|
||
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
|
||
</marker>
|
||
</defs>
|
||
|
||
<!-- 拓扑图层 -->
|
||
<g id="topology-graph">
|
||
<!-- 链路组 -->
|
||
<g id="links-group"></g>
|
||
<!-- 节点组 -->
|
||
<g id="nodes-group"></g>
|
||
<!-- 标签组 -->
|
||
<g id="labels-group"></g>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
|
||
<!-- 拓扑操作提示 -->
|
||
<div class="topology-hints">
|
||
<div class="hint-item">
|
||
<i class="fas fa-mouse-pointer"></i>
|
||
<span>拖拽节点移动</span>
|
||
</div>
|
||
<div class="hint-item">
|
||
<i class="fas fa-mouse"></i>
|
||
<span>滚轮缩放</span>
|
||
</div>
|
||
<div class="hint-item">
|
||
<i class="fas fa-hand-point-up"></i>
|
||
<span>双击左键菜单</span>
|
||
</div>
|
||
<div class="hint-item">
|
||
<i class="fas fa-search"></i>
|
||
<span>双击适应</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 拓扑信息面板 -->
|
||
<div class="topology-info-panel">
|
||
<!-- 拓扑统计 -->
|
||
<div class="info-section">
|
||
<h3><i class="fas fa-chart-bar"></i> 拓扑统计</h3>
|
||
<div class="stats-grid">
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-nodes">0</div>
|
||
<div class="stat-label">设备节点</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-links">0</div>
|
||
<div class="stat-label">连接链路</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-protocols">0</div>
|
||
<div class="stat-label">发现协议</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-vendors">0</div>
|
||
<div class="stat-label">设备厂商</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 选中节点信息 -->
|
||
<div class="info-section">
|
||
<h3><i class="fas fa-info-circle"></i> 节点详情</h3>
|
||
<div id="node-details" class="node-details">
|
||
<div class="no-selection">
|
||
<i class="fas fa-mouse-pointer"></i>
|
||
<span>点击节点查看详细信息</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 链路信息 -->
|
||
<div class="info-section">
|
||
<h3><i class="fas fa-link"></i> 链路详情</h3>
|
||
<div id="link-details" class="link-details">
|
||
<div class="no-selection">
|
||
<i class="fas fa-mouse-pointer"></i>
|
||
<span>点击链路查看连接信息</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作面板 -->
|
||
<div class="info-section">
|
||
<h3><i class="fas fa-tools"></i> 快速操作</h3>
|
||
<div class="action-buttons">
|
||
<button id="connect-to-device-btn" class="btn btn-sm" disabled>
|
||
<i class="fas fa-terminal"></i> 连接终端
|
||
</button>
|
||
<button id="ping-device-btn" class="btn btn-sm" disabled>
|
||
<i class="fas fa-satellite-dish"></i> Ping测试
|
||
</button>
|
||
<button id="discover-neighbors-btn" class="btn btn-sm" disabled>
|
||
<i class="fas fa-search"></i> 发现邻居
|
||
</button>
|
||
<button id="device-properties-btn" class="btn btn-sm" disabled>
|
||
<i class="fas fa-cog"></i> 设备属性
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 拓扑发现对话框 -->
|
||
<div id="topology-discover-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>网络拓扑发现</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="topology-discover-form">
|
||
<div class="form-group">
|
||
<label>选择种子设备</label>
|
||
<div class="device-selection">
|
||
<div id="seed-devices-list" class="checkbox-list">
|
||
<!-- 设备列表将通过JavaScript动态加载 -->
|
||
<div class="no-devices">
|
||
<p>没有可用的设备。请先在设备管理页面添加设备。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>发现选项</label>
|
||
<div class="discovery-options">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="discover-cdp" checked>
|
||
<span class="checkmark"></span>
|
||
启用CDP发现
|
||
</label>
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="discover-lldp" checked>
|
||
<span class="checkmark"></span>
|
||
启用LLDP发现
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-search"></i> 开始发现
|
||
</button>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- 发现进度 -->
|
||
<div id="discovery-progress" class="discovery-progress" style="display: none;">
|
||
<div class="progress-header">
|
||
<h3>正在发现网络拓扑...</h3>
|
||
<div class="progress-spinner">
|
||
<i class="fas fa-spinner fa-spin"></i>
|
||
</div>
|
||
</div>
|
||
<div class="progress-details">
|
||
<div id="discovery-status">正在连接种子设备...</div>
|
||
<div class="progress-bar">
|
||
<div id="discovery-progress-bar" class="progress-fill"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设置页面 -->
|
||
<div id="settings-content" class="page-content" style="display: none;">
|
||
<h1 class="page-title">系统设置</h1>
|
||
<div class="card">
|
||
<p>系统设置功能将在后续版本中推出</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="footer">
|
||
<div class="container">
|
||
© 2025 NetBrain MCP
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<!-- 设备连接对话框 -->
|
||
<div id="device-connect-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>连接设备</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="connect-device-form">
|
||
<div class="form-group">
|
||
<label for="device-select">选择设备</label>
|
||
<select id="device-select" class="form-control" required>
|
||
<option value="">-- 请选择设备 --</option>
|
||
<!-- 设备列表将通过API动态加载 -->
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-select">选择凭据</label>
|
||
<select id="credential-select" class="form-control" required>
|
||
<option value="">-- 请选择凭据 --</option>
|
||
<!-- 凭据列表将通过API动态加载 -->
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="terminal-name">终端名称</label>
|
||
<input type="text" id="terminal-name" class="form-control" placeholder="设备名称-会话">
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-link"></i> 连接</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存会话对话框 -->
|
||
<div id="save-session-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>保存会话</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="save-session-form">
|
||
<div class="form-group">
|
||
<label for="active-session-select">选择活跃会话</label>
|
||
<select id="active-session-select" class="form-control" required>
|
||
<option value="">-- 请选择会话 --</option>
|
||
<!-- 活跃会话列表将通过JS动态加载 -->
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="session-name">会话名称</label>
|
||
<input type="text" id="session-name" class="form-control" placeholder="我的设备会话" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="session-description">会话描述</label>
|
||
<textarea id="session-description" class="form-control" placeholder="描述此会话的用途或内容"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="session-group">所属分组(可选)</label>
|
||
<select id="session-group" class="form-control">
|
||
<option value="">-- 无分组 --</option>
|
||
<!-- 会话组列表将通过JS动态加载 -->
|
||
</select>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-save"></i> 保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理会话组对话框 -->
|
||
<div id="manage-groups-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>管理会话组</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="session-groups-manager">
|
||
<!-- 创建新组 -->
|
||
<div class="form-group">
|
||
<label for="new-group-name">创建新组</label>
|
||
<div class="group-input-container">
|
||
<input type="text" id="new-group-name" class="form-control" placeholder="输入新组名称">
|
||
<button id="create-group-btn" class="btn"><i class="fas fa-plus"></i> 创建</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 会话组列表 -->
|
||
<div class="session-groups-list">
|
||
<h3>现有会话组</h3>
|
||
<div id="manage-groups-list">
|
||
<!-- 会话组列表将通过JS动态加载 -->
|
||
<div class="group-empty">暂无会话组</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions right">
|
||
<button type="button" class="btn close-btn"><i class="fas fa-check"></i> 完成</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加设备对话框 -->
|
||
<div id="add-device-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>添加网络设备</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="add-device-form">
|
||
<div class="form-group">
|
||
<label for="device-name">设备名称</label>
|
||
<input type="text" id="device-name" class="form-control" placeholder="例如:Core-Router-01" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-ip">IP地址</label>
|
||
<input type="text" id="device-ip" class="form-control" placeholder="例如:192.168.1.1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-type">设备类型</label>
|
||
<select id="device-type" class="form-control" required>
|
||
<option value="router">路由器</option>
|
||
<option value="switch">交换机</option>
|
||
<option value="firewall">防火墙</option>
|
||
<option value="load_balancer">负载均衡器</option>
|
||
<option value="wireless_controller">无线控制器</option>
|
||
<option value="access_point">接入点</option>
|
||
<option value="other">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-vendor">设备厂商</label>
|
||
<select id="device-vendor" class="form-control" required>
|
||
<option value="cisco">思科(Cisco)</option>
|
||
<option value="huawei">华为(Huawei)</option>
|
||
<option value="h3c">新华三(H3C)</option>
|
||
<option value="juniper">Juniper</option>
|
||
<option value="arista">Arista</option>
|
||
<option value="fortinet">Fortinet</option>
|
||
<option value="checkpoint">CheckPoint</option>
|
||
<option value="other">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-platform">平台</label>
|
||
<input type="text" id="device-platform" class="form-control" placeholder="例如:ISR 4000系列">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-model">设备型号</label>
|
||
<input type="text" id="device-model" class="form-control" placeholder="例如:ISR 4431">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-os">操作系统版本</label>
|
||
<input type="text" id="device-os" class="form-control" placeholder="例如:IOS-XE 16.9.3">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-location">位置</label>
|
||
<input type="text" id="device-location" class="form-control" placeholder="例如:主数据中心1层">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-description">描述</label>
|
||
<textarea id="device-description" class="form-control" placeholder="设备用途和其他信息"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-tags">标签</label>
|
||
<input type="text" id="device-tags" class="form-control" placeholder="用逗号分隔多个标签,例如:核心,生产,关键">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="device-credential">关联凭据 (可选)</label>
|
||
<select id="device-credential" class="form-control">
|
||
<option value="">-- 无凭据 --</option>
|
||
<!-- 凭据列表将通过JS动态加载 -->
|
||
</select>
|
||
<small class="form-text text-muted">拓扑发现和设备连接需要凭据</small>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-plus"></i> 添加设备</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加凭据对话框 -->
|
||
<div id="add-credential-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>添加设备凭据</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="add-credential-form">
|
||
<div class="form-group">
|
||
<label for="credential-name">凭据名称</label>
|
||
<input type="text" id="credential-name" class="form-control" placeholder="例如:Cisco设备SSH凭据" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-username">用户名</label>
|
||
<input type="text" id="credential-username" class="form-control" placeholder="例如:admin" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-password">密码</label>
|
||
<input type="password" id="credential-password" class="form-control" placeholder="输入密码" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-protocol">连接协议</label>
|
||
<select id="credential-protocol" class="form-control" required>
|
||
<option value="ssh">SSH</option>
|
||
<option value="telnet">Telnet</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-port">端口</label>
|
||
<input type="number" id="credential-port" class="form-control" placeholder="22">
|
||
<small class="form-text text-muted">SSH默认为22,Telnet默认为23</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="credential-enable">Enable密码 (可选)</label>
|
||
<input type="password" id="credential-enable" class="form-control" placeholder="思科设备的特权模式密码">
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-plus"></i> 添加凭据</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 修改设备对话框 -->
|
||
<div id="edit-device-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>修改网络设备</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="edit-device-form">
|
||
<input type="hidden" id="edit-device-id">
|
||
<div class="form-group">
|
||
<label for="edit-device-name">设备名称</label>
|
||
<input type="text" id="edit-device-name" class="form-control" placeholder="例如:Core-Router-01" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-ip">IP地址</label>
|
||
<input type="text" id="edit-device-ip" class="form-control" placeholder="例如:192.168.1.1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-type">设备类型</label>
|
||
<select id="edit-device-type" class="form-control" required>
|
||
<option value="router">路由器</option>
|
||
<option value="switch">交换机</option>
|
||
<option value="firewall">防火墙</option>
|
||
<option value="load_balancer">负载均衡器</option>
|
||
<option value="wireless_controller">无线控制器</option>
|
||
<option value="access_point">接入点</option>
|
||
<option value="other">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-vendor">设备厂商</label>
|
||
<select id="edit-device-vendor" class="form-control" required>
|
||
<option value="cisco">思科(Cisco)</option>
|
||
<option value="huawei">华为(Huawei)</option>
|
||
<option value="h3c">新华三(H3C)</option>
|
||
<option value="juniper">Juniper</option>
|
||
<option value="arista">Arista</option>
|
||
<option value="fortinet">Fortinet</option>
|
||
<option value="checkpoint">CheckPoint</option>
|
||
<option value="other">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-platform">平台</label>
|
||
<input type="text" id="edit-device-platform" class="form-control" placeholder="例如:ISR 4000系列">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-model">设备型号</label>
|
||
<input type="text" id="edit-device-model" class="form-control" placeholder="例如:ISR 4431">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-os">操作系统版本</label>
|
||
<input type="text" id="edit-device-os" class="form-control" placeholder="例如:IOS-XE 16.9.3">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-status">状态</label>
|
||
<select id="edit-device-status" class="form-control">
|
||
<option value="online">在线</option>
|
||
<option value="offline">离线</option>
|
||
<option value="unreachable">不可达</option>
|
||
<option value="maintenance">维护中</option>
|
||
<option value="unknown">未知</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-location">位置</label>
|
||
<input type="text" id="edit-device-location" class="form-control" placeholder="例如:主数据中心1层">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-description">描述</label>
|
||
<textarea id="edit-device-description" class="form-control" placeholder="设备用途和其他信息"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-tags">标签</label>
|
||
<input type="text" id="edit-device-tags" class="form-control" placeholder="用逗号分隔多个标签,例如:核心,生产,关键">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-device-credential">关联凭据 (可选)</label>
|
||
<select id="edit-device-credential" class="form-control">
|
||
<option value="">-- 无凭据 --</option>
|
||
<!-- 凭据列表将通过JS动态加载 -->
|
||
</select>
|
||
<small class="form-text text-muted">拓扑发现和设备连接需要凭据</small>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-save"></i> 保存修改</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 修改凭据对话框 -->
|
||
<div id="edit-credential-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2>修改设备凭据</h2>
|
||
<span class="close-modal">×</span>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="edit-credential-form">
|
||
<input type="hidden" id="edit-credential-id">
|
||
<div class="form-group">
|
||
<label for="edit-credential-name">凭据名称</label>
|
||
<input type="text" id="edit-credential-name" class="form-control" placeholder="例如:Cisco设备SSH凭据" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-credential-username">用户名</label>
|
||
<input type="text" id="edit-credential-username" class="form-control" placeholder="例如:admin" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-credential-password">密码</label>
|
||
<input type="password" id="edit-credential-password" class="form-control" placeholder="输入新密码(留空表示不修改)">
|
||
<small class="form-text text-muted">留空表示不修改密码</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-credential-protocol">连接协议</label>
|
||
<select id="edit-credential-protocol" class="form-control" required>
|
||
<option value="ssh">SSH</option>
|
||
<option value="telnet">Telnet</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-credential-port">端口</label>
|
||
<input type="number" id="edit-credential-port" class="form-control" placeholder="22">
|
||
<small class="form-text text-muted">SSH默认为22,Telnet默认为23</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="edit-credential-enable">Enable密码 (可选)</label>
|
||
<input type="password" id="edit-credential-enable" class="form-control" placeholder="思科设备的特权模式密码">
|
||
<small class="form-text text-muted">留空表示不修改Enable密码</small>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-outline close-btn"><i class="fas fa-times"></i> 取消</button>
|
||
<button type="submit" class="btn"><i class="fas fa-save"></i> 保存修改</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
/* 页面特定样式 */
|
||
.login-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--light-bg);
|
||
padding: 4rem 2.5rem;
|
||
max-width: 400px;
|
||
width: 90%;
|
||
margin: 0 auto;
|
||
text-align: center;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
}
|
||
|
||
.login-logo {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.logo {
|
||
max-width: 120px;
|
||
}
|
||
|
||
.login-title {
|
||
font-size: 1.75rem;
|
||
margin-bottom: 1rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.login-description {
|
||
margin-bottom: 2rem;
|
||
color: var(--light-text);
|
||
font-size: 1rem;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.copyright {
|
||
margin-top: 3rem;
|
||
color: var(--light-text);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
width: 100%;
|
||
}
|
||
|
||
#login-page {
|
||
background-color: var(--bg-color);
|
||
position: relative;
|
||
}
|
||
|
||
.page-content {
|
||
padding: 1rem 0 3rem;
|
||
}
|
||
|
||
.page-title {
|
||
margin-bottom: 2rem;
|
||
font-weight: 600;
|
||
font-size: 1.75rem;
|
||
}
|
||
|
||
/* 增加整体容器最大宽度 */
|
||
.container {
|
||
max-width: 1400px;
|
||
width: 95%;
|
||
margin: 0 auto;
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
/* 终端页面布局样式 */
|
||
.terminal-layout {
|
||
display: flex;
|
||
gap: 1.5rem;
|
||
height: calc(100vh - 200px);
|
||
min-height: 500px;
|
||
}
|
||
|
||
/* 会话管理面板左侧垂直布局 */
|
||
.sessions-card {
|
||
width: 320px;
|
||
flex-shrink: 0;
|
||
background-color: var(--light-bg);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
padding: 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 100%;
|
||
}
|
||
|
||
/* 终端窗口右侧布局 */
|
||
.terminal-card {
|
||
flex: 1;
|
||
background-color: var(--light-bg);
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
padding: 1.5rem;
|
||
max-height: 100%;
|
||
}
|
||
|
||
/* 终端相关样式 */
|
||
.terminal-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
background-color: #1a1a1a;
|
||
border-radius: var(--border-radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.terminal-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
background-color: #333;
|
||
padding: 0.5rem;
|
||
border-bottom: 1px solid #555;
|
||
}
|
||
|
||
.terminal-tabs {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
max-width: 80%;
|
||
}
|
||
|
||
.terminal-tab {
|
||
padding: 0.5rem 1rem;
|
||
background-color: #444;
|
||
color: #ccc;
|
||
cursor: pointer;
|
||
margin-right: 0.5rem;
|
||
border-radius: 4px 4px 0 0;
|
||
user-select: none;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.terminal-tab.active {
|
||
background-color: #666;
|
||
color: white;
|
||
}
|
||
|
||
.terminal-tab-close {
|
||
margin-left: 0.5rem;
|
||
width: 16px;
|
||
height: 16px;
|
||
line-height: 16px;
|
||
text-align: center;
|
||
border-radius: 50%;
|
||
background-color: #555;
|
||
color: #ccc;
|
||
}
|
||
|
||
.terminal-tab-close:hover {
|
||
background-color: #777;
|
||
color: white;
|
||
}
|
||
|
||
.terminal-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.terminal-actions button {
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.4rem 0.8rem;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* 按钮内图标样式 */
|
||
.btn i {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.terminal-windows {
|
||
flex: 1;
|
||
position: relative;
|
||
background-color: black;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.terminal-instance {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 0.5rem;
|
||
display: none;
|
||
}
|
||
|
||
.terminal-instance.active {
|
||
display: block;
|
||
}
|
||
|
||
/* 设备列表样式 */
|
||
.device-manager {
|
||
width: 100%;
|
||
}
|
||
|
||
.device-list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.device-list-empty {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: #777;
|
||
}
|
||
|
||
/* 模态框样式 */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 1000;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: auto;
|
||
background-color: rgba(0,0,0,0.6);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
margin: 5% auto; /* 从10%改为5%,让对话框往上移 */
|
||
padding: 0;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: var(--box-shadow);
|
||
animation: modalFadeIn 0.3s;
|
||
position: relative;
|
||
top: -50px; /* 添加负的top值使对话框进一步上移 */
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 1.5rem;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
margin: 0;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.close-modal {
|
||
cursor: pointer;
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
color: #777;
|
||
}
|
||
|
||
.close-modal:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 1rem;
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
@keyframes modalFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 导航栏图标样式 */
|
||
.nav-link i {
|
||
margin-right: 8px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 激活状态的导航链接图标样式 */
|
||
.nav-link.active i {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
/* 会话管理面板样式 */
|
||
.sessions-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
margin-bottom: 1.2rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
padding-bottom: 1rem;
|
||
}
|
||
|
||
.sessions-header h2 {
|
||
margin: 0 0 0.8rem 0;
|
||
font-size: 1.2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.sessions-header h2 i {
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.sessions-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.sessions-actions button {
|
||
flex: 1;
|
||
text-align: center;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0.5rem 0.6rem;
|
||
min-width: 90px;
|
||
}
|
||
|
||
.sessions-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.2rem;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.sessions-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.sessions-section h3 {
|
||
font-size: 1rem;
|
||
margin-bottom: 0.7rem;
|
||
color: var(--secondary-color);
|
||
display: flex;
|
||
align-items: center;
|
||
padding-left: 0.3rem;
|
||
}
|
||
|
||
.sessions-section h3 i {
|
||
margin-right: 0.5rem;
|
||
width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sessions-list {
|
||
background-color: var(--bg-color);
|
||
border-radius: 8px;
|
||
padding: 0.8rem;
|
||
min-height: 100px;
|
||
max-height: 180px;
|
||
overflow-y: auto;
|
||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.session-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background-color: var(--light-bg);
|
||
padding: 0.8rem 1rem;
|
||
border-radius: 6px;
|
||
margin-bottom: 0.7rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.session-item:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
||
border-color: var(--border-color);
|
||
}
|
||
|
||
.session-item-content {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.session-item-title {
|
||
font-weight: 500;
|
||
font-size: 0.95rem;
|
||
margin-bottom: 0.25rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.session-item-details {
|
||
font-size: 0.8rem;
|
||
color: var(--light-text);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.session-item-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.session-item-action {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
background-color: var(--bg-color);
|
||
color: var(--text-color);
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.session-item-action:hover {
|
||
background-color: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.session-empty {
|
||
text-align: center;
|
||
color: var(--light-text);
|
||
font-size: 0.9rem;
|
||
padding: 2rem 1rem;
|
||
}
|
||
|
||
.session-status {
|
||
display: inline-block;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.session-status-active {
|
||
background-color: #4CAF50;
|
||
}
|
||
|
||
.session-status-connecting {
|
||
background-color: #FFC107;
|
||
}
|
||
|
||
.session-status-disconnected {
|
||
background-color: #F44336;
|
||
}
|
||
|
||
.session-group-item {
|
||
background-color: var(--light-bg);
|
||
border-radius: 6px;
|
||
margin-bottom: 0.7rem;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.session-group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.8rem 1rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.session-group-content {
|
||
padding: 0 0.8rem 0.8rem;
|
||
display: none;
|
||
}
|
||
|
||
.session-group-content.active {
|
||
display: block;
|
||
}
|
||
|
||
/* 对话框内的特殊样式 */
|
||
.group-input-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.group-input-container .form-control {
|
||
flex: 1;
|
||
}
|
||
|
||
.form-actions.right {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.session-groups-list {
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
.session-groups-list h3 {
|
||
font-size: 1.1rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.group-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background-color: var(--bg-color);
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.group-item-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.group-item-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.group-empty {
|
||
text-align: center;
|
||
color: var(--light-text);
|
||
padding: 2rem 0;
|
||
}
|
||
|
||
/* 表单控件样式增强 */
|
||
textarea.form-control {
|
||
min-height: 80px;
|
||
resize: vertical;
|
||
}
|
||
|
||
/* 响应式布局调整 */
|
||
@media (max-width: 768px) {
|
||
.terminal-layout {
|
||
flex-direction: column;
|
||
height: auto;
|
||
}
|
||
|
||
.sessions-card {
|
||
width: 100%;
|
||
height: auto;
|
||
max-height: 400px;
|
||
}
|
||
|
||
.terminal-card {
|
||
height: 500px;
|
||
}
|
||
}
|
||
|
||
/* 设备管理页面样式 */
|
||
.device-management-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.device-card, .credential-card {
|
||
flex: 1;
|
||
min-width: 400px;
|
||
}
|
||
|
||
.device-manager, .credential-manager {
|
||
width: 100%;
|
||
}
|
||
|
||
.device-list-header, .credential-list-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1.5rem;
|
||
padding-bottom: 1rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.device-list-header h2, .credential-list-header h2 {
|
||
margin: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.device-list-header h2 i, .credential-list-header h2 i {
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.device-list-empty, .credential-list-empty {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--light-text);
|
||
background-color: var(--bg-color);
|
||
border-radius: var(--border-radius);
|
||
}
|
||
|
||
.item-list {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.device-item, .credential-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background-color: var(--bg-color);
|
||
padding: 1rem;
|
||
border-radius: var(--border-radius);
|
||
margin-bottom: 0.8rem;
|
||
transition: all 0.2s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.device-item:hover, .credential-item:hover {
|
||
border-color: var(--border-color);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.device-item-content, .credential-item-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.device-item-title, .credential-item-title {
|
||
font-weight: 500;
|
||
font-size: 1.1rem;
|
||
margin-bottom: 0.3rem;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.device-item-title i, .credential-item-title i {
|
||
margin-right: 0.5rem;
|
||
width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.device-item-subtitle, .credential-item-subtitle {
|
||
font-size: 0.9rem;
|
||
color: var(--light-text);
|
||
}
|
||
|
||
.device-item-details, .credential-item-details {
|
||
font-size: 0.85rem;
|
||
color: var(--light-text);
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.device-item-actions, .credential-item-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.device-item-action, .credential-item-action {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background-color: var(--light-bg);
|
||
color: var(--text-color);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: none;
|
||
}
|
||
|
||
.device-item-action:hover, .credential-item-action:hover {
|
||
background-color: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.device-status {
|
||
display: inline-block !important;
|
||
width: 10px !important;
|
||
height: 10px !important;
|
||
border-radius: 50% !important;
|
||
margin-right: 8px !important;
|
||
flex-shrink: 0 !important;
|
||
flex-grow: 0 !important;
|
||
min-width: 10px !important;
|
||
min-height: 10px !important;
|
||
max-width: 10px !important;
|
||
max-height: 10px !important;
|
||
box-sizing: border-box !important;
|
||
vertical-align: middle !important;
|
||
position: relative !important;
|
||
aspect-ratio: 1 / 1 !important;
|
||
}
|
||
|
||
.device-status-online {
|
||
background-color: #4CAF50;
|
||
}
|
||
|
||
.device-status-offline {
|
||
background-color: #F44336;
|
||
}
|
||
|
||
.device-status-unreachable {
|
||
background-color: #FFC107;
|
||
}
|
||
|
||
.device-status-maintenance {
|
||
background-color: #2196F3;
|
||
}
|
||
|
||
.device-status-unknown {
|
||
background-color: #9E9E9E;
|
||
}
|
||
|
||
/* 设备详情悬浮卡片 */
|
||
.device-hover-card {
|
||
position: absolute;
|
||
background-color: white;
|
||
border-radius: var(--border-radius);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
padding: 1rem;
|
||
z-index: 100;
|
||
width: 300px;
|
||
display: none;
|
||
}
|
||
|
||
.device-hover-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.5rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.device-hover-card-title {
|
||
font-weight: 600;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.device-hover-card-content {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.device-hover-card-item {
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
}
|
||
|
||
.device-hover-card-label {
|
||
font-weight: 500;
|
||
width: 80px;
|
||
color: var(--light-text);
|
||
}
|
||
|
||
.device-hover-card-value {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 快速操作按钮 */
|
||
.quick-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.quick-action-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 1rem;
|
||
background: var(--bg-color);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--border-radius);
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
text-decoration: none;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.quick-action-btn:hover {
|
||
background: var(--hover-bg);
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--box-shadow);
|
||
}
|
||
|
||
.quick-action-btn i {
|
||
font-size: 1.5rem;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.quick-action-btn span {
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.device-overview-status.unknown {
|
||
background: var(--light-text);
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
// 设备管理UI类
|
||
class DeviceManagerUI {
|
||
constructor() {
|
||
// 获取DOM元素
|
||
this.deviceList = document.getElementById('device-list');
|
||
this.credentialList = document.getElementById('credential-list');
|
||
|
||
// 获取添加设备按钮和表单
|
||
this.addDeviceBtn = document.getElementById('add-device-btn');
|
||
this.addDeviceModal = document.getElementById('add-device-modal');
|
||
this.addDeviceForm = document.getElementById('add-device-form');
|
||
|
||
// 获取添加凭据按钮和表单
|
||
this.addCredentialBtn = document.getElementById('add-credential-btn');
|
||
this.addCredentialModal = document.getElementById('add-credential-modal');
|
||
this.addCredentialForm = document.getElementById('add-credential-form');
|
||
|
||
// 获取修改设备和凭据对话框
|
||
this.editDeviceModal = document.getElementById('edit-device-modal');
|
||
this.editDeviceForm = document.getElementById('edit-device-form');
|
||
this.editCredentialModal = document.getElementById('edit-credential-modal');
|
||
this.editCredentialForm = document.getElementById('edit-credential-form');
|
||
|
||
// 绑定添加设备按钮事件
|
||
this.addDeviceBtn.addEventListener('click', async () => {
|
||
// 加载凭据列表到选择器
|
||
await this.loadCredentialsForSelect('device-credential');
|
||
this.addDeviceModal.style.display = 'block';
|
||
});
|
||
|
||
// 绑定添加凭据按钮事件
|
||
this.addCredentialBtn.addEventListener('click', () => {
|
||
this.addCredentialModal.style.display = 'block';
|
||
});
|
||
|
||
// 绑定表单提交事件
|
||
this.addDeviceForm.addEventListener('submit', this.handleAddDevice.bind(this));
|
||
this.addCredentialForm.addEventListener('submit', this.handleAddCredential.bind(this));
|
||
|
||
// 绑定修改表单提交事件
|
||
this.editDeviceForm.addEventListener('submit', this.handleEditDevice.bind(this));
|
||
this.editCredentialForm.addEventListener('submit', this.handleEditCredential.bind(this));
|
||
|
||
// 绑定对话框关闭按钮事件
|
||
this.addDeviceModal.querySelector('.close-modal').addEventListener('click', () => {
|
||
this.addDeviceModal.style.display = 'none';
|
||
});
|
||
this.addCredentialModal.querySelector('.close-modal').addEventListener('click', () => {
|
||
this.addCredentialModal.style.display = 'none';
|
||
});
|
||
this.editDeviceModal.querySelector('.close-modal').addEventListener('click', () => {
|
||
this.editDeviceModal.style.display = 'none';
|
||
});
|
||
this.editCredentialModal.querySelector('.close-modal').addEventListener('click', () => {
|
||
this.editCredentialModal.style.display = 'none';
|
||
});
|
||
|
||
// 绑定取消按钮事件
|
||
this.addDeviceModal.querySelectorAll('.close-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this.addDeviceModal.style.display = 'none';
|
||
});
|
||
});
|
||
this.addCredentialModal.querySelectorAll('.close-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this.addCredentialModal.style.display = 'none';
|
||
});
|
||
});
|
||
this.editDeviceModal.querySelectorAll('.close-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this.editDeviceModal.style.display = 'none';
|
||
});
|
||
});
|
||
this.editCredentialModal.querySelectorAll('.close-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this.editCredentialModal.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
// 加载设备和凭据列表
|
||
this.loadDevices();
|
||
this.loadCredentials();
|
||
}
|
||
|
||
// 加载设备列表
|
||
async loadDevices() {
|
||
try {
|
||
const response = await fetch('/api/devices');
|
||
const devices = await response.json();
|
||
|
||
// 清空设备列表
|
||
this.deviceList.innerHTML = '';
|
||
|
||
if (devices.length === 0) {
|
||
this.deviceList.innerHTML = `
|
||
<div class="device-list-empty">
|
||
<p>尚未添加任何设备</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 填充设备列表
|
||
devices.forEach(device => {
|
||
this.addDeviceToUI(device);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载设备列表失败:', error);
|
||
this.deviceList.innerHTML = `
|
||
<div class="device-list-empty">
|
||
<p>加载设备列表失败,请刷新页面重试</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 加载凭据列表
|
||
async loadCredentials() {
|
||
try {
|
||
const response = await fetch('/api/credentials');
|
||
const credentials = await response.json();
|
||
|
||
// 清空凭据列表
|
||
this.credentialList.innerHTML = '';
|
||
|
||
if (credentials.length === 0) {
|
||
this.credentialList.innerHTML = `
|
||
<div class="credential-list-empty">
|
||
<p>尚未添加任何凭据</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 填充凭据列表
|
||
credentials.forEach(credential => {
|
||
this.addCredentialToUI(credential);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载凭据列表失败:', error);
|
||
this.credentialList.innerHTML = `
|
||
<div class="credential-list-empty">
|
||
<p>加载凭据列表失败,请刷新页面重试</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 处理添加设备
|
||
async handleAddDevice(e) {
|
||
e.preventDefault();
|
||
|
||
const deviceName = document.getElementById('device-name').value;
|
||
const deviceIp = document.getElementById('device-ip').value;
|
||
const deviceType = document.getElementById('device-type').value;
|
||
const deviceVendor = document.getElementById('device-vendor').value;
|
||
const devicePlatform = document.getElementById('device-platform').value;
|
||
const deviceModel = document.getElementById('device-model').value;
|
||
const deviceOs = document.getElementById('device-os').value;
|
||
const deviceLocation = document.getElementById('device-location').value;
|
||
const deviceDescription = document.getElementById('device-description').value;
|
||
const deviceTags = document.getElementById('device-tags').value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||
const deviceCredential = document.getElementById('device-credential').value || null;
|
||
|
||
// 验证必填字段
|
||
if (!deviceName || !deviceIp) {
|
||
alert('请填写设备名称和IP地址');
|
||
return;
|
||
}
|
||
|
||
// 准备设备数据
|
||
const deviceData = {
|
||
name: deviceName,
|
||
ip_address: deviceIp,
|
||
device_type: deviceType,
|
||
vendor: deviceVendor,
|
||
platform: devicePlatform,
|
||
model: deviceModel,
|
||
os_version: deviceOs,
|
||
location: deviceLocation,
|
||
description: deviceDescription,
|
||
tags: deviceTags,
|
||
credential_id: deviceCredential
|
||
};
|
||
|
||
console.log('准备发送的设备数据:', deviceData);
|
||
|
||
try {
|
||
// 发送API请求添加设备
|
||
const response = await fetch('/api/devices', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(deviceData)
|
||
});
|
||
|
||
console.log('收到响应状态:', response.status, response.statusText);
|
||
|
||
// 读取响应文本
|
||
const responseText = await response.text();
|
||
console.log('响应内容:', responseText);
|
||
|
||
// 处理错误情况
|
||
if (!response.ok) {
|
||
let errorDetail = responseText;
|
||
try {
|
||
const errorJson = JSON.parse(responseText);
|
||
if (errorJson.detail) {
|
||
errorDetail = errorJson.detail;
|
||
}
|
||
} catch (e) {
|
||
// 如果不是JSON格式,使用原始文本
|
||
}
|
||
throw new Error(`添加设备失败 (${response.status}): ${errorDetail}`);
|
||
}
|
||
|
||
// 解析成功的响应
|
||
let data;
|
||
try {
|
||
data = JSON.parse(responseText);
|
||
} catch (error) {
|
||
console.error('解析响应数据失败:', error);
|
||
throw new Error('解析响应数据失败');
|
||
}
|
||
|
||
// 添加到UI
|
||
this.addDeviceToUI(data);
|
||
|
||
// 关闭对话框
|
||
this.addDeviceModal.style.display = 'none';
|
||
|
||
// 重新加载设备列表
|
||
this.loadDevices();
|
||
|
||
// 重置表单
|
||
this.addDeviceForm.reset();
|
||
|
||
} catch (error) {
|
||
console.error('添加设备失败:', error);
|
||
alert(`添加设备失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 处理添加凭据
|
||
async handleAddCredential(e) {
|
||
e.preventDefault();
|
||
|
||
const credentialName = document.getElementById('credential-name').value;
|
||
const credentialUsername = document.getElementById('credential-username').value;
|
||
const credentialPassword = document.getElementById('credential-password').value;
|
||
const credentialProtocol = document.getElementById('credential-protocol').value;
|
||
const credentialPort = document.getElementById('credential-port').value || (credentialProtocol === 'ssh' ? '22' : '23');
|
||
const credentialEnable = document.getElementById('credential-enable').value;
|
||
|
||
// 验证必填字段
|
||
if (!credentialName || !credentialUsername || !credentialPassword) {
|
||
alert('请填写凭据名称、用户名和密码');
|
||
return;
|
||
}
|
||
|
||
// 准备凭据数据
|
||
const credentialData = {
|
||
name: credentialName,
|
||
username: credentialUsername,
|
||
password: credentialPassword,
|
||
protocol: credentialProtocol,
|
||
port: parseInt(credentialPort),
|
||
enable_password: credentialEnable || null
|
||
};
|
||
|
||
console.log('准备发送的凭据数据:', credentialData);
|
||
|
||
try {
|
||
// 发送API请求添加凭据
|
||
const response = await fetch('/api/credentials', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(credentialData)
|
||
});
|
||
|
||
console.log('收到响应状态:', response.status, response.statusText);
|
||
|
||
// 读取响应文本
|
||
const responseText = await response.text();
|
||
console.log('响应内容:', responseText);
|
||
|
||
// 处理错误情况
|
||
if (!response.ok) {
|
||
let errorDetail = responseText;
|
||
try {
|
||
const errorJson = JSON.parse(responseText);
|
||
if (errorJson.detail) {
|
||
errorDetail = errorJson.detail;
|
||
}
|
||
} catch (e) {
|
||
// 如果不是JSON格式,使用原始文本
|
||
}
|
||
throw new Error(`添加凭据失败 (${response.status}): ${errorDetail}`);
|
||
}
|
||
|
||
// 解析成功的响应
|
||
let data;
|
||
try {
|
||
data = JSON.parse(responseText);
|
||
} catch (error) {
|
||
console.error('解析响应数据失败:', error);
|
||
throw new Error('解析响应数据失败');
|
||
}
|
||
|
||
// 添加到UI
|
||
this.addCredentialToUI(data);
|
||
|
||
// 关闭对话框
|
||
this.addCredentialModal.style.display = 'none';
|
||
|
||
// 重新加载凭据列表
|
||
this.loadCredentials();
|
||
|
||
// 重置表单
|
||
this.addCredentialForm.reset();
|
||
|
||
} catch (error) {
|
||
console.error('添加凭据失败:', error);
|
||
alert(`添加凭据失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 添加设备到UI
|
||
addDeviceToUI(device) {
|
||
const deviceEmpty = this.deviceList.querySelector('.device-list-empty');
|
||
if (deviceEmpty) {
|
||
deviceEmpty.remove();
|
||
}
|
||
|
||
// 检查是否已存在同ID设备条目
|
||
const existingDevice = document.getElementById(`device-${device.id}`);
|
||
if (existingDevice) {
|
||
existingDevice.remove();
|
||
}
|
||
|
||
const deviceItem = document.createElement('div');
|
||
deviceItem.className = 'device-item';
|
||
deviceItem.id = `device-${device.id}`;
|
||
|
||
// 设备状态类
|
||
const statusClasses = {
|
||
'online': 'device-status-online',
|
||
'offline': 'device-status-offline',
|
||
'unreachable': 'device-status-unreachable',
|
||
'maintenance': 'device-status-maintenance',
|
||
'unknown': 'device-status-unknown'
|
||
};
|
||
|
||
// 设备类型图标
|
||
const typeIcons = {
|
||
'router': 'fas fa-route',
|
||
'switch': 'fas fa-network-wired',
|
||
'firewall': 'fas fa-shield-alt',
|
||
'load_balancer': 'fas fa-balance-scale',
|
||
'wireless_controller': 'fas fa-wifi',
|
||
'access_point': 'fas fa-broadcast-tower',
|
||
'other': 'fas fa-server'
|
||
};
|
||
|
||
deviceItem.innerHTML = `
|
||
<div class="device-item-content">
|
||
<div class="device-item-title">
|
||
<span class="device-status ${statusClasses[device.status] || 'device-status-unknown'}"></span>
|
||
<i class="${typeIcons[device.device_type] || 'fas fa-server'}"></i>
|
||
${device.name}
|
||
</div>
|
||
<div class="device-item-subtitle">
|
||
${device.ip_address} | ${device.vendor.toUpperCase()} ${device.platform ? device.platform + ' ' : ''}${device.model}
|
||
</div>
|
||
<div class="device-item-details">
|
||
${device.os_version ? `OS: ${device.os_version} | ` : ''}
|
||
${device.location ? `位置: ${device.location}` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="device-item-actions">
|
||
<button class="device-item-action connect-device" title="连接设备">
|
||
<i class="fas fa-terminal"></i>
|
||
</button>
|
||
<button class="device-item-action edit-device" title="编辑设备" data-id="${device.id}">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="device-item-action delete-device" title="删除设备" data-id="${device.id}">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 连接设备按钮
|
||
deviceItem.querySelector('.connect-device').addEventListener('click', () => {
|
||
// 显示连接对话框,预选此设备
|
||
const modal = document.getElementById('device-connect-modal');
|
||
modal.style.display = 'block';
|
||
|
||
// 调用终端管理器的showConnectDialog方法加载设备和凭据列表
|
||
window.terminalManager.showConnectDialog();
|
||
|
||
// 等待列表加载完成后选择当前设备
|
||
const checkDeviceSelect = setInterval(() => {
|
||
const deviceSelect = document.getElementById('device-select');
|
||
if (deviceSelect.options.length > 1) {
|
||
clearInterval(checkDeviceSelect);
|
||
|
||
for (let i = 0; i < deviceSelect.options.length; i++) {
|
||
if (deviceSelect.options[i].value === device.id) {
|
||
deviceSelect.selectedIndex = i;
|
||
|
||
// 触发change事件以更新终端名称
|
||
const event = new Event('change');
|
||
deviceSelect.dispatchEvent(event);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}, 100);
|
||
});
|
||
|
||
// 添加设备条目到列表
|
||
this.deviceList.appendChild(deviceItem);
|
||
|
||
// 绑定编辑和删除按钮事件
|
||
const editBtn = deviceItem.querySelector('.edit-device');
|
||
const deleteBtn = deviceItem.querySelector('.delete-device');
|
||
|
||
editBtn.addEventListener('click', () => {
|
||
this.showEditDeviceModal(device.id);
|
||
});
|
||
|
||
deleteBtn.addEventListener('click', () => {
|
||
this.handleDeleteDevice(device.id);
|
||
});
|
||
}
|
||
|
||
// 添加凭据到UI
|
||
addCredentialToUI(credential) {
|
||
const credentialEmpty = this.credentialList.querySelector('.credential-list-empty');
|
||
if (credentialEmpty) {
|
||
credentialEmpty.remove();
|
||
}
|
||
|
||
// 检查是否已存在同ID凭据条目
|
||
const existingCredential = document.getElementById(`credential-${credential.id}`);
|
||
if (existingCredential) {
|
||
existingCredential.remove();
|
||
}
|
||
|
||
// 协议图标
|
||
const protocolIcons = {
|
||
'ssh': 'fas fa-lock',
|
||
'telnet': 'fas fa-unlock',
|
||
'snmp': 'fas fa-chart-line',
|
||
'http': 'fas fa-globe',
|
||
'https': 'fas fa-shield-alt',
|
||
'netconf': 'fas fa-code'
|
||
};
|
||
|
||
const credentialItem = document.createElement('div');
|
||
credentialItem.className = 'credential-item';
|
||
credentialItem.id = `credential-${credential.id}`;
|
||
|
||
credentialItem.innerHTML = `
|
||
<div class="credential-item-content">
|
||
<div class="credential-item-title">
|
||
<i class="${protocolIcons[credential.protocol] || 'fas fa-key'}"></i>
|
||
${credential.name}
|
||
</div>
|
||
<div class="credential-item-subtitle">
|
||
${credential.username}@${credential.protocol.toUpperCase()} | 端口: ${credential.port}
|
||
</div>
|
||
</div>
|
||
<div class="credential-item-actions">
|
||
<button class="credential-item-action edit-credential" title="编辑凭据" data-id="${credential.id}">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="credential-item-action delete-credential" title="删除凭据" data-id="${credential.id}">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// 添加凭据条目到列表
|
||
this.credentialList.appendChild(credentialItem);
|
||
|
||
// 绑定编辑和删除按钮事件
|
||
const editBtn = credentialItem.querySelector('.edit-credential');
|
||
const deleteBtn = credentialItem.querySelector('.delete-credential');
|
||
|
||
editBtn.addEventListener('click', () => {
|
||
this.showEditCredentialModal(credential.id);
|
||
});
|
||
|
||
deleteBtn.addEventListener('click', () => {
|
||
this.handleDeleteCredential(credential.id);
|
||
});
|
||
}
|
||
|
||
// 显示修改设备对话框
|
||
async showEditDeviceModal(deviceId) {
|
||
try {
|
||
// 获取设备详情
|
||
const response = await fetch(`/api/devices/${deviceId}`);
|
||
if (!response.ok) {
|
||
throw new Error('获取设备详情失败');
|
||
}
|
||
|
||
const device = await response.json();
|
||
|
||
// 设置表单值
|
||
document.getElementById('edit-device-id').value = device.id;
|
||
document.getElementById('edit-device-name').value = device.name;
|
||
document.getElementById('edit-device-ip').value = device.ip_address;
|
||
document.getElementById('edit-device-type').value = device.device_type;
|
||
document.getElementById('edit-device-vendor').value = device.vendor;
|
||
document.getElementById('edit-device-platform').value = device.platform || '';
|
||
document.getElementById('edit-device-model').value = device.model || '';
|
||
document.getElementById('edit-device-os').value = device.os_version || '';
|
||
document.getElementById('edit-device-status').value = device.status;
|
||
document.getElementById('edit-device-location').value = device.location || '';
|
||
document.getElementById('edit-device-description').value = device.description || '';
|
||
document.getElementById('edit-device-tags').value = (device.tags || []).join(',');
|
||
|
||
// 加载并设置凭据选择器
|
||
await this.loadCredentialsForSelect('edit-device-credential', device.credential_id);
|
||
|
||
// 显示对话框
|
||
this.editDeviceModal.style.display = 'block';
|
||
|
||
} catch (error) {
|
||
console.error('加载设备详情失败:', error);
|
||
alert('加载设备详情失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 显示修改凭据对话框
|
||
async showEditCredentialModal(credentialId) {
|
||
try {
|
||
// 获取凭据详情
|
||
const response = await fetch(`/api/credentials/${credentialId}`);
|
||
if (!response.ok) {
|
||
throw new Error('获取凭据详情失败');
|
||
}
|
||
|
||
const credential = await response.json();
|
||
|
||
// 设置表单值
|
||
document.getElementById('edit-credential-id').value = credential.id;
|
||
document.getElementById('edit-credential-name').value = credential.name;
|
||
document.getElementById('edit-credential-username').value = credential.username;
|
||
document.getElementById('edit-credential-password').value = ''; // 不显示原密码
|
||
document.getElementById('edit-credential-protocol').value = credential.protocol;
|
||
document.getElementById('edit-credential-port').value = credential.port;
|
||
document.getElementById('edit-credential-enable').value = ''; // 不显示原Enable密码
|
||
|
||
// 显示对话框
|
||
this.editCredentialModal.style.display = 'block';
|
||
|
||
} catch (error) {
|
||
console.error('加载凭据详情失败:', error);
|
||
alert('加载凭据详情失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 处理修改设备
|
||
async handleEditDevice(e) {
|
||
e.preventDefault();
|
||
|
||
const deviceId = document.getElementById('edit-device-id').value;
|
||
const deviceName = document.getElementById('edit-device-name').value;
|
||
const deviceIp = document.getElementById('edit-device-ip').value;
|
||
const deviceType = document.getElementById('edit-device-type').value;
|
||
const deviceVendor = document.getElementById('edit-device-vendor').value;
|
||
const devicePlatform = document.getElementById('edit-device-platform').value;
|
||
const deviceModel = document.getElementById('edit-device-model').value;
|
||
const deviceOs = document.getElementById('edit-device-os').value;
|
||
const deviceStatus = document.getElementById('edit-device-status').value;
|
||
const deviceLocation = document.getElementById('edit-device-location').value;
|
||
const deviceDescription = document.getElementById('edit-device-description').value;
|
||
const deviceTags = document.getElementById('edit-device-tags').value.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||
const deviceCredential = document.getElementById('edit-device-credential').value || null;
|
||
|
||
// 验证必填字段
|
||
if (!deviceName || !deviceIp) {
|
||
alert('请填写设备名称和IP地址');
|
||
return;
|
||
}
|
||
|
||
// 准备设备数据
|
||
const deviceData = {
|
||
name: deviceName,
|
||
ip_address: deviceIp,
|
||
device_type: deviceType,
|
||
vendor: deviceVendor,
|
||
platform: devicePlatform,
|
||
model: deviceModel,
|
||
os_version: deviceOs,
|
||
status: deviceStatus,
|
||
location: deviceLocation,
|
||
description: deviceDescription,
|
||
tags: deviceTags,
|
||
credential_id: deviceCredential
|
||
};
|
||
|
||
try {
|
||
// 发送API请求修改设备
|
||
const response = await fetch(`/api/devices/${deviceId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(deviceData)
|
||
});
|
||
|
||
// 处理错误情况
|
||
if (!response.ok) {
|
||
const responseText = await response.text();
|
||
let errorDetail = responseText;
|
||
try {
|
||
const errorJson = JSON.parse(responseText);
|
||
if (errorJson.detail) {
|
||
errorDetail = errorJson.detail;
|
||
}
|
||
} catch (e) {}
|
||
throw new Error(`修改设备失败 (${response.status}): ${errorDetail}`);
|
||
}
|
||
|
||
// 解析成功的响应
|
||
const data = await response.json();
|
||
|
||
// 更新UI
|
||
this.addDeviceToUI(data);
|
||
|
||
// 关闭对话框
|
||
this.editDeviceModal.style.display = 'none';
|
||
|
||
// 提示成功
|
||
alert('设备修改成功');
|
||
|
||
} catch (error) {
|
||
console.error('修改设备失败:', error);
|
||
alert(`修改设备失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 处理修改凭据
|
||
async handleEditCredential(e) {
|
||
e.preventDefault();
|
||
|
||
const credentialId = document.getElementById('edit-credential-id').value;
|
||
const credentialName = document.getElementById('edit-credential-name').value;
|
||
const credentialUsername = document.getElementById('edit-credential-username').value;
|
||
const credentialPassword = document.getElementById('edit-credential-password').value;
|
||
const credentialProtocol = document.getElementById('edit-credential-protocol').value;
|
||
const credentialPortInput = document.getElementById('edit-credential-port');
|
||
const credentialPort = credentialPortInput.value === '' ? null : parseInt(credentialPortInput.value);
|
||
const credentialEnable = document.getElementById('edit-credential-enable').value;
|
||
|
||
// 验证必填字段
|
||
if (!credentialName || !credentialUsername) {
|
||
alert('请填写凭据名称和用户名');
|
||
return;
|
||
}
|
||
|
||
// 准备凭据数据
|
||
const credentialData = {
|
||
name: credentialName,
|
||
username: credentialUsername,
|
||
protocol: credentialProtocol,
|
||
port: credentialPort
|
||
};
|
||
|
||
// 只有在用户输入了新密码时才包含密码字段
|
||
if (credentialPassword) {
|
||
credentialData.password = credentialPassword;
|
||
}
|
||
|
||
// 只有在用户输入了新Enable密码时才包含Enable密码字段
|
||
if (credentialEnable) {
|
||
credentialData.enable_password = credentialEnable;
|
||
}
|
||
|
||
try {
|
||
// 发送API请求修改凭据
|
||
const response = await fetch(`/api/credentials/${credentialId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(credentialData)
|
||
});
|
||
|
||
// 处理错误情况
|
||
if (!response.ok) {
|
||
const responseText = await response.text();
|
||
let errorDetail = responseText;
|
||
try {
|
||
const errorJson = JSON.parse(responseText);
|
||
if (errorJson.detail) {
|
||
errorDetail = errorJson.detail;
|
||
}
|
||
} catch (e) {}
|
||
throw new Error(`修改凭据失败 (${response.status}): ${errorDetail}`);
|
||
}
|
||
|
||
// 解析成功的响应
|
||
const data = await response.json();
|
||
|
||
// 更新UI
|
||
this.addCredentialToUI(data);
|
||
|
||
// 关闭对话框
|
||
this.editCredentialModal.style.display = 'none';
|
||
|
||
// 提示成功
|
||
alert('凭据修改成功');
|
||
|
||
} catch (error) {
|
||
console.error('修改凭据失败:', error);
|
||
alert(`修改凭据失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 处理删除设备
|
||
async handleDeleteDevice(deviceId) {
|
||
if (!confirm('确定要删除此设备吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 发送API请求删除设备
|
||
const response = await fetch(`/api/devices/${deviceId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('删除设备失败');
|
||
}
|
||
|
||
// 删除成功,从UI中移除
|
||
const deviceElement = document.getElementById(`device-${deviceId}`);
|
||
if (deviceElement) {
|
||
deviceElement.remove();
|
||
}
|
||
|
||
// 如果列表为空,显示提示
|
||
if (this.deviceList.children.length === 0) {
|
||
this.deviceList.innerHTML = `
|
||
<div class="device-list-empty">
|
||
<p>尚未添加任何设备</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('删除设备失败:', error);
|
||
alert('删除设备失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 处理删除凭据
|
||
async handleDeleteCredential(credentialId) {
|
||
if (!confirm('确定要删除此凭据吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 发送API请求删除凭据
|
||
const response = await fetch(`/api/credentials/${credentialId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('删除凭据失败');
|
||
}
|
||
|
||
// 删除成功,从UI中移除
|
||
const credentialElement = document.getElementById(`credential-${credentialId}`);
|
||
if (credentialElement) {
|
||
credentialElement.remove();
|
||
}
|
||
|
||
// 如果列表为空,显示提示
|
||
if (this.credentialList.children.length === 0) {
|
||
this.credentialList.innerHTML = `
|
||
<div class="credential-list-empty">
|
||
<p>尚未添加任何凭据</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('删除凭据失败:', error);
|
||
alert('删除凭据失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 为选择器加载凭据列表
|
||
async loadCredentialsForSelect(selectId, selectedCredentialId = null) {
|
||
try {
|
||
const response = await fetch('/api/credentials');
|
||
const credentials = await response.json();
|
||
|
||
const selectElement = document.getElementById(selectId);
|
||
if (!selectElement) {
|
||
console.error(`未找到选择器元素: ${selectId}`);
|
||
return;
|
||
}
|
||
|
||
// 清空选择器
|
||
selectElement.innerHTML = '<option value="">-- 无凭据 --</option>';
|
||
|
||
// 填充凭据选项
|
||
credentials.forEach(credential => {
|
||
const option = document.createElement('option');
|
||
option.value = credential.id;
|
||
option.textContent = `${credential.name} (${credential.username}@${credential.protocol.toUpperCase()})`;
|
||
|
||
// 设置选中状态
|
||
if (selectedCredentialId && credential.id === selectedCredentialId) {
|
||
option.selected = true;
|
||
}
|
||
|
||
selectElement.appendChild(option);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('加载凭据选择器失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================================
|
||
// 全局主题管理系统
|
||
// ================================
|
||
|
||
// 初始化全局主题系统
|
||
function initializeGlobalTheme() {
|
||
console.log('初始化全局主题系统...');
|
||
|
||
// 获取主题切换器
|
||
const loginThemeToggle = document.getElementById('login-theme-toggle');
|
||
const dashboardThemeToggle = document.getElementById('theme-toggle');
|
||
|
||
// 获取当前主题
|
||
const currentTheme = localStorage.getItem('theme') || 'light';
|
||
|
||
// 应用主题
|
||
applyTheme(currentTheme);
|
||
|
||
// 设置切换器状态
|
||
if (loginThemeToggle) {
|
||
loginThemeToggle.checked = currentTheme === 'dark';
|
||
// 绑定登录页面主题切换事件
|
||
loginThemeToggle.addEventListener('change', function() {
|
||
const newTheme = this.checked ? 'dark' : 'light';
|
||
changeTheme(newTheme);
|
||
});
|
||
}
|
||
|
||
if (dashboardThemeToggle) {
|
||
dashboardThemeToggle.checked = currentTheme === 'dark';
|
||
// 绑定仪表盘主题切换事件
|
||
dashboardThemeToggle.addEventListener('change', function() {
|
||
const newTheme = this.checked ? 'dark' : 'light';
|
||
changeTheme(newTheme);
|
||
});
|
||
}
|
||
|
||
console.log(`主题系统初始化完成,当前主题:${currentTheme}`);
|
||
}
|
||
|
||
// 应用主题
|
||
function applyTheme(theme) {
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
|
||
// 根据主题切换logo图片
|
||
const loginLogo = document.getElementById('login-logo');
|
||
const headerLogo = document.getElementById('header-logo');
|
||
|
||
if (theme === 'dark') {
|
||
// 暗色主题使用logod.png
|
||
if (loginLogo) {
|
||
loginLogo.src = loginLogo.src.replace('logo.png', 'logod.png');
|
||
}
|
||
if (headerLogo) {
|
||
headerLogo.src = headerLogo.src.replace('logo.png', 'logod.png');
|
||
}
|
||
} else {
|
||
// 明亮主题使用logo.png
|
||
if (loginLogo) {
|
||
loginLogo.src = loginLogo.src.replace('logod.png', 'logo.png');
|
||
}
|
||
if (headerLogo) {
|
||
headerLogo.src = headerLogo.src.replace('logod.png', 'logo.png');
|
||
}
|
||
}
|
||
|
||
// 同步所有主题切换器状态
|
||
const loginThemeToggle = document.getElementById('login-theme-toggle');
|
||
const dashboardThemeToggle = document.getElementById('theme-toggle');
|
||
|
||
if (loginThemeToggle) {
|
||
loginThemeToggle.checked = theme === 'dark';
|
||
}
|
||
|
||
if (dashboardThemeToggle) {
|
||
dashboardThemeToggle.checked = theme === 'dark';
|
||
}
|
||
}
|
||
|
||
// 切换主题
|
||
function changeTheme(newTheme) {
|
||
// 应用新主题
|
||
applyTheme(newTheme);
|
||
|
||
// 保存到本地存储
|
||
localStorage.setItem('theme', newTheme);
|
||
|
||
// 添加活动记录(如果仪表盘已初始化)
|
||
if (typeof addActivity === 'function') {
|
||
addActivity(`切换到${newTheme === 'dark' ? '暗色' : '明亮'}主题`, 'info');
|
||
}
|
||
|
||
console.log(`主题已切换到:${newTheme}`);
|
||
}
|
||
|
||
// 页面路由管理
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 初始化主题系统
|
||
initializeGlobalTheme();
|
||
|
||
// 页面加载完成后初始化
|
||
console.log('初始化会话管理器...');
|
||
if (!window.sessionManager) {
|
||
window.sessionManager = new SessionManager();
|
||
console.log('会话管理器初始化完成');
|
||
}
|
||
|
||
console.log('初始化会话管理UI...');
|
||
if (!window.sessionManagerUI) {
|
||
window.sessionManagerUI = new SessionManagerUI();
|
||
console.log('会话管理UI初始化完成');
|
||
}
|
||
|
||
console.log('初始化终端管理器...');
|
||
if (!window.terminalManager) {
|
||
window.terminalManager = new TerminalManager();
|
||
console.log('终端管理器初始化完成');
|
||
}
|
||
|
||
console.log('初始化设备管理UI...');
|
||
if (!window.deviceManagerUI) {
|
||
window.deviceManagerUI = new DeviceManagerUI();
|
||
console.log('设备管理UI初始化完成');
|
||
}
|
||
|
||
// 预加载设备和凭据列表到缓存
|
||
function preloadDevicesAndCredentials() {
|
||
console.log('预加载设备和凭据列表...');
|
||
|
||
// 加载设备列表
|
||
fetch('/api/devices')
|
||
.then(response => response.ok ? response.json() : [])
|
||
.then(devices => {
|
||
console.log('预加载设备列表成功:', devices);
|
||
window.cachedDevices = devices;
|
||
})
|
||
.catch(error => {
|
||
console.error('预加载设备列表失败:', error);
|
||
});
|
||
|
||
// 加载凭据列表
|
||
fetch('/api/credentials')
|
||
.then(response => response.ok ? response.json() : [])
|
||
.then(credentials => {
|
||
console.log('预加载凭据列表成功:', credentials);
|
||
window.cachedCredentials = credentials;
|
||
})
|
||
.catch(error => {
|
||
console.error('预加载凭据列表失败:', error);
|
||
});
|
||
}
|
||
|
||
// 页面加载时预加载数据
|
||
preloadDevicesAndCredentials();
|
||
|
||
// 添加备用的拓扑发现按钮事件监听器
|
||
function ensureTopologyInitialized() {
|
||
if (!window.topologyVisualization && window.TopologyVisualization) {
|
||
console.log('备用初始化:创建拓扑可视化实例');
|
||
try {
|
||
window.topologyVisualization = new window.TopologyVisualization();
|
||
console.log('备用拓扑可视化模块初始化成功');
|
||
} catch (error) {
|
||
console.error('备用拓扑可视化模块初始化失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 为拓扑发现按钮添加备用事件监听器
|
||
const discoverTopologyBtn = document.getElementById('discover-topology-btn');
|
||
if (discoverTopologyBtn) {
|
||
discoverTopologyBtn.addEventListener('click', function(e) {
|
||
console.log('发现拓扑按钮被点击');
|
||
|
||
// 确保拓扑模块已初始化
|
||
ensureTopologyInitialized();
|
||
|
||
// 如果拓扑模块仍然没有初始化,显示错误提示
|
||
if (!window.topologyVisualization) {
|
||
console.error('拓扑可视化模块未能初始化');
|
||
alert('拓扑可视化模块加载失败,请刷新页面重试');
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
return false;
|
||
}
|
||
|
||
console.log('拓扑模块已准备就绪');
|
||
});
|
||
}
|
||
|
||
// 按钮点击事件 - 从登录页面到主页(默认显示仪表盘)
|
||
document.getElementById('start-btn').addEventListener('click', function() {
|
||
document.getElementById('login-page').style.display = 'none';
|
||
document.getElementById('home-page').style.display = 'block';
|
||
|
||
// 默认显示仪表盘页面
|
||
document.querySelectorAll('.page-content').forEach(content => {
|
||
content.style.display = 'none';
|
||
});
|
||
document.getElementById('dashboard-content').style.display = 'block';
|
||
|
||
// 初始化仪表盘
|
||
if (typeof initializeDashboard === 'function') {
|
||
initializeDashboard();
|
||
}
|
||
|
||
window.history.pushState({page: 'dashboard'}, 'Dashboard', '/dashboard');
|
||
});
|
||
|
||
// 导航链接点击处理
|
||
const navLinks = document.querySelectorAll('.nav-link');
|
||
const pageContents = document.querySelectorAll('.page-content');
|
||
|
||
navLinks.forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
|
||
// 移除所有活动状态
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
// 添加当前活动状态
|
||
this.classList.add('active');
|
||
|
||
// 隐藏所有内容
|
||
pageContents.forEach(content => {
|
||
content.style.display = 'none';
|
||
});
|
||
|
||
// 显示选中的内容
|
||
const pageId = this.getAttribute('data-page');
|
||
document.getElementById(`${pageId}-content`).style.display = 'block';
|
||
|
||
// 更新URL
|
||
window.history.pushState({page: pageId}, pageId, `#${pageId}`);
|
||
|
||
// 特殊页面处理
|
||
if (pageId === 'terminal') {
|
||
// 如果是终端页面,重新适应终端大小
|
||
const activeTerminal = window.terminalManager.getTerminalById(window.terminalManager.activeTerminalId);
|
||
if (activeTerminal && activeTerminal.fitAddon) {
|
||
setTimeout(() => activeTerminal.fitAddon.fit(), 10);
|
||
}
|
||
} else if (pageId === 'topology') {
|
||
// 如果是拓扑页面,初始化拓扑可视化
|
||
console.log('切换到拓扑页面,初始化拓扑可视化模块...');
|
||
if (!window.topologyVisualization) {
|
||
try {
|
||
window.topologyVisualization = new TopologyVisualization();
|
||
console.log('拓扑可视化模块初始化成功');
|
||
} catch (error) {
|
||
console.error('拓扑可视化模块初始化失败:', error);
|
||
}
|
||
} else {
|
||
console.log('拓扑可视化模块已存在,跳过初始化');
|
||
// 如果已经初始化,可以触发窗口大小调整
|
||
if (window.topologyVisualization.handleResize) {
|
||
setTimeout(() => window.topologyVisualization.handleResize(), 10);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 处理浏览器的后退/前进按钮
|
||
window.addEventListener('popstate', function(event) {
|
||
if (event.state && event.state.page) {
|
||
if (event.state.page === 'home') {
|
||
document.getElementById('login-page').style.display = 'none';
|
||
document.getElementById('home-page').style.display = 'block';
|
||
} else if (event.state.page === 'login') {
|
||
document.getElementById('login-page').style.display = 'block';
|
||
document.getElementById('home-page').style.display = 'none';
|
||
} else {
|
||
// 处理内部页面导航
|
||
navLinks.forEach(link => {
|
||
if (link.getAttribute('data-page') === event.state.page) {
|
||
link.click();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// 初始化页面状态
|
||
const initialPath = window.location.pathname;
|
||
if (initialPath === '/home') {
|
||
document.getElementById('login-page').style.display = 'none';
|
||
document.getElementById('home-page').style.display = 'block';
|
||
window.history.replaceState({page: 'home'}, 'Home', '/home');
|
||
} else {
|
||
window.history.replaceState({page: 'login'}, 'Login', '/');
|
||
}
|
||
|
||
// 处理锚点导航
|
||
const hash = window.location.hash.substring(1);
|
||
if (hash) {
|
||
const targetLink = document.querySelector(`.nav-link[data-page="${hash}"]`);
|
||
if (targetLink) {
|
||
targetLink.click();
|
||
}
|
||
}
|
||
|
||
// 设备连接表单处理
|
||
document.getElementById('connect-device-form').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
|
||
const deviceId = document.getElementById('device-select').value;
|
||
const credentialId = document.getElementById('credential-select').value;
|
||
let terminalName = document.getElementById('terminal-name').value;
|
||
|
||
if (!deviceId || !credentialId) {
|
||
alert('请选择设备和凭据');
|
||
return;
|
||
}
|
||
|
||
// 如果没有提供名称,使用默认格式
|
||
if (!terminalName) {
|
||
const deviceOption = document.getElementById('device-select').selectedOptions[0];
|
||
terminalName = deviceOption.textContent.split(' ')[0] + '-会话';
|
||
}
|
||
|
||
// 创建新的终端并连接设备
|
||
const termInfo = window.terminalManager.createTerminal(terminalName, deviceId, credentialId);
|
||
|
||
// 创建会话记录
|
||
window.sessionManager.connectToDevice(deviceId, credentialId, terminalName);
|
||
|
||
// 关闭对话框
|
||
document.getElementById('device-connect-modal').style.display = 'none';
|
||
|
||
// 清空表单
|
||
this.reset();
|
||
|
||
// 自动跳转到终端页面
|
||
const terminalLink = document.querySelector('.nav-link[data-page="terminal"]');
|
||
if (terminalLink) {
|
||
terminalLink.click();
|
||
}
|
||
});
|
||
|
||
// 通用模态框关闭处理
|
||
document.querySelectorAll('.close-modal, .close-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const modal = this.closest('.modal');
|
||
if (modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// 点击对话框外部关闭
|
||
window.addEventListener('click', function(e) {
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
if (e.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// ================================
|
||
// 仪表盘相关功能
|
||
// ================================
|
||
|
||
// 初始化仪表盘
|
||
function initializeDashboard() {
|
||
console.log('初始化仪表盘...');
|
||
|
||
// 初始化主题切换器
|
||
initializeThemeSwitcher();
|
||
|
||
// 更新当前时间
|
||
updateCurrentTime();
|
||
setInterval(updateCurrentTime, 1000);
|
||
|
||
// 加载仪表盘数据
|
||
loadDashboardData();
|
||
|
||
// 绑定仪表盘事件
|
||
bindDashboardEvents();
|
||
|
||
// 添加活动记录
|
||
addActivity('系统启动成功', 'success');
|
||
}
|
||
|
||
// 主题切换器初始化
|
||
function initializeThemeSwitcher() {
|
||
// 主题切换器现在由全局主题系统管理
|
||
// 这个函数保留用于兼容性,实际功能在initializeGlobalTheme中实现
|
||
console.log('仪表盘主题切换器由全局主题系统管理');
|
||
}
|
||
|
||
// 更新当前时间
|
||
function 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 function loadDashboardData() {
|
||
try {
|
||
// 并行加载各种数据
|
||
const [devices, topology, sessions] = await Promise.all([
|
||
fetch('/api/devices').then(r => r.ok ? r.json() : []).catch(() => []),
|
||
fetch('/api/topology').then(r => r.ok ? r.json() : {}).catch(() => ({})),
|
||
// 假设有会话API,暂时返回空数组
|
||
Promise.resolve([])
|
||
]);
|
||
|
||
// 更新统计数据
|
||
updateDashboardStats(devices, topology, sessions);
|
||
|
||
// 更新设备概览
|
||
updateDevicesOverview(devices);
|
||
|
||
// 检查网络状态
|
||
checkNetworkStatus();
|
||
|
||
} catch (error) {
|
||
console.error('加载仪表盘数据失败:', error);
|
||
addActivity('加载仪表盘数据失败', 'error');
|
||
}
|
||
}
|
||
|
||
// 更新仪表盘统计数据
|
||
function updateDashboardStats(devices, topology, sessions) {
|
||
// 设备数量
|
||
const devicesCount = devices.length;
|
||
document.getElementById('dashboard-devices-count').textContent = devicesCount;
|
||
document.getElementById('dashboard-devices-change').textContent = `+${devicesCount}`;
|
||
|
||
// 活跃连接数(暂时设为0)
|
||
document.getElementById('dashboard-connections-count').textContent = '0';
|
||
document.getElementById('dashboard-connections-change').textContent = '0';
|
||
|
||
// 拓扑节点数
|
||
const topologyNodes = topology.nodes ? Object.keys(topology.nodes).length : 0;
|
||
document.getElementById('dashboard-topology-nodes').textContent = topologyNodes;
|
||
document.getElementById('dashboard-topology-change').textContent = `+${topologyNodes}`;
|
||
|
||
// 终端会话数
|
||
const sessionsCount = sessions.length;
|
||
document.getElementById('dashboard-sessions-count').textContent = sessionsCount;
|
||
document.getElementById('dashboard-sessions-change').textContent = `+${sessionsCount}`;
|
||
}
|
||
|
||
// 更新设备概览
|
||
function updateDevicesOverview(devices) {
|
||
const devicesOverview = document.getElementById('devices-overview');
|
||
|
||
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="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">
|
||
<div class="device-overview-icon">
|
||
<i class="fas fa-server"></i>
|
||
</div>
|
||
<div class="device-overview-info">
|
||
<div class="device-overview-name">${device.name}</div>
|
||
<div class="device-overview-details">${device.ip_address} • ${device.vendor}</div>
|
||
</div>
|
||
<div class="device-overview-status ${device.status || 'unknown'}"></div>
|
||
</div>
|
||
`).join('');
|
||
|
||
devicesOverview.innerHTML = deviceItems;
|
||
|
||
if (devices.length > 5) {
|
||
devicesOverview.innerHTML += `
|
||
<div class="device-overview-item" onclick="goToDevices()" style="cursor: pointer;">
|
||
<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>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 检查网络状态
|
||
async function checkNetworkStatus() {
|
||
try {
|
||
const response = await fetch('/api/devices', { method: 'HEAD' });
|
||
const statusIcon = document.getElementById('network-status');
|
||
const statusText = document.getElementById('network-status-text');
|
||
|
||
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) {
|
||
const statusIcon = document.getElementById('network-status');
|
||
const statusText = document.getElementById('network-status-text');
|
||
statusIcon.className = 'status-icon offline';
|
||
statusIcon.innerHTML = '<i class="fas fa-times-circle"></i>';
|
||
statusText.textContent = '连接失败';
|
||
}
|
||
}
|
||
|
||
// 绑定仪表盘事件
|
||
function bindDashboardEvents() {
|
||
// 刷新仪表盘按钮
|
||
const refreshBtn = document.getElementById('refresh-dashboard');
|
||
if (refreshBtn) {
|
||
refreshBtn.addEventListener('click', function() {
|
||
loadDashboardData();
|
||
addActivity('刷新仪表盘数据', 'info');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加活动记录
|
||
function addActivity(text, type = 'info') {
|
||
const activitiesList = document.getElementById('recent-activities');
|
||
if (!activitiesList) return;
|
||
|
||
const activityItem = document.createElement('div');
|
||
activityItem.className = 'activity-item';
|
||
activityItem.innerHTML = `
|
||
<div class="activity-icon ${type}">
|
||
<i class="fas fa-${getActivityIcon(type)}"></i>
|
||
</div>
|
||
<div class="activity-content">
|
||
<div class="activity-text">${text}</div>
|
||
<div class="activity-time">刚刚</div>
|
||
</div>
|
||
`;
|
||
|
||
// 插入到列表顶部
|
||
activitiesList.insertBefore(activityItem, activitiesList.firstChild);
|
||
|
||
// 限制活动数量(最多保留10条)
|
||
const activities = activitiesList.querySelectorAll('.activity-item');
|
||
if (activities.length > 10) {
|
||
activitiesList.removeChild(activities[activities.length - 1]);
|
||
}
|
||
}
|
||
|
||
// 获取活动图标
|
||
function getActivityIcon(type) {
|
||
const icons = {
|
||
info: 'info-circle',
|
||
success: 'check-circle',
|
||
warning: 'exclamation-triangle',
|
||
error: 'times-circle'
|
||
};
|
||
return icons[type] || 'info-circle';
|
||
}
|
||
|
||
// 仪表盘快速操作功能
|
||
function goToDevices() {
|
||
const devicesLink = document.querySelector('.nav-link[data-page="devices"]');
|
||
if (devicesLink) {
|
||
devicesLink.click();
|
||
addActivity('跳转到设备管理页面', 'info');
|
||
}
|
||
}
|
||
|
||
function openTerminal() {
|
||
const terminalLink = document.querySelector('.nav-link[data-page="terminal"]');
|
||
if (terminalLink) {
|
||
terminalLink.click();
|
||
addActivity('打开终端页面', 'info');
|
||
}
|
||
}
|
||
|
||
function discoverTopology() {
|
||
const topologyLink = document.querySelector('.nav-link[data-page="topology"]');
|
||
if (topologyLink) {
|
||
topologyLink.click();
|
||
// 延迟触发拓扑发现
|
||
setTimeout(() => {
|
||
const discoverBtn = document.getElementById('discover-topology-btn');
|
||
if (discoverBtn) {
|
||
discoverBtn.click();
|
||
addActivity('启动拓扑发现', 'info');
|
||
}
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
function scanNetwork() {
|
||
// 暂时跳转到设备页面
|
||
goToDevices();
|
||
addActivity('网络扫描功能开发中', 'warning');
|
||
}
|
||
|
||
function refreshSystemStatus() {
|
||
checkNetworkStatus();
|
||
addActivity('刷新系统状态', 'info');
|
||
}
|
||
|
||
function refreshDevicesOverview() {
|
||
loadDashboardData();
|
||
addActivity('刷新设备概览', 'info');
|
||
}
|
||
|
||
function clearActivities() {
|
||
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>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 暴露initializeDashboard函数到全局作用域
|
||
window.initializeDashboard = initializeDashboard;
|
||
</script>
|
||
</body>
|
||
</html> |