diff --git a/HANDOFF.md b/HANDOFF.md index db2c562..443b697 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -98,7 +98,7 @@ README.md 运行、路由、库结构、登录说明 | Tab | 能力 | | --- | --- | | 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/格式/图标/排序/状态)、删除;行内状态开关即时生效;图标可留空 | -| 内容 | 选分组新增/编辑/删除条目;条目只属于一个分组 | +| 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 跳转链接 / 所属分组 / 操作;弹窗新增/编辑,编辑时显示不可改的「标识」 | | 侧边栏 | 分组树逐项勾选(`settings.sidebar.tree_hidden`),子分组可单独勾选 | | 首页卡片 | 新建/改名/删除卡片;勾选若干分组(可混合格式),每组单独设条数 | | 页头 | 启用开关、速度、条目、标签与色调 | diff --git a/README.md b/README.md index ed51a18..31fff13 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 11e5398..40564dd 100644 --- a/docs/ADMIN-PLAN.md +++ b/docs/ADMIN-PLAN.md @@ -147,6 +147,14 @@ DELETE /api/items/:id ## 11. 实施记录 +### P11(2026-10-31,内容页列表化) + +- 内容页改为与分组管理一致的列表:标题 / 跳转链接 / 所属分组 / 操作; +- 选中主分组时递归列出其所有子分组的条目(显示所属分组),选子分组只列该子树; +- 编辑弹窗始终显示「标识」字段(编辑时只读,创建时可填); +- 条目接口补齐图文的 `href`,列表统一展示跳转链接(空值显示 —); +- 验证:主分组 20 条、子分组 4 条;编辑弹窗标识为 `github` 且只读;`cargo test` 59 passed。 + ### P10(2026-10-31,分组管理页重做) - 参考 `TT-Helper/需求/5297/1.html` 重做分组管理:树形表格(分组名称 / 排序权重 / 关联网页数 / 状态 / 操作),去掉「展开全部 / 折叠全部」,新建根分组放左侧; diff --git a/src/db/catalog.rs b/src/db/catalog.rs index 8c80e2e..4df6a16 100644 --- a/src/db/catalog.rs +++ b/src/db/catalog.rs @@ -488,6 +488,7 @@ fn article_item(entry: &EntryRow, meta: Option<&Vec>) -> Value { if let Some(body) = entry.body.as_ref().filter(|body| !body.trim().is_empty()) { object.insert("body".into(), json!(body)); } + insert_optional(&mut object, "href", &entry.href); Value::Object(object) } diff --git a/stash-web/src/App.css b/stash-web/src/App.css index 9789a5a..9dffb90 100644 --- a/stash-web/src/App.css +++ b/stash-web/src/App.css @@ -1228,6 +1228,45 @@ gap: var(--su-space-3); } +/* 内容页:与分组管理一致的列表 */ +.entry-table .entry-col-href { + width: 40%; +} + +.entry-table .entry-col-group { + width: 180px; +} + +.entry-title { + color: var(--su-text-main); + font-weight: 500; +} + +.entry-href { + display: inline-block; + max-width: 100%; + overflow: hidden; + color: var(--su-color-primary); + font-size: var(--su-font-size-sm); + text-overflow: ellipsis; + text-decoration: none; + vertical-align: bottom; + white-space: nowrap; +} + +a.entry-href:hover { + text-decoration: underline; +} + +.entry-href-empty { + color: var(--su-text-placeholder); +} + +.entry-group { + color: var(--su-text-sub); + font-size: var(--su-font-size-sm); +} + .admin-category-row > * { min-width: 0; } diff --git a/stash-web/src/components/admin/AdminNext.tsx b/stash-web/src/components/admin/AdminNext.tsx index 8829682..cc7c484 100644 --- a/stash-web/src/components/admin/AdminNext.tsx +++ b/stash-web/src/components/admin/AdminNext.tsx @@ -42,6 +42,7 @@ import { updateCard, updateNode, type CardItem, + type GroupEntry, type GroupFormat, type GroupNode, } from '../../manage' @@ -782,6 +783,31 @@ function GroupDialog({ // ---------- 内容 ---------- +type EntryWithGroup = { entry: GroupEntry; group: GroupNode } + +/** 选中某个分组时,把它子树里的条目全部取出来(含子分组) */ +function collectGroupEntries(group: GroupNode): EntryWithGroup[] { + return [ + ...group.entries.map((entry) => ({ entry, group })), + ...group.children.flatMap((child) => collectGroupEntries(child)), + ] +} + +function EntryLink({ href }: { href?: string }) { + const value = (href ?? '').trim() + if (value === '' || value === '#') { + return — + } + if (/^https?:\/\//i.test(value) || value.startsWith('/')) { + return ( + + {value} + + ) + } + return {value} +} + function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: () => void }) { const options = useMemo(() => flattenCardsGroups(groups), [groups]) const [groupKey, setGroupKey] = useState(options[0]?.key ?? '') @@ -794,11 +820,12 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: () }, [options, groupKey]) const group = findGroup(groups, groupKey) + const rows = group === undefined ? [] : collectGroupEntries(group) return ( - + - 条目只属于一个分组;新增 / 编辑时选择分组即可调整归属。 + 条目只属于一个分组;选中主分组时会连同其所有子分组的条目一起列出。 @@ -806,7 +833,7 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: () update({ key: event.target.value })} + /> + +
- {entryKey === null && ( - update({ key: event.target.value })} - /> - )}