feat(admin): 分组管理改为树形表格(图标/排序/状态)
- 参考需求页重做分组管理:树形表格(名称/排序权重/关联网页数/状态/操作),去掉展开全部/折叠全部,新建根分组置于左侧 - 分组弹窗支持上级分组移动、格式、图标(可留空)、排序权重、启停状态 - 数据层 v20:node.enabled 启停;icon/sort 可编辑;禁用分组前台侧栏与卡片隐藏 - 修复同一父级下编辑排序权重被重置为末尾 - 测试 59 passed;Playwright 验证无图标/图标/排序/禁用/折叠
This commit is contained in:
+4
-1
@@ -63,13 +63,14 @@ struct TreeNode {
|
||||
bar_color: Option<String>,
|
||||
preview_limit: Option<i64>,
|
||||
sort: i64,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
/// 新的原子视图:完整分组树(多层、带格式、带条目),供新首页/侧栏/后台使用。
|
||||
pub fn load_tree(conn: &Connection) -> Result<Value, StashError> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT key, parent_key, title, view_mode, icon, bar_color, preview_limit, sort
|
||||
"SELECT key, parent_key, title, view_mode, icon, bar_color, preview_limit, sort, enabled
|
||||
FROM node ORDER BY sort, key",
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
@@ -85,6 +86,7 @@ pub fn load_tree(conn: &Connection) -> Result<Value, StashError> {
|
||||
bar_color: row.get(5)?,
|
||||
preview_limit: row.get(6)?,
|
||||
sort: row.get(7)?,
|
||||
enabled: row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(map_error)?
|
||||
@@ -160,6 +162,7 @@ pub fn load_tree(conn: &Connection) -> Result<Value, StashError> {
|
||||
"barColor": node.bar_color.clone().unwrap_or_default(),
|
||||
"previewLimit": node.preview_limit,
|
||||
"sort": node.sort,
|
||||
"enabled": node.enabled,
|
||||
"entries": items,
|
||||
"children": child_values,
|
||||
}))
|
||||
|
||||
+76
-20
@@ -23,6 +23,15 @@ pub struct NewNode {
|
||||
/// 根分组的格式:apps / posters / articles(子分组继承,不需要传)
|
||||
#[serde(default)]
|
||||
pub format: Option<String>,
|
||||
/// 图标:可以是 iconfont 名或字符;空字符串表示不显示
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
/// 排序权重;不传则排到同级末尾
|
||||
#[serde(default)]
|
||||
pub sort: Option<i64>,
|
||||
/// 是否启用,默认 true
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
@@ -34,6 +43,9 @@ pub struct UpdateNode {
|
||||
pub sort: Option<i64>,
|
||||
/// 只对根分组生效
|
||||
pub format: Option<String>,
|
||||
/// 图标:空字符串表示清除图标
|
||||
pub icon: Option<String>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
fn valid_key(key: &str) -> bool {
|
||||
@@ -87,7 +99,7 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
|
||||
}
|
||||
};
|
||||
|
||||
let sort: i64 = match new.parent_key.as_deref() {
|
||||
let default_sort: i64 = match new.parent_key.as_deref() {
|
||||
Some(parent) => conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(sort), 0) + 1 FROM node WHERE parent_key = ?1",
|
||||
@@ -103,12 +115,29 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
|
||||
)
|
||||
.map_err(map_error)?,
|
||||
};
|
||||
let sort = new.sort.unwrap_or(default_sort);
|
||||
let icon = new
|
||||
.icon
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string);
|
||||
let enabled = new.enabled.unwrap_or(true);
|
||||
|
||||
let tx = conn.transaction().map_err(map_error)?;
|
||||
tx.execute(
|
||||
"INSERT INTO node (key, parent_key, title, kind, view_mode, sort, show_in_sidebar, allow_groups)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, 1)",
|
||||
params![key, new.parent_key, title, kind, view_mode, sort],
|
||||
"INSERT INTO node (key, parent_key, title, kind, view_mode, icon, enabled, sort, show_in_sidebar, allow_groups)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1, 1)",
|
||||
params![
|
||||
key,
|
||||
new.parent_key,
|
||||
title,
|
||||
kind,
|
||||
view_mode,
|
||||
icon,
|
||||
enabled,
|
||||
sort
|
||||
],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
entries::audit(&tx, "node.create", key, title)?;
|
||||
@@ -117,16 +146,27 @@ pub fn create(conn: &mut Connection, new: NewNode) -> Result<(), StashError> {
|
||||
}
|
||||
|
||||
pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(), StashError> {
|
||||
let current: Option<(String, Option<String>, Option<String>, i64)> = conn
|
||||
let current: Option<(String, Option<String>, Option<String>, i64, Option<String>, bool)> = conn
|
||||
.query_row(
|
||||
"SELECT title, parent_key, view_mode, sort FROM node WHERE key = ?1",
|
||||
"SELECT title, parent_key, view_mode, sort, icon, enabled FROM node WHERE key = ?1",
|
||||
[key],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
row.get(5)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(map_error)?;
|
||||
|
||||
let Some((current_title, current_parent, current_format, current_sort)) = current else {
|
||||
let Some((current_title, current_parent, current_format, current_sort, current_icon, current_enabled)) =
|
||||
current
|
||||
else {
|
||||
return Err(StashError::NotFound(format!("分组不存在: {key}")));
|
||||
};
|
||||
|
||||
@@ -198,24 +238,40 @@ pub fn update(conn: &mut Connection, key: &str, patch: UpdateNode) -> Result<(),
|
||||
return Err(StashError::InvalidDirectory("根分组缺少格式".into()));
|
||||
}
|
||||
|
||||
let icon = match patch.icon {
|
||||
Some(icon) => {
|
||||
let icon = icon.trim().to_string();
|
||||
(!icon.is_empty()).then_some(icon)
|
||||
}
|
||||
None => current_icon,
|
||||
};
|
||||
let enabled = patch.enabled.unwrap_or(current_enabled);
|
||||
|
||||
let sort = patch.sort.unwrap_or(current_sort);
|
||||
|
||||
let tx = conn.transaction().map_err(map_error)?;
|
||||
// 移动时重新排到新父级末尾;不移动则保留原顺序
|
||||
let final_sort = match patch.parent_key {
|
||||
Some(parent) => tx
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(sort), 0) + 1 FROM node WHERE parent_key = ?1 AND key <> ?2",
|
||||
params![parent, key],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.map_err(map_error)?,
|
||||
None => sort,
|
||||
// 只有真正换了父级才排到末尾;同一父级下编辑排序权重时按传入值保存
|
||||
let parent_changed = patch
|
||||
.parent_key
|
||||
.as_deref()
|
||||
.is_some_and(|parent| Some(parent) != current_parent.as_deref());
|
||||
let final_sort = if parent_changed {
|
||||
let parent = patch.parent_key.as_deref().unwrap_or_default();
|
||||
tx.query_row(
|
||||
"SELECT COALESCE(MAX(sort), 0) + 1 FROM node WHERE parent_key = ?1 AND key <> ?2",
|
||||
params![parent, key],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.map_err(map_error)?
|
||||
} else {
|
||||
sort
|
||||
};
|
||||
|
||||
tx.execute(
|
||||
"UPDATE node SET title = ?1, parent_key = ?2, view_mode = ?3, sort = ?4 WHERE key = ?5",
|
||||
params![title, parent_key, format, final_sort, key],
|
||||
"UPDATE node SET title = ?1, parent_key = ?2, view_mode = ?3, sort = ?4,
|
||||
icon = ?5, enabled = ?6
|
||||
WHERE key = ?7",
|
||||
params![title, parent_key, format, final_sort, icon, enabled, key],
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
entries::audit(&tx, "node.update", key, &title)?;
|
||||
|
||||
+13
-1
@@ -48,6 +48,8 @@ CREATE TABLE IF NOT EXISTS node (
|
||||
-- 是否出现在侧栏:0 表示只当首页卡片/内容,不进导航
|
||||
allow_groups INTEGER NOT NULL DEFAULT 0 CHECK (allow_groups IN (0, 1)),
|
||||
-- 是否允许子分组(与类型无关,后台开关)
|
||||
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
||||
-- 是否启用(禁用后前台侧栏/卡片不展示)
|
||||
sort INTEGER NOT NULL DEFAULT 0, -- 同级顺序,越小越靠前
|
||||
CHECK (
|
||||
(kind = 'section' AND parent_key IS NULL AND view_mode IS NOT NULL)
|
||||
@@ -210,6 +212,11 @@ WHERE kind = 'section' AND key IN (
|
||||
);
|
||||
"#;
|
||||
|
||||
/// v19 → v20:分组增加启停状态。
|
||||
const V20_SQL: &str = r#"
|
||||
ALTER TABLE node ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
||||
"#;
|
||||
|
||||
/// v18 → v19:首页卡片改为「卡片 + 勾选分组」。
|
||||
const V19_SQL: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS card (
|
||||
@@ -269,7 +276,7 @@ pub struct SeedReport {
|
||||
///
|
||||
/// 从 v16 起有了第一套迁移:`migrate` 会把老库逐级升上来,
|
||||
/// 只有「比程序更新」或「没有迁移路径」的版本才拒绝启动。
|
||||
pub const SCHEMA_VERSION: i64 = 19;
|
||||
pub const SCHEMA_VERSION: i64 = 20;
|
||||
|
||||
fn current_version(conn: &Connection) -> Result<i64, StashError> {
|
||||
conn.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||
@@ -324,6 +331,11 @@ pub fn migrate(conn: &Connection) -> Result<i64, StashError> {
|
||||
version = 19;
|
||||
}
|
||||
|
||||
if version == 19 {
|
||||
conn.execute_batch(V20_SQL).map_err(map_error)?;
|
||||
version = 20;
|
||||
}
|
||||
|
||||
conn.pragma_update(None, "user_version", version)
|
||||
.map_err(map_error)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user