feat(config): add live slow-sql threshold setting

This commit is contained in:
2026-02-07 14:31:24 +08:00
parent 6a9858cdec
commit b84a5abb8a
28 changed files with 197 additions and 63 deletions

View File

@@ -11,6 +11,7 @@ const loading = ref(false)
const maxConcurrentGlobal = ref(2)
const maxConcurrentPerAccount = ref(1)
const maxScreenshotConcurrent = ref(3)
const dbSlowQueryMs = ref(120)
const proxyEnabled = ref(false)
const proxyApiUrl = ref('')
@@ -67,6 +68,7 @@ async function loadAll() {
maxConcurrentGlobal.value = system.max_concurrent_global ?? 2
maxConcurrentPerAccount.value = system.max_concurrent_per_account ?? 1
maxScreenshotConcurrent.value = system.max_screenshot_concurrent ?? 3
dbSlowQueryMs.value = system.db_slow_query_ms ?? 120
autoApproveEnabled.value = (system.auto_approve_enabled ?? 0) === 1
autoApproveHourlyLimit.value = system.auto_approve_hourly_limit ?? 10
@@ -100,11 +102,12 @@ async function saveConcurrency() {
max_concurrent_global: Number(maxConcurrentGlobal.value),
max_concurrent_per_account: Number(maxConcurrentPerAccount.value),
max_screenshot_concurrent: Number(maxScreenshotConcurrent.value),
db_slow_query_ms: Number(dbSlowQueryMs.value),
}
try {
await ElMessageBox.confirm(
`确定更新并发配置吗?\n\n全局并发数: ${payload.max_concurrent_global}\n单账号并发数: ${payload.max_concurrent_per_account}\n截图并发数: ${payload.max_screenshot_concurrent}`,
`确定更新并发配置吗?\n\n全局并发数: ${payload.max_concurrent_global}\n单账号并发数: ${payload.max_concurrent_per_account}\n截图并发数: ${payload.max_screenshot_concurrent}\n慢 SQL 阈值: ${payload.db_slow_query_ms}ms`,
'保存并发配置',
{ confirmButtonText: '保存', cancelButtonText: '取消', type: 'warning' },
)
@@ -337,6 +340,11 @@ onMounted(loadAll)
<el-input-number v-model="maxScreenshotConcurrent" :min="1" :max="50" />
<div class="help">截图资源占用较低可按机器性能逐步提高</div>
</el-form-item>
<el-form-item label="慢 SQL 阈值(ms)">
<el-input-number v-model="dbSlowQueryMs" :min="0" :max="60000" />
<div class="help">低于该阈值不会计入慢 SQL0 表示关闭慢 SQL 采样</div>
</el-form-item>
</el-form>
<div class="row-actions">

View File

@@ -120,7 +120,7 @@ config = get_config()
DB_FILE = config.DB_FILE
# 数据库版本 (用于迁移管理)
DB_VERSION = 19
DB_VERSION = 20
# ==================== 系统配置缓存P1 / O-03 ====================
@@ -183,6 +183,12 @@ def init_database():
ensure_default_admin()
try:
config_value = get_system_config()
db_pool.configure_slow_query_runtime(threshold_ms=config_value.get("db_slow_query_ms"))
except Exception:
pass
def migrate_database():
"""数据库迁移(对外保留接口)。"""
@@ -240,6 +246,7 @@ def update_system_config(
kdocs_admin_notify_email=None,
kdocs_row_start=None,
kdocs_row_end=None,
db_slow_query_ms=None,
):
"""更新系统配置(写入后立即失效缓存)。"""
ok = _update_system_config(
@@ -268,7 +275,13 @@ def update_system_config(
kdocs_admin_notify_email=kdocs_admin_notify_email,
kdocs_row_start=kdocs_row_start,
kdocs_row_end=kdocs_row_end,
db_slow_query_ms=db_slow_query_ms,
)
if ok:
invalidate_system_config_cache()
try:
latest_config = get_system_config()
db_pool.configure_slow_query_runtime(threshold_ms=latest_config.get("db_slow_query_ms"))
except Exception:
pass
return ok

View File

@@ -17,6 +17,7 @@ _DEFAULT_SYSTEM_CONFIG = {
"max_concurrent_global": 2,
"max_concurrent_per_account": 1,
"max_screenshot_concurrent": 3,
"db_slow_query_ms": 120,
"schedule_enabled": 0,
"schedule_time": "02:00",
"schedule_browse_type": "应读",
@@ -49,6 +50,7 @@ _SYSTEM_CONFIG_UPDATERS = (
("schedule_weekdays", "schedule_weekdays"),
("max_concurrent_per_account", "max_concurrent_per_account"),
("max_screenshot_concurrent", "max_screenshot_concurrent"),
("db_slow_query_ms", "db_slow_query_ms"),
("enable_screenshot", "enable_screenshot"),
("proxy_enabled", "proxy_enabled"),
("proxy_api_url", "proxy_api_url"),
@@ -265,6 +267,7 @@ def update_system_config(
kdocs_admin_notify_email=None,
kdocs_row_start=None,
kdocs_row_end=None,
db_slow_query_ms=None,
) -> bool:
"""更新系统配置仅更新DB不做缓存处理"""
arg_values = {
@@ -293,6 +296,7 @@ def update_system_config(
"kdocs_admin_notify_email": kdocs_admin_notify_email,
"kdocs_row_start": kdocs_row_start,
"kdocs_row_end": kdocs_row_end,
"db_slow_query_ms": db_slow_query_ms,
}
updates = []

View File

@@ -74,6 +74,7 @@ def _get_migration_steps():
(17, _migrate_to_v17),
(18, _migrate_to_v18),
(19, _migrate_to_v19),
(20, _migrate_to_v20),
]
@@ -884,3 +885,21 @@ def _migrate_to_v19(conn):
cursor.execute(statement)
conn.commit()
def _migrate_to_v20(conn):
"""迁移到版本20 - 慢SQL阈值系统配置"""
cursor = conn.cursor()
columns = _get_table_columns(cursor, "system_config")
_add_column_if_missing(
cursor,
"system_config",
columns,
"db_slow_query_ms",
"INTEGER DEFAULT 120",
ok_message=" [OK] 添加 system_config.db_slow_query_ms 字段",
)
conn.commit()

View File

@@ -210,6 +210,7 @@ def ensure_schema(conn) -> None:
proxy_expire_minutes INTEGER DEFAULT 3,
max_screenshot_concurrent INTEGER DEFAULT 3,
max_concurrent_per_account INTEGER DEFAULT 1,
db_slow_query_ms INTEGER DEFAULT 120,
schedule_weekdays TEXT DEFAULT '1,2,3,4,5,6,7',
enable_screenshot INTEGER DEFAULT 1,
auto_approve_enabled INTEGER DEFAULT 0,

View File

@@ -27,6 +27,45 @@ DB_LOCK_RETRY_BASE_MS = max(10, int(getattr(config, "DB_LOCK_RETRY_BASE_MS", 50)
DB_SLOW_QUERY_MS = max(0, int(getattr(config, "DB_SLOW_QUERY_MS", 120)))
DB_SLOW_QUERY_SQL_MAX_LEN = max(80, int(getattr(config, "DB_SLOW_QUERY_SQL_MAX_LEN", 240)))
_slow_query_runtime_lock = threading.Lock()
_slow_query_runtime_threshold_ms = DB_SLOW_QUERY_MS
_slow_query_runtime_sql_max_len = DB_SLOW_QUERY_SQL_MAX_LEN
def _get_slow_query_runtime_values() -> tuple[int, int]:
with _slow_query_runtime_lock:
return int(_slow_query_runtime_threshold_ms), int(_slow_query_runtime_sql_max_len)
def get_slow_query_runtime() -> dict:
threshold_ms, sql_max_len = _get_slow_query_runtime_values()
return {"threshold_ms": threshold_ms, "sql_max_len": sql_max_len}
def configure_slow_query_runtime(*, threshold_ms=None, sql_max_len=None) -> dict:
global _slow_query_runtime_threshold_ms, _slow_query_runtime_sql_max_len
with _slow_query_runtime_lock:
if threshold_ms is not None:
_slow_query_runtime_threshold_ms = max(0, int(threshold_ms))
if sql_max_len is not None:
_slow_query_runtime_sql_max_len = max(80, int(sql_max_len))
runtime_threshold_ms = int(_slow_query_runtime_threshold_ms)
runtime_sql_max_len = int(_slow_query_runtime_sql_max_len)
try:
from services.slow_sql_metrics import configure_slow_sql_runtime
configure_slow_sql_runtime(
threshold_ms=runtime_threshold_ms,
sql_max_len=runtime_sql_max_len,
)
except Exception:
pass
return {"threshold_ms": runtime_threshold_ms, "sql_max_len": runtime_sql_max_len}
def _is_lock_conflict_error(error: sqlite3.OperationalError) -> bool:
message = str(error or "").lower()
@@ -34,10 +73,11 @@ def _is_lock_conflict_error(error: sqlite3.OperationalError) -> bool:
def _compact_sql(sql: str) -> str:
_, sql_max_len = _get_slow_query_runtime_values()
statement = " ".join(str(sql or "").split())
if len(statement) <= DB_SLOW_QUERY_SQL_MAX_LEN:
if len(statement) <= sql_max_len:
return statement
return statement[: DB_SLOW_QUERY_SQL_MAX_LEN - 3] + "..."
return statement[: sql_max_len - 3] + "..."
def _describe_params(parameters) -> str:
@@ -298,9 +338,10 @@ class PooledConnection:
return False
def _on_query_executed(self, sql: str, parameters, elapsed_ms: float) -> None:
if DB_SLOW_QUERY_MS <= 0:
slow_query_ms, _ = _get_slow_query_runtime_values()
if slow_query_ms <= 0:
return
if elapsed_ms < DB_SLOW_QUERY_MS:
if elapsed_ms < slow_query_ms:
return
params_info = _describe_params(parameters)

View File

@@ -34,6 +34,7 @@ def update_system_config_api():
schedule_weekdays = data.get("schedule_weekdays")
new_max_concurrent_per_account = data.get("max_concurrent_per_account")
new_max_screenshot_concurrent = data.get("max_screenshot_concurrent")
db_slow_query_ms = data.get("db_slow_query_ms")
enable_screenshot = data.get("enable_screenshot")
auto_approve_enabled = data.get("auto_approve_enabled")
auto_approve_hourly_limit = data.get("auto_approve_hourly_limit")
@@ -62,6 +63,14 @@ def update_system_config_api():
if not isinstance(new_max_screenshot_concurrent, int) or new_max_screenshot_concurrent < 1:
return jsonify({"error": "截图并发数必须大于0建议根据服务器配置设置wkhtmltoimage 资源占用较低)"}), 400
if db_slow_query_ms is not None:
try:
db_slow_query_ms = int(db_slow_query_ms)
except (ValueError, TypeError):
return jsonify({"error": "慢 SQL 阈值必须是数字(毫秒)"}), 400
if db_slow_query_ms < 0 or db_slow_query_ms > 60000:
return jsonify({"error": "慢 SQL 阈值范围应在 0-60000 毫秒之间"}), 400
if enable_screenshot is not None:
if isinstance(enable_screenshot, bool):
enable_screenshot = 1 if enable_screenshot else 0
@@ -197,6 +206,7 @@ def update_system_config_api():
kdocs_admin_notify_email=kdocs_admin_notify_email,
kdocs_row_start=kdocs_row_start,
kdocs_row_end=kdocs_row_end,
db_slow_query_ms=db_slow_query_ms,
):
return jsonify({"error": "更新失败"}), 400
@@ -207,6 +217,14 @@ def update_system_config_api():
max_global=int(new_config.get("max_concurrent_global", old_config.get("max_concurrent_global", 2))),
max_per_user=int(new_config.get("max_concurrent_per_account", old_config.get("max_concurrent_per_account", 1))),
)
try:
import db_pool
db_pool.configure_slow_query_runtime(threshold_ms=new_config.get("db_slow_query_ms"))
except Exception as slow_sql_error:
logger.warning(f"慢 SQL 运行时阈值更新失败: {slow_sql_error}")
if new_max_screenshot_concurrent is not None:
try:
from browser_pool_worker import resize_browser_worker_pool
@@ -224,5 +242,7 @@ def update_system_config_api():
logger.info(f"单用户并发数已更新为: {new_max_concurrent_per_account}")
if new_max_screenshot_concurrent is not None:
logger.info(f"截图并发数已更新为: {new_max_screenshot_concurrent}")
if db_slow_query_ms is not None:
logger.info(f"慢 SQL 阈值已更新为: {db_slow_query_ms}ms")
return jsonify({"message": "系统配置已更新"})

View File

@@ -21,6 +21,10 @@ _RECENT_LIMIT = max(10, int(os.environ.get("DB_SLOW_SQL_RECENT_LIMIT", "50") or
_MAX_EVENTS = max(_RECENT_LIMIT, int(os.environ.get("DB_SLOW_SQL_MAX_EVENTS", "20000") or 20000))
_SQL_MAX_LEN = max(80, int(os.environ.get("DB_SLOW_QUERY_SQL_MAX_LEN", "240") or 240))
_runtime_lock = threading.Lock()
_runtime_threshold_ms = _SLOW_SQL_THRESHOLD_MS
_runtime_sql_max_len = _SQL_MAX_LEN
_lock = threading.Lock()
_state = {
@@ -38,8 +42,29 @@ def _compact_text(value: str, max_len: int) -> str:
return f"{text[: max_len - 3]}..."
def _get_runtime_values() -> tuple[float, int]:
with _runtime_lock:
return float(_runtime_threshold_ms), int(_runtime_sql_max_len)
def configure_slow_sql_runtime(*, threshold_ms=None, sql_max_len=None) -> dict:
global _runtime_threshold_ms, _runtime_sql_max_len
with _runtime_lock:
if threshold_ms is not None:
_runtime_threshold_ms = max(0.0, float(threshold_ms))
if sql_max_len is not None:
_runtime_sql_max_len = max(80, int(sql_max_len))
return {
"threshold_ms": float(_runtime_threshold_ms),
"sql_max_len": int(_runtime_sql_max_len),
}
def _compact_sql(sql: str) -> str:
return _compact_text(str(sql or ""), _SQL_MAX_LEN)
_, sql_max_len = _get_runtime_values()
return _compact_text(str(sql or ""), sql_max_len)
def _compact_params(params_info: str) -> str:
@@ -165,12 +190,14 @@ def get_slow_sql_metrics_snapshot() -> dict:
total_events = len(events)
avg_duration_ms = round((total_duration_ms / total_events), 2) if total_events > 0 else 0.0
runtime_threshold_ms, _ = _get_runtime_values()
return {
"since_ts": int(float(events[0].get("time") or 0.0)) if events else 0,
"window_seconds": _WINDOW_SECONDS,
"top_limit": _TOP_LIMIT,
"recent_limit": _RECENT_LIMIT,
"slow_threshold_ms": _SLOW_SQL_THRESHOLD_MS,
"slow_threshold_ms": runtime_threshold_ms,
"total_slow_queries": total_events,
"unique_sql": len(grouped),
"avg_duration_ms": avg_duration_ms,

View File

@@ -1,6 +1,6 @@
{
"_MetricGrid-Cm-6uQPC.js": {
"file": "assets/MetricGrid-Cm-6uQPC.js",
"_MetricGrid-tQwTnWXY.js": {
"file": "assets/MetricGrid-tQwTnWXY.js",
"name": "MetricGrid",
"imports": [
"index.html",
@@ -14,29 +14,29 @@
"file": "assets/MetricGrid-yP_dkP6X.css",
"src": "_MetricGrid-yP_dkP6X.css"
},
"_email-UCdCFkiw.js": {
"file": "assets/email-UCdCFkiw.js",
"_email-BgLsevU3.js": {
"file": "assets/email-BgLsevU3.js",
"name": "email",
"imports": [
"index.html"
]
},
"_system-BqUnjCUC.js": {
"file": "assets/system-BqUnjCUC.js",
"_system-COpUDNdl.js": {
"file": "assets/system-COpUDNdl.js",
"name": "system",
"imports": [
"index.html"
]
},
"_tasks-DW-oE78v.js": {
"file": "assets/tasks-DW-oE78v.js",
"_tasks-Bprl-b9c.js": {
"file": "assets/tasks-Bprl-b9c.js",
"name": "tasks",
"imports": [
"index.html"
]
},
"_users-LqVgYOIa.js": {
"file": "assets/users-LqVgYOIa.js",
"_users-oERXj-b9.js": {
"file": "assets/users-oERXj-b9.js",
"name": "users",
"imports": [
"index.html"
@@ -54,7 +54,7 @@
"src": "_vendor-C68yOrAy.css"
},
"index.html": {
"file": "assets/index-C51y9xCM.js",
"file": "assets/index-z8QOtKVT.js",
"name": "index",
"src": "index.html",
"isEntry": true,
@@ -77,7 +77,7 @@
]
},
"src/pages/AnnouncementsPage.vue": {
"file": "assets/AnnouncementsPage-D4vGLsnP.js",
"file": "assets/AnnouncementsPage-B1ijXegw.js",
"name": "AnnouncementsPage",
"src": "src/pages/AnnouncementsPage.vue",
"isDynamicEntry": true,
@@ -90,14 +90,14 @@
]
},
"src/pages/EmailPage.vue": {
"file": "assets/EmailPage-BgnCtp6d.js",
"file": "assets/EmailPage-BA_Nuk9g.js",
"name": "EmailPage",
"src": "src/pages/EmailPage.vue",
"isDynamicEntry": true,
"imports": [
"_email-UCdCFkiw.js",
"_email-BgLsevU3.js",
"index.html",
"_MetricGrid-Cm-6uQPC.js",
"_MetricGrid-tQwTnWXY.js",
"_vendor-BczUEOE_.js"
],
"css": [
@@ -105,13 +105,13 @@
]
},
"src/pages/FeedbacksPage.vue": {
"file": "assets/FeedbacksPage-B-5AB1AS.js",
"file": "assets/FeedbacksPage-Dg3BwCE1.js",
"name": "FeedbacksPage",
"src": "src/pages/FeedbacksPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_MetricGrid-Cm-6uQPC.js",
"_MetricGrid-tQwTnWXY.js",
"_vendor-BczUEOE_.js"
],
"css": [
@@ -119,13 +119,13 @@
]
},
"src/pages/LogsPage.vue": {
"file": "assets/LogsPage-DF5QqYRJ.js",
"file": "assets/LogsPage-B80ridTr.js",
"name": "LogsPage",
"src": "src/pages/LogsPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-LqVgYOIa.js",
"_tasks-DW-oE78v.js",
"_users-oERXj-b9.js",
"_tasks-Bprl-b9c.js",
"index.html",
"_vendor-BczUEOE_.js"
],
@@ -134,30 +134,30 @@
]
},
"src/pages/ReportPage.vue": {
"file": "assets/ReportPage-CJumwOUP.js",
"file": "assets/ReportPage-CVnILNWh.js",
"name": "ReportPage",
"src": "src/pages/ReportPage.vue",
"isDynamicEntry": true,
"imports": [
"_vendor-BczUEOE_.js",
"index.html",
"_email-UCdCFkiw.js",
"_tasks-DW-oE78v.js",
"_system-BqUnjCUC.js",
"_MetricGrid-Cm-6uQPC.js"
"_email-BgLsevU3.js",
"_tasks-Bprl-b9c.js",
"_system-COpUDNdl.js",
"_MetricGrid-tQwTnWXY.js"
],
"css": [
"assets/ReportPage-Csv-Q6wC.css"
]
},
"src/pages/SecurityPage.vue": {
"file": "assets/SecurityPage-rmlzXSVY.js",
"file": "assets/SecurityPage-tMcfFXjg.js",
"name": "SecurityPage",
"src": "src/pages/SecurityPage.vue",
"isDynamicEntry": true,
"imports": [
"index.html",
"_MetricGrid-Cm-6uQPC.js",
"_MetricGrid-tQwTnWXY.js",
"_vendor-BczUEOE_.js"
],
"css": [
@@ -165,7 +165,7 @@
]
},
"src/pages/SettingsPage.vue": {
"file": "assets/SettingsPage-Czik5vp6.js",
"file": "assets/SettingsPage-Bagp2mPP.js",
"name": "SettingsPage",
"src": "src/pages/SettingsPage.vue",
"isDynamicEntry": true,
@@ -178,26 +178,26 @@
]
},
"src/pages/SystemPage.vue": {
"file": "assets/SystemPage-CYpwmM8C.js",
"file": "assets/SystemPage-8v_X5n5H.js",
"name": "SystemPage",
"src": "src/pages/SystemPage.vue",
"isDynamicEntry": true,
"imports": [
"_system-BqUnjCUC.js",
"_system-COpUDNdl.js",
"index.html",
"_vendor-BczUEOE_.js"
],
"css": [
"assets/SystemPage-DMyNBO3N.css"
"assets/SystemPage-DYBocGi2.css"
]
},
"src/pages/UsersPage.vue": {
"file": "assets/UsersPage-hjzizufJ.js",
"file": "assets/UsersPage-CJwQe-N8.js",
"name": "UsersPage",
"src": "src/pages/UsersPage.vue",
"isDynamicEntry": true,
"imports": [
"_users-LqVgYOIa.js",
"_users-oERXj-b9.js",
"index.html",
"_vendor-BczUEOE_.js"
],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{_}from"./index-C51y9xCM.js";import{r as n,l as s,o as t,F as r,m as u,V as p,k as o,i as l,j as y,w as h,c as i,A as k,B as c,C as v,a0 as f}from"./vendor-BczUEOE_.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},B={class:"metric-label"},C={class:"metric-value"},g={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,w)=>{const d=n("el-icon"),m=n("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(k(e.icon)))]),_:2},1024)])):l("",!0),o("div",B,c(e?.label||"-"),1)]),o("div",C,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[v(c(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",g,c(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},A=_(N,[["__scopeId","data-v-00e217d4"]]);export{A as M};
import{_}from"./index-z8QOtKVT.js";import{r as n,l as s,o as t,F as r,m as u,V as p,k as o,i as l,j as y,w as h,c as i,A as k,B as c,C as v,a0 as f}from"./vendor-BczUEOE_.js";const b={class:"metric-top"},x={key:0,class:"metric-icon"},B={class:"metric-label"},C={class:"metric-value"},g={key:0,class:"metric-hint app-muted"},N={__name:"MetricGrid",props:{items:{type:Array,default:()=>[]},loading:{type:Boolean,default:!1},minWidth:{type:Number,default:180}},setup(a){return(V,w)=>{const d=n("el-icon"),m=n("el-skeleton");return t(),s("div",{class:"metric-grid",style:f({"--metric-min":`${a.minWidth}px`})},[(t(!0),s(r,null,u(a.items,e=>(t(),s("div",{key:e?.key||e?.label,class:p(["metric-card",`metric-tone--${e?.tone||"blue"}`])},[o("div",b,[e?.icon?(t(),s("div",x,[y(d,null,{default:h(()=>[(t(),i(k(e.icon)))]),_:2},1024)])):l("",!0),o("div",B,c(e?.label||"-"),1)]),o("div",C,[a.loading?(t(),i(m,{key:0,rows:1,animated:""})):(t(),s(r,{key:1},[v(c(e?.value??0),1)],64))]),e?.hint||e?.sub?(t(),s("div",g,c(e?.hint||e?.sub),1)):l("",!0)],2))),128))],4)}}},A=_(N,[["__scopeId","data-v-00e217d4"]]);export{A as M};

View File

@@ -1 +1 @@
import{a as m,_ as h}from"./index-C51y9xCM.js";import{e as p,r as u,l as T,o as C,k as r,j as a,w as o,C as x,b as i,E as b}from"./vendor-BczUEOE_.js";async function P(l){const{data:s}=await m.put("/admin/username",{new_username:l});return s}async function E(l){const{data:s}=await m.put("/admin/password",{new_password:l});return s}async function S(){const{data:l}=await m.post("/logout");return l}const U={class:"page-stack"},A={__name:"SettingsPage",setup(l){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await S()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const t=d.value;if(!t){i.error("请输入新密码");return}const e=k(t);if(!e.ok){i.error(e.message);return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(t),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=u("el-input"),w=u("el-form-item"),v=u("el-form"),y=u("el-button"),_=u("el-card");return C(),T("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新用户名"},{default:o(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:o(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新密码"},{default:o(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:B},{default:o(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},j=h(A,[["__scopeId","data-v-83d3840a"]]);export{j as default};
import{a as m,_ as h}from"./index-z8QOtKVT.js";import{e as p,r as u,l as T,o as C,k as r,j as a,w as o,C as x,b as i,E as b}from"./vendor-BczUEOE_.js";async function P(l){const{data:s}=await m.put("/admin/username",{new_username:l});return s}async function E(l){const{data:s}=await m.put("/admin/password",{new_password:l});return s}async function S(){const{data:l}=await m.post("/logout");return l}const U={class:"page-stack"},A={__name:"SettingsPage",setup(l){const s=p(""),d=p(""),n=p(!1);function k(t){const e=String(t||"");return e.length<8?{ok:!1,message:"密码长度至少8位"}:e.length>128?{ok:!1,message:"密码长度不能超过128个字符"}:!/[a-zA-Z]/.test(e)||!/\d/.test(e)?{ok:!1,message:"密码必须包含字母和数字"}:{ok:!0,message:""}}async function f(){try{await S()}catch{}finally{window.location.href="/yuyx"}}async function V(){const t=s.value.trim();if(!t){i.error("请输入新用户名");return}try{await b.confirm(`确定将管理员用户名修改为「${t}」吗?修改后需要重新登录。`,"修改用户名",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await P(t),i.success("用户名修改成功,请重新登录"),s.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}async function B(){const t=d.value;if(!t){i.error("请输入新密码");return}const e=k(t);if(!e.ok){i.error(e.message);return}try{await b.confirm("确定修改管理员密码吗?修改后需要重新登录。","修改密码",{confirmButtonText:"确认修改",cancelButtonText:"取消",type:"warning"})}catch{return}n.value=!0;try{await E(t),i.success("密码修改成功,请重新登录"),d.value="",setTimeout(f,1200)}catch{}finally{n.value=!1}}return(t,e)=>{const g=u("el-input"),w=u("el-form-item"),v=u("el-form"),y=u("el-button"),_=u("el-card");return C(),T("div",U,[e[7]||(e[7]=r("div",{class:"app-page-title"},[r("h2",null,"设置"),r("span",{class:"app-muted"},"管理员账号设置")],-1)),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[3]||(e[3]=r("h3",{class:"section-title"},"修改管理员用户名",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新用户名"},{default:o(()=>[a(g,{modelValue:s.value,"onUpdate:modelValue":e[0]||(e[0]=c=>s.value=c),placeholder:"输入新用户名",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:V},{default:o(()=>[...e[2]||(e[2]=[x("保存用户名",-1)])]),_:1},8,["loading"])]),_:1}),a(_,{shadow:"never","body-style":{padding:"16px"},class:"card"},{default:o(()=>[e[5]||(e[5]=r("h3",{class:"section-title"},"修改管理员密码",-1)),a(v,{"label-width":"120px"},{default:o(()=>[a(w,{label:"新密码"},{default:o(()=>[a(g,{modelValue:d.value,"onUpdate:modelValue":e[1]||(e[1]=c=>d.value=c),type:"password","show-password":"",placeholder:"输入新密码",disabled:n.value},null,8,["modelValue","disabled"])]),_:1})]),_:1}),a(y,{type:"primary",loading:n.value,onClick:B},{default:o(()=>[...e[4]||(e[4]=[x("保存密码",-1)])]),_:1},8,["loading"]),e[6]||(e[6]=r("div",{class:"help"},"建议使用更强密码至少8位且包含字母与数字。",-1))]),_:1})])}}},j=h(A,[["__scopeId","data-v-83d3840a"]]);export{j as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.page-stack[data-v-c90d5cb2]{display:flex;flex-direction:column;gap:14px;min-width:0}.config-grid[data-v-c90d5cb2]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card[data-v-c90d5cb2]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-card[data-v-c90d5cb2]{min-width:0}.section-title[data-v-c90d5cb2]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.section-sub[data-v-c90d5cb2]{margin-top:6px;margin-bottom:10px;font-size:12px}.section-head[data-v-c90d5cb2]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}.status-inline[data-v-c90d5cb2]{font-size:12px}.kdocs-form[data-v-c90d5cb2]{margin-top:6px}.kdocs-inline[data-v-c90d5cb2]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;width:100%}.kdocs-range[data-v-c90d5cb2]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.kdocs-qr[data-v-c90d5cb2]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-c90d5cb2]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-c90d5cb2]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-c90d5cb2]{display:flex;flex-wrap:wrap;gap:10px}@media(max-width:1200px){.config-grid[data-v-c90d5cb2]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:768px){.config-grid[data-v-c90d5cb2],.kdocs-inline[data-v-c90d5cb2]{grid-template-columns:1fr}.kdocs-range[data-v-c90d5cb2]{align-items:stretch}}

View File

@@ -0,0 +1 @@
.page-stack[data-v-a5c40f1a]{display:flex;flex-direction:column;gap:14px;min-width:0}.config-grid[data-v-a5c40f1a]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card[data-v-a5c40f1a]{border-radius:var(--app-radius);border:1px solid var(--app-border);background:var(--app-card-bg);box-shadow:var(--app-shadow-soft)}.section-card[data-v-a5c40f1a]{min-width:0}.section-title[data-v-a5c40f1a]{margin:0;font-size:15px;font-weight:800;letter-spacing:.2px}.section-sub[data-v-a5c40f1a]{margin-top:6px;margin-bottom:10px;font-size:12px}.section-head[data-v-a5c40f1a]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:10px}.status-inline[data-v-a5c40f1a]{font-size:12px}.kdocs-form[data-v-a5c40f1a]{margin-top:6px}.kdocs-inline[data-v-a5c40f1a]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;width:100%}.kdocs-range[data-v-a5c40f1a]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.kdocs-qr[data-v-a5c40f1a]{display:flex;flex-direction:column;align-items:center;gap:12px}.kdocs-qr img[data-v-a5c40f1a]{width:260px;max-width:100%;border:1px solid var(--app-border);border-radius:8px;padding:8px;background:#fff}.help[data-v-a5c40f1a]{margin-top:6px;font-size:12px;color:var(--app-muted)}.row-actions[data-v-a5c40f1a]{display:flex;flex-wrap:wrap;gap:10px}@media(max-width:1200px){.config-grid[data-v-a5c40f1a]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:768px){.config-grid[data-v-a5c40f1a],.kdocs-inline[data-v-a5c40f1a]{grid-template-columns:1fr}.kdocs-range[data-v-a5c40f1a]{align-items:stretch}}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{a as n}from"./index-C51y9xCM.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};
import{a as n}from"./index-z8QOtKVT.js";async function i(){const{data:a}=await n.get("/email/settings");return a}async function e(a){const{data:t}=await n.post("/email/settings",a);return t}async function c(){const{data:a}=await n.get("/email/stats");return a}async function o(a){const{data:t}=await n.get("/email/logs",{params:a});return t}async function l(a){const{data:t}=await n.post("/email/logs/cleanup",{days:a});return t}export{o as a,i as b,l as c,c as f,e as u};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{a}from"./index-C51y9xCM.js";async function o(){const{data:t}=await a.get("/system/config");return t}async function e(t){const{data:n}=await a.post("/system/config",t);return n}export{o as f,e as u};
import{a}from"./index-z8QOtKVT.js";async function o(){const{data:t}=await a.get("/system/config");return t}async function e(t){const{data:n}=await a.post("/system/config",t);return n}export{o as f,e as u};

View File

@@ -1 +1 @@
import{a}from"./index-C51y9xCM.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function r(){const{data:t}=await a.get("/request_metrics");return t}async function o(){const{data:t}=await a.get("/slow_sql_metrics");return t}async function i(){const{data:t}=await a.get("/task/stats");return t}async function u(){const{data:t}=await a.get("/task/running");return t}async function f(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function g(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{u as a,c as b,e as c,r as d,o as e,i as f,f as g,g as h};
import{a}from"./index-z8QOtKVT.js";async function c(){const{data:t}=await a.get("/server/info");return t}async function e(){const{data:t}=await a.get("/docker_stats");return t}async function r(){const{data:t}=await a.get("/request_metrics");return t}async function o(){const{data:t}=await a.get("/slow_sql_metrics");return t}async function i(){const{data:t}=await a.get("/task/stats");return t}async function u(){const{data:t}=await a.get("/task/running");return t}async function f(t){const{data:s}=await a.get("/task/logs",{params:t});return s}async function g(t){const{data:s}=await a.post("/task/logs/clear",{days:t});return s}export{u as a,c as b,e as c,r as d,o as e,i as f,f as g,g as h};

View File

@@ -1 +1 @@
import{a as t}from"./index-C51y9xCM.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};
import{a as t}from"./index-z8QOtKVT.js";async function n(){const{data:s}=await t.get("/users");return s}async function o(s){const{data:a}=await t.post(`/users/${s}/approve`);return a}async function c(s){const{data:a}=await t.post(`/users/${s}/reject`);return a}async function i(s){const{data:a}=await t.delete(`/users/${s}`);return a}async function u(s,a){const{data:e}=await t.post(`/users/${s}/vip`,{days:a});return e}async function p(s){const{data:a}=await t.delete(`/users/${s}/vip`);return a}async function d(s,a){const{data:e}=await t.post(`/users/${s}/reset_password`,{new_password:a});return e}export{o as a,p as b,d as c,i as d,n as f,c as r,u as s};

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>后台管理 - 知识管理平台</title>
<script type="module" crossorigin src="./assets/index-C51y9xCM.js"></script>
<script type="module" crossorigin src="./assets/index-z8QOtKVT.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-BczUEOE_.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-C68yOrAy.css">
<link rel="stylesheet" crossorigin href="./assets/index-a3a11Ghn.css">