feat: 搜索框下滚动、本地服务卡片排序、拖拽末位修复

- 恢复内容区内部滚动:页头/搜索框固定,滚动区域从搜索框下方开始;侧栏跟随内容滚动,侧栏滚轮驱动内容区
- v22:本地服务以 card 行 local-service 参与排序;首页按卡片顺序渲染,后台可拖动排序
- 本地服务卡片后端禁止删除/改名/配置分组
- 修复向下拖动时插到目标之后,倒数第二项可拖到最后
- 验证:搜索框位置固定、侧栏 scrollTop 跟随、本地服务与倒数第二拖末位均落库、首页顺序一致
This commit is contained in:
2026-10-31 15:30:09 +08:00
parent 0e6c772466
commit 57ab90a1fa
7 changed files with 116 additions and 40 deletions
+31 -5
View File
@@ -117,16 +117,23 @@ pub fn create(conn: &mut Connection, new: NewCard) -> Result<i64, StashError> {
}
pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), StashError> {
let current: Option<(String, i64)> = conn
.query_row("SELECT title, sort FROM card WHERE id = ?1", [id], |row| {
Ok((row.get(0)?, row.get(1)?))
})
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_title, current_sort)) = current else {
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) => {
@@ -152,6 +159,15 @@ pub fn update(conn: &mut Connection, id: i64, patch: UpdateCard) -> Result<(), S
}
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)?;
@@ -168,6 +184,16 @@ pub fn set_groups(
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)?;
+14 -1
View File
@@ -182,6 +182,9 @@ FROM section_block b
JOIN node n ON n.key = b.node_key
LEFT JOIN node p ON p.key = n.parent_key
JOIN card c ON c.key = 'card-' || COALESCE(p.key, n.key);
-- 本地服务是运行时卡片,但用一行 card 记录它的位置,参与首页排序
INSERT OR IGNORE INTO card (key, title, sort) VALUES ('local-service', '本地服务', -1);
"#;
/// v16 → v17:条目加 `updated_at`(乐观锁),新增审计表。
@@ -218,6 +221,11 @@ const V20_SQL: &str = r#"
ALTER TABLE node ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
"#;
/// v21 → v22:本地服务卡片入库,用 card.sort 参与首页排序(内容仍来自运行时项目)。
const V22_SQL: &str = r#"
INSERT OR IGNORE INTO card (key, title, sort) VALUES ('local-service', '本地服务', -1);
"#;
/// v20 → v21:格式从分组下沉到条目;老数据按所属根分组的 view_mode 回填。
const V21_SQL: &str = r#"
ALTER TABLE entry ADD COLUMN format TEXT NOT NULL DEFAULT 'apps';
@@ -294,7 +302,7 @@ pub struct SeedReport {
///
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
pub const SCHEMA_VERSION: i64 = 21;
pub const SCHEMA_VERSION: i64 = 22;
fn current_version(conn: &Connection) -> Result<i64, StashError> {
conn.pragma_query_value(None, "user_version", |row| row.get(0))
@@ -359,6 +367,11 @@ pub fn migrate(conn: &Connection) -> Result<i64, StashError> {
version = 21;
}
if version == 21 {
conn.execute_batch(V22_SQL).map_err(map_error)?;
version = 22;
}
conn.pragma_update(None, "user_version", version)
.map_err(map_error)?;