Files
stash/stash-web/src/components/content/ItemEditor.tsx
T
CNWei 11f992cdad feat: 格式从分组下沉到条目(v21)
- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入
- 分组管理移除格式字段/显示,根分组只作容器
- 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式
- 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板
- 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
2026-10-31 14:26:28 +08:00

210 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}