Files
stash/src/db/cards.rs
T
CNWei 57ab90a1fa feat: 搜索框下滚动、本地服务卡片排序、拖拽末位修复
- 恢复内容区内部滚动:页头/搜索框固定,滚动区域从搜索框下方开始;侧栏跟随内容滚动,侧栏滚轮驱动内容区
- v22:本地服务以 card 行 local-service 参与排序;首页按卡片顺序渲染,后台可拖动排序
- 本地服务卡片后端禁止删除/改名/配置分组
- 修复向下拖动时插到目标之后,倒数第二项可拖到最后
- 验证:搜索框位置固定、侧栏 scrollTop 跟随、本地服务与倒数第二拖末位均落库、首页顺序一致
2026-10-31 15:30:09 +08:00

238 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 首页卡片:只存标题/顺序,内容由 `card_group` 勾选分组组成。
//!
//! 卡片与分组是多对多:一张卡可以放不同格式的分组,分组也可以被多张卡复用。
//! 卡片本身不拥有数据,删卡不影响内容。
use rusqlite::{Connection, OptionalExtension, params};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{db::entries, db::map_error, error::StashError};
#[derive(Debug, Clone, Deserialize)]
pub struct NewCard {
pub title: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCard {
pub title: Option<String>,
pub sort: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CardGroupInput {
pub group_key: String,
#[serde(default)]
pub preview_limit: Option<i64>,
}
pub fn load(conn: &Connection) -> Result<Vec<Value>, StashError> {
let mut stmt = conn
.prepare("SELECT id, key, title, sort FROM card ORDER BY sort, id")
.map_err(map_error)?;
let cards = stmt
.query_map([], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i64>(3)?,
))
})
.map_err(map_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(map_error)?;
drop(stmt);
let mut result = Vec::with_capacity(cards.len());
for (id, key, title, sort) in cards {
let mut group_stmt = conn
.prepare(
"SELECT group_key, preview_limit FROM card_group
WHERE card_id = ?1 ORDER BY sort",
)
.map_err(map_error)?;
let groups = group_stmt
.query_map([id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<i64>>(1)?,
))
})
.map_err(map_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(map_error)?;
result.push(json!({
"id": id,
"key": key,
"title": title,
"sort": sort,
"groups": groups
.into_iter()
.map(|(group_key, preview_limit)| json!({
"groupKey": group_key,
"previewLimit": preview_limit,
}))
.collect::<Vec<_>>(),
}));
}
Ok(result)
}
pub fn create(conn: &mut Connection, new: NewCard) -> Result<i64, StashError> {
let title = new.title.trim();
if title.is_empty() || title.chars().count() > 40 {
return Err(StashError::InvalidDirectory("卡片名称需为 1–40 个字".into()));
}
let tx = conn.transaction().map_err(map_error)?;
let sort: i64 = tx
.query_row("SELECT COALESCE(MAX(sort), 0) + 1 FROM card", [], |row| {
row.get(0)
})
.map_err(map_error)?;
tx.execute(
"INSERT INTO card (key, title, sort) VALUES ('pending', ?1, ?2)",
params![title, sort],
)
.map_err(map_error)?;
let id = tx.last_insert_rowid();
tx.execute(
"UPDATE card SET key = ?1 WHERE id = ?2",
params![format!("card-{id}"), id],
)
.map_err(map_error)?;
entries::audit(&tx, "card.create", &format!("card-{id}"), title)?;
tx.commit().map_err(map_error)?;
Ok(id)
}
pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), StashError> {
let current: Option<(String, String, i64)> = conn
.query_row(
"SELECT key, title, sort FROM card WHERE id = ?1",
[id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.optional()
.map_err(map_error)?;
let Some((current_key, current_title, current_sort)) = current else {
return Err(StashError::NotFound(format!("卡片不存在: {id}")));
};
if current_key == "local-service" && patch.title.is_some() {
return Err(StashError::InvalidDirectory(
"本地服务卡片由系统生成,不能改名".into(),
));
}
let title = match patch.title {
Some(title) => {
let title = title.trim().to_string();
if title.is_empty() || title.chars().count() > 40 {
return Err(StashError::InvalidDirectory("卡片名称需为 1–40 个字".into()));
}
title
}
None => current_title,
};
let sort = patch.sort.unwrap_or(current_sort);
let tx = conn.transaction().map_err(map_error)?;
tx.execute(
"UPDATE card SET title = ?1, sort = ?2 WHERE id = ?3",
params![title, sort, id],
)
.map_err(map_error)?;
entries::audit(&tx, "card.update", &format!("card-{id}"), &title)?;
tx.commit().map_err(map_error)?;
Ok(())
}
pub fn remove(conn: &Connection, id: i64) -> Result<(), StashError> {
let key: Option<String> = conn
.query_row("SELECT key FROM card WHERE id = ?1", [id], |row| row.get(0))
.optional()
.map_err(map_error)?;
if key.as_deref() == Some("local-service") {
return Err(StashError::InvalidDirectory(
"本地服务卡片由系统生成,不能删除".into(),
));
}
let removed = conn
.execute("DELETE FROM card WHERE id = ?1", [id])
.map_err(map_error)?;
if removed == 0 {
return Err(StashError::NotFound(format!("卡片不存在: {id}")));
}
entries::audit(conn, "card.delete", &format!("card-{id}"), "")?;
Ok(())
}
/// 整组替换卡片勾选的分组(顺序即数组顺序)。
pub fn set_groups(
conn: &mut Connection,
id: i64,
groups: Vec<CardGroupInput>,
) -> Result<(), StashError> {
let key: Option<String> = conn
.query_row("SELECT key FROM card WHERE id = ?1", [id], |row| row.get(0))
.optional()
.map_err(map_error)?;
if key.as_deref() == Some("local-service") {
return Err(StashError::InvalidDirectory(
"本地服务卡片由系统生成,不能配置分组".into(),
));
}
let exists: i64 = conn
.query_row("SELECT COUNT(*) FROM card WHERE id = ?1", [id], |row| row.get(0))
.map_err(map_error)?;
if exists == 0 {
return Err(StashError::NotFound(format!("卡片不存在: {id}")));
}
for group in &groups {
let node_exists: i64 = conn
.query_row("SELECT COUNT(*) FROM node WHERE key = ?1", [&group.group_key], |row| {
row.get(0)
})
.map_err(map_error)?;
if node_exists == 0 {
return Err(StashError::NotFound(format!(
"分组不存在: {}",
group.group_key
)));
}
if let Some(limit) = group.preview_limit {
if !(1..=60).contains(&limit) {
return Err(StashError::InvalidDirectory("预览条数需在 1–60 之间".into()));
}
}
}
let tx = conn.transaction().map_err(map_error)?;
tx.execute("DELETE FROM card_group WHERE card_id = ?1", [id])
.map_err(map_error)?;
for (sort, group) in groups.iter().enumerate() {
tx.execute(
"INSERT INTO card_group (card_id, group_key, sort, preview_limit)
VALUES (?1, ?2, ?3, ?4)",
params![id, group.group_key, sort as i64, group.preview_limit],
)
.map_err(map_error)?;
}
entries::audit(&tx, "card.groups", &format!("card-{id}"), "")?;
tx.commit().map_err(map_error)?;
Ok(())
}