首次推送

This commit is contained in:
2026-07-27 00:00:27 +08:00
commit d6ef302b8c
28 changed files with 4760 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frps-console</title>
<!-- Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.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" />
<link rel="stylesheet" href="/static/style-4.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">
<!-- Logo 区域 -->
<div class="login-logo">
<img src="/static/logo.svg" alt="frps-console" class="logo-img" />
<span class="logo-text">frps-console</span>
</div>
<!-- 首次启动:注册页 -->
<template v-if="isFirstRun">
<div class="login-header">
<h1>首次设置</h1>
</div>
<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>
</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 }">
<!-- 顶部导航 -->
<div class="top-bar">
<div class="top-left">
<img src="/static/logo.svg" alt="frps-console" class="nav-logo" />
<div class="brand-info">
<span class="logo-text">frps-console</span>
<span class="status-wrapper">
<span class="status-dot" :class="{ active: frpsRunning }"></span>
<span class="status-text">{{ frpsRunning ? 'frps 运行中' : 'frps 已停止' }}</span>
</span>
</div>
</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 === 'clients' }"
@click="activeTab = 'clients'">客户端列表</span>
<span class="tab-item" :class="{ active: activeTab === 'config' }"
@click="activeTab = 'config'">全局配置信息</span>
<span class="tab-item" :class="{ active: activeTab === 'logs' }"
@click="activeTab = 'logs'">运行日志</span>
</div>
<!-- 内容区 -->
<div class="content-area">
<!-- ====== 客户端列表 ====== -->
<div v-if="activeTab === 'clients'" 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>
</div>
<div class="toolbar-right">
<input class="search-input" placeholder="搜索客户端..." v-model="searchKeyword" />
</div>
</div>
<div class="proxy-list">
<div v-for="c in filteredClients" :key="c.id" class="proxy-card">
<div class="proxy-row">
<div class="proxy-left">
<span class="status-dot" :class="{ active: c.status === 'online' }"></span>
<span class="proxy-name">{{ c.name || c.id }}</span>
<span class="proxy-tag">{{ c.os }} / {{ c.arch }}</span>
<span class="proxy-tag">v{{ c.version }}</span>
</div>
<div class="proxy-right">
<span class="remote-port">{{ c.status === 'online' ? '在线' : '离线' }}</span>
</div>
</div>
<div class="proxy-footer">
<span class="status-label">连接时间: {{ c.connTime || '--' }}</span>
<span class="status-label">ID: {{ c.id }}</span>
</div>
</div>
<div v-if="filteredClients.length === 0" class="empty-state">
{{ clientsLoaded ? '暂无客户端连接' : '加载中...' }}
</div>
</div>
</div>
<!-- ====== 全局配置信息 ====== -->
<div v-if="activeTab === 'config'" class="tab-content config-tab-content">
<!-- 页面标题 -->
<div class="config-page-header">
<h2>全局配置信息</h2>
<p class="config-subtitle">管理 frps 服务端参数与传输设置</p>
</div>
<!-- 首次使用提示 -->
<div v-if="showDefaultTip" class="config-tip">
首次使用请修改「认证令牌」为您的真实 frps 配置
</div>
<!-- ===== 账户管理 ===== -->
<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>
<!-- ===== 卡片1: 服务器连接 ===== -->
<div class="config-card">
<div class="config-card-header">
<span class="card-title">服务器连接</span>
</div>
<div class="config-card-body">
<div class="form-row">
<label>绑定端口</label>
<input type="number" v-model="globalConfig.bindPort" placeholder="9358" />
</div>
<div class="form-row" style="grid-column: 1 / -1;">
<label>认证令牌</label>
<div class="token-input-wrapper">
<input :type="showToken ? 'text' : 'password'" v-model="globalConfig.token"
placeholder="CHANGE_ME" />
<button class="token-toggle-btn" @click="showToken = !showToken">
{{ showToken ? '隐藏' : '显示' }}
</button>
</div>
</div>
</div>
</div>
<!-- ===== 卡片2: 传输配置 ===== -->
<div class="config-card">
<div class="config-card-header">
<span class="card-title">传输配置</span>
</div>
<div class="config-card-body">
<!-- TCP 多路复用(只读,占满一行) -->
<div class="form-row" style="grid-column: 1 / -1;">
<label>TCP 多路复用</label>
<div class="readonly-value">
<span class="status-dot active"></span>
<span class="status-text active">已强制开启</span>
<span class="hint-text">优化连接性能,减少延迟,frp 官方推荐开启</span>
</div>
</div>
<!-- v2 协议状态(只读展示,无实际配置) -->
<div class="form-row v2-switch-row">
<label>frp v2 协议支持</label>
<div class="v2-switch-wrapper">
<span class="status-dot active"></span>
<span class="status-text active">已就绪</span>
<span class="v2-badge v2-enabled">frps 已支持 v2 协议,请在 frpc 侧开启</span>
</div>
<span class="hint-text v2-hint">
frps 已内置 v2 协议支持,无需额外配置。如需启用,请在 frpc 配置中设置 transport.wireProtocol = "v2"
</span>
</div>
</div>
</div>
<!-- ===== 卡片3: 日志配置 ===== -->
<div class="config-card">
<div class="config-card-header">
<span class="card-title">日志配置</span>
</div>
<div class="config-card-body">
<div class="form-row">
<label>日志级别</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>日志保留天数(天)</label>
<input type="number" v-model="globalConfig.logMaxDays" />
</div>
</div>
</div>
<!-- 保存按钮 -->
<button class="save-config-btn" @click="saveConfig">保存配置并热加载</button>
</div>
<!-- ====== 运行日志 ====== -->
<div v-else-if="activeTab === 'logs'" class="tab-content log-tab-content">
<!-- 工具栏 -->
<div class="log-toolbar">
<div class="log-toolbar-left">
<span class="log-info-badge">共 {{ logTotal }} 行</span>
<span v-if="logError" class="log-error-badge">{{ logError }}</span>
</div>
<div class="log-toolbar-right">
<label class="log-auto-scroll-label">
<input type="checkbox" v-model="logAutoScroll" />
自动滚动
</label>
<button class="log-btn" @click="refreshLogs">刷新</button>
<button class="log-btn" @click="scrollLogToBottom">↓ 滚动到底部</button>
</div>
</div>
<!-- 日志显示区域 -->
<div class="log-terminal" id="logContainer">
<div v-if="logLines.length === 0 && !logError" class="log-empty">
暂无日志,frps 尚未产生输出
</div>
<div v-else class="log-lines">
<div v-for="(line, idx) in logLines" :key="idx" class="log-line">{{ line }}</div>
</div>
</div>
<!-- 底部状态 -->
<div class="log-footer">
<span>上次更新: {{ logLastUpdate || '--:--:--' }}</span>
<span class="log-polling-status">自动刷新中 (8s)</span>
</div>
</div>
</div>
</div>
</div>
<!-- 隐藏文件选择器 -->
<input type="file" id="tomlFileInput" accept=".toml" style="display:none" @change="handleImport" />
</div>
<script src="/static/app.js"></script>
</body>
</html>