feat(admin): 首页卡片按侧边栏逻辑交互
- 分组勾选:标题在左、开关在右,勾选即保存,去掉每卡保存按钮 - 卡片标题与每组条数改为失焦保存 - 保留删除卡片按钮 - Playwright 验证:无保存按钮、开关右置、勾选/条数/改名即时落库
This commit is contained in:
+1
-1
@@ -100,7 +100,7 @@ README.md 运行、路由、库结构、登录说明
|
||||
| 分组管理 | 树形表格:新建根分组(左侧)、加子分组、编辑(名称/上级/格式/图标/排序/状态)、删除;行内状态开关即时生效;图标可留空 |
|
||||
| 内容 | 选分组以列表展示条目(选主分组时包含所有子分组);列:标题 / 跳转链接 / 所属分组 / 操作;弹窗新增/编辑,编辑时显示不可改的「标识」 |
|
||||
| 侧边栏 | 分组树逐项勾选,开关在右侧、**拨动即保存**(无单独保存/撤销按钮);子分组可单独勾选 |
|
||||
| 首页卡片 | 新建/改名/删除卡片;勾选若干分组(可混合格式),每组单独设条数 |
|
||||
| 首页卡片 | 新建/改名/删除卡片;分组开关在右侧、勾选即保存,标题与条数失焦保存(无每卡保存按钮),可混合格式 |
|
||||
| 页头 | 启用开关、速度、条目、标签与色调 |
|
||||
| 资料 | 头像 URL、名称、角色、简介、位置 |
|
||||
| 系统 | 修改密码、下载备份 |
|
||||
|
||||
@@ -147,6 +147,12 @@ DELETE /api/items/:id
|
||||
|
||||
## 11. 实施记录
|
||||
|
||||
### P13(2026-10-31,首页卡片按侧边栏逻辑)
|
||||
|
||||
- 每张卡片的分组勾选改为与侧边栏一致:标题在左、开关在右、勾选/取消即时保存(`PUT /api/cards/{id}/groups`),去掉每张卡的「保存」按钮;
|
||||
- 卡片标题、单组预览条数改为失焦保存;条数清空表示使用默认;
|
||||
- 卡片删除仍保留图标按钮;验证:无卡片级保存按钮、开关在右、勾选后 `card_group` 即时更新、条数 2 与改名均落库。
|
||||
|
||||
### P12(2026-10-31,内容/侧边栏交互微调)
|
||||
|
||||
- 内容页与条目弹窗的分组下拉只显示分组名,去掉格式后缀与缩进空格,选项统一左对齐;
|
||||
|
||||
@@ -1281,6 +1281,21 @@ a.entry-href:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 首页卡片:标题行 + 分组勾选行(开关在右) */
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
}
|
||||
|
||||
.card-group-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 100px auto;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.admin-category-row > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1259,7 +1259,7 @@ function CardsPanel({
|
||||
return (
|
||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||
<Text size="sm" tone="muted">
|
||||
卡片只是展示组合:新建卡片后勾选要展示的分组(可混合格式),每张卡可单独设条数。
|
||||
卡片只是展示组合:新建卡片后勾选要展示的分组(可混合格式),开关和条数修改后即时保存。
|
||||
</Text>
|
||||
|
||||
<div className="admin-new-node-row">
|
||||
@@ -1320,24 +1320,9 @@ function CardRow({
|
||||
)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const toggle = (key: string, checked: boolean) => {
|
||||
setSelected((current) => {
|
||||
const next = { ...current }
|
||||
if (checked) next[key] = ''
|
||||
else delete next[key]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const persistGroups = async (nextSelected: Record<string, string>) => {
|
||||
setBusy(true)
|
||||
const titleResult = await updateCard(card.id, { title })
|
||||
if (!titleResult.ok) {
|
||||
setBusy(false)
|
||||
toast.error(titleResult.error)
|
||||
return
|
||||
}
|
||||
const groups = Object.entries(selected).map(([groupKey, limit]) => ({
|
||||
const groups = Object.entries(nextSelected).map(([groupKey, limit]) => ({
|
||||
groupKey,
|
||||
previewLimit: limit.trim() === '' ? undefined : Number(limit),
|
||||
}))
|
||||
@@ -1345,25 +1330,46 @@ function CardRow({
|
||||
setBusy(false)
|
||||
if (!result.ok) {
|
||||
toast.error(result.error)
|
||||
onReload()
|
||||
return
|
||||
}
|
||||
onReload()
|
||||
}
|
||||
|
||||
const toggle = (key: string, checked: boolean) => {
|
||||
const next = { ...selected }
|
||||
if (checked) next[key] = ''
|
||||
else delete next[key]
|
||||
setSelected(next)
|
||||
void persistGroups(next)
|
||||
}
|
||||
|
||||
const saveTitle = async () => {
|
||||
const next = title.trim()
|
||||
if (next === '' || next === card.title) {
|
||||
setTitle(card.title)
|
||||
return
|
||||
}
|
||||
const result = await updateCard(card.id, { title: next })
|
||||
if (!result.ok) {
|
||||
toast.error(result.error)
|
||||
setTitle(card.title)
|
||||
return
|
||||
}
|
||||
toast.success('卡片已保存')
|
||||
onReload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-section-block">
|
||||
<div className="admin-node-row">
|
||||
<div className="card-title-row">
|
||||
<div className="admin-node-title">
|
||||
<Input
|
||||
value={title}
|
||||
aria-label={`${card.title} 标题`}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
onBlur={() => void saveTitle()}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" loading={busy} onClick={() => void save()}>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -1378,18 +1384,14 @@ function CardRow({
|
||||
{options.map((option) => {
|
||||
const checked = option.key in selected
|
||||
return (
|
||||
<div className="admin-item-row admin-nav-row" key={option.key}>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={(next) => toggle(option.key, next)}
|
||||
label={`卡片包含 ${option.title}`}
|
||||
/>
|
||||
<Text className="admin-item-title">
|
||||
{' '.repeat(option.depth)}
|
||||
<div className="card-group-row" key={option.key}>
|
||||
<Text
|
||||
className="admin-item-title"
|
||||
style={{ paddingLeft: option.depth * 18 }}
|
||||
>
|
||||
{option.title}
|
||||
</Text>
|
||||
{checked ? (
|
||||
<div style={{ width: 100 }}>
|
||||
<Input
|
||||
type="number"
|
||||
value={selected[option.key]}
|
||||
@@ -1401,11 +1403,17 @@ function CardRow({
|
||||
[option.key]: event.target.value,
|
||||
}))
|
||||
}
|
||||
onBlur={() => void persistGroups(selected)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={(next) => toggle(option.key, next)}
|
||||
label={`卡片包含 ${option.title}`}
|
||||
disabled={busy}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user