feat: 格式从分组下沉到条目(v21)

- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入
- 分组管理移除格式字段/显示,根分组只作容器
- 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式
- 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板
- 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
This commit is contained in:
2026-10-31 14:26:28 +08:00
parent 8d094175fd
commit 11f992cdad
12 changed files with 173 additions and 72 deletions
+20 -20
View File
@@ -499,7 +499,6 @@ function GroupsPanel({
)}
<GroupMark icon={node.icon} />
<span className="group-name">{node.title}</span>
<span className="group-format">{FORMAT_LABEL[node.format]}</span>
</div>
</td>
<td className="group-sort">{node.sort}</td>
@@ -583,7 +582,6 @@ function GroupDialog({
const [key, setKey] = useState('')
const [title, setTitle] = useState('')
const [parentKey, setParentKey] = useState('')
const [format, setFormat] = useState<GroupFormat>('apps')
const [icon, setIcon] = useState('')
const [sort, setSort] = useState('')
const [enabled, setEnabled] = useState(true)
@@ -599,7 +597,6 @@ function GroupDialog({
setKey(state.group.key)
setTitle(state.group.title)
setParentKey(state.group.parentKey ?? '')
setFormat(state.group.format)
setIcon(state.group.icon)
setSort(String(state.group.sort))
setEnabled(state.group.enabled)
@@ -608,7 +605,6 @@ function GroupDialog({
setKey('')
setTitle('')
setFormat('apps')
setIcon('')
setSort('')
setEnabled(true)
@@ -625,7 +621,6 @@ function GroupDialog({
const rootMode = parentKey === '' || isRootEdit
const showKey = editing === undefined
const submit = async () => {
if (title.trim() === '') {
setError('分组名称不能为空')
@@ -645,9 +640,7 @@ function GroupDialog({
icon,
sort: sort.trim() === '' ? undefined : Number(sort),
enabled,
...(isRootEdit
? { format }
: { parentKey: parentKey }),
...(isRootEdit ? {} : { parentKey }),
})
setBusy(false)
if (!result.ok) {
@@ -663,7 +656,6 @@ function GroupDialog({
key: key.trim(),
title: title.trim(),
parentKey: rootMode ? undefined : parentKey,
format: rootMode ? format : undefined,
icon,
sort: sort.trim() === '' ? undefined : Number(sort),
enabled,
@@ -750,16 +742,6 @@ function GroupDialog({
</FormField>
)}
{rootMode && (
<FormField label="格式" htmlFor="group-format" hint="根分组决定这一棵树的渲染格式">
<Select
options={FORMAT_OPTIONS}
value={format}
onChange={(next) => setFormat(next as GroupFormat)}
/>
</FormField>
)}
<FormField label="图标样式" htmlFor="group-icon" hint="留空则不显示图标;可填 iconfont 图标名或一个字符">
<div className="group-icon-field">
<span className="group-icon-preview">
@@ -877,6 +859,7 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
<thead>
<tr>
<th>标题</th>
<th className="entry-col-format">格式</th>
<th className="entry-col-href">跳转链接</th>
<th className="entry-col-group">所属分组</th>
<th className="group-col-actions">操作</th>
@@ -888,6 +871,11 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
<td>
<span className="entry-title">{entry.name ?? entry.title ?? entry.id}</span>
</td>
<td>
<span className="entry-format">
{FORMAT_LABEL[(entry.format ?? 'apps') as GroupFormat]}
</span>
</td>
<td>
<EntryLink href={entry.href} />
</td>
@@ -925,7 +913,7 @@ function ContentPanel({ groups, onReload }: { groups: GroupNode[]; onReload: ()
{rows.length === 0 && (
<tr>
<td colSpan={4} className="group-empty">
<td colSpan={5} className="group-empty">
该分组下还没有内容
</td>
</tr>
@@ -972,6 +960,7 @@ function EntryDialog({
const [error, setError] = useState('')
const [form, setForm] = useState({
key: '',
format: 'apps' as GroupFormat,
name: '',
href: '',
icon: '',
@@ -994,6 +983,7 @@ function EntryDialog({
setUpdatedAt(0)
setForm({
key: '',
format: 'apps',
name: '',
href: '',
icon: '',
@@ -1016,6 +1006,7 @@ function EntryDialog({
setUpdatedAt(result.entry.updated_at)
setForm({
key: result.entry.key,
format: result.entry.format,
name: result.entry.name,
href: result.entry.href,
icon: result.entry.icon,
@@ -1044,6 +1035,7 @@ function EntryDialog({
setBusy(true)
setError('')
const item = {
format: form.format,
name: form.name.trim(),
description: form.description,
icon: form.icon,
@@ -1111,6 +1103,14 @@ function EntryDialog({
/>
</div>
<FormField label="格式" htmlFor="entry-format" hint="每条内容单独选择格式,同一个分组下可以混合">
<Select
options={FORMAT_OPTIONS}
value={form.format}
onChange={(next) => update({ format: next as GroupFormat })}
/>
</FormField>
<FormField
label="标识"
htmlFor="entry-key"
@@ -91,6 +91,7 @@ export function ItemEditor({ open, itemKey, onClose, onSaved }: ItemEditorProps)
const result = await saveItem(entry.key, entry.updated_at, {
...form,
format: entry.format,
icon: entry.icon,
tags: form.tags
.split(/[,,]/)