Files
frpc-console/static/index.html
T

261 lines
13 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>frpc-console</title>
<!-- Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Naive UI CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/naive-ui@2/dist/styles.css" />
<!-- TOML 解析库(用于导入/导出) -->
<script src="https://cdn.jsdelivr.net/npm/@iarna/toml@3.0.0/dist/toml.min.js"></script>
<!-- 自定义样式 -->
<link rel="stylesheet" href="/static/style-1.css" />
<link rel="stylesheet" href="/static/style-2.css" />
<link rel="stylesheet" href="/static/style-3.css" />
</head>
<body>
<div id="app">
<!-- 背景层:始终存在的磨砂玻璃质感 -->
<div class="app-backdrop" :class="{ 'is-logged-in': loggedIn }"></div>
<div class="loading-ring" :class="{ visible: transitioning }"></div>
<!-- 主容器:登录态决定尺寸 -->
<div class="app-container" :class="{ 'is-logged-in': loggedIn }">
<!-- ====== 登录页 ====== -->
<div v-if="!loggedIn" class="login-card">
<div class="login-header">
<span class="login-icon">🚀</span>
<h1>console login</h1>
</div>
<div class="login-form">
<div class="input-group">
<label>user</label>
<input v-model="loginForm.username" type="text" placeholder="用户名" @keydown.enter="doLogin" />
</div>
<div class="input-group">
<label>passwd</label>
<input v-model="loginForm.password" type="password" placeholder="密码" @keydown.enter="doLogin" />
</div>
<button class="login-btn" @click="doLogin" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p v-if="loginError" class="login-error">{{ loginError }}</p>
</div>
</div>
<!-- ====== 主界面(登录后) ====== -->
<div v-else class="main-panel" :class="{ visible: contentVisible }">
<!-- 顶部导航 -->
<div class="top-bar">
<div class="top-left">
<span class="logo">🚀 frpc-console</span>
<span class="status-dot" :class="{ active: frpcRunning }"></span>
<span class="status-text">{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span>
</div>
<div class="top-right">
<span class="user-name">{{ loginForm.username }}</span>
<button class="logout-btn" @click="doLogout">退出</button>
</div>
</div>
<!-- Tab 切换 -->
<div class="tab-bar">
<span class="tab-item" :class="{ active: activeTab === 'proxies' }" @click="activeTab = 'proxies'">
隧道列表
</span>
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
全局配置信息
</span>
</div>
<!-- 内容区 -->
<div class="content-area">
<!-- 全局配置 -->
<div v-if="activeTab === 'config'" class="tab-content">
<div class="config-grid">
<div class="metric-card">
<div class="metric-label">TCP Multiplexing</div>
<div class="metric-value">
<span class="status-dot active"></span>
<span class="status-text active">已启用(固定)</span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">连接池大小</div>
<div class="metric-value">
<span class="big-number">{{ globalConfig.poolCount || 8 }}</span>
<span class="unit"></span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">心跳间隔 / 超时</div>
<div class="metric-value">
<span class="digit">{{ globalConfig.heartbeatInterval || 15 }}s</span>
<span class="sep">/</span>
<span class="digit">{{ globalConfig.heartbeatTimeout || 70 }}s</span>
</div>
</div>
</div>
<div class="detail-collapse" @click="showDetail = !showDetail">
<span>{{ showDetail ? '收起全部配置 ▲' : '展开全部配置 ▼' }}</span>
</div>
<div v-show="showDetail" class="detail-panel">
<div class="form-row">
<label>Server Address</label>
<input v-model="globalConfig.serverAddr" />
</div>
<div class="form-row">
<label>Server Port</label>
<input type="number" v-model="globalConfig.serverPort" />
</div>
<div class="form-row">
<label>Token</label>
<input type="password" v-model="globalConfig.token" />
</div>
<div class="form-row">
<label>Log Level</label>
<select v-model="globalConfig.logLevel">
<option value="trace">trace</option>
<option value="debug">debug</option>
<option value="info">info</option>
<option value="warn">warn</option>
<option value="error">error</option>
</select>
</div>
<div class="form-row">
<label>Log Max Days</label>
<input type="number" v-model="globalConfig.logMaxDays" />
</div>
<div class="form-row">
<label>TCP Mux Keepalive</label>
<input type="number" v-model="globalConfig.tcpMuxKeepalive" />
</div>
<div class="form-row">
<label>Heartbeat Interval</label>
<input type="number" v-model="globalConfig.heartbeatInterval" />
</div>
<div class="form-row">
<label>Heartbeat Timeout</label>
<input type="number" v-model="globalConfig.heartbeatTimeout" />
</div>
<div class="form-row">
<label>Pool Count</label>
<input type="number" v-model="globalConfig.poolCount" />
</div>
<button class="save-btn" @click="saveConfig">
保存配置并热加载
</button>
</div>
</div>
<!-- 隧道列表 -->
<div v-else-if="activeTab === 'proxies'" class="tab-content">
<div class="toolbar">
<div class="toolbar-left">
<button class="btn-import" @click="triggerImport">
导入 TOML
</button>
<button class="btn-export" @click="exportToml">
导出 TOML
</button>
<button class="btn-add" @click="openAddDialog">
+ 新增隧道
</button>
</div>
<div class="toolbar-right">
<input class="search-input" placeholder="搜索隧道..." v-model="searchKeyword" />
</div>
</div>
<!-- 隧道卡片列表 -->
<div class="proxy-list">
<div v-for="p in filteredProxies" :key="p.id" class="proxy-card">
<div class="proxy-row">
<div class="proxy-left">
<span class="status-dot" :class="{ active: p.enabled }"></span>
<span class="proxy-name">{{ p.name }}</span>
<span class="proxy-addr">{{ p.localIP }}:{{ p.localPort }}</span>
<span class="proxy-tag">{{ p.type }}</span>
</div>
<div class="proxy-right">
<span class="remote-port">{{ p.remotePort }}</span>
<button class="icon-btn" @click="openEditDialog(p)">
</button>
<button class="icon-btn danger" @click="deleteProxy(p.id)">
</button>
</div>
</div>
<div class="proxy-footer">
<label class="switch small">
<input type="checkbox" v-model="p.enabled" @change="toggleProxy(p)" />
<span class="slider"></span>
</label>
<span class="status-label">{{ p.enabled ? '已启用' : '已禁用' }}</span>
</div>
</div>
<div v-if="filteredProxies.length === 0" class="empty-state">
暂无隧道,点击「新增隧道」添加
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ====== 新增/编辑弹窗 ====== -->
<div v-if="dialogVisible" class="dialog-overlay" @click.self="dialogVisible = false">
<div class="dialog-card">
<h3>{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}</h3>
<div class="dialog-form">
<div class="form-row">
<label>名称</label>
<input v-model="dialogForm.name" />
</div>
<div class="form-row">
<label>类型</label>
<select v-model="dialogForm.type">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
<option value="http">http</option>
<option value="https">https</option>
</select>
</div>
<div class="form-row">
<label>本地 IP</label>
<input v-model="dialogForm.localIP" />
</div>
<div class="form-row">
<label>本地端口</label>
<input type="number" v-model="dialogForm.localPort" />
</div>
<div class="form-row">
<label>远程端口</label>
<input type="number" v-model="dialogForm.remotePort" />
</div>
</div>
<div class="dialog-actions">
<button class="btn-cancel" @click="dialogVisible = false">
取消
</button>
<button class="btn-confirm" @click="confirmDialog">确认</button>
</div>
</div>
</div>
<!-- ====== 隐藏的文件输入(导入 TOML ====== -->
<input type="file" id="tomlFileInput" accept=".toml" style="display: none" @change="handleImport" />
</div>
<!-- 应用逻辑 -->
<script src="/static/app.js"></script>
</body>
</html>