fix(admin): 内容下拉去格式后缀,侧边栏开关右置并即时保存

- 内容页/条目弹窗的分组下拉只显示名称,去掉格式后缀与缩进,统一对齐
- 侧边栏开关移到右侧;拨动即保存到 settings.sidebar.tree_hidden
- 移除侧边栏页的保存/撤销按钮
- Playwright 验证:选项无括号且对齐、开关在右、无保存按钮、设置即时落库
This commit is contained in:
2026-10-31 11:19:30 +08:00
parent b04842978c
commit 87b3c3b35a
4 changed files with 53 additions and 29 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ README.md 运行、路由、库结构、登录说明
| --- | --- |
| 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/格式/图标/排序/状态)、删除;行内状态开关即时生效;图标可留空 |
| 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 跳转链接 / 所属分组 / 操作;弹窗新增/编辑,编辑时显示不可改的「标识」 |
| 侧边栏 | 分组树逐项勾选(`settings.sidebar.tree_hidden`),子分组可单独勾选 |
| 侧边栏 | 分组树逐项勾选,开关在右侧、**拨动即保存**(无单独保存/撤销按钮);子分组可单独勾选 |
| 首页卡片 | 新建/改名/删除卡片;勾选若干分组(可混合格式),每组单独设条数 |
| 页头 | 启用开关、速度、条目、标签与色调 |
| 资料 | 头像 URL、名称、角色、简介、位置 |
+6
View File
@@ -147,6 +147,12 @@ DELETE /api/items/:id
## 11. 实施记录
### P12(2026-10-31,内容/侧边栏交互微调)
- 内容页与条目弹窗的分组下拉只显示分组名,去掉格式后缀与缩进空格,选项统一左对齐;
- 侧边栏页:开关移到右侧,拨动即写入 `settings.sidebar.tree_hidden`;移除「保存侧边栏 / 撤销改动 / 保存」按钮,保存成功用 Toast 提示;
- 验证:下拉选项无括号、x 对齐;侧边栏无保存按钮、开关在右、切换后设置即时落库。
### P11(2026-10-31,内容页列表化)
- 内容页改为与分组管理一致的列表:标题 / 跳转链接 / 所属分组 / 操作;
+14
View File
@@ -1267,6 +1267,20 @@ a.entry-href:hover {
font-size: var(--su-font-size-sm);
}
/* 侧边栏勾选:标题在左,开关在右 */
.sidebar-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--su-space-3);
padding: var(--su-space-1) 0;
}
.sidebar-toggle-row .admin-item-title {
flex: 1;
min-width: 0;
}
.admin-category-row > * {
min-width: 0;
}
+32 -28
View File
@@ -136,6 +136,22 @@ export function AdminNext() {
toast.success('已保存')
}
/** 侧边栏开关即时保存(不再单独点保存按钮) */
const saveSidebar = async (sidebar: SiteSettings['sidebar']) => {
if (document === null || draft === null) return
setSaving(true)
const result = await saveSettings(document.version, { ...draft, sidebar })
setSaving(false)
if (!result.ok) {
toast.error(result.error)
void load()
return
}
setDocument(result.document)
setDraft(clone(result.document.settings))
toast.success('侧边栏已保存')
}
const confirmDelete = async () => {
if (pending === null) return
setDeleting(true)
@@ -234,9 +250,13 @@ export function AdminNext() {
<SidebarPanel
groups={groups}
value={draft.sidebar}
onChange={(sidebar) => setDraft({ ...draft, sidebar })}
onSave={save}
saving={saving}
onToggle={(key, visible) => {
const hidden = new Set(draft.sidebar.tree_hidden)
if (visible) hidden.delete(key)
else hidden.add(key)
void saveSidebar({ ...draft.sidebar, tree_hidden: [...hidden] })
}}
/>
)}
@@ -265,7 +285,7 @@ export function AdminNext() {
{tab === 'system' && <SystemPanel />}
{(tab === 'header' || tab === 'profile' || tab === 'sidebar') && (
{(tab === 'header' || tab === 'profile') && (
<Space gap="sm" justify="end" className="admin-actions">
<Button
variant="secondary"
@@ -833,7 +853,7 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
<Select
options={options.map((option) => ({
value: option.key,
label: `${' '.repeat(option.depth)}${option.title}(${FORMAT_LABEL[option.format]})`,
label: option.title,
}))}
value={group?.key ?? ''}
onChange={setGroupKey}
@@ -1084,7 +1104,7 @@ function EntryDialog({
<Select
options={options.map((option) => ({
value: option.key,
label: `${' '.repeat(option.depth)}${option.title}`,
label: option.title,
}))}
value={groupKey}
onChange={setGroupKey}
@@ -1169,38 +1189,27 @@ function EntryDialog({
function SidebarPanel({
groups,
value,
onChange,
onSave,
onToggle,
saving,
}: {
groups: GroupNode[]
value: SiteSettings['sidebar']
onChange: (sidebar: SiteSettings['sidebar']) => void
onSave: () => void
onToggle: (key: string, visible: boolean) => void
saving: boolean
}) {
const hidden = new Set(value.tree_hidden)
const toggle = (key: string, visible: boolean) => {
const next = new Set(hidden)
if (visible) next.delete(key)
else next.add(key)
onChange({ ...value, tree_hidden: [...next] })
}
function render(nodes: GroupNode[], depth: number) {
return nodes.map((node) => (
<div key={node.key} style={{ marginLeft: depth * 22 }}>
<div className="admin-item-row admin-nav-row">
<div className="sidebar-toggle-row">
<Text className="admin-item-title">{node.title}</Text>
<Switch
checked={!hidden.has(node.key)}
onChange={(visible) => toggle(node.key, visible)}
onChange={(visible) => onToggle(node.key, visible)}
label={`侧栏显示 ${node.title}`}
disabled={saving}
/>
<Text className="admin-item-title">{node.title}</Text>
<Text size="sm" tone="muted">
{node.format}
</Text>
</div>
{render(node.children, depth + 1)}
</div>
@@ -1210,14 +1219,9 @@ function SidebarPanel({
return (
<Space direction="column" gap="md" align="stretch" className="admin-form">
<Text size="sm" tone="muted">
勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。
勾选要在侧边栏显示的分组;子分组可以单独勾选,父分组不勾选也不影响子项。开关修改后立即保存。
</Text>
{render(groups, 0)}
<Space gap="sm" justify="end">
<Button loading={saving} icon={<Icon name="check_line" />} onClick={onSave}>
保存侧边栏
</Button>
</Space>
</Space>
)
}