优化部分设置逻辑
This commit is contained in:
+225
-304
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frpc-console</title>
|
||||
@@ -8,331 +9,251 @@
|
||||
<!-- 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"
|
||||
/>
|
||||
<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.css" />
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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-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 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 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>
|
||||
<!-- ====== 主界面(登录后) ====== -->
|
||||
<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 === 'config' }"
|
||||
@click="activeTab = 'config'"
|
||||
>
|
||||
全局配置
|
||||
</span>
|
||||
<span
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'proxies' }"
|
||||
@click="activeTab = 'proxies'"
|
||||
>
|
||||
隧道列表
|
||||
</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">
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="globalConfig.tcpMux"
|
||||
@change="saveConfig"
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span
|
||||
class="status-text"
|
||||
:class="{ active: globalConfig.tcpMux }"
|
||||
>
|
||||
{{ globalConfig.tcpMux ? '已启用' : '已禁用' }}
|
||||
<!-- Tab 切换 -->
|
||||
<div class="tab-bar">
|
||||
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
|
||||
全局配置
|
||||
</span>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'proxies' }" @click="activeTab = 'proxies'">
|
||||
隧道列表
|
||||
</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 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 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 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 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 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 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 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>
|
||||
</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"
|
||||
/>
|
||||
<!-- ====== 隐藏的文件输入(导入 TOML) ====== -->
|
||||
<input type="file" id="tomlFileInput" accept=".toml" style="display: none" @change="handleImport" />
|
||||
</div>
|
||||
|
||||
<!-- 应用逻辑 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user