feat: 格式从分组下沉到条目(v21)

- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入
- 分组管理移除格式字段/显示,根分组只作容器
- 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式
- 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板
- 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
This commit is contained in:
2026-10-31 14:26:28 +08:00
parent 8d094175fd
commit 11f992cdad
12 changed files with 173 additions and 72 deletions
+33 -16
View File
@@ -16,6 +16,7 @@ const MAX_TAG_LEN: usize = 40;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct EntryRecord {
pub key: String,
pub format: String,
pub name: String,
pub description: String,
pub icon: String,
@@ -57,6 +58,9 @@ pub struct EntryPatch {
/// 可选:把条目移动到另一个分区 / 分组(不传则保持原位)
#[serde(default)]
pub node_key: Option<String>,
/// 条目自身的展示格式:apps / posters / articles;不传默认 apps
#[serde(default)]
pub format: Option<String>,
}
/// 新建条目的请求体:`key` 是业务键,`nodeKey` 决定挂在哪个分区 / 分组。
@@ -151,6 +155,11 @@ fn looks_like_domain(value: &str) -> bool {
}
fn validate(patch: &EntryPatch) -> Result<(), StashError> {
if let Some(format) = patch.format.as_deref() {
if !matches!(format, "apps" | "posters" | "articles") {
return Err(StashError::InvalidDirectory("格式不合法".into()));
}
}
if patch.name.trim().is_empty() {
return Err(StashError::InvalidDirectory("标题不能为空".into()));
}
@@ -181,7 +190,7 @@ fn validate(patch: &EntryPatch) -> Result<(), StashError> {
pub fn load(conn: &Connection, key: &str) -> Result<Option<EntryRecord>, StashError> {
let row = conn
.query_row(
"SELECT key, name, COALESCE(description, ''), COALESCE(icon, ''), COALESCE(href, ''),
"SELECT key, COALESCE(format, 'apps'), name, COALESCE(description, ''), COALESCE(icon, ''), COALESCE(href, ''),
COALESCE(cover, ''), COALESCE(banner, ''), COALESCE(badge, ''),
COALESCE(comment, ''), COALESCE(summary, ''), COALESCE(body, ''), updated_at
FROM entry WHERE key = ?1",
@@ -189,18 +198,19 @@ pub fn load(conn: &Connection, key: &str) -> Result<Option<EntryRecord>, StashEr
|row| {
Ok(EntryRecord {
key: row.get(0)?,
name: row.get(1)?,
description: row.get(2)?,
icon: row.get(3)?,
href: row.get(4)?,
cover: row.get(5)?,
banner: row.get(6)?,
badge: row.get(7)?,
comment: row.get(8)?,
summary: row.get(9)?,
body: row.get(10)?,
format: row.get(1)?,
name: row.get(2)?,
description: row.get(3)?,
icon: row.get(4)?,
href: row.get(5)?,
cover: row.get(6)?,
banner: row.get(7)?,
badge: row.get(8)?,
comment: row.get(9)?,
summary: row.get(10)?,
body: row.get(11)?,
tags: Vec::new(),
updated_at: row.get(11)?,
updated_at: row.get(12)?,
})
},
)
@@ -285,6 +295,7 @@ pub fn create(conn: &mut Connection, new: NewEntry) -> Result<EntryRecord, Stash
let patch = new.item;
validate(&patch)?;
let format = patch.format.clone().unwrap_or_else(|| "apps".into());
let href = normalize_href(&patch.href)?;
validate_target(conn, &new.node_key)?;
@@ -316,12 +327,13 @@ pub fn create(conn: &mut Connection, new: NewEntry) -> Result<EntryRecord, Stash
.map_err(map_error)?;
tx.execute(
"INSERT INTO entry (key, node_key, name, description, icon, href, cover, banner,
"INSERT INTO entry (key, node_key, format, name, description, icon, href, cover, banner,
badge, comment, summary, body, hot, views, likes, sort, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 0, 0, 0, ?13, ?14)",
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 0, 0, 0, ?14, ?15)",
params![
key,
new.node_key,
format,
patch.name.trim(),
patch.description,
patch.icon,
@@ -370,6 +382,7 @@ pub fn update( conn: &mut Connection,
expected_updated_at: i64,
) -> Result<EntryRecord, StashError> {
validate(&patch)?;
let format = patch.format.clone().unwrap_or_else(|| "apps".into());
// 跳转地址先规范化:补 https、拒绝 javascript: 等危险协议
let href = normalize_href(&patch.href)?;
if let Some(node_key) = patch.node_key.as_deref() {
@@ -392,7 +405,8 @@ pub fn update( conn: &mut Connection,
name = ?1, description = ?2, icon = ?3, href = ?4,
cover = ?5, banner = ?6, badge = ?7, comment = ?8,
summary = ?9, body = ?10, updated_at = ?11,
node_key = COALESCE(?14, node_key)
node_key = COALESCE(?14, node_key),
format = ?15
WHERE key = ?12 AND updated_at = ?13",
params![
patch.name.trim(),
@@ -408,7 +422,8 @@ pub fn update( conn: &mut Connection,
timestamp,
key,
expected_updated_at,
patch.node_key
patch.node_key,
format
],
)
.map_err(map_error)?;
@@ -488,6 +503,7 @@ mod tests {
"CREATE TABLE entry (
key TEXT PRIMARY KEY,
node_key TEXT NOT NULL,
format TEXT NOT NULL DEFAULT 'apps',
name TEXT NOT NULL,
description TEXT, icon TEXT, href TEXT, cover TEXT, banner TEXT,
badge TEXT, comment TEXT, summary TEXT, body TEXT,
@@ -539,6 +555,7 @@ mod tests {
body: "## 标题".into(),
tags: vec!["推荐电影".into(), "动画".into()],
node_key: None,
format: None,
}
}