fix: 修复暗色主题UI对比度、端口占用死循环及多项Web表单bug

后端修复:
- engine/instances.py: Socks5Server.start() 端口占用时死循环, 增加超时和异常透传
- engine/instances.py: import time 移至顶层
- engine/instances.py: sync_instances/start_instance 容忍单实例启动失败
- run.py: 启动时 sync_instances 失败不退出进程
- web/routes.py: instances start/restart/edit 路由捕获 OSError, flash 友好提示
- api/v1.py: instances start/restart/update API 返回 JSON 错误而非 500 崩溃
- web/routes.py: toggle_user/ban_user 支持 hidden 字段语义, 缺字段时切换状态
- web/routes.py: inject_nav 增加独立 nav.dashboard 和 page_name, 修复双 active bug
- auth.py: login 路由添加 logging import, 修 NameError

前端重构:
- templates/base.html: 全局对比度提升(--text #f1f5f9, --accent #a5b4fc), 标题色显式声明, alert/modal/progress/table 完整暗色样式
- templates/base.html: main-content padding-top 修复 navbar 遮挡内容
- templates/base.html: 侧边栏 active 高亮 (白字 + elevated bg + 左边框)
- templates/base.html: 顶栏动态显示当前页面名
- templates/*.html: 统一 page-header 类, 所有图标按钮添加 title tooltip
- templates/users.html: toggle/ban 表单添加 hidden enabled/ban 字段
- templates/connections.html: fmtBytes 改为 Jinja macro, 时间格式化
- templates/dashboard.html: 时间格式化为 YYYY-MM-DD HH:MM:SS, chart canvas 高度调整
- templates/logs.html: 时间格式化为可读格式
- templates/stats.html: 提升 chart 颜色对比度
- templates/system.html: 使用 time.localtime() 显示本地时间
This commit is contained in:
Your Name
2026-07-16 23:01:39 +08:00
parent 6504726431
commit 0bd9bcac12
13 changed files with 268 additions and 103 deletions
+20 -9
View File
@@ -106,9 +106,12 @@ def update_instance(iid):
# 重启以应用配置 # 重启以应用配置
if inst.running: if inst.running:
current_app.instance_manager.restart_instance(iid) try:
current_app.instance_manager.restart_instance(iid)
except OSError as e:
return {"error": f"重启失败(端口可能被占用): {e}"}, 500
return jsonify({"ok": True}) return {"ok": True}
@bp.route("/instances/<int:iid>", methods=["DELETE"]) @bp.route("/instances/<int:iid>", methods=["DELETE"])
@@ -126,25 +129,33 @@ def delete_instance(iid):
@bp.route("/instances/<int:iid>/start", methods=["POST"]) @bp.route("/instances/<int:iid>/start", methods=["POST"])
@login_required @login_required
def start_instance(iid): def start_instance(iid):
if current_app.instance_manager.start_instance(iid): try:
return jsonify({"ok": True}) ok = current_app.instance_manager.start_instance(iid)
return {"error": "启动失败"}, 500 if not ok:
return {"error": "启动失败"}, 500
return {"ok": True}
except OSError as e:
return {"error": f"启动失败: {e}"}, 500
@bp.route("/instances/<int:iid>/stop", methods=["POST"]) @bp.route("/instances/<int:iid>/stop", methods=["POST"])
@login_required @login_required
def stop_instance(iid): def stop_instance(iid):
if current_app.instance_manager.stop_instance(iid): if current_app.instance_manager.stop_instance(iid):
return jsonify({"ok": True}) return {"ok": True}
return {"error": "停止失败"}, 500 return {"error": "停止失败"}, 500
@bp.route("/instances/<int:iid>/restart", methods=["POST"]) @bp.route("/instances/<int:iid>/restart", methods=["POST"])
@login_required @login_required
def restart_instance(iid): def restart_instance(iid):
if current_app.instance_manager.restart_instance(iid): try:
return jsonify({"ok": True}) ok = current_app.instance_manager.restart_instance(iid)
return {"error": "重启失败"}, 500 if not ok:
return {"error": "重启失败"}, 500
return {"ok": True}
except OSError as e:
return {"error": f"重启失败: {e}"}, 500
@bp.route("/instances/sync", methods=["POST"]) @bp.route("/instances/sync", methods=["POST"])
+2
View File
@@ -1,10 +1,12 @@
"""登录认证。""" """登录认证。"""
import os import os
import hashlib import hashlib
import logging
import secrets import secrets
from functools import wraps from functools import wraps
from flask import Blueprint, request, session, redirect, url_for, current_app from flask import Blueprint, request, session, redirect, url_for, current_app
log = logging.getLogger("socks.auth")
bp = Blueprint("auth", __name__) bp = Blueprint("auth", __name__)
+42 -8
View File
@@ -2,6 +2,7 @@
import asyncio import asyncio
import logging import logging
import threading import threading
import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from engine.server import ConnectionHandler from engine.server import ConnectionHandler
@@ -51,20 +52,41 @@ class Socks5Server:
self._active_connections = 0 self._active_connections = 0
self.running = True self.running = True
self.server = None
self._start_error = None # 用于在主线程透传子线程异常
def run_loop(): def run_loop():
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
self.server = self.loop.run_until_complete( try:
self._create_server() self.server = self.loop.run_until_complete(
) self._create_server()
self.loop.run_forever() )
self.loop.run_forever()
except Exception as e:
# 端口占用等异常:保存异常并停止循环,避免主线程死等
self._start_error = e
log.error("[%s] SOCKS5 服务器启动失败: %s",
self.config.name, e)
try:
self.loop.stop()
except Exception:
pass
self.thread = threading.Thread(target=run_loop, daemon=True, self.thread = threading.Thread(target=run_loop, daemon=True,
name=f"socks5-{self.config.name}") name=f"socks5-{self.config.name}")
self.thread.start() self.thread.start()
# 等 server 创建完成 # 等 server 创建完成 (或失败) — 设了上限避免永久死等
while self.server is None and self.running: deadline = time.time() + 5.0
while self.server is None and self.running and self._start_error is None:
time.sleep(0.05) time.sleep(0.05)
if time.time() > deadline:
log.error("[%s] SOCKS5 服务器启动超时", self.config.name)
self.running = False
break
# 如果子线程失败, 把异常抛回调用方
if self._start_error is not None:
self.running = False
raise self._start_error
if self.server: if self.server:
log.info("[%s] SOCKS5 服务器已启动: %s:%d", log.info("[%s] SOCKS5 服务器已启动: %s:%d",
self.config.name, self.config.listen_host, self.config.listen_port) self.config.name, self.config.listen_host, self.config.listen_port)
@@ -157,7 +179,11 @@ class InstanceManager:
for name in current_names - running_names: for name in current_names - running_names:
cfg = self._config_cache[name] cfg = self._config_cache[name]
srv = Socks5Server(cfg, self.user_service, self.flask_app) srv = Socks5Server(cfg, self.user_service, self.flask_app)
srv.start() try:
srv.start()
except OSError as e:
log.error("[%s] 跳过启动(端口冲突?: %s", name, e)
continue
with self._lock: with self._lock:
self._instances[name] = srv self._instances[name] = srv
@@ -198,7 +224,15 @@ class InstanceManager:
) )
self._config_cache[inst.name] = cfg self._config_cache[inst.name] = cfg
srv = Socks5Server(cfg, self.user_service, self.flask_app) srv = Socks5Server(cfg, self.user_service, self.flask_app)
srv.start() try:
srv.start()
except OSError as e:
# 端口占用等启动失败: 回滚状态, 清理缓存, 返回 False
log.error("[%s] 启动实例失败: %s", inst.name, e)
self._config_cache.pop(inst.name, None)
inst.running = False
db.session.commit()
raise
with self._lock: with self._lock:
self._instances[inst.name] = srv self._instances[inst.name] = srv
inst.running = True inst.running = True
+4 -1
View File
@@ -12,7 +12,10 @@ if __name__ == "__main__":
debug = os.environ.get("SM_DEBUG", "false").lower() == "true" debug = os.environ.get("SM_DEBUG", "false").lower() == "true"
# 启动时同步实例 # 启动时同步实例
app.instance_manager.sync_instances() try:
app.instance_manager.sync_instances()
except OSError as e:
print(f"⚠️ 部分实例启动失败(端口冲突?): {e}", flush=True)
# 记录流量快照 # 记录流量快照
from services.stats_service import record_traffic_snapshot from services.stats_service import record_traffic_snapshot
+75 -43
View File
@@ -11,21 +11,31 @@
/* ── 全局 ─────────────────────────────────────────────────────── */ /* ── 全局 ─────────────────────────────────────────────────────── */
:root { :root {
--bg-primary:#0b0e14; --bg-secondary:#161922; --bg-tertiary:#1e2235; --bg-primary:#0b0e14; --bg-secondary:#161922; --bg-tertiary:#1e2235;
--border:#374151; --text:#e2e8f0; --text-muted:#9ca3af; --bg-elevated:#1f2433;
--accent:#818cf8; --accent-hover:#6366f1; --border:#3a4254; --border-strong:#4b5563;
--green:#10b981; --red:#ef4444; --yellow:#f59e0b; --gray:#6b7280; --text:#f1f5f9; /* 提高主文字亮度, 之前 #e2e8f0 在小字号下偏暗 */
--text-muted:#a3acbf; /* 之前 #9ca3af, 稍微提亮 */
--accent:#a5b4fc; /* 提亮主色, 之前 #818cf8 对比度太低 */
--accent-hover:#818cf8;
--accent-dim:rgba(165,180,252,.15);
--green:#34d399; --red:#f87171; --yellow:#fbbf24; --gray:#94a3b8;
--card-bg:#161922; --input-bg:#0b0e14; --card-bg:#161922; --input-bg:#0b0e14;
} }
* { box-sizing:border-box; } * { box-sizing:border-box; }
body { background:var(--bg-primary); color:var(--text); body { background:var(--bg-primary); color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
font-size:16px; line-height:1.7; } font-size:15px; line-height:1.6; }
a { color:var(--accent); text-decoration:none; } a { color:var(--accent); text-decoration:none; }
a:hover { color:var(--accent-hover); } a:hover { color:var(--accent-hover); }
/* 显式覆盖 Bootstrap 默认的标题色 (默认是 inherit, 但某些情况下浏览器渲染为黑) */
h1, h2, h3, h4, h5, h6 { color:var(--text); margin-top:0; }
.h1, .h2, .h3, .h4, .h5, .h6 { color:var(--text); }
.text-secondary, .text-body { color:var(--text) !important; }
/* ── 导航 ─────────────────────────────────────────────────────── */ /* ── 导航 ─────────────────────────────────────────────────────── */
.navbar { background:var(--bg-secondary)!important; border-bottom:1px solid var(--border)!important; .navbar { background:var(--bg-secondary)!important; border-bottom:1px solid var(--border)!important;
padding:.6rem 1rem; } padding:.6rem 1rem; height:56px; }
.navbar-brand { color:var(--accent)!important; font-weight:700; font-size:1.1rem; } .navbar-brand { color:var(--accent)!important; font-weight:700; font-size:1.1rem; }
.navbar-brand i { margin-right:6px; } .navbar-brand i { margin-right:6px; }
.nav-link { color:var(--text-muted)!important; font-size:14px; padding:.5rem 1rem!important; } .nav-link { color:var(--text-muted)!important; font-size:14px; padding:.5rem 1rem!important; }
@@ -36,70 +46,78 @@ a:hover { color:var(--accent-hover); }
background:var(--bg-secondary); border-right:1px solid var(--border); padding:.5rem 0; z-index:100; } background:var(--bg-secondary); border-right:1px solid var(--border); padding:.5rem 0; z-index:100; }
.sidebar .nav-link { padding:.55rem 1.2rem; border-radius:0 .4rem .4rem 0; margin:.15rem .6rem; font-size:14px; } .sidebar .nav-link { padding:.55rem 1.2rem; border-radius:0 .4rem .4rem 0; margin:.15rem .6rem; font-size:14px; }
.sidebar .nav-link:hover { background:var(--bg-tertiary); color:var(--accent)!important; } .sidebar .nav-link:hover { background:var(--bg-tertiary); color:var(--accent)!important; }
.sidebar .nav-link.active { background:var(--bg-tertiary); color:var(--accent)!important; .sidebar .nav-link.active { background:var(--bg-elevated); color:#fff!important;
border-left:3px solid var(--accent); } border-left:3px solid var(--accent); font-weight:600; }
.sidebar .nav-link i { margin-right:10px; width:18px; text-align:center; } .sidebar .nav-link i { margin-right:10px; width:18px; text-align:center; }
.sidebar .sidebar-section { padding:.3rem 1.2rem .1rem; font-size:11px; color:var(--text-muted); .sidebar .sidebar-section { padding:.3rem 1.2rem .1rem; font-size:11px; color:var(--text-muted);
text-transform:uppercase; letter-spacing:.05em; margin-top:.8rem; } text-transform:uppercase; letter-spacing:.05em; margin-top:.8rem; }
/* ── 主内容 ───────────────────────────────────────────────────── */ /* ── 主内容 ───────────────────────────────────────────────────── */
.main-content { margin-left:220px; padding:1.2rem 1.5rem; min-height:calc(100vh - 56px); } .main-content { margin-left:220px; padding:1.2rem 1.5rem; min-height:calc(100vh - 56px); padding-top:calc(56px + 1.2rem); }
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; }
.page-header h5 { margin:0; color:var(--text); font-size:1.1rem; font-weight:600; }
/* ── 卡片 ─────────────────────────────────────────────────────── */ /* ── 卡片 ─────────────────────────────────────────────────────── */
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; margin-bottom:1rem; } .card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; margin-bottom:1rem; }
.card-header { background:transparent; border-bottom:1px solid var(--border); padding:.8rem 1rem; } .card-header { background:transparent; border-bottom:1px solid var(--border); padding:.8rem 1rem;
.card-body { padding:1rem; } color:var(--text); font-weight:600; }
.card-header i { color:var(--accent); margin-right:.4rem; }
.card-body { padding:1rem; color:var(--text); }
.card-footer { background:transparent; border-top:1px solid var(--border); } .card-footer { background:transparent; border-top:1px solid var(--border); }
/* ── 表格 ─────────────────────────────────────────────────────── */ /* ── 表格 ─────────────────────────────────────────────────────── */
.table { color:var(--text); margin-bottom:0; } .table { color:var(--text); margin-bottom:0; }
.table thead th { background:var(--bg-tertiary); color:var(--text-muted); font-weight:600; .table thead th { background:var(--bg-tertiary); color:#cbd5e1; font-weight:600;
font-size:12px; text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--border); font-size:12px; text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--border);
padding:.65rem .8rem; } padding:.7rem .8rem; }
.table tbody td { padding:.6rem .8rem; border-bottom:1px solid var(--border); font-size:14px; .table tbody td { padding:.65rem .8rem; border-bottom:1px solid var(--border); font-size:14px;
vertical-align:middle; } vertical-align:middle; color:var(--text); }
.table tbody tr:hover { background:var(--bg-tertiary); } .table tbody tr:hover { background:var(--bg-tertiary); }
.table-dark { background:var(--card-bg); } .table-dark { background:var(--card-bg); }
.table-dark th, .table-dark td { color:var(--text); border-color:var(--border); }
.table code { background:var(--bg-tertiary); color:var(--accent); }
/* ── 表单 ─────────────────────────────────────────────────────── */ /* ── 表单 ─────────────────────────────────────────────────────── */
.form-control, .form-select { background:#1a1d2b; border:1px solid #4b5563; .form-control, .form-select { background:#1a1d2b; border:1px solid var(--border-strong);
color:var(--text); border-radius:.4rem; font-size:14px; padding:.5rem .7rem; } color:var(--text); border-radius:.4rem; font-size:14px; padding:.5rem .7rem; }
.form-control:focus, .form-select:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(129,140,248,.12); .form-control:focus, .form-select:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(165,180,252,.15);
background:#1a1d2b; color:var(--text); } background:#1a1d2b; color:var(--text); }
.form-control::placeholder { color:#5a6178; } .form-control::placeholder { color:#6b7592; }
.form-text { color:var(--text-muted); font-size:12px; } .form-text { color:var(--text-muted); font-size:12px; }
.form-check-input { background-color:var(--input-bg); border-color:var(--border); } .form-label { color:#d1d9e6 !important; font-size:13px; margin-bottom:.3rem; font-weight:500; }
.form-check-input { background-color:var(--input-bg); border-color:var(--border-strong); }
.form-check-input:checked { background-color:var(--accent); border-color:var(--accent); } .form-check-input:checked { background-color:var(--accent); border-color:var(--accent); }
/* ── 按钮 ─────────────────────────────────────────────────────── */ /* ── 按钮 ─────────────────────────────────────────────────────── */
.btn-primary { background:var(--accent); border-color:var(--accent); color:#fff; } .btn-primary { background:var(--accent); border-color:var(--accent); color:#0b0e14; font-weight:600; }
.btn-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); color:#fff; } .btn-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); color:#0b0e14; }
.btn-primary:active { background:var(--accent-hover); } .btn-primary:active { background:var(--accent-hover); }
.btn-primary:focus { box-shadow:0 0 0 3px rgba(129,140,248,.3); } .btn-primary:focus { box-shadow:0 0 0 3px rgba(165,180,252,.3); }
.btn-outline-primary { color:var(--accent); border-color:var(--accent); } .btn-outline-primary { color:var(--accent); border-color:var(--accent); }
.btn-outline-primary:hover { background:var(--accent); border-color:var(--accent); } .btn-outline-primary:hover { background:var(--accent); border-color:var(--accent); color:#0b0e14; }
.btn-outline-secondary { color:var(--text-muted); border-color:var(--border); } .btn-outline-secondary { color:var(--text-muted); border-color:var(--border-strong); }
.btn-outline-secondary:hover { color:var(--text); border-color:var(--text-muted); background:var(--bg-tertiary); } .btn-outline-secondary:hover { color:var(--text); border-color:var(--text-muted); background:var(--bg-tertiary); }
.btn-outline-danger { color:var(--red); border-color:var(--red); } .btn-outline-danger { color:var(--red); border-color:var(--red); }
.btn-outline-danger:hover { background:var(--red); border-color:var(--red); color:#fff; } .btn-outline-danger:hover { background:var(--red); border-color:var(--red); color:#fff; }
.btn-outline-success { color:var(--green); border-color:var(--green); } .btn-outline-success { color:var(--green); border-color:var(--green); }
.btn-outline-success:hover { background:var(--green); border-color:var(--green); color:#fff; } .btn-outline-success:hover { background:var(--green); border-color:var(--green); color:#0b0e14; }
.btn-outline-warning { color:var(--yellow); border-color:var(--yellow); } .btn-outline-warning { color:var(--yellow); border-color:var(--yellow); }
.btn-outline-warning:hover { background:var(--yellow); border-color:var(--yellow); color:#000; } .btn-outline-warning:hover { background:var(--yellow); border-color:var(--yellow); color:#0b0e14; }
.btn-sm { font-size:13px; padding:.3rem .6rem; } .btn-sm { font-size:13px; padding:.3rem .6rem; }
.btn-group-sm > .btn { padding:.25rem .55rem; }
/* ── 徽章 ─────────────────────────────────────────────────────── */ /* ── 徽章 ─────────────────────────────────────────────────────── */
.badge { font-size:11px; padding:.3em .6em; border-radius:.3rem; } .badge { font-size:11px; padding:.3em .6em; border-radius:.3rem; font-weight:600; }
.badge-success { background:rgba(16,185,129,.15); color:var(--green); } .badge-success { background:rgba(52,211,153,.18); color:#34d399; }
.badge-danger { background:rgba(239,68,68,.15); color:var(--red); } .badge-danger { background:rgba(248,113,113,.18); color:#f87171; }
.badge-warning { background:rgba(245,158,11,.15); color:var(--yellow); } .badge-warning { background:rgba(251,191,36,.18); color:#fbbf24; }
.badge-secondary { background:rgba(107,114,128,.15); color:var(--gray); } .badge-secondary { background:rgba(148,163,184,.18); color:#cbd5e1; }
.badge-info { background:rgba(129,140,248,.15); color:var(--accent); } .badge-info { background:var(--accent-dim); color:var(--accent); }
/* ── 状态点 ───────────────────────────────────────────────────── */ /* ── 状态点 ───────────────────────────────────────────────────── */
.status-dot { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px; .status-dot { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px;
vertical-align:middle; } vertical-align:middle; }
.dot-online { background:var(--green); box-shadow:0 0 6px rgba(16,185,129,.6); } .dot-online { background:var(--green); box-shadow:0 0 6px rgba(52,211,153,.6); }
.dot-offline { background:var(--red); } .dot-offline { background:var(--red); }
.dot-warning { background:var(--yellow); } .dot-warning { background:var(--yellow); }
.dot-gray { background:var(--gray); } .dot-gray { background:var(--gray); }
@@ -107,7 +125,7 @@ a:hover { color:var(--accent-hover); }
/* ── 统计卡片 ─────────────────────────────────────────────────── */ /* ── 统计卡片 ─────────────────────────────────────────────────── */
.stat-card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; padding:1rem; } .stat-card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; padding:1rem; }
.stat-card .stat-label { color:var(--text-muted); font-size:12px; text-transform:uppercase; letter-spacing:.04em; } .stat-card .stat-label { color:var(--text-muted); font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.stat-card .stat-value { font-size:1.8rem; font-weight:700; margin:.2rem 0; } .stat-card .stat-value { font-size:1.8rem; font-weight:700; margin:.2rem 0; color:var(--text); }
.stat-card .stat-sub { color:var(--text-muted); font-size:12px; } .stat-card .stat-sub { color:var(--text-muted); font-size:12px; }
/* ── 表格代码 ─────────────────────────────────────────────────── */ /* ── 表格代码 ─────────────────────────────────────────────────── */
@@ -117,35 +135,49 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
/* ── 分页 ─────────────────────────────────────────────────────── */ /* ── 分页 ─────────────────────────────────────────────────────── */
.pagination { margin-bottom:0; } .pagination { margin-bottom:0; }
.page-link { color:var(--accent); background:var(--card-bg); border-color:var(--border); } .page-link { color:var(--accent); background:var(--card-bg); border-color:var(--border); }
.page-link:hover { background:var(--bg-tertiary); color:var(--accent); } .page-link:hover { background:var(--bg-tertiary); color:var(--accent-hover); }
.page-item.active .page-link { background:var(--accent); border-color:var(--accent); color:#fff; } .page-item.active .page-link { background:var(--accent); border-color:var(--accent); color:#0b0e14; }
.page-item.disabled .page-link { color:var(--gray); } .page-item.disabled .page-link { color:var(--gray); }
/* ── 进度条 ──────────────────────────────────────────────────── */
.progress { background:var(--bg-tertiary); }
.progress-bar { font-weight:600; }
/* ── 模态框 ───────────────────────────────────────────────────── */ /* ── 模态框 ───────────────────────────────────────────────────── */
.modal-content { background:var(--card-bg); border:1px solid var(--border); color:var(--text); } .modal-content { background:var(--card-bg); border:1px solid var(--border-strong); color:var(--text); }
.modal-header { border-bottom:1px solid var(--border); } .modal-header { border-bottom:1px solid var(--border); }
.modal-header .modal-title { color:var(--text); font-weight:600; }
.modal-footer { border-top:1px solid var(--border); } .modal-footer { border-top:1px solid var(--border); }
.btn-close { filter:invert(1); } .btn-close { filter:invert(1) grayscale(100%) brightness(200%); }
/* ── Alert ─────────────────────────────────────────────────────── */
.alert { border:1px solid var(--border); color:var(--text); background:var(--card-bg); }
.alert-success { background:rgba(52,211,153,.12); border-color:rgba(52,211,153,.4); color:#34d399; }
.alert-danger { background:rgba(248,113,113,.12); border-color:rgba(248,113,113,.4); color:#f87171; }
.alert-warning { background:rgba(251,191,36,.12); border-color:rgba(251,191,36,.4); color:#fbbf24; }
.alert-info { background:var(--accent-dim); border-color:rgba(165,180,252,.4); color:var(--accent); }
/* ── 消息 ─────────────────────────────────────────────────────── */ /* ── 消息 ─────────────────────────────────────────────────────── */
.toast { background:var(--card-bg); border:1px solid var(--border); color:var(--text); } .toast { background:var(--card-bg); border:1px solid var(--border); color:var(--text); }
.toast-container { position:fixed; top:1rem; right:1rem; z-index:1060; } .toast-container { position:fixed; top:1rem; right:1rem; z-index:1060; }
/* ── Chart.js canvas 容器 ─────────────────────────────────────── */
canvas { max-width:100%; }
/* ── 响应式 ───────────────────────────────────────────────────── */ /* ── 响应式 ───────────────────────────────────────────────────── */
@media(max-width:768px){ @media(max-width:768px){
.sidebar { display:none; } .sidebar { display:none; }
.main-content { margin-left:0; padding:1rem; } .main-content { margin-left:0; padding:1rem; padding-top:calc(56px + 1rem); }
} }
/* ── 滚动条 ───────────────────────────────────────────────────── */ /* ── 滚动条 ───────────────────────────────────────────────────── */
::-webkit-scrollbar { width:6px; height:6px; } ::-webkit-scrollbar { width:8px; height:8px; }
::-webkit-scrollbar-track { background:var(--bg-primary); } ::-webkit-scrollbar-track { background:var(--bg-primary); }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; } ::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:4px; }
::-webkit-scrollbar-thumb:hover { background:var(--gray); } ::-webkit-scrollbar-thumb:hover { background:var(--gray); }
/* ── 覆盖 Bootstrap text-muted 提高深色背景对比度 ─────────────── */ /* ── 覆盖 Bootstrap text-muted 提高深色背景对比度 ─────────────── */
.text-muted { color:var(--text-muted) !important; } .text-muted { color:var(--text-muted) !important; }
.form-label { color:#cbd5e0 !important; font-size:13px; margin-bottom:.3rem; font-weight:500; }
</style> </style>
{% block extra_style %}{% endblock %} {% block extra_style %}{% endblock %}
</head> </head>
@@ -155,7 +187,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"><i class="bi bi-hdd-rack"></i> SOCKS Manager</a> <a class="navbar-brand" href="/"><i class="bi bi-hdd-rack"></i> SOCKS Manager</a>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
{% if nav.instances %}<span class="text-muted small">仪表盘</span>{% endif %} {% if page_name %}<span class="text-muted small">{{ page_name }}</span>{% endif %}
<a href="/logout" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-right"></i> 退出</a> <a href="/logout" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-right"></i> 退出</a>
</div> </div>
</div> </div>
@@ -165,7 +197,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
<div class="sidebar"> <div class="sidebar">
<div class="nav flex-column"> <div class="nav flex-column">
<div class="sidebar-section">概览</div> <div class="sidebar-section">概览</div>
<a class="nav-link {% if nav.instances %}active{% endif %}" href="/dashboard"> <a class="nav-link {% if nav.dashboard %}active{% endif %}" href="/dashboard">
<i class="bi bi-speedometer2"></i> 仪表盘</a> <i class="bi bi-speedometer2"></i> 仪表盘</a>
<a class="nav-link {% if nav.instances %}active{% endif %}" href="/instances"> <a class="nav-link {% if nav.instances %}active{% endif %}" href="/instances">
<i class="bi bi-server"></i> 代理实例</a> <i class="bi bi-server"></i> 代理实例</a>
+18 -2
View File
@@ -1,8 +1,24 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}活跃连接 · SOCKS Manager{% endblock %} {% block title %}活跃连接 · SOCKS Manager{% endblock %}
{% macro fmtBytes(b) -%}
{%- if not b %}0 B
{%- else -%}
{%- set u = ['B','KB','MB','GB','TB'] -%}
{%- set ns = namespace(v=b, i=0) -%}
{%- for _ in range(4) -%}
{%- if ns.v >= 1024 and ns.i < 4 -%}
{%- set ns.v = ns.v / 1024 -%}
{%- set ns.i = ns.i + 1 -%}
{%- endif -%}
{%- endfor -%}
{{ '%.1f'|format(ns.v) }} {{ u[ns.i] }}
{%- endif -%}
{%- endmacro %}
{% block content %} {% block content %}
<h5 class="mb-3"><i class="bi bi-activity"></i> 活跃连接 <span class="text-muted small">({{ conns|length }} 条)</span></h5> <div class="page-header">
<h5><i class="bi bi-activity"></i> 活跃连接 <span class="text-muted small">({{ conns|length }} 条)</span></h5>
</div>
<div class="card"> <div class="card">
<div class="card-body p-0"> <div class="card-body p-0">
@@ -20,7 +36,7 @@
<td><code>{{ c.src }}</code></td> <td><code>{{ c.src }}</code></td>
<td><code>{{ c.dst }}</code></td> <td><code>{{ c.dst }}</code></td>
<td><span class="badge badge-info">{{ c.protocol }}</span></td> <td><span class="badge badge-info">{{ c.protocol }}</span></td>
<td><small class="text-muted">{{ c.started }}</small></td> <td><small class="text-muted">{{ c.started[:19].replace('T',' ') if c.started else '-' }}</small></td>
<td>{{ fmtBytes(c.bytes_in) }}</td> <td>{{ fmtBytes(c.bytes_in) }}</td>
<td>{{ fmtBytes(c.bytes_out) }}</td> <td>{{ fmtBytes(c.bytes_out) }}</td>
<td> <td>
+14 -6
View File
@@ -2,7 +2,9 @@
{% block title %}仪表盘 · SOCKS Manager{% endblock %} {% block title %}仪表盘 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<h5 class="mb-3"><i class="bi bi-speedometer2"></i> 仪表盘</h5> <div class="page-header">
<h5><i class="bi bi-speedometer2"></i> 仪表盘</h5>
</div>
<!-- 系统状态卡片 --> <!-- 系统状态卡片 -->
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
@@ -75,7 +77,7 @@
<span class="text-muted small">每 10 秒刷新</span> <span class="text-muted small">每 10 秒刷新</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<canvas id="metricsChart" height="80"></canvas> <canvas id="metricsChart" style="height:240px"></canvas>
</div> </div>
</div> </div>
@@ -118,14 +120,20 @@ const chart = new Chart(ctx, {
maintainAspectRatio: false, maintainAspectRatio: false,
animation: false, animation: false,
plugins: { plugins: {
legend: { labels: { color: '#94a3b8' } }, legend: { labels: { color: '#cbd5e1' } },
}, },
scales: { scales: {
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } }, x: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf', maxTicksLimit: 10 } },
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' }, min: 0, max: 100 }, y: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf' }, min: 0, max: 100 },
}, },
} }
}); });
// 工具函数
function fmtTs(s) {
if (!s) return '-';
// 把 ISO 格式 "2026-07-16T14:33:38.297700" 转成 "2026-07-16 14:33:38"
return s.replace('T', ' ').slice(0, 19);
}
// 加载最近日志 // 加载最近日志
async function loadRecentLogs() { async function loadRecentLogs() {
@@ -144,7 +152,7 @@ async function loadRecentLogs() {
else if (l.event.includes('fail') || l.event.includes('reject')) badgeClass = 'danger'; else if (l.event.includes('fail') || l.event.includes('reject')) badgeClass = 'danger';
else if (l.event.includes('start') || l.event.includes('stop')) badgeClass = 'warning'; else if (l.event.includes('start') || l.event.includes('stop')) badgeClass = 'warning';
return `<tr> return `<tr>
<td><small>${l.timestamp.slice(0,19).replace('T',' ')}</small></td> <td><small>${fmtTs(l.timestamp)}</small></td>
<td><span class="badge badge-${badgeClass}">${l.event}</span></td> <td><span class="badge badge-${badgeClass}">${l.event}</span></td>
<td>${l.user || '-'}</td> <td>${l.user || '-'}</td>
<td><code>${l.src_ip || '-'}</code></td> <td><code>${l.src_ip || '-'}</code></td>
+7 -7
View File
@@ -2,8 +2,8 @@
{% block title %}代理实例 · SOCKS Manager{% endblock %} {% block title %}代理实例 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="page-header">
<h5 class="mb-0"><i class="bi bi-server"></i> 代理实例</h5> <h5><i class="bi bi-server"></i> 代理实例</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addInstanceModal"> <button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addInstanceModal">
<i class="bi bi-plus-lg"></i> 新建实例</button> <i class="bi bi-plus-lg"></i> 新建实例</button>
</div> </div>
@@ -44,17 +44,17 @@
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
{% if not inst.running %} {% if not inst.running %}
<form method="post" action="/instances/{{ inst.id }}/start" style="display:inline"> <form method="post" action="/instances/{{ inst.id }}/start" style="display:inline">
<button class="btn btn-outline-success"><i class="bi bi-play-fill"></i></button> <button class="btn btn-outline-success" title="启动"><i class="bi bi-play-fill"></i></button>
</form> </form>
{% else %} {% else %}
<form method="post" action="/instances/{{ inst.id }}/stop" style="display:inline"> <form method="post" action="/instances/{{ inst.id }}/stop" style="display:inline">
<button class="btn btn-outline-warning"><i class="bi bi-pause-fill"></i></button> <button class="btn btn-outline-warning" title="停止"><i class="bi bi-pause-fill"></i></button>
</form> </form>
<form method="post" action="/instances/{{ inst.id }}/restart" style="display:inline"> <form method="post" action="/instances/{{ inst.id }}/restart" style="display:inline">
<button class="btn btn-outline-primary"><i class="bi bi-arrow-clockwise"></i></button> <button class="btn btn-outline-primary" title="重启"><i class="bi bi-arrow-clockwise"></i></button>
</form> </form>
{% endif %} {% endif %}
<button class="btn btn-outline-secondary" data-bs-toggle="modal" <button class="btn btn-outline-secondary" title="编辑" data-bs-toggle="modal"
data-bs-target="#editInstanceModal" data-bs-target="#editInstanceModal"
data-name="{{ inst.name }}" data-name="{{ inst.name }}"
data-host="{{ inst.host }}" data-host="{{ inst.host }}"
@@ -70,7 +70,7 @@
</button> </button>
<form method="post" action="/instances/{{ inst.id }}/delete" style="display:inline" <form method="post" action="/instances/{{ inst.id }}/delete" style="display:inline"
onsubmit="return confirmDelete('确认删除实例 {{ inst.name }}')"> onsubmit="return confirmDelete('确认删除实例 {{ inst.name }}')">
<button class="btn btn-outline-danger"><i class="bi bi-trash"></i></button> <button class="btn btn-outline-danger" title="删除"><i class="bi bi-trash"></i></button>
</form> </form>
</div> </div>
</td> </td>
+4 -2
View File
@@ -2,7 +2,9 @@
{% block title %}审计日志 · SOCKS Manager{% endblock %} {% block title %}审计日志 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<h5 class="mb-3"><i class="bi bi-journal-text"></i> 审计日志 <span class="text-muted small">({{ total }} 条)</span></h5> <div class="page-header">
<h5><i class="bi bi-journal-text"></i> 审计日志 <span class="text-muted small">({{ total }} 条)</span></h5>
</div>
<!-- 筛选 --> <!-- 筛选 -->
<div class="card mb-3"> <div class="card mb-3">
@@ -43,7 +45,7 @@
<tbody> <tbody>
{% for l in logs %} {% for l in logs %}
<tr> <tr>
<td><small>{{ l.timestamp }}</small></td> <td><small>{{ l.timestamp[:19].replace('T',' ') if l.timestamp else '-' }}</small></td>
<td> <td>
<span class="badge badge-{{ <span class="badge badge-{{
'success' if 'success' in l.event else 'success' if 'success' in l.event else
+6 -4
View File
@@ -2,7 +2,9 @@
{% block title %}流量统计 · SOCKS Manager{% endblock %} {% block title %}流量统计 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<h5 class="mb-3"><i class="bi bi-graph-up"></i> 流量统计</h5> <div class="page-header">
<h5><i class="bi bi-graph-up"></i> 流量统计</h5>
</div>
<!-- 流量趋势图表 --> <!-- 流量趋势图表 -->
<div class="card mb-3"> <div class="card mb-3">
@@ -89,10 +91,10 @@ new Chart(ctx, {
}, },
options: { options: {
responsive: true, maintainAspectRatio: false, responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#94a3b8' } } }, plugins: { legend: { labels: { color: '#cbd5e1' } } },
scales: { scales: {
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280', maxTicksLimit: 15 } }, x: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf', maxTicksLimit: 10 } },
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } }, y: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf' } },
}, },
} }
}); });
+3 -1
View File
@@ -2,7 +2,9 @@
{% block title %}系统管理 · SOCKS Manager{% endblock %} {% block title %}系统管理 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<h5 class="mb-3"><i class="bi bi-shield-lock"></i> 系统管理</h5> <div class="page-header">
<h5><i class="bi bi-shield-lock"></i> 系统管理</h5>
</div>
<!-- 备份管理 --> <!-- 备份管理 -->
<div class="card mb-3"> <div class="card mb-3">
+10 -6
View File
@@ -2,7 +2,7 @@
{% block title %}用户管理 · SOCKS Manager{% endblock %} {% block title %}用户管理 · SOCKS Manager{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="page-header">
<div> <div>
<h5 class="mb-0"><i class="bi bi-people"></i> 用户管理</h5> <h5 class="mb-0"><i class="bi bi-people"></i> 用户管理</h5>
<span class="text-muted small">共 {{ total }} 个用户</span> <span class="text-muted small">共 {{ total }} 个用户</span>
@@ -11,7 +11,7 @@
<form method="get" class="d-flex"> <form method="get" class="d-flex">
<input name="search" class="form-control form-control-sm" placeholder="搜索用户名" <input name="search" class="form-control form-control-sm" placeholder="搜索用户名"
value="{{ search }}" style="width:180px"> value="{{ search }}" style="width:180px">
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="bi bi-search"></i></button> <button type="submit" class="btn btn-outline-secondary btn-sm" title="搜索"><i class="bi bi-search"></i></button>
</form> </form>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal"> <button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-plus-lg"></i> 新建用户</button> <i class="bi bi-plus-lg"></i> 新建用户</button>
@@ -55,7 +55,7 @@
<td><small class="text-muted">{{ u.created_at[:10] }}</small></td> <td><small class="text-muted">{{ u.created_at[:10] }}</small></td>
<td> <td>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editUserModal" <button class="btn btn-outline-secondary" title="编辑" data-bs-toggle="modal" data-bs-target="#editUserModal"
data-id="{{ u.id }}" data-id="{{ u.id }}"
data-username="{{ u.username }}" data-username="{{ u.username }}"
data-bw-down="{{ u.bandwidth_down }}" data-bw-down="{{ u.bandwidth_down }}"
@@ -69,18 +69,22 @@
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</button> </button>
<form method="post" action="/users/{{ u.id }}/toggle" style="display:inline"> <form method="post" action="/users/{{ u.id }}/toggle" style="display:inline">
<button class="btn btn-outline-{{ 'warning' if u.enabled else 'success' }}"> <input type="hidden" name="enabled" value="{{ 'false' if u.enabled else 'true' }}">
<button class="btn btn-outline-{{ 'warning' if u.enabled else 'success' }}"
title="{{ '禁用' if u.enabled else '启用' }}">
<i class="bi bi-{{ 'eye-slash' if u.enabled else 'eye' }}"></i> <i class="bi bi-{{ 'eye-slash' if u.enabled else 'eye' }}"></i>
</button> </button>
</form> </form>
<form method="post" action="/users/{{ u.id }}/ban" style="display:inline"> <form method="post" action="/users/{{ u.id }}/ban" style="display:inline">
<button class="btn btn-outline-{{ 'warning' if not u.banned else 'secondary' }}"> <input type="hidden" name="ban" value="{{ 'false' if u.banned else 'true' }}">
<button class="btn btn-outline-{{ 'warning' if not u.banned else 'secondary' }}"
title="{{ '封禁' if not u.banned else '解封' }}">
<i class="bi bi-{{ 'ban' if not u.banned else 'check-circle' }}"></i> <i class="bi bi-{{ 'ban' if not u.banned else 'check-circle' }}"></i>
</button> </button>
</form> </form>
<form method="post" action="/users/{{ u.id }}/delete" style="display:inline" <form method="post" action="/users/{{ u.id }}/delete" style="display:inline"
onsubmit="return confirmDelete('确认删除用户 {{ u.username }}')"> onsubmit="return confirmDelete('确认删除用户 {{ u.username }}')">
<button class="btn btn-outline-danger"><i class="bi bi-trash"></i></button> <button class="btn btn-outline-danger" title="删除"><i class="bi bi-trash"></i></button>
</form> </form>
</div> </div>
</td> </td>
+63 -14
View File
@@ -12,15 +12,33 @@ bp = Blueprint("web", __name__)
@bp.app_context_processor @bp.app_context_processor
def inject_nav(): def inject_nav():
from flask import session as flask_session from flask import session as flask_session
p = request.path
if p == "/" or p.startswith("/dashboard"):
page_name = "仪表盘"
elif p.startswith("/instances"):
page_name = "代理实例"
elif p.startswith("/users"):
page_name = "用户管理"
elif p == "/stats":
page_name = "流量统计"
elif p.startswith("/stats/connections"):
page_name = "活跃连接"
elif p.startswith("/logs"):
page_name = "审计日志"
elif p.startswith("/system"):
page_name = "系统管理"
else:
page_name = ""
return { return {
"nav": { "nav": {
"instances": request.path.startswith("/instances") or "dashboard": p == "/" or p.startswith("/dashboard"),
request.path == "/" or request.path == "/dashboard", "instances": p.startswith("/instances"),
"users": request.path.startswith("/users"), "users": p.startswith("/users"),
"stats": request.path.startswith("/stats"), "stats": p.startswith("/stats"),
"logs": request.path.startswith("/logs"), "logs": p.startswith("/logs"),
"system": request.path.startswith("/system"), "system": p.startswith("/system"),
}, },
"page_name": page_name,
"logged_in": flask_session.get("logged_in", False), "logged_in": flask_session.get("logged_in", False),
} }
@@ -45,8 +63,11 @@ def instances():
@bp.route("/instances/<int:iid>/start", methods=["POST"]) @bp.route("/instances/<int:iid>/start", methods=["POST"])
@login_required @login_required
def start_instance(iid): def start_instance(iid):
current_app.instance_manager.start_instance(iid) try:
flash("实例已启动", "success") current_app.instance_manager.start_instance(iid)
flash("实例已启动", "success")
except OSError as e:
flash(f"启动失败(端口可能被占用): {e}", "danger")
return redirect(url_for("web.instances")) return redirect(url_for("web.instances"))
@@ -61,8 +82,11 @@ def stop_instance(iid):
@bp.route("/instances/<int:iid>/restart", methods=["POST"]) @bp.route("/instances/<int:iid>/restart", methods=["POST"])
@login_required @login_required
def restart_instance(iid): def restart_instance(iid):
current_app.instance_manager.restart_instance(iid) try:
flash("实例已重启", "info") current_app.instance_manager.restart_instance(iid)
flash("实例已重启", "info")
except OSError as e:
flash(f"重启失败(端口可能被占用): {e}", "danger")
return redirect(url_for("web.instances")) return redirect(url_for("web.instances"))
@@ -116,7 +140,10 @@ def edit_instance(iid):
db.session.commit() db.session.commit()
if inst.running: if inst.running:
current_app.instance_manager.restart_instance(iid) try:
current_app.instance_manager.restart_instance(iid)
except OSError as e:
flash(f"重启失败(端口可能被占用): {e}", "warning")
flash("配置已更新", "success") flash("配置已更新", "success")
return redirect(url_for("web.instances")) return redirect(url_for("web.instances"))
@@ -205,7 +232,19 @@ def delete_user(uid):
@bp.route("/users/<int:uid>/toggle", methods=["POST"]) @bp.route("/users/<int:uid>/toggle", methods=["POST"])
@login_required @login_required
def toggle_user(uid): def toggle_user(uid):
enabled = "enabled" in request.form # 支持两种调用方式:
# 1) 隐藏字段 enabled=on/off (web form)
# 2) 不带字段时切换当前状态 (传统行为, 防止表单无字段导致总是禁用)
if "enabled" in request.form:
enabled = request.form.get("enabled") in ("on", "true", "1", "yes")
else:
from models import User
from database import db
u = db.session.query(User).get(uid)
if not u:
flash("用户不存在", "danger")
return redirect(url_for("web.users"))
enabled = not u.enabled
result = current_app.user_service.toggle_user(uid, enabled) result = current_app.user_service.toggle_user(uid, enabled)
flash("" + ("启用" if enabled else "禁用") + "该用户", "success") flash("" + ("启用" if enabled else "禁用") + "该用户", "success")
return redirect(url_for("web.users")) return redirect(url_for("web.users"))
@@ -214,7 +253,17 @@ def toggle_user(uid):
@bp.route("/users/<int:uid>/ban", methods=["POST"]) @bp.route("/users/<int:uid>/ban", methods=["POST"])
@login_required @login_required
def ban_user(uid): def ban_user(uid):
banned = "ban" in request.form # 同 toggle_user 的两种调用方式
if "ban" in request.form:
banned = request.form.get("ban") in ("on", "true", "1", "yes")
else:
from models import User
from database import db
u = db.session.query(User).get(uid)
if not u:
flash("用户不存在", "danger")
return redirect(url_for("web.users"))
banned = not u.banned
result = current_app.user_service.ban_user(uid, banned) result = current_app.user_service.ban_user(uid, banned)
flash("" + ("封禁" if banned else "解封") + "该用户", "success") flash("" + ("封禁" if banned else "解封") + "该用户", "success")
return redirect(url_for("web.users")) return redirect(url_for("web.users"))
@@ -265,7 +314,7 @@ def system():
sys_version=platform.python_version(), sys_version=platform.python_version(),
sys_platform=platform.platform(), sys_platform=platform.platform(),
sys_hostname=platform.node(), sys_hostname=platform.node(),
uptime=time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())) uptime=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
@bp.route("/system/backup", methods=["POST"]) @bp.route("/system/backup", methods=["POST"])