feat: 添加项目规则、环境配置示例及开发文档
This commit is contained in:
377
src/minenasai/webtui/static/css/style.css
Normal file
377
src/minenasai/webtui/static/css/style.css
Normal file
@@ -0,0 +1,377 @@
|
||||
/* MineNASAI Web Terminal Styles */
|
||||
|
||||
:root {
|
||||
--bg-primary: #1a1b26;
|
||||
--bg-secondary: #24283b;
|
||||
--bg-tertiary: #1f2335;
|
||||
--text-primary: #c0caf5;
|
||||
--text-secondary: #565f89;
|
||||
--accent-primary: #7aa2f7;
|
||||
--accent-success: #9ece6a;
|
||||
--accent-warning: #e0af68;
|
||||
--accent-error: #f7768e;
|
||||
--border-color: #3b4261;
|
||||
--header-height: 48px;
|
||||
--footer-height: 24px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
height: var(--header-height);
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-error);
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: var(--accent-success);
|
||||
}
|
||||
|
||||
.status-dot.connecting {
|
||||
background-color: var(--accent-warning);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent-primary);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #89b4fa;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
height: 36px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.terminal-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.terminal-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 4px 4px 0 0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.terminal-tab.active {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
color: var(--accent-error);
|
||||
}
|
||||
|
||||
.btn-new-tab {
|
||||
background: none;
|
||||
border: 1px dashed var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-new-tab:hover {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
height: var(--footer-height);
|
||||
background-color: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-small {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--accent-error);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.form-group input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Fullscreen */
|
||||
.fullscreen .header,
|
||||
.fullscreen .footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fullscreen .terminal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header-center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
128
src/minenasai/webtui/static/index.html
Normal file
128
src/minenasai/webtui/static/index.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MineNASAI - Web Terminal</title>
|
||||
<!-- xterm.js -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<h1 class="logo">MineNASAI</h1>
|
||||
<span class="version">v0.1.0</span>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<span class="connection-status" id="connectionStatus">
|
||||
<span class="status-dot disconnected"></span>
|
||||
<span class="status-text">未连接</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-icon" id="settingsBtn" title="设置">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-primary" id="connectBtn">连接</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<!-- 终端容器 -->
|
||||
<div class="terminal-container">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-tabs">
|
||||
<div class="terminal-tab active" data-tab="main">
|
||||
<span>主终端</span>
|
||||
<button class="tab-close" title="关闭">×</button>
|
||||
</div>
|
||||
<button class="btn-new-tab" id="newTabBtn" title="新建终端">+</button>
|
||||
</div>
|
||||
<div class="terminal-actions">
|
||||
<button class="btn btn-small" id="clearBtn" title="清屏">清屏</button>
|
||||
<button class="btn btn-small" id="fullscreenBtn" title="全屏">全屏</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal" class="terminal"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<footer class="footer">
|
||||
<div class="footer-left">
|
||||
<span id="sessionInfo">会话: -</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span id="terminalSize">-</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<div class="modal" id="settingsModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>设置</h2>
|
||||
<button class="modal-close" id="closeSettingsBtn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>字体大小</label>
|
||||
<input type="number" id="fontSizeInput" value="14" min="10" max="24">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>主题</label>
|
||||
<select id="themeSelect">
|
||||
<option value="dark">深色</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="monokai">Monokai</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="cursorBlinkCheck" checked>
|
||||
光标闪烁
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" id="resetSettingsBtn">重置</button>
|
||||
<button class="btn btn-primary" id="saveSettingsBtn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录模态框 -->
|
||||
<div class="modal" id="loginModal">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h2>连接到终端</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Token</label>
|
||||
<input type="password" id="tokenInput" placeholder="输入访问令牌">
|
||||
</div>
|
||||
<p class="hint">提示:令牌可从通讯工具消息中获取</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" id="cancelLoginBtn">取消</button>
|
||||
<button class="btn btn-primary" id="doConnectBtn">连接</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
393
src/minenasai/webtui/static/js/app.js
Normal file
393
src/minenasai/webtui/static/js/app.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* MineNASAI Web Terminal
|
||||
*/
|
||||
|
||||
class WebTerminal {
|
||||
constructor() {
|
||||
this.terminal = null;
|
||||
this.fitAddon = null;
|
||||
this.socket = null;
|
||||
this.sessionId = null;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
|
||||
this.settings = this.loadSettings();
|
||||
this.init();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const defaults = {
|
||||
fontSize: 14,
|
||||
theme: 'dark',
|
||||
cursorBlink: true
|
||||
};
|
||||
try {
|
||||
const saved = localStorage.getItem('terminal_settings');
|
||||
return saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
localStorage.setItem('terminal_settings', JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
getTheme(name) {
|
||||
const themes = {
|
||||
dark: {
|
||||
background: '#1a1b26',
|
||||
foreground: '#c0caf5',
|
||||
cursor: '#c0caf5',
|
||||
cursorAccent: '#1a1b26',
|
||||
selection: 'rgba(122, 162, 247, 0.3)',
|
||||
black: '#15161e',
|
||||
red: '#f7768e',
|
||||
green: '#9ece6a',
|
||||
yellow: '#e0af68',
|
||||
blue: '#7aa2f7',
|
||||
magenta: '#bb9af7',
|
||||
cyan: '#7dcfff',
|
||||
white: '#a9b1d6',
|
||||
brightBlack: '#414868',
|
||||
brightRed: '#f7768e',
|
||||
brightGreen: '#9ece6a',
|
||||
brightYellow: '#e0af68',
|
||||
brightBlue: '#7aa2f7',
|
||||
brightMagenta: '#bb9af7',
|
||||
brightCyan: '#7dcfff',
|
||||
brightWhite: '#c0caf5'
|
||||
},
|
||||
light: {
|
||||
background: '#f5f5f5',
|
||||
foreground: '#1a1b26',
|
||||
cursor: '#1a1b26',
|
||||
cursorAccent: '#f5f5f5',
|
||||
selection: 'rgba(122, 162, 247, 0.3)'
|
||||
},
|
||||
monokai: {
|
||||
background: '#272822',
|
||||
foreground: '#f8f8f2',
|
||||
cursor: '#f8f8f2',
|
||||
cursorAccent: '#272822',
|
||||
selection: 'rgba(73, 72, 62, 0.5)',
|
||||
black: '#272822',
|
||||
red: '#f92672',
|
||||
green: '#a6e22e',
|
||||
yellow: '#f4bf75',
|
||||
blue: '#66d9ef',
|
||||
magenta: '#ae81ff',
|
||||
cyan: '#a1efe4',
|
||||
white: '#f8f8f2'
|
||||
}
|
||||
};
|
||||
return themes[name] || themes.dark;
|
||||
}
|
||||
|
||||
init() {
|
||||
// 初始化终端
|
||||
this.terminal = new Terminal({
|
||||
fontSize: this.settings.fontSize,
|
||||
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace',
|
||||
cursorBlink: this.settings.cursorBlink,
|
||||
cursorStyle: 'block',
|
||||
theme: this.getTheme(this.settings.theme),
|
||||
allowTransparency: true,
|
||||
scrollback: 10000
|
||||
});
|
||||
|
||||
// 添加插件
|
||||
this.fitAddon = new FitAddon.FitAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
this.terminal.loadAddon(new WebLinksAddon.WebLinksAddon());
|
||||
|
||||
// 挂载终端
|
||||
const container = document.getElementById('terminal');
|
||||
this.terminal.open(container);
|
||||
this.fitAddon.fit();
|
||||
|
||||
// 显示欢迎信息
|
||||
this.showWelcome();
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {
|
||||
this.fitAddon.fit();
|
||||
this.sendResize();
|
||||
});
|
||||
|
||||
// 更新设置UI
|
||||
this.updateSettingsUI();
|
||||
}
|
||||
|
||||
showWelcome() {
|
||||
const welcome = [
|
||||
'\x1b[1;34m',
|
||||
' __ __ _ _ _ _ ____ _ ___ ',
|
||||
' | \\/ (_)_ __ ___| \\ | | / \\ / ___| / \\ |_ _|',
|
||||
' | |\\/| | | \'_ \\ / _ \\ \\| | / _ \\ \\___ \\ / _ \\ | | ',
|
||||
' | | | | | | | | __/ |\\ |/ ___ \\ ___) / ___ \\ | | ',
|
||||
' |_| |_|_|_| |_|\\___|_| \\_/_/ \\_\\____/_/ \\_\\___|',
|
||||
'\x1b[0m',
|
||||
'',
|
||||
'\x1b[33mWeb Terminal v0.1.0\x1b[0m',
|
||||
'',
|
||||
'点击右上角 \x1b[1;32m连接\x1b[0m 按钮开始使用',
|
||||
''
|
||||
];
|
||||
welcome.forEach(line => this.terminal.writeln(line));
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 连接按钮
|
||||
document.getElementById('connectBtn').addEventListener('click', () => {
|
||||
if (this.isConnected) {
|
||||
this.disconnect();
|
||||
} else {
|
||||
this.showLoginModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 登录模态框
|
||||
document.getElementById('doConnectBtn').addEventListener('click', () => {
|
||||
this.connect();
|
||||
});
|
||||
|
||||
document.getElementById('cancelLoginBtn').addEventListener('click', () => {
|
||||
this.hideLoginModal();
|
||||
});
|
||||
|
||||
document.getElementById('tokenInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') this.connect();
|
||||
});
|
||||
|
||||
// 设置按钮
|
||||
document.getElementById('settingsBtn').addEventListener('click', () => {
|
||||
document.getElementById('settingsModal').classList.add('active');
|
||||
});
|
||||
|
||||
document.getElementById('closeSettingsBtn').addEventListener('click', () => {
|
||||
document.getElementById('settingsModal').classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById('saveSettingsBtn').addEventListener('click', () => {
|
||||
this.applySettings();
|
||||
document.getElementById('settingsModal').classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById('resetSettingsBtn').addEventListener('click', () => {
|
||||
this.settings = { fontSize: 14, theme: 'dark', cursorBlink: true };
|
||||
this.updateSettingsUI();
|
||||
this.applySettings();
|
||||
});
|
||||
|
||||
// 清屏
|
||||
document.getElementById('clearBtn').addEventListener('click', () => {
|
||||
this.terminal.clear();
|
||||
});
|
||||
|
||||
// 全屏
|
||||
document.getElementById('fullscreenBtn').addEventListener('click', () => {
|
||||
document.body.classList.toggle('fullscreen');
|
||||
setTimeout(() => this.fitAddon.fit(), 100);
|
||||
});
|
||||
|
||||
// 终端输入
|
||||
this.terminal.onData(data => {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify({
|
||||
type: 'input',
|
||||
data: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// ESC 退出全屏
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && document.body.classList.contains('fullscreen')) {
|
||||
document.body.classList.remove('fullscreen');
|
||||
setTimeout(() => this.fitAddon.fit(), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSettingsUI() {
|
||||
document.getElementById('fontSizeInput').value = this.settings.fontSize;
|
||||
document.getElementById('themeSelect').value = this.settings.theme;
|
||||
document.getElementById('cursorBlinkCheck').checked = this.settings.cursorBlink;
|
||||
}
|
||||
|
||||
applySettings() {
|
||||
this.settings.fontSize = parseInt(document.getElementById('fontSizeInput').value);
|
||||
this.settings.theme = document.getElementById('themeSelect').value;
|
||||
this.settings.cursorBlink = document.getElementById('cursorBlinkCheck').checked;
|
||||
|
||||
this.terminal.options.fontSize = this.settings.fontSize;
|
||||
this.terminal.options.cursorBlink = this.settings.cursorBlink;
|
||||
this.terminal.options.theme = this.getTheme(this.settings.theme);
|
||||
|
||||
this.saveSettings();
|
||||
this.fitAddon.fit();
|
||||
}
|
||||
|
||||
showLoginModal() {
|
||||
document.getElementById('loginModal').classList.add('active');
|
||||
document.getElementById('tokenInput').focus();
|
||||
}
|
||||
|
||||
hideLoginModal() {
|
||||
document.getElementById('loginModal').classList.remove('active');
|
||||
document.getElementById('tokenInput').value = '';
|
||||
}
|
||||
|
||||
connect() {
|
||||
const token = document.getElementById('tokenInput').value.trim();
|
||||
|
||||
this.hideLoginModal();
|
||||
this.updateStatus('connecting', '连接中...');
|
||||
this.terminal.writeln('\x1b[33m正在连接...\x1b[0m');
|
||||
|
||||
// 获取 WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/terminal`;
|
||||
|
||||
try {
|
||||
this.socket = new WebSocket(wsUrl);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
// 发送认证
|
||||
this.socket.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: token || 'anonymous'
|
||||
}));
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
this.handleMessage(msg);
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
this.isConnected = false;
|
||||
this.updateStatus('disconnected', '已断开');
|
||||
this.updateConnectButton(false);
|
||||
|
||||
if (event.code !== 1000) {
|
||||
this.terminal.writeln(`\x1b[31m连接已断开 (${event.code})\x1b[0m`);
|
||||
this.tryReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
this.terminal.writeln('\x1b[31m连接错误\x1b[0m');
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
this.terminal.writeln(`\x1b[31m连接失败: ${e.message}\x1b[0m`);
|
||||
this.updateStatus('disconnected', '连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(msg) {
|
||||
switch (msg.type) {
|
||||
case 'auth_ok':
|
||||
this.isConnected = true;
|
||||
this.sessionId = msg.session_id;
|
||||
this.reconnectAttempts = 0;
|
||||
this.updateStatus('connected', '已连接');
|
||||
this.updateConnectButton(true);
|
||||
this.terminal.writeln('\x1b[32m已连接到终端\x1b[0m\r\n');
|
||||
document.getElementById('sessionInfo').textContent = `会话: ${this.sessionId.slice(0, 8)}`;
|
||||
|
||||
// 发送终端尺寸
|
||||
this.sendResize();
|
||||
break;
|
||||
|
||||
case 'auth_error':
|
||||
this.terminal.writeln(`\x1b[31m认证失败: ${msg.message}\x1b[0m`);
|
||||
this.updateStatus('disconnected', '认证失败');
|
||||
this.socket.close();
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
this.terminal.write(msg.data);
|
||||
break;
|
||||
|
||||
case 'resize_ok':
|
||||
const cols = msg.cols;
|
||||
const rows = msg.rows;
|
||||
document.getElementById('terminalSize').textContent = `${cols}x${rows}`;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.terminal.writeln(`\x1b[31m错误: ${msg.message}\x1b[0m`);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
this.socket.send(JSON.stringify({ type: 'pong' }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendResize() {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
const dims = this.fitAddon.proposeDimensions();
|
||||
if (dims) {
|
||||
this.socket.send(JSON.stringify({
|
||||
type: 'resize',
|
||||
cols: dims.cols,
|
||||
rows: dims.rows
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close(1000, 'User disconnect');
|
||||
this.socket = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
this.terminal.writeln('\r\n\x1b[33m已断开连接\x1b[0m');
|
||||
this.updateStatus('disconnected', '已断开');
|
||||
this.updateConnectButton(false);
|
||||
}
|
||||
|
||||
tryReconnect() {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||
this.terminal.writeln(`\x1b[33m${delay/1000}秒后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})\x1b[0m`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!this.isConnected) {
|
||||
this.connect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(status, text) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
const dot = statusEl.querySelector('.status-dot');
|
||||
const textEl = statusEl.querySelector('.status-text');
|
||||
|
||||
dot.className = 'status-dot ' + status;
|
||||
textEl.textContent = text;
|
||||
}
|
||||
|
||||
updateConnectButton(connected) {
|
||||
const btn = document.getElementById('connectBtn');
|
||||
btn.textContent = connected ? '断开' : '连接';
|
||||
btn.classList.toggle('btn-primary', !connected);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.webTerminal = new WebTerminal();
|
||||
});
|
||||
Reference in New Issue
Block a user