加入首次使用的引导页、写入readme文件

This commit is contained in:
2026-07-23 22:17:48 +08:00
parent 892fc98be2
commit 55fb5d49f7
11 changed files with 766 additions and 1828 deletions
+129 -121
View File
@@ -8,11 +8,7 @@
<!-- 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" />
@@ -20,40 +16,86 @@
<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>
<!-- Logo 区域(可替换) -->
<div class="login-logo">
<svg viewBox="0 0 64 64" preserveAspectRatio="xMidYMid meet" style="width:100%; height:100%; display:block;" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="56" height="56" rx="16" stroke="#4a7cff" stroke-width="2" opacity="0.3"/>
<path d="M20 44L32 20L44 44H36L32 36L28 44H20Z" fill="#4a7cff" opacity="0.9"/>
<circle cx="32" cy="32" r="4" fill="#4a7cff" opacity="0.6"/>
<path d="M32 12V18M32 46V52M14 32H20M44 32H50M18 18L22 22M42 42L46 46M18 46L22 42M42 22L46 18" stroke="#4a7cff" stroke-width="1.5" stroke-linecap="round" opacity="0.4"/>
</svg>
<span class="logo-text">frpc-console</span>
</div>
<div class="login-form">
<div class="input-group">
<label>user</label>
<input v-model="loginForm.username" type="text" placeholder="用户名" @keydown.enter="doLogin" />
<!-- 首次启动:注册页 -->
<template v-if="isFirstRun">
<div class="login-header">
<h1>首次设置</h1>
</div>
<div class="input-group">
<label>passwd</label>
<input v-model="loginForm.password" type="password" placeholder="密码" @keydown.enter="doLogin" />
<div class="login-form">
<div class="input-group">
<label>用户名(至少 5 位)</label>
<input v-model="registerForm.username" type="text" placeholder="设置管理员用户名" @keydown.enter="doRegister" />
</div>
<div class="input-group">
<label>密码</label>
<input v-model="registerForm.password" type="password" placeholder="至少8位,含大小写/数字/特殊字符" @keydown.enter="doRegister" />
</div>
<div class="input-group">
<label>确认密码</label>
<input v-model="registerForm.confirmPassword" type="password" placeholder="再次输入密码" @keydown.enter="doRegister" />
</div>
<button class="login-btn" @click="doRegister" :disabled="registering">
{{ registering ? '注册中...' : '注册' }}
</button>
<p v-if="registerError" class="login-error">{{ registerError }}</p>
</div>
<button class="login-btn" @click="doLogin" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p v-if="loginError" class="login-error">{{ loginError }}</p>
</div>
</template>
<!-- 正常:登录页 -->
<template v-else>
<div class="login-header">
<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>
</template>
</div>
<!-- ====== 主界面(登录后) ====== -->
<!-- ====== 主界面 ====== -->
<div v-else class="main-panel" :class="{ visible: contentVisible }">
<!-- 顶部导航 -->
<!-- 顶部导航(含 Logo 小标) -->
<div class="top-bar">
<div class="top-left">
<span class="logo">🚀 frpc-console</span>
<svg viewBox="0 0 28 28" preserveAspectRatio="xMidYMid meet" style="width:100%; height:100%; display:block;" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0;">
<rect x="2" y="2" width="24" height="24" rx="6" stroke="#4a7cff" stroke-width="1.5" opacity="0.3"/>
<path d="M9 19L14 9L19 19H16L14 16L12 19H9Z" fill="#4a7cff" opacity="0.9"/>
<circle cx="14" cy="14" r="2" fill="#4a7cff" opacity="0.5"/>
</svg>
<span class="logo">frpc-console</span>
<span class="status-dot" :class="{ active: frpcRunning }"></span>
<span class="status-text">{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span>
</div>
@@ -63,20 +105,44 @@
</div>
</div>
<!-- Tab 切换 -->
<!-- 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>
<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="profile-section">
<div class="profile-header">
<span>👤 账户管理</span>
<span class="profile-username">{{ loginForm.username }}</span>
</div>
<div class="profile-form">
<div class="form-row">
<label>当前密码</label>
<input v-model="passwordChange.oldPassword" type="password" placeholder="输入当前密码" />
</div>
<div class="form-row">
<label>新密码</label>
<input v-model="passwordChange.newPassword" type="password" placeholder="至少8位,含大小写/数字/特殊字符" />
</div>
<div class="form-row">
<label>确认新密码</label>
<input v-model="passwordChange.confirmPassword" type="password" placeholder="再次输入新密码" />
</div>
<button class="save-btn" @click="changePassword">修改密码</button>
<p v-if="passwordChangeError" class="login-error">{{ passwordChangeError }}</p>
<p v-if="passwordChangeSuccess" class="login-success">{{ passwordChangeSuccess }}</p>
</div>
</div>
<!-- 配置卡片 -->
<div class="config-grid">
<div class="metric-card">
<div class="metric-label">TCP Multiplexing</div>
@@ -106,74 +172,39 @@
<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>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="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 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>
<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">
@@ -185,13 +216,8 @@
</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="(function(){ console.log('点击了删除按钮, id:', p.id); removeProxy(p.id); })()">
</button>
<button class="icon-btn" @click="openEditDialog(p)"></button>
<button class="icon-btn danger" @click="removeProxy(p.id)"></button>
</div>
</div>
<div class="proxy-footer">
@@ -202,61 +228,43 @@
<span class="status-label">{{ p.enabled ? '已启用' : '已禁用' }}</span>
</div>
</div>
<div v-if="filteredProxies.length === 0" class="empty-state">
暂无隧道,点击「新增隧道」添加
</div>
<div v-if="filteredProxies.length === 0" class="empty-state">暂无隧道,点击「新增隧道」添加</div>
</div>
</div>
</div>
</div>
</div>
<!-- ====== 新增/编辑弹窗 ====== -->
<!-- ====== 弹窗 ====== -->
<Transition name="dialog">
<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><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>
<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 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-cancel" @click="dialogVisible = false">取消</button>
<button class="btn-confirm" @click="confirmDialog">确认</button>
</div>
</div>
</div>
</Transition>
<!-- ====== 隐藏文件输入(导入 TOML ====== -->
<input type="file" id="tomlFileInput" accept=".toml" style="display: none" @change="handleImport" />
<!-- 隐藏文件选择器 -->
<input type="file" id="tomlFileInput" accept=".toml" style="display:none" @change="handleImport" />
</div>
<!-- 应用逻辑 -->
<script src="/static/app.js"></script>
</body>