- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入 - 分组管理移除格式字段/显示,根分组只作容器 - 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式 - 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板 - 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
210 lines
5.5 KiB
TypeScript
210 lines
5.5 KiB
TypeScript
import { Button, Drawer, FormField, Icon, Input, Notice, Space, Textarea } from '@stash/ui'
|
||
import { useEffect, useState } from 'react'
|
||
import { fetchItem, saveItem, type EntryRecord } from '../../items'
|
||
|
||
/**
|
||
* 条目编辑器(P2):右侧 Drawer 表单,就地改标题 / 简介 / 标签 / 正文等。
|
||
*
|
||
* `updatedAt` 做乐观锁:别人先改过就提示刷新,避免静默覆盖。
|
||
*/
|
||
type ItemEditorProps = {
|
||
open: boolean
|
||
itemKey: string | null
|
||
onClose: () => void
|
||
onSaved: (entry: EntryRecord) => void
|
||
}
|
||
|
||
type FormState = {
|
||
name: string
|
||
description: string
|
||
tags: string
|
||
badge: string
|
||
comment: string
|
||
summary: string
|
||
body: string
|
||
cover: string
|
||
banner: string
|
||
href: string
|
||
}
|
||
|
||
const emptyForm: FormState = {
|
||
name: '',
|
||
description: '',
|
||
tags: '',
|
||
badge: '',
|
||
comment: '',
|
||
summary: '',
|
||
body: '',
|
||
cover: '',
|
||
banner: '',
|
||
href: '',
|
||
}
|
||
|
||
export function ItemEditor({ open, itemKey, onClose, onSaved }: ItemEditorProps) {
|
||
const [entry, setEntry] = useState<EntryRecord | null>(null)
|
||
const [form, setForm] = useState<FormState>(emptyForm)
|
||
const [loading, setLoading] = useState(false)
|
||
const [saving, setSaving] = useState(false)
|
||
const [error, setError] = useState('')
|
||
|
||
useEffect(() => {
|
||
if (!open || itemKey === null) return
|
||
|
||
let alive = true
|
||
setLoading(true)
|
||
setError('')
|
||
fetchItem(itemKey).then((result) => {
|
||
if (!alive) return
|
||
setLoading(false)
|
||
if (!result.ok) {
|
||
setError(result.error)
|
||
return
|
||
}
|
||
|
||
const next = result.entry
|
||
setEntry(next)
|
||
setForm({
|
||
name: next.name,
|
||
description: next.description,
|
||
tags: next.tags.join(','),
|
||
badge: next.badge,
|
||
comment: next.comment,
|
||
summary: next.summary,
|
||
body: next.body,
|
||
cover: next.cover,
|
||
banner: next.banner,
|
||
href: next.href,
|
||
})
|
||
})
|
||
|
||
return () => {
|
||
alive = false
|
||
}
|
||
}, [open, itemKey])
|
||
|
||
const update = (patch: Partial<FormState>) => setForm((previous) => ({ ...previous, ...patch }))
|
||
|
||
const save = async () => {
|
||
if (entry === null) return
|
||
setSaving(true)
|
||
setError('')
|
||
|
||
const result = await saveItem(entry.key, entry.updated_at, {
|
||
...form,
|
||
format: entry.format,
|
||
icon: entry.icon,
|
||
tags: form.tags
|
||
.split(/[,,]/)
|
||
.map((tag) => tag.trim())
|
||
.filter(Boolean),
|
||
})
|
||
|
||
setSaving(false)
|
||
if (!result.ok) {
|
||
setError(result.error)
|
||
return
|
||
}
|
||
|
||
onSaved(result.entry)
|
||
onClose()
|
||
}
|
||
|
||
return (
|
||
<Drawer
|
||
open={open}
|
||
onClose={onClose}
|
||
width={520}
|
||
title="编辑条目"
|
||
footer={
|
||
<Space gap="sm" justify="end">
|
||
<Button variant="secondary" onClick={onClose}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
loading={saving}
|
||
disabled={loading}
|
||
icon={<Icon name="check_line" />}
|
||
onClick={save}
|
||
>
|
||
保存
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
{loading && <p>正在读取…</p>}
|
||
{error !== '' && (
|
||
<Notice tone="error" title="保存失败">
|
||
{error}
|
||
</Notice>
|
||
)}
|
||
|
||
{!loading && entry !== null && (
|
||
<Space direction="column" gap="md" align="stretch">
|
||
<FormField label="标题" htmlFor="edit-name" required>
|
||
<Input
|
||
id="edit-name"
|
||
value={form.name}
|
||
onChange={(event) => update({ name: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="简介" htmlFor="edit-description" hint="列表卡片上的一行说明">
|
||
<Input
|
||
id="edit-description"
|
||
value={form.description}
|
||
onChange={(event) => update({ description: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="标签" htmlFor="edit-tags" hint="用逗号分隔,例如:推荐电影,动画">
|
||
<Input
|
||
id="edit-tags"
|
||
value={form.tags}
|
||
onChange={(event) => update({ tags: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="一句话评价" htmlFor="edit-comment">
|
||
<Input
|
||
id="edit-comment"
|
||
value={form.comment}
|
||
onChange={(event) => update({ comment: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="正文" htmlFor="edit-body" hint="支持 Markdown:##、段落、链接、图片">
|
||
<Textarea
|
||
id="edit-body"
|
||
rows={8}
|
||
value={form.body}
|
||
onChange={(event) => update({ body: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="封面地址" htmlFor="edit-cover">
|
||
<Input
|
||
id="edit-cover"
|
||
value={form.cover}
|
||
placeholder="https://…"
|
||
onChange={(event) => update({ cover: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="跳转地址"
|
||
htmlFor="edit-href"
|
||
hint="可填 https://、www. 开头的域名或站内路径 /foo;留空则不显示跳转"
|
||
>
|
||
<Input
|
||
id="edit-href"
|
||
value={form.href}
|
||
placeholder="https://… 或 /project/"
|
||
onChange={(event) => update({ href: event.target.value })}
|
||
/>
|
||
</FormField>
|
||
</Space>
|
||
)}
|
||
</Drawer>
|
||
)
|
||
}
|