diff --git a/HANDOFF.md b/HANDOFF.md index 63bcc43..7906088 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -99,8 +99,8 @@ README.md 运行、路由、库结构、登录说明 | --- | --- | | 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/图标/排序/状态)、删除;格式已下沉到条目,不再在分组上设置 | | 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 格式 / 跳转链接 / 所属分组 / 操作;每条内容单独选格式(应用/海报/图文),同一分组可混合 | -| 侧边栏 | 分组树逐项勾选,开关在右侧、**拨动即保存**(无单独保存/撤销按钮);子分组可单独勾选 | -| 首页卡片 | 左侧卡片列表 + 右侧分组表格;新建卡片弹窗输入标题;卡片可拖动排序(影响首页顺序);表格内开关/条数即时保存,标题用「保存」按钮 | +| 侧边栏 | 顶部「侧栏卡片」控制本地管理员 / 本地服务显隐;下方「分组导航」逐项勾选;开关即保存 | +| 首页卡片 | 左侧列表含只读「本地服务」表(不可删除)+ 自建卡片;右侧分组表格;卡片可拖动排序,表格内开关/条数即时保存 | | 页头 | 启用开关、速度、条目、标签与色调 | | 资料 | 头像 URL、名称、角色、简介、位置 | | 系统 | 修改密码、下载备份 | @@ -166,6 +166,7 @@ GET /api/backup SQLite 快照附件 需登录 - 文件:开发态在 `target/debug/data/stash.db`;可用 `--root` / 数据目录覆盖(见 `config.rs`)。 - 当前 `PRAGMA user_version = 21`;`schema::migrate` 逐级升级:0 建全量、15→16→17→18→19→20→21、更高版本拒绝启动。 - 格式在 `entry.format`(apps / posters / articles),同组可混合;`node` 的旧 `view_mode` 仅保留兼容,不再决定渲染。 +- `settings.sidebar.show_profile / show_service` 控制侧栏两张卡片显隐(默认 true);`card` 表只存自建卡片,首页「本地服务」卡片由 `/api/projects` 运行时生成,不落库、不可删。 - `node` 是统一分组树(根节点 `kind='section'`,子节点 `kind='group'` 可多层);`enabled` 控制启停;`icon` 可为空。 - 主要表:`node`、`entry`、`entry_tag`、`section_block`、`admin`、`session`、`setting`、`audit`。 - `entry.updated_at` 用于编辑乐观锁;`setting` 表 `key='site'` 存站点 JSON。 diff --git a/README.md b/README.md index 8cb1f57..62e7b5b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ cargo run -- --root ./sites --host 0.0.0.0 --port 3000 ### 登录与设置(v0.1.1 起) - 登录是**页内弹窗**:点右上角用户图标;首次使用弹窗里是「设置密码 + 确认」(至少 8 位); -- 登录后点「站点设置」进入 `/admin`:分组管理(树形表格,可建根/子分组、图标可空、排序权重、启停、删除)、内容(选分组以列表查看/编辑条目,每条可单独设格式,主分组含子分组)、侧边栏(勾选分组树)、首页卡片(左侧卡片列表 + 分组表格),以及页头滚动信息、资料与系统设置; +- 登录后点「站点设置」进入 `/admin`:分组管理(树形表格,可建根/子分组、图标可空、排序权重、启停、删除)、内容(选分组以列表查看/编辑条目,每条可单独设格式,主分组含子分组)、侧边栏(侧栏卡片显隐 + 分组导航勾选)、首页卡片(本地服务只读表 + 左侧卡片列表 + 分组表格),以及页头滚动信息、资料与系统设置; - 详情页的 `[编辑]` 只在登录后显示;保存后局部刷新,`updated_at` 乐观锁; - `/admin` 的「系统」页可以改密码、下载整库备份(`VACUUM INTO` 快照); - 密码用 argon2id 哈希存在 `data/stash.db` 的 `admin` 表,会话是 HttpOnly + SameSite=Strict 的 Cookie(7 天); diff --git a/docs/ADMIN-PLAN.md b/docs/ADMIN-PLAN.md index 307084f..c8adfc9 100644 --- a/docs/ADMIN-PLAN.md +++ b/docs/ADMIN-PLAN.md @@ -147,6 +147,14 @@ DELETE /api/items/:id ## 11. 实施记录 +### P17(2026-10-31,侧栏卡片显隐、本地服务卡片、同步滚动) + +- 侧边栏设置顶部新增「侧栏卡片」:本地管理员 / 本地服务各一个开关,即时保存到 `settings.sidebar.show_profile / show_service`;与下方分组导航分区显示; +- 首页恢复「本地服务」卡片(由 `/api/projects` 运行时生成,应用网格),固定排在最前;移除卡片右上角三点; +- 首页卡片后台新增只读「本地服务」表:列出项目名 / 访问地址,标记不可删除,不提供删除/改名; +- 前台放弃整屏内滚动,改为页面整体滚动,左右两栏同步滚动; +- 验证:开关即落库、前台隐藏对应卡片、三点数量为 0、本地服务表无删除按钮、滚动 300px 两栏同步位移 300px。 + ### P16(2026-10-31,格式下沉到条目) - 数据库 v21:`entry.format`(apps / posters / articles),老数据按所属根分组的 `view_mode` 自动回填;新库 seed 按分区写入; diff --git a/src/db/settings.rs b/src/db/settings.rs index f4553bb..208a884 100644 --- a/src/db/settings.rs +++ b/src/db/settings.rs @@ -79,9 +79,35 @@ impl Default for HeaderSettings { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +fn default_true() -> bool { + true +} + +impl Default for SidebarSettings { + fn default() -> Self { + Self { + show_profile: true, + show_service: true, + hidden: Vec::new(), + order: Vec::new(), + hidden_groups: Vec::new(), + shown_groups: Vec::new(), + group_order: std::collections::HashMap::new(), + tree_hidden: Vec::new(), + tree_order: std::collections::HashMap::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(default)] pub struct SidebarSettings { + /// 是否显示「本地管理员」资料卡 + #[serde(default = "default_true")] + pub show_profile: bool, + /// 是否显示「本地服务」面板 + #[serde(default = "default_true")] + pub show_service: bool, /// 隐藏的分类 id pub hidden: Vec, /// 分类显示顺序(id);未列出的排在后面,保持原有顺序 diff --git a/stash-web/src/App.css b/stash-web/src/App.css index 90b9c13..881e5f8 100644 --- a/stash-web/src/App.css +++ b/stash-web/src/App.css @@ -1299,6 +1299,11 @@ a.entry-href:hover { min-width: 0; } +.sidebar-divider { + margin: var(--su-space-2) 0; + border-top: 1px dashed var(--su-border-strong); +} + /* 首页卡片:左侧卡片列表 + 右侧分组表格 */ .cards-manager { display: grid; diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx index 081f390..e28f3c0 100644 --- a/stash-web/src/AppNext.tsx +++ b/stash-web/src/AppNext.tsx @@ -208,6 +208,8 @@ export default function AppNext() { const site: SiteSettings = settings?.settings ?? { header: { enabled: true, speed: 24, items: [] }, sidebar: { + show_profile: true, + show_service: true, hidden: [], order: [], hidden_groups: [], @@ -309,6 +311,30 @@ export default function AppNext() { ) } + function renderLocalService() { + const items: AppItem[] = projects.map((project) => ({ + id: project.name, + name: project.name, + desc: project.route_path, + icon: project.name.slice(0, 1).toUpperCase(), + href: project.route_path, + badge: '本地项目', + comment: '本地静态站点,由 Stash 直接托管。', + views: 0, + likes: 0, + })) + + return ( + + {items.length > 0 ? ( + + ) : ( + + )} + + ) + } + function renderCard(card: CardItem) { const resolved = card.groups .map((ref) => ({ ref, group: findGroup(manage.groups, ref.groupKey) })) @@ -327,7 +353,6 @@ export default function AppNext() { title={card.title} count={total} barColor={first?.barColor} - onMore={() => first !== undefined && navigate(`/g/${encodeURIComponent(first.key)}`)} > {resolved.map(({ group, ref }) => renderGroupBlock(group, ref.previewLimit ?? defaultLimit(group), resolved.length > 1), @@ -342,6 +367,7 @@ export default function AppNext() {
{loading && } {!loading && cards.length === 0 && } + {renderLocalService()} {cards.map(renderCard)} {keywordLower !== '' && cards.length > 0 && ( 在卡片与分组内按标题过滤。 @@ -521,9 +547,6 @@ export default function AppNext() { siderBordered={false} contentPadding="none" contentOffset={88} - height="screen" - scrollable - hideScrollbar contentId="content-scroll" className="app" header={ @@ -546,13 +569,15 @@ export default function AppNext() { } sider={
- - + {site.sidebar.show_profile !== false && } + {site.sidebar.show_service !== false && ( + + )}
([]) const [tab, setTab] = useState('groups') const [saving, setSaving] = useState(false) + const { projects } = useProjects() const [pending, setPending] = useState<{ kind: 'group' | 'card' id: string @@ -257,6 +261,14 @@ export function AdminNext() { else hidden.add(key) void saveSidebar({ ...draft.sidebar, tree_hidden: [...hidden] }) }} + onToggleCard={(card, visible) => { + void saveSidebar({ + ...draft.sidebar, + ...(card === 'profile' + ? { show_profile: visible } + : { show_service: visible }), + }) + }} /> )} @@ -264,6 +276,7 @@ export function AdminNext() { setPending({ kind: 'card', id: String(id), title })} /> @@ -1190,11 +1203,13 @@ function SidebarPanel({ groups, value, onToggle, + onToggleCard, saving, }: { groups: GroupNode[] value: SiteSettings['sidebar'] onToggle: (key: string, visible: boolean) => void + onToggleCard: (card: 'profile' | 'service', visible: boolean) => void saving: boolean }) { const hidden = new Set(value.tree_hidden) @@ -1219,7 +1234,38 @@ function SidebarPanel({ return ( - 勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。开关修改后立即保存。 + 侧栏卡片与分组导航分开控制;开关修改后立即保存。 + + + + 侧栏卡片 + +
+ 本地管理员 + onToggleCard('profile', visible)} + label="显示本地管理员卡片" + disabled={saving} + /> +
+
+ 本地服务 + onToggleCard('service', visible)} + label="显示本地服务卡片" + disabled={saving} + /> +
+ +
+ + + 分组导航 + + + 勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。 {render(groups, 0)} @@ -1231,16 +1277,19 @@ function SidebarPanel({ function CardsPanel({ groups, cards, + projects, onReload, onDelete, }: { groups: GroupNode[] cards: CardItem[] + projects: Project[] onReload: () => void onDelete: (id: number, title: string) => void }) { const options = useMemo(() => flattenCardsGroups(groups), [groups]) const [activeId, setActiveId] = useState(cards[0]?.id ?? null) + const [view, setView] = useState<'local' | 'card'>('local') const [pendingId, setPendingId] = useState(null) const [newOpen, setNewOpen] = useState(false) const [newTitle, setNewTitle] = useState('') @@ -1256,6 +1305,7 @@ function CardsPanel({ return } if (pendingId !== null && cards.some((card) => card.id === pendingId)) { + setView('card') setActiveId(pendingId) setPendingId(null) return @@ -1328,6 +1378,15 @@ function CardsPanel({
+ + {cards.map((card) => (