feat(admin): 跳转规范化、侧栏子分类与归类管理

- 条目保存时规范化 href(裸域名补 https,拒绝 javascript:/data: 等协议),详情按钮按解析结果置灰/跳转
- settings.sidebar 增加 hidden_groups / shown_groups / group_order,侧栏 Tab 支持分组显隐与排序
- 新增 PATCH /api/categories/{id} 与「归类」Tab:编辑类型/标题/图标/色条/条数;改类型自动迁移条目与首页卡片
- 测试 54 passed(href 规范化 3 例、分类更新 3 例)
This commit is contained in:
2026-10-30 12:33:52 +08:00
parent 00e0360c85
commit 94e16b783c
13 changed files with 891 additions and 47 deletions
+32 -1
View File
@@ -745,6 +745,34 @@ async fn create_category(
}
}
/// PATCH /api/categories/{id} —— 修改分区(需要登录)
async fn update_category(
State(state): State<AppState>,
Path(id): Path<String>,
headers: HeaderMap,
Json(body): Json<crate::db::categories::UpdateCategory>,
) -> 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);
match tokio::task::spawn_blocking(move || db.update_category(&id, body)).await {
Ok(Ok(())) => Json(serde_json::json!({ "ok": true })).into_response(),
Ok(Err(StashError::NotFound(message))) => api_error(StatusCode::NOT_FOUND, &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()),
}
}
/// DELETE /api/categories/{id} —— 删除分区(需要登录,级联内容)
async fn delete_category(
State(state): State<AppState>,
@@ -802,7 +830,10 @@ fn build_router(state: AppState) -> Router {
.route("/api/items/{key}/view", post(post_view))
.route("/api/items/{key}/like", post(post_like))
.route("/api/categories", post(create_category))
.route("/api/categories/{id}", axum::routing::delete(delete_category))
.route(
"/api/categories/{id}",
patch(update_category).delete(delete_category),
)
.route("/api/backup", get(backup_db))
.route("/", get(web_index))
.route("/index.html", get(web_index))