feat(admin): 分区增加「允许子分组」开关(与类型解耦)
- node 新增 allow_groups(迁移 v18):应用默认开、海报/图文默认关,已有分组的非应用分区自动开启 - 后台每行一个「子分组」开关;分组管理、卡片预览、内容弹窗选组、侧栏建组都跟随开关 - 关闭开关时条目平铺且不丢内容;首页卡片指向分组会自动挪回分区 - 三种类型共用同一模板,类型只决定渲染方式 - 测试 59 passed(迁移 15→18、开关建组、类型切换)
This commit is contained in:
+95
-33
@@ -32,6 +32,8 @@ pub struct UpdateCategory {
|
||||
pub empty_text: Option<String>,
|
||||
/// 首页卡片预览哪个节点(分区自己或它下面的分组)
|
||||
pub block_node_key: Option<String>,
|
||||
/// 是否允许子分组(与类型无关,后台开关)
|
||||
pub allow_groups: Option<bool>,
|
||||
}
|
||||
|
||||
/// 新建分组(子分类)
|
||||
@@ -90,9 +92,9 @@ pub fn create(conn: &mut Connection, new: NewCategory) -> Result<(), StashError>
|
||||
.map_err(map_error)?;
|
||||
|
||||
tx.execute(
|
||||
"INSERT INTO node (key, parent_key, title, kind, view_mode, sort, show_in_sidebar)
|
||||
VALUES (?1, NULL, ?2, 'section', ?3, ?4, 1)",
|
||||
params![id, title, new.kind, sort],
|
||||
"INSERT INTO node (key, parent_key, title, kind, view_mode, allow_groups, sort, show_in_sidebar)
|
||||
VALUES (?1, NULL, ?2, 'section', ?3, ?4, ?5, 1)",
|
||||
params![id, title, new.kind, i64::from(new.kind == "apps"), sort],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
|
||||
@@ -121,6 +123,7 @@ struct SectionRow {
|
||||
view_mode: Option<String>,
|
||||
preview_limit: Option<i64>,
|
||||
empty_text: Option<String>,
|
||||
allow_groups: bool,
|
||||
}
|
||||
|
||||
fn valid_color(color: &str) -> bool {
|
||||
@@ -148,7 +151,7 @@ pub fn update(conn: &mut Connection, id: &str, patch: UpdateCategory) -> Result<
|
||||
|
||||
let current = tx
|
||||
.query_row(
|
||||
"SELECT title, icon, bar_color, view_mode, preview_limit, empty_text
|
||||
"SELECT title, icon, bar_color, view_mode, preview_limit, empty_text, allow_groups
|
||||
FROM node WHERE key = ?1 AND kind = 'section'",
|
||||
[id],
|
||||
|row| {
|
||||
@@ -159,6 +162,7 @@ pub fn update(conn: &mut Connection, id: &str, patch: UpdateCategory) -> Result<
|
||||
view_mode: row.get(3)?,
|
||||
preview_limit: row.get(4)?,
|
||||
empty_text: row.get(5)?,
|
||||
allow_groups: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -237,9 +241,10 @@ pub fn update(conn: &mut Connection, id: &str, patch: UpdateCategory) -> Result<
|
||||
|
||||
let was_apps = current.view_mode.as_deref() == Some("apps");
|
||||
let is_apps = kind == "apps";
|
||||
let allow_groups = patch.allow_groups.unwrap_or(current.allow_groups);
|
||||
|
||||
if is_apps && !was_apps {
|
||||
// 切到应用:条目必须有分组可挂;没有就建一个「全部」
|
||||
if is_apps && !was_apps && allow_groups {
|
||||
// 切到应用且开启子分组:条目必须有分组可挂;没有就建一个「全部」
|
||||
let group_count: i64 = tx
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM node WHERE kind = 'group' AND parent_key = ?1",
|
||||
@@ -277,8 +282,10 @@ pub fn update(conn: &mut Connection, id: &str, patch: UpdateCategory) -> Result<
|
||||
params![group_key, id],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
} else if !is_apps && was_apps {
|
||||
// 离开应用:首页卡片从「某个分组」挪回分区,标题交还给分区
|
||||
}
|
||||
|
||||
// 关闭子分组:首页卡片如果指着分组,挪回分区,避免标题/预览错位
|
||||
if !allow_groups {
|
||||
tx.execute(
|
||||
"UPDATE section_block SET node_key = ?1, title = NULL
|
||||
WHERE node_key IN (
|
||||
@@ -291,9 +298,18 @@ pub fn update(conn: &mut Connection, id: &str, patch: UpdateCategory) -> Result<
|
||||
|
||||
tx.execute(
|
||||
"UPDATE node SET title = ?1, icon = ?2, bar_color = ?3, view_mode = ?4,
|
||||
preview_limit = ?5, empty_text = ?6
|
||||
WHERE key = ?7",
|
||||
params![title, icon, bar_color, kind, preview_limit, empty_text, id],
|
||||
preview_limit = ?5, empty_text = ?6, allow_groups = ?7
|
||||
WHERE key = ?8",
|
||||
params![
|
||||
title,
|
||||
icon,
|
||||
bar_color,
|
||||
kind,
|
||||
preview_limit,
|
||||
empty_text,
|
||||
allow_groups,
|
||||
id
|
||||
],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
|
||||
@@ -361,21 +377,21 @@ pub fn create_group(conn: &mut Connection, section_id: &str, new: NewGroup) -> R
|
||||
return Err(StashError::InvalidDirectory("分组标题需为 1–40 个字".into()));
|
||||
}
|
||||
|
||||
let view_mode: Option<String> = conn
|
||||
let allow_groups: Option<bool> = conn
|
||||
.query_row(
|
||||
"SELECT view_mode FROM node WHERE key = ?1 AND kind = 'section'",
|
||||
"SELECT allow_groups FROM node WHERE key = ?1 AND kind = 'section'",
|
||||
[section_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(map_error)?;
|
||||
|
||||
let Some(view_mode) = view_mode else {
|
||||
let Some(allow_groups) = allow_groups else {
|
||||
return Err(StashError::NotFound(format!("分区不存在: {section_id}")));
|
||||
};
|
||||
if view_mode != "apps" {
|
||||
if !allow_groups {
|
||||
return Err(StashError::InvalidDirectory(
|
||||
"只有应用类分区可以添加分组".into(),
|
||||
"该分区未开启「允许子分组」".into(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -500,7 +516,8 @@ mod tests {
|
||||
preview_limit INTEGER,
|
||||
empty_text TEXT,
|
||||
sort INTEGER NOT NULL DEFAULT 0,
|
||||
show_in_sidebar INTEGER NOT NULL DEFAULT 1
|
||||
show_in_sidebar INTEGER NOT NULL DEFAULT 1,
|
||||
allow_groups INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE entry (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -635,7 +652,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_apps_to_posters_moves_card_back_to_section() {
|
||||
fn switching_apps_to_posters_keeps_groups_until_disabled() {
|
||||
let mut conn = db();
|
||||
create(&mut conn, section("books", "书单", "apps")).expect("应能创建");
|
||||
conn.execute(
|
||||
@@ -643,28 +660,45 @@ mod tests {
|
||||
[],
|
||||
)
|
||||
.expect("应能建分组");
|
||||
conn.execute(
|
||||
"INSERT INTO entry (key, node_key) VALUES ('b1', 'books-g')",
|
||||
[],
|
||||
)
|
||||
.expect("应能建条目");
|
||||
conn.execute("INSERT INTO entry (key, node_key) VALUES ('b1', 'books-g')", [])
|
||||
.expect("应能建条目");
|
||||
// create() 会顺手建一张分区卡片;这里只保留分组卡片,模拟真实配置
|
||||
conn.execute("DELETE FROM section_block", []).expect("应能清卡片");
|
||||
conn.execute(
|
||||
"INSERT INTO section_block (node_key, title, sort) VALUES ('books-g', '分组', 1)",
|
||||
[],
|
||||
)
|
||||
.expect("应能建卡片");
|
||||
|
||||
update(&mut conn, "books", UpdateCategory { kind: Some("posters".into()), ..Default::default() })
|
||||
.expect("应能改类型");
|
||||
|
||||
// 类型切到海报但子分组仍开启:分组和卡片都保留
|
||||
update(
|
||||
&mut conn,
|
||||
"books",
|
||||
UpdateCategory {
|
||||
kind: Some("posters".into()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("应能改类型");
|
||||
let block: String = conn
|
||||
.query_row("SELECT node_key FROM section_block", [], |row| row.get(0))
|
||||
.expect("应能查");
|
||||
assert_eq!(block, "books", "卡片应挪回分区");
|
||||
let mode: String = conn
|
||||
.query_row("SELECT view_mode FROM node WHERE key='books'", [], |row| row.get(0))
|
||||
assert_eq!(block, "books-g");
|
||||
|
||||
// 关闭子分组:卡片挪回分区
|
||||
update(
|
||||
&mut conn,
|
||||
"books",
|
||||
UpdateCategory {
|
||||
allow_groups: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("应能关闭");
|
||||
let block: String = conn
|
||||
.query_row("SELECT node_key FROM section_block", [], |row| row.get(0))
|
||||
.expect("应能查");
|
||||
assert_eq!(mode, "posters");
|
||||
assert_eq!(block, "books", "关闭子分组后卡片应指回分区");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -725,8 +759,28 @@ mod tests {
|
||||
}
|
||||
)
|
||||
.is_err(),
|
||||
"海报 / 图文分区不提供分组"
|
||||
"未开启子分组的分区不能建组"
|
||||
);
|
||||
|
||||
// 打开「允许子分组」后,海报分区也可以建组
|
||||
update(
|
||||
&mut conn,
|
||||
"films",
|
||||
UpdateCategory {
|
||||
allow_groups: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("应能开启");
|
||||
create_group(
|
||||
&mut conn,
|
||||
"films",
|
||||
NewGroup {
|
||||
id: "g1".into(),
|
||||
title: "片单".into(),
|
||||
},
|
||||
)
|
||||
.expect("开启后应能建组");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -740,8 +794,16 @@ mod tests {
|
||||
)
|
||||
.expect("应能建卡片");
|
||||
|
||||
update(&mut conn, "films", UpdateCategory { kind: Some("apps".into()), ..Default::default() })
|
||||
.expect("应能改类型");
|
||||
update(
|
||||
&mut conn,
|
||||
"films",
|
||||
UpdateCategory {
|
||||
kind: Some("apps".into()),
|
||||
allow_groups: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("应能改类型");
|
||||
|
||||
let (entry_node, block_node): (String, String) = conn
|
||||
.query_row(
|
||||
|
||||
Reference in New Issue
Block a user