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:
+33
-16
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user