feat(admin): 内容页列表化,主分组汇总子分组条目
- 内容页改为列表:标题 / 跳转链接 / 所属分组 / 操作 - 选中主分组时递归列出全部子分组条目;选子分组只列该子树 - 编辑弹窗始终显示「标识」(编辑时只读) - 图文条目接口补齐 href,列表统一显示跳转链接 - 测试 59 passed;Playwright 验证 20 条汇总、子分组 4 条、标识只读
This commit is contained in:
@@ -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 <span className="entry-href entry-href-empty">—</span>
|
||||
}
|
||||
if (/^https?:\/\//i.test(value) || value.startsWith('/')) {
|
||||
return (
|
||||
<a className="entry-href" href={value} target="_blank" rel="noreferrer" title={value}>
|
||||
{value}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return <span className="entry-href">{value}</span>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||
<Space direction="column" gap="md" align="stretch" className="admin-form">
|
||||
<Text size="sm" tone="muted">
|
||||
条目只属于一个分组;新增 / 编辑时选择分组即可调整归属。
|
||||
条目只属于一个分组;选中主分组时会连同其所有子分组的条目一起列出。
|
||||
</Text>
|
||||
|
||||
<Space gap="sm" wrap>
|
||||
@@ -806,7 +833,7 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
|
||||
<Select
|
||||
options={options.map((option) => ({
|
||||
value: option.key,
|
||||
label: `${' '.repeat(option.depth)}${option.title}(${option.format})`,
|
||||
label: `${' '.repeat(option.depth)}${option.title}(${FORMAT_LABEL[option.format]})`,
|
||||
}))}
|
||||
value={group?.key ?? ''}
|
||||
onChange={setGroupKey}
|
||||
@@ -821,42 +848,71 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Space direction="column" gap="sm" align="stretch">
|
||||
<Text size="sm" tone="muted">
|
||||
{group?.title ?? ''} 下 {group?.entries.length ?? 0} 条
|
||||
</Text>
|
||||
{(group?.entries ?? []).map((entry) => (
|
||||
<div className="admin-entry-row" key={entry.id}>
|
||||
<Text>{entry.name ?? entry.title ?? entry.id}</Text>
|
||||
<Text size="sm" tone="muted">
|
||||
{group?.title}
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon={<Icon name="pencil_line" />}
|
||||
aria-label="编辑条目"
|
||||
onClick={() => setDialog({ key: entry.id, groupKey: group?.key ?? '' })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="admin-remove"
|
||||
icon={<Icon name="delete_2_line" />}
|
||||
aria-label="删除条目"
|
||||
onClick={async () => {
|
||||
const result = await deleteItem(entry.id)
|
||||
if (!result.ok) {
|
||||
toast.error(result.error)
|
||||
return
|
||||
}
|
||||
toast.success('条目已删除')
|
||||
onReload()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<Text size="sm" tone="muted">
|
||||
{group?.title ?? ''} 下共 {rows.length} 条(含子分组)
|
||||
</Text>
|
||||
|
||||
<div className="group-table-wrap">
|
||||
<table className="group-table entry-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th className="entry-col-href">跳转链接</th>
|
||||
<th className="entry-col-group">所属分组</th>
|
||||
<th className="group-col-actions">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(({ entry, group: owner }) => (
|
||||
<tr key={entry.id}>
|
||||
<td>
|
||||
<span className="entry-title">{entry.name ?? entry.title ?? entry.id}</span>
|
||||
</td>
|
||||
<td>
|
||||
<EntryLink href={entry.href} />
|
||||
</td>
|
||||
<td>
|
||||
<span className="entry-group">{owner.title}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="group-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="group-action"
|
||||
onClick={() => setDialog({ key: entry.id, groupKey: owner.key })}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group-action group-action-danger"
|
||||
onClick={async () => {
|
||||
const result = await deleteItem(entry.id)
|
||||
if (!result.ok) {
|
||||
toast.error(result.error)
|
||||
return
|
||||
}
|
||||
toast.success('条目已删除')
|
||||
onReload()
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="group-empty">
|
||||
该分组下还没有内容
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<EntryDialog
|
||||
open={dialog !== null}
|
||||
@@ -1034,15 +1090,22 @@ function EntryDialog({
|
||||
onChange={setGroupKey}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
label="标识"
|
||||
htmlFor="entry-key"
|
||||
hint={entryKey === null ? '小写字母、数字与连字符,创建后不可改' : '标识创建后不可修改'}
|
||||
>
|
||||
<Input
|
||||
id="entry-key"
|
||||
value={form.key}
|
||||
disabled={entryKey !== null}
|
||||
placeholder="如 my-site"
|
||||
onChange={(event) => update({ key: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="admin-content-form">
|
||||
{entryKey === null && (
|
||||
<Input
|
||||
value={form.key}
|
||||
placeholder="标识,如 my-site"
|
||||
aria-label="条目标识"
|
||||
onChange={(event) => update({ key: event.target.value })}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
value={form.name}
|
||||
placeholder="标题"
|
||||
|
||||
Reference in New Issue
Block a user