diff --git a/frpc-console.db b/frpc-console.db
new file mode 100644
index 0000000..5bb67bd
Binary files /dev/null and b/frpc-console.db differ
diff --git a/frpc.log b/frpc.log
new file mode 100644
index 0000000..5c10e9d
--- /dev/null
+++ b/frpc.log
@@ -0,0 +1,9 @@
+2026-07-23 23:08:37.413 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
+2026-07-23 23:08:37.422 [I] [client/service.go:308] try to connect to server...
+2026-07-23 23:08:37.587 [I] [client/service.go:328] [e42214c3339795cc] login to server success, get run id [e42214c3339795cc]
+2026-07-23 23:11:52.981 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
+2026-07-23 23:11:52.991 [I] [client/service.go:308] try to connect to server...
+2026-07-23 23:11:53.214 [I] [client/service.go:328] [5fc7383f8cd6b111] login to server success, get run id [5fc7383f8cd6b111]
+2026-07-23 23:13:55.183 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
+2026-07-23 23:13:55.201 [I] [client/service.go:308] try to connect to server...
+2026-07-23 23:13:55.366 [I] [client/service.go:328] [39fcc2092c9b87ae] login to server success, get run id [39fcc2092c9b87ae]
diff --git a/main.go b/main.go
index 824cce7..82c8d71 100644
--- a/main.go
+++ b/main.go
@@ -5,25 +5,22 @@ import (
)
func main() {
- // 1. 初始化数据库
if err := InitDB(); err != nil {
log.Fatal("❌ 数据库初始化失败:", err)
}
- // 2. 首次启动设置管理员
+ // ✅ Web 注册已取代 CLI 初始化,不再需要终端交互
+ // 前端会自动检测是否首次启动并显示注册页
// InitAdminUser()
- // 3. 启动时生成 frpc.toml
if err := GenerateFrpcConfig(); err != nil {
log.Println("⚠️ 生成配置文件失败:", err)
}
- // 4. 尝试启动 frpc
if err := StartFrpc(); err != nil {
log.Println("⚠️ 启动 frpc 失败:", err)
}
- // 5. 启动 Web 服务
r := SetupRouter()
log.Println("🚀 frpc-console 启动成功!")
log.Println("📍 访问地址: http://localhost:8080")
diff --git a/static/index.html b/static/index.html
index 81b863d..e3e4c84 100644
--- a/static/index.html
+++ b/static/index.html
@@ -28,12 +28,7 @@
diff --git a/static/logo.svg b/static/logo.svg
new file mode 100644
index 0000000..b9ff8d4
--- /dev/null
+++ b/static/logo.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/static/style-1.css b/static/style-1.css
index a4f9728..38eec5f 100644
--- a/static/style-1.css
+++ b/static/style-1.css
@@ -136,7 +136,7 @@ body {
/* 登录态尺寸 */
.app-container:not(.is-logged-in) {
width: 600px;
- height: 450px;
+ height: 500px;
}
/* 登录后尺寸 */
diff --git a/static/style-2.css b/static/style-2.css
index 12648ff..8eb3728 100644
--- a/static/style-2.css
+++ b/static/style-2.css
@@ -7,7 +7,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
- padding: 32px 48px;
+ padding: 32px 48px 48px 48px; /* 上左右不变,下边距加大 */
}
.login-header {
@@ -113,8 +113,18 @@
letter-spacing: 2px;
}
-.login-logo svg {
+.login-logo img {
width: 64px;
height: 64px;
flex-shrink: 0;
+}
+
+/* ---------- 登录页补充优化 ---------- */
+.login-card .login-form {
+ margin-bottom: 4px;
+}
+
+/* 注册按钮距上方输入框远一点 */
+.login-card .login-form .login-btn {
+ margin-top: 16px;
}
\ No newline at end of file
diff --git a/static/style-3.css b/static/style-3.css
index 9908897..29c4cf9 100644
--- a/static/style-3.css
+++ b/static/style-3.css
@@ -9,29 +9,57 @@
border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0;
}
+
.top-left {
display: flex;
align-items: center;
- gap: 16px;
-}
-.logo {
- font-size: 18px;
- font-weight: 500;
- color: var(--text-primary);
+ gap: 14px;
}
-.status-dot {
+.top-left .nav-logo {
+ width: 48px;
+ height: 48px;
+ flex-shrink: 0;
+ display: block;
+ object-fit: contain;
+}
+
+.brand-info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 2px;
+}
+
+.brand-info .logo-text {
+ font-size: 22px;
+ font-weight: 600;
+ color: var(--text-primary);
+ line-height: 1.2;
+ letter-spacing: 0.5px;
+}
+
+.status-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-wrapper .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #444;
transition: background 0.3s;
+ flex-shrink: 0;
}
-.status-dot.active {
+
+.status-wrapper .status-dot.active {
background: #63e2b7;
box-shadow: 0 0 12px rgba(99, 226, 183, 0.4);
}
-.status-text {
+
+.status-wrapper .status-text {
font-size: 13px;
color: var(--text-muted);
}
@@ -41,10 +69,12 @@
align-items: center;
gap: 16px;
}
+
.user-name {
font-size: 13px;
color: var(--text-secondary);
}
+
.logout-btn {
padding: 6px 16px;
background: rgba(255, 255, 255, 0.06);
@@ -55,6 +85,7 @@
cursor: pointer;
transition: background 0.2s;
}
+
.logout-btn:hover {
background: rgba(255, 70, 70, 0.15);
border-color: rgba(255, 70, 70, 0.3);
@@ -69,6 +100,7 @@
flex-shrink: 0;
border-bottom: 1px solid var(--border-subtle);
}
+
.tab-item {
font-size: 14px;
color: var(--text-muted);
@@ -77,9 +109,11 @@
transition: color 0.3s, border-color 0.3s;
border-bottom: 2px solid transparent;
}
+
.tab-item:hover {
color: var(--text-secondary);
}
+
.tab-item.active {
color: var(--primary-blue);
border-bottom-color: var(--primary-blue);
@@ -93,16 +127,20 @@
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.06) transparent;
}
+
.content-area::-webkit-scrollbar {
width: 4px;
}
+
.content-area::-webkit-scrollbar-track {
background: transparent;
}
+
.content-area::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.06);
border-radius: 4px;
}
+
.tab-content {
height: 100%;
}
@@ -114,12 +152,14 @@
gap: 16px;
margin-bottom: 20px;
}
+
.metric-card {
background: var(--bg-metric);
border-radius: 16px;
padding: 16px 20px;
border: 1px solid var(--border-subtle);
}
+
.metric-label {
font-size: 12px;
color: var(--text-dim);
@@ -127,25 +167,30 @@
letter-spacing: 1px;
margin-bottom: 8px;
}
+
.metric-value {
display: flex;
align-items: center;
gap: 12px;
}
+
.big-number {
font-size: 28px;
font-weight: 600;
color: #fff;
}
+
.unit {
font-size: 14px;
color: var(--text-dim);
}
+
.digit {
font-family: 'JetBrains Mono', monospace;
font-size: 18px;
color: #fff;
}
+
.sep {
color: var(--text-dim);
}
@@ -159,9 +204,11 @@
user-select: none;
transition: color 0.3s;
}
+
.detail-collapse:hover {
color: var(--text-secondary);
}
+
.detail-panel {
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
@@ -172,17 +219,20 @@
gap: 12px 24px;
margin-bottom: 16px;
}
+
.detail-panel .form-row {
display: flex;
flex-direction: column;
gap: 4px;
}
+
.detail-panel .form-row label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-dim);
}
+
.detail-panel .form-row input,
.detail-panel .form-row select {
padding: 8px 12px;
@@ -194,6 +244,7 @@
outline: none;
transition: border-color 0.3s;
}
+
.detail-panel .form-row input:focus,
.detail-panel .form-row select:focus {
border-color: var(--primary-blue-border);
@@ -212,11 +263,87 @@
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 4px;
}
+
.save-btn:hover {
transform: scale(1.01);
box-shadow: 0 4px 16px var(--primary-blue-glow);
}
+/* ---------- 账户管理区域 ---------- */
+.profile-section {
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 16px;
+ padding: 16px 20px;
+ border: 1px solid var(--border-subtle);
+ margin-bottom: 20px;
+}
+
+.profile-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border-subtle);
+ margin-bottom: 12px;
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.profile-username {
+ color: var(--primary-blue);
+ font-weight: 500;
+}
+
+.profile-form {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 12px 16px;
+ align-items: end;
+}
+
+.profile-form .form-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.profile-form .form-row label {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-dim);
+}
+
+.profile-form .form-row input {
+ padding: 8px 12px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--border-subtle);
+ border-radius: 8px;
+ color: #fff;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s;
+}
+
+.profile-form .form-row input:focus {
+ border-color: var(--primary-blue-border);
+}
+
+.profile-form .save-btn {
+ grid-column: auto;
+ padding: 8px 20px;
+ margin-top: 0;
+ height: 40px;
+}
+
+.login-success {
+ color: #63e2b7;
+ font-size: 14px;
+ text-align: center;
+ margin-top: 4px;
+ min-height: 24px;
+}
+
/* ---------- 隧道列表 ---------- */
.toolbar {
display: flex;
@@ -226,11 +353,13 @@
flex-wrap: wrap;
gap: 12px;
}
+
.toolbar-left {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
+
.toolbar-left button {
padding: 8px 18px;
border-radius: 10px;
@@ -241,22 +370,27 @@
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
+
.toolbar-left button:hover {
background: rgba(255, 255, 255, 0.06);
}
+
.btn-import:hover {
border-color: var(--primary-blue-border);
color: var(--primary-blue);
}
+
.btn-export:hover {
border-color: rgba(99, 150, 255, 0.3);
color: #6a9fff;
}
+
.btn-add {
background: var(--primary-blue-dim) !important;
border-color: var(--primary-blue-border) !important;
color: var(--primary-blue) !important;
}
+
.btn-add:hover {
background: rgba(74, 124, 255, 0.2) !important;
}
@@ -272,9 +406,11 @@
width: 200px;
transition: border-color 0.3s;
}
+
.toolbar-right .search-input:focus {
border-color: var(--primary-blue-border);
}
+
.toolbar-right .search-input::placeholder {
color: var(--text-dim);
}
@@ -293,29 +429,35 @@
padding: 12px 16px;
transition: border-color 0.3s;
}
+
.proxy-card:hover {
border-color: rgba(255, 255, 255, 0.08);
}
+
.proxy-row {
display: flex;
justify-content: space-between;
align-items: center;
}
+
.proxy-left {
display: flex;
align-items: center;
gap: 12px;
}
+
.proxy-name {
font-size: 15px;
font-weight: 500;
color: #fff;
}
+
.proxy-addr {
font-size: 13px;
color: var(--text-dim);
font-family: 'JetBrains Mono', monospace;
}
+
.proxy-tag {
font-size: 10px;
padding: 2px 10px;
@@ -325,16 +467,19 @@
text-transform: uppercase;
letter-spacing: 0.5px;
}
+
.proxy-right {
display: flex;
align-items: center;
gap: 12px;
}
+
.remote-port {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
color: var(--text-secondary);
}
+
.icon-btn {
background: none;
border: none;
@@ -345,10 +490,12 @@
transition: color 0.2s;
border-radius: 6px;
}
+
.icon-btn:hover {
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.04);
}
+
.icon-btn.danger:hover {
color: #f87171;
background: rgba(248, 113, 113, 0.1);
@@ -362,10 +509,12 @@
align-items: center;
gap: 10px;
}
+
.status-label {
font-size: 12px;
color: var(--text-dim);
}
+
.empty-state {
padding: 48px 0;
text-align: center;
@@ -381,11 +530,13 @@
display: inline-block;
cursor: pointer;
}
+
.switch input {
opacity: 0;
width: 0;
height: 0;
}
+
.switch .slider {
position: absolute;
top: 0;
@@ -396,6 +547,7 @@
border-radius: 24px;
transition: background 0.3s;
}
+
.switch .slider::before {
content: '';
position: absolute;
@@ -407,23 +559,28 @@
border-radius: 50%;
transition: transform 0.3s;
}
+
.switch input:checked + .slider {
background: var(--primary-blue);
}
+
.switch input:checked + .slider::before {
transform: translateX(20px);
background: #0a0a0f;
}
+
.switch.small {
width: 32px;
height: 18px;
}
+
.switch.small .slider::before {
height: 12px;
width: 12px;
left: 3px;
bottom: 3px;
}
+
.switch.small input:checked + .slider::before {
transform: translateX(14px);
}
@@ -436,6 +593,7 @@
background: #333;
transition: background 0.3s;
}
+
.status-dot.active {
background: #63e2b7;
box-shadow: 0 0 10px rgba(99, 226, 183, 0.3);
@@ -455,6 +613,7 @@
justify-content: center;
align-items: center;
}
+
.dialog-card {
background: #1a1a24;
border: 1px solid var(--border-subtle);
@@ -464,26 +623,31 @@
max-width: 90vw;
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.6);
}
+
.dialog-card h3 {
font-size: 18px;
font-weight: 500;
color: #fff;
margin-bottom: 20px;
}
+
.dialog-form {
display: flex;
flex-direction: column;
gap: 14px;
}
+
.dialog-form .form-row {
display: flex;
flex-direction: column;
gap: 4px;
}
+
.dialog-form .form-row label {
font-size: 12px;
color: var(--text-dim);
}
+
.dialog-form .form-row input,
.dialog-form .form-row select {
padding: 8px 14px;
@@ -495,16 +659,19 @@
outline: none;
transition: border-color 0.3s;
}
+
.dialog-form .form-row input:focus,
.dialog-form .form-row select:focus {
border-color: var(--primary-blue-border);
}
+
.dialog-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: flex-end;
}
+
.dialog-actions button {
padding: 8px 24px;
border-radius: 10px;
@@ -513,18 +680,22 @@
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
+
.btn-cancel {
background: rgba(255, 255, 255, 0.04);
color: var(--text-secondary);
}
+
.btn-cancel:hover {
background: rgba(255, 255, 255, 0.08);
}
+
.btn-confirm {
background: linear-gradient(135deg, var(--primary-blue), #2a5adf);
color: #0a0a0f;
font-weight: 600;
}
+
.btn-confirm:hover {
transform: scale(1.02);
box-shadow: 0 4px 16px var(--primary-blue-glow);
@@ -553,37 +724,43 @@ select {
background-position: right 14px center;
padding-right: 40px !important;
}
+
select:hover {
border-color: rgba(74, 124, 255, 0.4) !important;
background: rgba(20, 20, 40, 0.9) !important;
}
+
select:focus {
border-color: var(--primary-blue) !important;
box-shadow: 0 0 0 3px rgba(74, 124, 255, 0.15), inset 0 0 0 1px rgba(74, 124, 255, 0.05);
}
+
select option {
background: #0d0d1a !important;
color: #e0e0e0 !important;
padding: 8px 14px !important;
border: none !important;
}
+
select option:hover,
select option:checked,
select option:focus {
background: rgba(74, 124, 255, 0.2) !important;
color: #ffffff !important;
}
+
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #e0e0e0;
}
+
select:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
-/* ===== 弹窗入场/出场动画 ===== */
+/* ---------- 弹窗入场/出场动画 ---------- */
.dialog-enter-active,
.dialog-leave-active {
transition: all 0.3s ease;
@@ -604,76 +781,4 @@ select:disabled {
.dialog-leave-from .dialog-card {
transform: scale(1) translateY(0);
opacity: 1;
-}
-
-/* ---------- 账户管理区域 ---------- */
-.profile-section {
- background: rgba(255, 255, 255, 0.02);
- border-radius: 16px;
- padding: 16px 20px;
- border: 1px solid var(--border-subtle);
- margin-bottom: 20px;
-}
-.profile-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 12px;
- border-bottom: 1px solid var(--border-subtle);
- margin-bottom: 12px;
- color: var(--text-secondary);
- font-size: 14px;
-}
-.profile-username {
- color: var(--primary-blue);
- font-weight: 500;
-}
-.profile-form {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 12px 16px;
- align-items: end;
-}
-.profile-form .form-row {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-.profile-form .form-row label {
- font-size: 11px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--text-dim);
-}
-.profile-form .form-row input {
- padding: 8px 12px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid var(--border-subtle);
- border-radius: 8px;
- color: #fff;
- font-size: 14px;
- outline: none;
- transition: border-color 0.3s;
-}
-.profile-form .form-row input:focus {
- border-color: var(--primary-blue-border);
-}
-.profile-form .save-btn {
- grid-column: auto;
- padding: 8px 20px;
- margin-top: 0;
- height: 40px;
-}
-.login-success {
- color: #63e2b7;
- font-size: 14px;
- text-align: center;
- margin-top: 4px;
- min-height: 24px;
-}
-
-.top-left svg {
- width: 28px;
- height: 28px;
- flex-shrink: 0;
}
\ No newline at end of file