后端:
- 将 handlers.rs (1338行) 拆分为 helpers/papers/notes/sync 四模块
- 将 batch_sync.rs 拆分为 batch/{mod,meta,asset} 三模块
- 新增 POST /api/upload 多部件文件上传接口
- 新增 POST /api/no_resource 标记文献"无全文资源"
- 新增 GET/POST /api/active_bibcode 追踪活跃文献
- StandardPaper 结构体扩展 pdf_error / html_error 错误诊断字段
- download.rs 记录下载失败详情至数据库
- 新增 health_check 二进制工具,支持只读扫描与 --fix 自动修复
- 移除 scratch/ 目录、recovered_handlers.rs 及调试日志
前端:
- 新建 CustomSelect 可复用组件,替换全部原生 select
- LibraryPanel:同步按钮反馈动画、下载失败/无资源状态筛选与计数、
文献类型筛选、状态优先排序、搜索一键清空
- 详情弹窗:错误诊断展示、手动 PDF/HTML 上传区、无资源标记/恢复
- SearchPanel:扩展文献类型徽章、下载失败状态提示
- SyncPanel:同步启动乐观 UI 更新、日志容器内自动滚动
- Tab 状态 localStorage 持久化、弹窗 z-index 修复
114 lines
3.2 KiB
Rust
114 lines
3.2 KiB
Rust
// src/api/notes.rs
|
|
use axum::{
|
|
extract::{Query, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use sqlx::Row;
|
|
|
|
use super::AppState;
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct NoteRecord {
|
|
pub id: i64,
|
|
pub bibcode: String,
|
|
pub paragraph_index: i64,
|
|
pub note_text: String,
|
|
pub highlight_color: String,
|
|
pub selected_text: String,
|
|
pub created_at: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct CreateNoteRequest {
|
|
pub bibcode: String,
|
|
pub paragraph_index: i64,
|
|
pub note_text: Option<String>,
|
|
pub highlight_color: Option<String>,
|
|
pub selected_text: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct DeleteNoteParams {
|
|
pub id: i64,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct GetNotesParams {
|
|
pub bibcode: String,
|
|
}
|
|
|
|
// 创建笔记
|
|
pub async fn create_note(
|
|
State(state): State<Arc<AppState>>,
|
|
Json(req): Json<CreateNoteRequest>,
|
|
) -> Result<Json<NoteRecord>, (StatusCode, String)> {
|
|
let note_text = req.note_text.unwrap_or_default();
|
|
let highlight_color = req.highlight_color.unwrap_or_else(|| "yellow".to_string());
|
|
let selected_text = req.selected_text.unwrap_or_default();
|
|
|
|
let row = sqlx::query(
|
|
"INSERT INTO notes (bibcode, paragraph_index, note_text, highlight_color, selected_text) VALUES (?, ?, ?, ?, ?) RETURNING id, bibcode, paragraph_index, note_text, highlight_color, selected_text, created_at"
|
|
)
|
|
.bind(&req.bibcode)
|
|
.bind(req.paragraph_index)
|
|
.bind(¬e_text)
|
|
.bind(&highlight_color)
|
|
.bind(&selected_text)
|
|
.fetch_one(&state.db)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("保存笔记失败: {}", e)))?;
|
|
|
|
Ok(Json(NoteRecord {
|
|
id: row.get(0),
|
|
bibcode: row.get(1),
|
|
paragraph_index: row.get(2),
|
|
note_text: row.get(3),
|
|
highlight_color: row.get(4),
|
|
selected_text: row.get(5),
|
|
created_at: row.get(6),
|
|
}))
|
|
}
|
|
|
|
// 查询某篇文献的全部笔记
|
|
pub async fn get_notes(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(params): Query<GetNotesParams>,
|
|
) -> Result<Json<Vec<NoteRecord>>, (StatusCode, String)> {
|
|
let rows = sqlx::query(
|
|
"SELECT id, bibcode, paragraph_index, note_text, highlight_color, selected_text, created_at FROM notes WHERE bibcode = ? ORDER BY paragraph_index, created_at"
|
|
)
|
|
.bind(¶ms.bibcode)
|
|
.fetch_all(&state.db)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("查询笔记失败: {}", e)))?;
|
|
|
|
let notes: Vec<NoteRecord> = rows.iter().map(|r| NoteRecord {
|
|
id: r.get(0),
|
|
bibcode: r.get(1),
|
|
paragraph_index: r.get(2),
|
|
note_text: r.get(3),
|
|
highlight_color: r.get(4),
|
|
selected_text: r.get(5),
|
|
created_at: r.get(6),
|
|
}).collect();
|
|
|
|
Ok(Json(notes))
|
|
}
|
|
|
|
// 删除指定 id 的笔记
|
|
pub async fn delete_note(
|
|
State(state): State<Arc<AppState>>,
|
|
Query(params): Query<DeleteNoteParams>,
|
|
) -> Result<StatusCode, (StatusCode, String)> {
|
|
sqlx::query("DELETE FROM notes WHERE id = ?")
|
|
.bind(params.id)
|
|
.execute(&state.db)
|
|
.await
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("删除笔记失败: {}", e)))?;
|
|
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|