Files
NetBrain_MCP/web/templates/index.html
T
2026-05-22 12:17:24 +08:00

3370 lines
151 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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默认为22Telnet默认为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">&times;</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">&times;</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默认为22Telnet默认为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>