feat: 搜索带并入 Layout toolbar,后台重组首页配置

前台
- 搜索框从内容列移到 Layout 的 toolbar 插槽,去掉 fixed + TOP_OFFSET + top-blur 三处补偿
- 左右栏滚动容器不变,左栏吸顶改用 CSS sticky(修回顶时侧栏抖动)
- 只让分组导航滚动,资料卡与本地服务固定
- 页头操作支持配置:搜索改为刷新(重扫项目 + 重拉内容库)
- 站点级设置:页面标题 / favicon / 品牌 logo / 品牌文字
- 停用的分组在侧边栏、首页卡片、分组页、条目页统一不再出现

后台
- 「侧边栏」「首页卡片」合并为「首页」Tab
- 首页卡片表格新增状态列与顺序列(上下箭头即时保存)
- 分组显隐只由「分组管理 → 状态」决定,去掉侧栏独立勾选
- 页头 Tab 增加站点信息、首页 Logo(带实时预览)、操作工具开关
- 后台页头与前台同构并吸顶

后端
- settings 新增 site(标题 / favicon / 品牌 / 操作工具)与 sidebar.show_nav
- 写接口成功后广播内容库变更,SSE 改为「广播 + data_version 兜底」
- catalog SSE 事件名与前端监听对齐(catalog)
This commit is contained in:
2026-11-01 15:15:20 +08:00
parent bb5a615ed2
commit 59fd61d832
9 changed files with 822 additions and 296 deletions
+84
View File
@@ -88,6 +88,7 @@ impl Default for SidebarSettings {
Self {
show_profile: true,
show_service: true,
show_nav: true,
hidden: Vec::new(),
order: Vec::new(),
hidden_groups: Vec::new(),
@@ -108,6 +109,9 @@ pub struct SidebarSettings {
/// 是否显示「本地服务」面板
#[serde(default = "default_true")]
pub show_service: bool,
/// 是否显示「分组导航」卡片(首页 / 分组树)
#[serde(default = "default_true")]
pub show_nav: bool,
/// 隐藏的分类 id
pub hidden: Vec<String>,
/// 分类显示顺序(id);未列出的排在后面,保持原有顺序
@@ -160,6 +164,70 @@ impl Default for ProfileSettings {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct BrandSettings {
/// 品牌文字(页头左侧,可留空只显示 logo)
pub name: String,
/// 没有 logo 图片时,圆形标记里显示的首字
pub mark: String,
/// logo 图片地址;空字符串表示用首字
pub logo: String,
}
impl Default for BrandSettings {
fn default() -> Self {
Self {
name: "STASH".into(),
mark: "S".into(),
logo: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct ActionSettings {
/// 刷新图标(重扫本地项目并重拉内容库)
pub refresh: bool,
/// 站点设置图标(仅登录后出现)
pub settings: bool,
/// 登录 / 退出登录
pub session: bool,
}
impl Default for ActionSettings {
fn default() -> Self {
Self {
refresh: true,
settings: true,
session: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct SiteMetaSettings {
/// 浏览器标签标题
pub title: String,
/// favicon 地址;空字符串表示用内置图标
pub favicon: String,
pub brand: BrandSettings,
pub actions: ActionSettings,
}
impl Default for SiteMetaSettings {
fn default() -> Self {
Self {
title: "Stash · 本地应用工作区".into(),
favicon: String::new(),
brand: BrandSettings::default(),
actions: ActionSettings::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default)]
pub struct Settings {
@@ -167,6 +235,7 @@ pub struct Settings {
pub sidebar: SidebarSettings,
pub sections: SectionsSettings,
pub profile: ProfileSettings,
pub site: SiteMetaSettings,
}
/// 读到/保存后的完整状态:设置本体 + 版本号(前端提交时要带回来)。
@@ -244,6 +313,21 @@ pub fn validate(settings: &Settings) -> Result<(), StashError> {
if settings.profile.location.chars().count() > 40 {
return Err(StashError::InvalidDirectory("位置最多 40 个字".into()));
}
if settings.site.title.chars().count() > 120 {
return Err(StashError::InvalidDirectory("页面标题最多 120 个字".into()));
}
if settings.site.favicon.chars().count() > 500 {
return Err(StashError::InvalidDirectory("favicon 地址最多 500 个字符".into()));
}
if settings.site.brand.name.chars().count() > 20 {
return Err(StashError::InvalidDirectory("品牌文字最多 20 个字".into()));
}
if settings.site.brand.mark.chars().count() > 4 {
return Err(StashError::InvalidDirectory("logo 标记最多 4 个字".into()));
}
if settings.site.brand.logo.chars().count() > 500 {
return Err(StashError::InvalidDirectory("logo 地址最多 500 个字符".into()));
}
Ok(())
}