384 lines
21 KiB
HTML
384 lines
21 KiB
HTML
<!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>
|
|
<!-- 样式 -->
|
|
<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="frpc-console" class="logo-img" />
|
|
<span class="logo-text">frpc-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="frpc-console" class="nav-logo" />
|
|
<div class="brand-info">
|
|
<span class="logo-text">frpc-console</span>
|
|
<span class="status-wrapper">
|
|
<span class="status-dot" :class="{ active: frpcRunning }"></span>
|
|
<span class="status-text">{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</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 === 'proxies' }"
|
|
@click="activeTab = 'proxies'">隧道列表</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 === '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="removeProxy(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 v-if="activeTab === 'config'" class="tab-content config-tab-content">
|
|
<!-- 页面标题 -->
|
|
<div class="config-page-header">
|
|
<h2>全局配置信息</h2>
|
|
<p class="config-subtitle">管理 frpc 连接参数与传输设置</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 v-model="globalConfig.serverAddr" placeholder="例如: frp.example.com" />
|
|
</div>
|
|
<div class="form-row">
|
|
<label>服务器端口</label>
|
|
<input type="number" v-model="globalConfig.serverPort" placeholder="7000" />
|
|
</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>
|
|
<!-- 保活间隔 -->
|
|
<div class="form-row">
|
|
<label>保活间隔(秒)</label>
|
|
<input type="number" v-model="globalConfig.tcpMuxKeepalive" />
|
|
</div>
|
|
<!-- 心跳间隔 -->
|
|
<div class="form-row">
|
|
<label>心跳间隔(秒)</label>
|
|
<input type="number" v-model="globalConfig.heartbeatInterval" />
|
|
</div>
|
|
<!-- 心跳超时 -->
|
|
<div class="form-row">
|
|
<label>心跳超时(秒)</label>
|
|
<input type="number" v-model="globalConfig.heartbeatTimeout" />
|
|
</div>
|
|
<!-- 连接池大小 -->
|
|
<div class="form-row">
|
|
<label>连接池大小(个)</label>
|
|
<input type="number" v-model="globalConfig.poolCount" />
|
|
</div>
|
|
<!-- v2 协议开关(v1.5 灰标禁用) -->
|
|
<div class="form-row v2-switch-row">
|
|
<label>frp v2 隧道支持</label>
|
|
<div class="v2-switch-wrapper">
|
|
<label class="switch disabled">
|
|
<input type="checkbox" v-model="globalConfig.wireProtocolV2" disabled />
|
|
<span class="slider"></span>
|
|
</label>
|
|
<span class="v2-badge">即将推出</span>
|
|
</div>
|
|
<span class="hint-text v2-hint">
|
|
此功能为 frp 0.70.0 以上版本才能开启,若要打开必须搭配同版本 frps 使用,否则此功能不生效
|
|
</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">
|
|
暂无日志,frpc 尚未产生输出
|
|
</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>
|
|
|
|
<!-- ====== 弹窗 ====== -->
|
|
<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>
|
|
<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>
|
|
</Transition>
|
|
|
|
<!-- 隐藏文件选择器 -->
|
|
<input type="file" id="tomlFileInput" accept=".toml" style="display:none" @change="handleImport" />
|
|
</div>
|
|
|
|
<script src="/static/app.js"></script>
|
|
</body>
|
|
|
|
</html> |