调整UI结构,优化密码设置逻辑
This commit is contained in:
@@ -47,19 +47,19 @@ func ParseJWT(tokenString string) (*Claims, error) {
|
|||||||
|
|
||||||
// 密码复杂度验证:至少3个大写 + 8个数字 + 1个特殊字符
|
// 密码复杂度验证:至少3个大写 + 8个数字 + 1个特殊字符
|
||||||
func ValidatePassword(pwd string) bool {
|
func ValidatePassword(pwd string) bool {
|
||||||
var upper, digit, special int
|
if len(pwd) < 8 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var hasLetter, hasDigit bool
|
||||||
for _, ch := range pwd {
|
for _, ch := range pwd {
|
||||||
if ch >= 'A' && ch <= 'Z' {
|
if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') {
|
||||||
upper++
|
hasLetter = true
|
||||||
}
|
}
|
||||||
if ch >= '0' && ch <= '9' {
|
if ch >= '0' && ch <= '9' {
|
||||||
digit++
|
hasDigit = true
|
||||||
}
|
|
||||||
if strings.ContainsAny(string(ch), "!@#$%^&*()_+-=[]{}|;:,.<>?") {
|
|
||||||
special++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return upper >= 3 && digit >= 8 && special >= 1 && len(pwd) >= 12
|
return hasLetter && hasDigit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首次启动强制设置管理员账户
|
// 首次启动强制设置管理员账户
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
2026-07-23 18:16:48.909 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
2026-07-23 18:16:48.909 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||||
2026-07-23 18:16:48.919 [I] [client/service.go:308] try to connect to server...
|
2026-07-23 18:16:48.919 [I] [client/service.go:308] try to connect to server...
|
||||||
2026-07-23 18:16:49.115 [I] [client/service.go:328] [bb35e22da9644b61] login to server success, get run id [bb35e22da9644b61]
|
2026-07-23 18:16:49.115 [I] [client/service.go:328] [bb35e22da9644b61] login to server success, get run id [bb35e22da9644b61]
|
||||||
|
2026-07-23 18:22:55.829 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||||
|
2026-07-23 18:22:55.837 [I] [client/service.go:308] try to connect to server...
|
||||||
|
2026-07-23 18:22:56.033 [I] [client/service.go:328] [faf7244985d4f465] login to server success, get run id [faf7244985d4f465]
|
||||||
|
2026-07-23 18:25:55.659 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||||
|
2026-07-23 18:25:55.683 [I] [client/service.go:308] try to connect to server...
|
||||||
|
2026-07-23 18:25:55.853 [I] [client/service.go:328] [fd6724e72ed69f47] login to server success, get run id [fd6724e72ed69f47]
|
||||||
|
|||||||
+127
-36
@@ -1,6 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frpc-console</title>
|
<title>frpc-console</title>
|
||||||
@@ -8,17 +8,21 @@
|
|||||||
<!-- Vue 3 CDN -->
|
<!-- Vue 3 CDN -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<!-- Naive UI CDN -->
|
<!-- 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 解析库(用于导入/导出) -->
|
<!-- TOML 解析库(用于导入/导出) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@iarna/toml@3.0.0/dist/toml.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@iarna/toml@3.0.0/dist/toml.min.js"></script>
|
||||||
<!-- 自定义样式 -->
|
<!-- 自定义样式 -->
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
|
|
||||||
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- 背景层:始终存在的磨砂玻璃质感 -->
|
<!-- 背景层:始终存在的磨砂玻璃质感 -->
|
||||||
<div class="app-backdrop"></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 class="app-container" :class="{ 'is-logged-in': loggedIn }">
|
||||||
@@ -31,11 +35,21 @@
|
|||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label>user</label>
|
<label>user</label>
|
||||||
<input v-model="loginForm.username" type="text" placeholder="用户名" @keydown.enter="doLogin" />
|
<input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
type="text"
|
||||||
|
placeholder="用户名"
|
||||||
|
@keydown.enter="doLogin"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label>passwd</label>
|
<label>passwd</label>
|
||||||
<input v-model="loginForm.password" type="password" placeholder="密码" @keydown.enter="doLogin" />
|
<input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="密码"
|
||||||
|
@keydown.enter="doLogin"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="login-btn" @click="doLogin" :disabled="loading">
|
<button class="login-btn" @click="doLogin" :disabled="loading">
|
||||||
{{ loading ? '登录中...' : '登录' }}
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
@@ -45,13 +59,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 主界面(登录后) ====== -->
|
<!-- ====== 主界面(登录后) ====== -->
|
||||||
<div v-else class="main-panel">
|
<div v-else class="main-panel" :class="{ visible: contentVisible }">
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="top-left">
|
<div class="top-left">
|
||||||
<span class="logo">🚀 frpc-console</span>
|
<span class="logo">🚀 frpc-console</span>
|
||||||
<span class="status-dot" :class="{ active: frpcRunning }"></span>
|
<span class="status-dot" :class="{ active: frpcRunning }"></span>
|
||||||
<span class="status-text">{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span>
|
<span class="status-text"
|
||||||
|
>{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-right">
|
<div class="top-right">
|
||||||
<span class="user-name">{{ loginForm.username }}</span>
|
<span class="user-name">{{ loginForm.username }}</span>
|
||||||
@@ -61,10 +77,18 @@
|
|||||||
|
|
||||||
<!-- Tab 切换 -->
|
<!-- Tab 切换 -->
|
||||||
<div class="tab-bar">
|
<div class="tab-bar">
|
||||||
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
|
<span
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === 'config' }"
|
||||||
|
@click="activeTab = 'config'"
|
||||||
|
>
|
||||||
全局配置
|
全局配置
|
||||||
</span>
|
</span>
|
||||||
<span class="tab-item" :class="{ active: activeTab === 'proxies' }" @click="activeTab = 'proxies'">
|
<span
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === 'proxies' }"
|
||||||
|
@click="activeTab = 'proxies'"
|
||||||
|
>
|
||||||
隧道列表
|
隧道列表
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,10 +102,17 @@
|
|||||||
<div class="metric-label">TCP Multiplexing</div>
|
<div class="metric-label">TCP Multiplexing</div>
|
||||||
<div class="metric-value">
|
<div class="metric-value">
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox" v-model="globalConfig.tcpMux" @change="saveConfig" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="globalConfig.tcpMux"
|
||||||
|
@change="saveConfig"
|
||||||
|
/>
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="status-text" :class="{ active: globalConfig.tcpMux }">
|
<span
|
||||||
|
class="status-text"
|
||||||
|
:class="{ active: globalConfig.tcpMux }"
|
||||||
|
>
|
||||||
{{ globalConfig.tcpMux ? '已启用' : '已禁用' }}
|
{{ globalConfig.tcpMux ? '已启用' : '已禁用' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,22 +120,30 @@
|
|||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<div class="metric-label">连接池大小</div>
|
<div class="metric-label">连接池大小</div>
|
||||||
<div class="metric-value">
|
<div class="metric-value">
|
||||||
<span class="big-number">{{ globalConfig.poolCount || 8 }}</span>
|
<span class="big-number"
|
||||||
|
>{{ globalConfig.poolCount || 8 }}</span
|
||||||
|
>
|
||||||
<span class="unit">个</span>
|
<span class="unit">个</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<div class="metric-label">心跳间隔 / 超时</div>
|
<div class="metric-label">心跳间隔 / 超时</div>
|
||||||
<div class="metric-value">
|
<div class="metric-value">
|
||||||
<span class="digit">{{ globalConfig.heartbeatInterval || 15 }}s</span>
|
<span class="digit"
|
||||||
|
>{{ globalConfig.heartbeatInterval || 15 }}s</span
|
||||||
|
>
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
<span class="digit">{{ globalConfig.heartbeatTimeout || 70 }}s</span>
|
<span class="digit"
|
||||||
|
>{{ globalConfig.heartbeatTimeout || 70 }}s</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail-collapse" @click="showDetail = !showDetail">
|
<div class="detail-collapse" @click="showDetail = !showDetail">
|
||||||
<span>{{ showDetail ? '收起全部配置 ▲' : '展开全部配置 ▼' }}</span>
|
<span
|
||||||
|
>{{ showDetail ? '收起全部配置 ▲' : '展开全部配置 ▼' }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showDetail" class="detail-panel">
|
<div v-show="showDetail" class="detail-panel">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -139,17 +178,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Heartbeat Interval</label>
|
<label>Heartbeat Interval</label>
|
||||||
<input type="number" v-model="globalConfig.heartbeatInterval" />
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="globalConfig.heartbeatInterval"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Heartbeat Timeout</label>
|
<label>Heartbeat Timeout</label>
|
||||||
<input type="number" v-model="globalConfig.heartbeatTimeout" />
|
<input
|
||||||
|
type="number"
|
||||||
|
v-model="globalConfig.heartbeatTimeout"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Pool Count</label>
|
<label>Pool Count</label>
|
||||||
<input type="number" v-model="globalConfig.poolCount" />
|
<input type="number" v-model="globalConfig.poolCount" />
|
||||||
</div>
|
</div>
|
||||||
<button class="save-btn" @click="saveConfig">保存配置并热加载</button>
|
<button class="save-btn" @click="saveConfig">
|
||||||
|
保存配置并热加载
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,37 +204,69 @@
|
|||||||
<div v-else-if="activeTab === 'proxies'" class="tab-content">
|
<div v-else-if="activeTab === 'proxies'" class="tab-content">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<button class="btn-import" @click="triggerImport">导入 TOML</button>
|
<button class="btn-import" @click="triggerImport">
|
||||||
<button class="btn-export" @click="exportToml">导出 TOML</button>
|
导入 TOML
|
||||||
<button class="btn-add" @click="openAddDialog">+ 新增隧道</button>
|
</button>
|
||||||
|
<button class="btn-export" @click="exportToml">
|
||||||
|
导出 TOML
|
||||||
|
</button>
|
||||||
|
<button class="btn-add" @click="openAddDialog">
|
||||||
|
+ 新增隧道
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<input class="search-input" placeholder="搜索隧道..." v-model="searchKeyword" />
|
<input
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索隧道..."
|
||||||
|
v-model="searchKeyword"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 隧道卡片列表 -->
|
<!-- 隧道卡片列表 -->
|
||||||
<div class="proxy-list">
|
<div class="proxy-list">
|
||||||
<div v-for="p in filteredProxies" :key="p.id" class="proxy-card">
|
<div
|
||||||
|
v-for="p in filteredProxies"
|
||||||
|
:key="p.id"
|
||||||
|
class="proxy-card"
|
||||||
|
>
|
||||||
<div class="proxy-row">
|
<div class="proxy-row">
|
||||||
<div class="proxy-left">
|
<div class="proxy-left">
|
||||||
<span class="status-dot" :class="{ active: p.enabled }"></span>
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:class="{ active: p.enabled }"
|
||||||
|
></span>
|
||||||
<span class="proxy-name">{{ p.name }}</span>
|
<span class="proxy-name">{{ p.name }}</span>
|
||||||
<span class="proxy-addr">{{ p.localIP }}:{{ p.localPort }}</span>
|
<span class="proxy-addr"
|
||||||
|
>{{ p.localIP }}:{{ p.localPort }}</span
|
||||||
|
>
|
||||||
<span class="proxy-tag">{{ p.type }}</span>
|
<span class="proxy-tag">{{ p.type }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="proxy-right">
|
<div class="proxy-right">
|
||||||
<span class="remote-port">{{ p.remotePort }}</span>
|
<span class="remote-port">{{ p.remotePort }}</span>
|
||||||
<button class="icon-btn" @click="openEditDialog(p)">✎</button>
|
<button class="icon-btn" @click="openEditDialog(p)">
|
||||||
<button class="icon-btn danger" @click="deleteProxy(p.id)">✕</button>
|
✎
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="icon-btn danger"
|
||||||
|
@click="deleteProxy(p.id)"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="proxy-footer">
|
<div class="proxy-footer">
|
||||||
<label class="switch small">
|
<label class="switch small">
|
||||||
<input type="checkbox" v-model="p.enabled" @change="toggleProxy(p)" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="p.enabled"
|
||||||
|
@change="toggleProxy(p)"
|
||||||
|
/>
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
<span class="status-label">{{ p.enabled ? '已启用' : '已禁用' }}</span>
|
<span class="status-label"
|
||||||
|
>{{ p.enabled ? '已启用' : '已禁用' }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredProxies.length === 0" class="empty-state">
|
<div v-if="filteredProxies.length === 0" class="empty-state">
|
||||||
@@ -200,7 +279,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 新增/编辑弹窗 ====== -->
|
<!-- ====== 新增/编辑弹窗 ====== -->
|
||||||
<div v-if="dialogVisible" class="dialog-overlay" @click.self="dialogVisible = false">
|
<div
|
||||||
|
v-if="dialogVisible"
|
||||||
|
class="dialog-overlay"
|
||||||
|
@click.self="dialogVisible = false"
|
||||||
|
>
|
||||||
<div class="dialog-card">
|
<div class="dialog-card">
|
||||||
<h3>{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}</h3>
|
<h3>{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}</h3>
|
||||||
<div class="dialog-form">
|
<div class="dialog-form">
|
||||||
@@ -231,17 +314,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-actions">
|
<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>
|
<button class="btn-confirm" @click="confirmDialog">确认</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 隐藏的文件输入(导入 TOML) ====== -->
|
<!-- ====== 隐藏的文件输入(导入 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>
|
</div>
|
||||||
|
|
||||||
<!-- 应用逻辑 -->
|
<!-- 应用逻辑 -->
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+53
-15
@@ -40,14 +40,14 @@ body {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
box-shadow: 0 40px 80px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录态尺寸:800x650 */
|
/* 登录态尺寸:800x650 */
|
||||||
.app-container:not(.is-logged-in) {
|
.app-container:not(.is-logged-in) {
|
||||||
width: 800px;
|
width: 600px;
|
||||||
height: 650px;
|
height: 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录后尺寸:1600x900(响应式) */
|
/* 登录后尺寸:1600x900(响应式) */
|
||||||
@@ -64,34 +64,34 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 48px 64px;
|
padding: 32px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-icon {
|
.login-icon {
|
||||||
font-size: 36px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header h1 {
|
.login-header h1 {
|
||||||
font-size: 28px;
|
font-size: 22px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: 6px;
|
letter-spacing: 4px;
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 340px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
@@ -108,13 +108,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-group input {
|
.input-group input {
|
||||||
height: 56px;
|
height: 44px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -128,12 +128,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
height: 56px;
|
height: 44px;
|
||||||
margin-top: 12px;
|
margin-top: 6px;
|
||||||
background: linear-gradient(135deg, #63e2b7, #3b9e8a);
|
background: linear-gradient(135deg, #63e2b7, #3b9e8a);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #0a0a0f;
|
color: #0a0a0f;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -774,3 +774,41 @@ body {
|
|||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
box-shadow: 0 4px 16px rgba(99, 226, 183, 0.2);
|
box-shadow: 0 4px 16px rgba(99, 226, 183, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 旋转圈过渡 ===== */
|
||||||
|
.loading-ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-top: 3px solid #63e2b7;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-ring.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容默认透明,登录后淡入 */
|
||||||
|
.main-panel {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-panel.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user