feat(admin): 登录改页内弹窗 + P1 站点设置(页头/侧栏)

- 登录:LoginDialog(Modal)替代 /login 页面;/login 重定向回首页
- 设置接口:GET /api/settings 公开读、PATCH 需登录 + version 乐观锁(409/400)
- setting 表存 site JSON:页头(启用/速度/条目+标签+色调)、侧栏(hidden/order)
- /admin:Tabs 页头/侧栏,条目增删与上下移,保存后首页生效
- 首页应用设置:ticker 文本占位符与速度、侧栏顺序与显隐
- 测试 42 passed(settings 5 例)
This commit is contained in:
stash
2026-10-29 12:52:29 +08:00
parent dea7781b86
commit 9ae5a455c5
14 changed files with 1205 additions and 195 deletions
+48
View File
@@ -534,6 +534,53 @@ async fn auth_password(
}
}
// ---------------- 站点设置 ----------------
#[derive(serde::Deserialize)]
struct SettingsPatch {
version: i64,
settings: crate::db::Settings,
}
/// GET /api/settings —— 读接口公开,前端首屏直接取
async fn get_settings(State(state): State<AppState>) -> Response {
let db = Arc::clone(&state.db);
match tokio::task::spawn_blocking(move || db.load_settings()).await {
Ok(Ok(document)) => Json(document).into_response(),
Ok(Err(error)) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
Err(error) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
}
}
/// PATCH /api/settings —— 需要登录;`version` 做乐观锁
async fn patch_settings(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<SettingsPatch>,
) -> Response {
if !same_origin(&headers) {
return api_error(StatusCode::FORBIDDEN, "跨站请求被拒绝");
}
match read_session(&state, &headers).await {
Ok(session) if session.authenticated => {}
Ok(_) => return api_error(StatusCode::UNAUTHORIZED, "请先登录"),
Err(response) => return response,
}
let db = Arc::clone(&state.db);
let (settings, version) = (body.settings, body.version);
match tokio::task::spawn_blocking(move || db.save_settings(settings, version)).await {
Ok(Ok(document)) => Json(document).into_response(),
Ok(Err(StashError::Conflict(message))) => api_error(StatusCode::CONFLICT, &message),
Ok(Err(StashError::InvalidDirectory(message))) => {
api_error(StatusCode::BAD_REQUEST, &message)
}
Ok(Err(error)) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
Err(error) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
}
}
/// 组装路由:API + 内嵌前端 + 托管目录。
/// 单独抽出来是为了测试能直接对 `Router` 发请求:`start` 要绑定端口、
/// 起监听任务,没法在单元测试里用。
@@ -559,6 +606,7 @@ fn build_router(state: AppState) -> Router {
.route("/api/auth/login", post(auth_login))
.route("/api/auth/logout", post(auth_logout))
.route("/api/auth/password", patch(auth_password))
.route("/api/settings", get(get_settings).patch(patch_settings))
.route("/", get(web_index))
.route("/index.html", get(web_index))
.route("/favicon.svg", get(web_root_file))