Files
stash/stash-web/src/components/content/ItemEditor.tsx
T
CNWei 5982e0db5a feat(admin): 后台新增条目与分组 CRUD
- POST /api/items + DELETE /api/items/{key}:新增应用/海报/图文,按分区/分组挂载;PATCH 支持 nodeKey 移动
- 分组 CRUD:POST /api/categories/{id}/groups、PATCH/DELETE /api/groups/{id}(删除级联组内条目)
- PATCH /api/categories/{id} 增加 blockNodeKey,首页卡片可选择预览分组
- /admin 新增「内容」Tab(新增/删除条目),「分区」Tab 内联分组管理与卡片预览
- 测试 59 passed(条目创建/移动/删除、分组增删改)
2026-10-30 12:53:49 +08:00

209 lines
5.4 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,
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>
)
}