feat(admin): 内容弹窗新增/编辑、侧栏直接建子分组
- 内容 Tab 改为弹窗式新增;列表支持编辑(含换分区/分组)与删除 - 条目 PATCH 支持 nodeKey 移动,EntryPatch 改为 camelCase 解析 - 侧栏导航的每个应用分区下可直接新建子分组 - 测试 59 passed;Playwright 覆盖 建组→新增→编辑→移动分组
This commit is contained in:
@@ -31,6 +31,7 @@ pub struct EntryRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EntryPatch {
|
pub struct EntryPatch {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@
|
|||||||
|
|
||||||
.admin-entry-row {
|
.admin-entry-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 140px auto;
|
grid-template-columns: minmax(0, 1fr) 140px auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--su-space-2);
|
gap: var(--su-space-2);
|
||||||
padding: var(--su-space-2) 0;
|
padding: var(--su-space-2) 0;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
type SettingsTone,
|
type SettingsTone,
|
||||||
type SiteSettings,
|
type SiteSettings,
|
||||||
} from '../../settings'
|
} from '../../settings'
|
||||||
import { createItem, deleteItem } from '../../items'
|
import { createItem, deleteItem, fetchItem, saveItem } from '../../items'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 站点设置页(/admin):页头滚动信息 + 侧栏顺序/显隐。
|
* 站点设置页(/admin):页头滚动信息 + 侧栏顺序/显隐。
|
||||||
@@ -538,6 +538,12 @@ function SidebarForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{category.kind === 'apps' && (
|
||||||
|
<div className="admin-nav-group-row">
|
||||||
|
<NewGroupRow sectionId={category.id} onReload={onReload} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -1051,7 +1057,13 @@ function NewGroupRow({ sectionId, onReload }: { sectionId: string; onReload: ()
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 内容(新增条目) ----------
|
// ---------- 内容(条目列表 + 新增/编辑弹窗) ----------
|
||||||
|
|
||||||
|
type EntryRow = EntrySummary & {
|
||||||
|
groupName?: string
|
||||||
|
sectionId: string
|
||||||
|
groupId?: string
|
||||||
|
}
|
||||||
|
|
||||||
function ContentManager({
|
function ContentManager({
|
||||||
categories,
|
categories,
|
||||||
@@ -1061,23 +1073,16 @@ function ContentManager({
|
|||||||
onReload: () => void
|
onReload: () => void
|
||||||
}) {
|
}) {
|
||||||
const [sectionId, setSectionId] = useState(categories[0]?.id ?? '')
|
const [sectionId, setSectionId] = useState(categories[0]?.id ?? '')
|
||||||
const [groupId, setGroupId] = useState('')
|
const [dialog, setDialog] = useState<{
|
||||||
const [key, setKey] = useState('')
|
open: boolean
|
||||||
const [name, setName] = useState('')
|
key: string | null
|
||||||
const [href, setHref] = useState('')
|
location: { sectionId: string; groupId?: string } | null
|
||||||
const [description, setDescription] = useState('')
|
}>({ open: false, key: null, location: null })
|
||||||
const [icon, setIcon] = useState('')
|
const [pendingEntry, setPendingEntry] = useState<EntryRow | null>(null)
|
||||||
const [cover, setCover] = useState('')
|
|
||||||
const [tags, setTags] = useState('')
|
|
||||||
const [comment, setComment] = useState('')
|
|
||||||
const [body, setBody] = useState('')
|
|
||||||
const [busy, setBusy] = useState(false)
|
|
||||||
const [pendingEntry, setPendingEntry] = useState<EntrySummary | null>(null)
|
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
|
||||||
const section =
|
const section =
|
||||||
categories.find((category) => category.id === sectionId) ?? categories[0]
|
categories.find((category) => category.id === sectionId) ?? categories[0]
|
||||||
const groups = section?.kind === 'apps' ? (section.groups ?? []) : []
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (categories.length === 0) return
|
if (categories.length === 0) return
|
||||||
@@ -1086,17 +1091,7 @@ function ContentManager({
|
|||||||
}
|
}
|
||||||
}, [categories, sectionId])
|
}, [categories, sectionId])
|
||||||
|
|
||||||
useEffect(() => {
|
const entries: EntryRow[] =
|
||||||
if (section?.kind !== 'apps') return
|
|
||||||
if (!groups.some((group) => group.id === groupId)) {
|
|
||||||
setGroupId(groups[0]?.id ?? '')
|
|
||||||
}
|
|
||||||
}, [section, groups, groupId])
|
|
||||||
|
|
||||||
const nodeKey =
|
|
||||||
section === undefined ? '' : section.kind === 'apps' ? groupId : section.id
|
|
||||||
|
|
||||||
const entries =
|
|
||||||
section === undefined
|
section === undefined
|
||||||
? []
|
? []
|
||||||
: section.kind === 'apps'
|
: section.kind === 'apps'
|
||||||
@@ -1104,56 +1099,15 @@ function ContentManager({
|
|||||||
(group.items ?? []).map((item) => ({
|
(group.items ?? []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
|
sectionId: section.id,
|
||||||
|
groupId: group.id,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
: (section.items ?? []).map((item) => ({ ...item, groupName: undefined }))
|
: (section.items ?? []).map((item) => ({
|
||||||
|
...item,
|
||||||
const submit = async () => {
|
groupName: undefined,
|
||||||
if (section === undefined || nodeKey === '') {
|
sectionId: section.id,
|
||||||
toast.error('请先选择分区与分组')
|
}))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setBusy(true)
|
|
||||||
const result = await createItem({
|
|
||||||
key: key.trim(),
|
|
||||||
nodeKey,
|
|
||||||
item: {
|
|
||||||
name: name.trim(),
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
tags: tags
|
|
||||||
.split(/[,,]/)
|
|
||||||
.map((tag) => tag.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
badge: '',
|
|
||||||
comment,
|
|
||||||
summary: '',
|
|
||||||
body,
|
|
||||||
cover,
|
|
||||||
banner: '',
|
|
||||||
href,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
setBusy(false)
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
toast.error(result.error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success(`已添加「${name.trim()}」`)
|
|
||||||
setKey('')
|
|
||||||
setName('')
|
|
||||||
setHref('')
|
|
||||||
setDescription('')
|
|
||||||
setIcon('')
|
|
||||||
setCover('')
|
|
||||||
setTags('')
|
|
||||||
setComment('')
|
|
||||||
setBody('')
|
|
||||||
onReload()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeEntry = async () => {
|
const removeEntry = async () => {
|
||||||
if (pendingEntry === null) return
|
if (pendingEntry === null) return
|
||||||
@@ -1172,8 +1126,8 @@ function ContentManager({
|
|||||||
return (
|
return (
|
||||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||||
<Text size="sm" tone="muted">
|
<Text size="sm" tone="muted">
|
||||||
在下面新增网址 / 海报 / 图文条目,并选择它挂在哪个分区、哪个分组(应用类必须选分组)。
|
新增网址 / 海报 / 图文条目时,选择它挂在哪个分区、哪个分组(应用类必须选分组);
|
||||||
新增后,分组出现在侧栏与首页卡片的条件是它被设为显示。
|
列表里的条目可以点铅笔修改,包括换分区 / 分组。
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Space gap="sm" wrap>
|
<Space gap="sm" wrap>
|
||||||
@@ -1187,95 +1141,15 @@ function ContentManager({
|
|||||||
onChange={setSectionId}
|
onChange={setSectionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{section?.kind === 'apps' && (
|
|
||||||
<div style={{ width: 220 }}>
|
|
||||||
<Select
|
|
||||||
options={groups.map((group) => ({ value: group.id, label: group.name }))}
|
|
||||||
value={groupId}
|
|
||||||
onChange={setGroupId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{section?.kind === 'apps' && groups.length === 0 && (
|
|
||||||
<Text size="sm" tone="muted">
|
|
||||||
这个分区还没有分组,请先到「分区」Tab 新建分组。
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
<div className="admin-content-form">
|
|
||||||
<Input
|
|
||||||
value={key}
|
|
||||||
placeholder="标识,如 my-site"
|
|
||||||
aria-label="条目标识"
|
|
||||||
onChange={(event) => setKey(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
placeholder="标题,如 我的站点"
|
|
||||||
aria-label="条目标题"
|
|
||||||
onChange={(event) => setName(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={href}
|
|
||||||
placeholder="跳转地址,如 https://example.com 或 /project/"
|
|
||||||
aria-label="跳转地址"
|
|
||||||
onChange={(event) => setHref(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={icon}
|
|
||||||
placeholder="字符图标,如 G"
|
|
||||||
aria-label="图标"
|
|
||||||
onChange={(event) => setIcon(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={description}
|
|
||||||
placeholder="一句话简介"
|
|
||||||
aria-label="简介"
|
|
||||||
onChange={(event) => setDescription(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={cover}
|
|
||||||
placeholder="封面 / 海报图片地址"
|
|
||||||
aria-label="封面地址"
|
|
||||||
onChange={(event) => setCover(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={tags}
|
|
||||||
placeholder="标签,用逗号分隔"
|
|
||||||
aria-label="标签"
|
|
||||||
onChange={(event) => setTags(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={comment}
|
|
||||||
placeholder="一句话评价"
|
|
||||||
aria-label="一句话评价"
|
|
||||||
onChange={(event) => setComment(event.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="admin-col-span">
|
|
||||||
<Textarea
|
|
||||||
rows={4}
|
|
||||||
value={body}
|
|
||||||
placeholder="正文(图文 / Markdown),可留空"
|
|
||||||
aria-label="正文"
|
|
||||||
onChange={(event) => setBody(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Space gap="sm">
|
|
||||||
<Button
|
<Button
|
||||||
loading={busy}
|
|
||||||
icon={<Icon name="add_line" />}
|
icon={<Icon name="add_line" />}
|
||||||
disabled={
|
onClick={() =>
|
||||||
section === undefined ||
|
setDialog({
|
||||||
nodeKey === '' ||
|
open: true,
|
||||||
key.trim() === '' ||
|
key: null,
|
||||||
name.trim() === ''
|
location: section === undefined ? null : { sectionId: section.id },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
onClick={submit}
|
|
||||||
>
|
>
|
||||||
新增条目
|
新增条目
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1291,6 +1165,19 @@ function ContentManager({
|
|||||||
<Text size="sm" tone="muted">
|
<Text size="sm" tone="muted">
|
||||||
{entry.groupName ?? '直属分区'}
|
{entry.groupName ?? '直属分区'}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon={<Icon name="pencil_line" />}
|
||||||
|
aria-label="编辑条目"
|
||||||
|
onClick={() =>
|
||||||
|
setDialog({
|
||||||
|
open: true,
|
||||||
|
key: entry.id,
|
||||||
|
location: { sectionId: entry.sectionId, groupId: entry.groupId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1303,6 +1190,18 @@ function ContentManager({
|
|||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
<EntryDialog
|
||||||
|
open={dialog.open}
|
||||||
|
entryKey={dialog.key}
|
||||||
|
categories={categories}
|
||||||
|
defaultLocation={dialog.location}
|
||||||
|
onClose={() => setDialog({ open: false, key: null, location: null })}
|
||||||
|
onSaved={() => {
|
||||||
|
setDialog({ open: false, key: null, location: null })
|
||||||
|
onReload()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={pendingEntry !== null}
|
open={pendingEntry !== null}
|
||||||
title={
|
title={
|
||||||
@@ -1319,6 +1218,300 @@ function ContentManager({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EntryForm = {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
href: string
|
||||||
|
icon: string
|
||||||
|
description: string
|
||||||
|
cover: string
|
||||||
|
tags: string
|
||||||
|
comment: string
|
||||||
|
body: string
|
||||||
|
badge: string
|
||||||
|
summary: string
|
||||||
|
banner: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_ENTRY_FORM: EntryForm = {
|
||||||
|
key: '',
|
||||||
|
name: '',
|
||||||
|
href: '',
|
||||||
|
icon: '',
|
||||||
|
description: '',
|
||||||
|
cover: '',
|
||||||
|
tags: '',
|
||||||
|
comment: '',
|
||||||
|
body: '',
|
||||||
|
badge: '',
|
||||||
|
summary: '',
|
||||||
|
banner: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
function EntryDialog({
|
||||||
|
open,
|
||||||
|
entryKey,
|
||||||
|
categories,
|
||||||
|
defaultLocation,
|
||||||
|
onClose,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
open: boolean
|
||||||
|
entryKey: string | null
|
||||||
|
categories: CategorySummary[]
|
||||||
|
defaultLocation: { sectionId: string; groupId?: string } | null
|
||||||
|
onClose: () => void
|
||||||
|
onSaved: () => void
|
||||||
|
}) {
|
||||||
|
const editing = entryKey !== null
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [updatedAt, setUpdatedAt] = useState(0)
|
||||||
|
const [sectionId, setSectionId] = useState('')
|
||||||
|
const [groupId, setGroupId] = useState('')
|
||||||
|
const [form, setForm] = useState<EntryForm>(EMPTY_ENTRY_FORM)
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const section =
|
||||||
|
categories.find((category) => category.id === sectionId) ?? categories[0]
|
||||||
|
const groups = section?.kind === 'apps' ? (section.groups ?? []) : []
|
||||||
|
|
||||||
|
// 打开时重置:新建用当前选择,编辑先拉完整条目
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
const startSection =
|
||||||
|
defaultLocation?.sectionId ?? categories[0]?.id ?? ''
|
||||||
|
setSectionId(startSection)
|
||||||
|
setGroupId(defaultLocation?.groupId ?? '')
|
||||||
|
|
||||||
|
if (entryKey === null) {
|
||||||
|
setLoading(false)
|
||||||
|
setUpdatedAt(0)
|
||||||
|
setForm(EMPTY_ENTRY_FORM)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
fetchItem(entryKey).then((result) => {
|
||||||
|
setLoading(false)
|
||||||
|
if (!result.ok) {
|
||||||
|
setError(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const entry = result.entry
|
||||||
|
setUpdatedAt(entry.updated_at)
|
||||||
|
setForm({
|
||||||
|
key: entry.key,
|
||||||
|
name: entry.name,
|
||||||
|
href: entry.href,
|
||||||
|
icon: entry.icon,
|
||||||
|
description: entry.description,
|
||||||
|
cover: entry.cover,
|
||||||
|
tags: entry.tags.join(','),
|
||||||
|
comment: entry.comment,
|
||||||
|
body: entry.body,
|
||||||
|
badge: entry.badge,
|
||||||
|
summary: entry.summary,
|
||||||
|
banner: entry.banner,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 只在打开/切换条目时重置,表单输入不应触发重新拉取
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open, entryKey])
|
||||||
|
|
||||||
|
// 分区切换后保证分组仍然合法
|
||||||
|
useEffect(() => {
|
||||||
|
if (section?.kind !== 'apps') {
|
||||||
|
setGroupId('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!groups.some((group) => group.id === groupId)) {
|
||||||
|
setGroupId(groups[0]?.id ?? '')
|
||||||
|
}
|
||||||
|
}, [section, groups, groupId])
|
||||||
|
|
||||||
|
const nodeKey =
|
||||||
|
section === undefined ? '' : section.kind === 'apps' ? groupId : section.id
|
||||||
|
|
||||||
|
const update = (patch: Partial<EntryForm>) =>
|
||||||
|
setForm((previous) => ({ ...previous, ...patch }))
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (section === undefined || nodeKey === '') {
|
||||||
|
setError('请先选择分区与分组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!editing && form.key.trim() === '') {
|
||||||
|
setError('标识不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.name.trim() === '') {
|
||||||
|
setError('标题不能为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy(true)
|
||||||
|
setError('')
|
||||||
|
const item = {
|
||||||
|
name: form.name.trim(),
|
||||||
|
description: form.description,
|
||||||
|
icon: form.icon,
|
||||||
|
tags: form.tags
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
badge: form.badge,
|
||||||
|
comment: form.comment,
|
||||||
|
summary: form.summary,
|
||||||
|
body: form.body,
|
||||||
|
cover: form.cover,
|
||||||
|
banner: form.banner,
|
||||||
|
href: form.href,
|
||||||
|
nodeKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
editing && entryKey !== null
|
||||||
|
? await saveItem(entryKey, updatedAt, item)
|
||||||
|
: await createItem({ key: form.key.trim(), nodeKey, item })
|
||||||
|
setBusy(false)
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
setError(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(editing ? '条目已保存' : `已添加「${form.name.trim()}」`)
|
||||||
|
onSaved()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onClose={busy ? () => undefined : onClose}
|
||||||
|
title={editing ? '编辑条目' : '新增条目'}
|
||||||
|
size="lg"
|
||||||
|
footer={
|
||||||
|
<Space gap="sm" justify="end">
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
loading={busy}
|
||||||
|
disabled={loading}
|
||||||
|
icon={<Icon name="check_line" />}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Text>正在读取…</Text>
|
||||||
|
) : (
|
||||||
|
<Space direction="column" gap="md" align="stretch">
|
||||||
|
{error !== '' && (
|
||||||
|
<Notice tone="error" title="操作失败">
|
||||||
|
{error}
|
||||||
|
</Notice>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Space gap="sm" wrap>
|
||||||
|
<div style={{ width: 200 }}>
|
||||||
|
<Select
|
||||||
|
options={categories.map((category) => ({
|
||||||
|
value: category.id,
|
||||||
|
label: category.title,
|
||||||
|
}))}
|
||||||
|
value={section?.id ?? ''}
|
||||||
|
onChange={setSectionId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{section?.kind === 'apps' && (
|
||||||
|
<div style={{ width: 220 }}>
|
||||||
|
<Select
|
||||||
|
options={groups.map((group) => ({
|
||||||
|
value: group.id,
|
||||||
|
label: group.name,
|
||||||
|
}))}
|
||||||
|
value={groupId}
|
||||||
|
onChange={setGroupId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<div className="admin-content-form">
|
||||||
|
{!editing && (
|
||||||
|
<Input
|
||||||
|
value={form.key}
|
||||||
|
placeholder="标识,如 my-site"
|
||||||
|
aria-label="条目标识"
|
||||||
|
onChange={(event) => update({ key: event.target.value })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
value={form.name}
|
||||||
|
placeholder="标题,如 我的站点"
|
||||||
|
aria-label="条目标题"
|
||||||
|
onChange={(event) => update({ name: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.href}
|
||||||
|
placeholder="跳转地址,如 https://example.com 或 /project/"
|
||||||
|
aria-label="跳转地址"
|
||||||
|
onChange={(event) => update({ href: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.icon}
|
||||||
|
placeholder="字符图标,如 G"
|
||||||
|
aria-label="图标"
|
||||||
|
onChange={(event) => update({ icon: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.description}
|
||||||
|
placeholder="一句话简介"
|
||||||
|
aria-label="简介"
|
||||||
|
onChange={(event) => update({ description: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.cover}
|
||||||
|
placeholder="封面 / 海报图片地址"
|
||||||
|
aria-label="封面地址"
|
||||||
|
onChange={(event) => update({ cover: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.tags}
|
||||||
|
placeholder="标签,用逗号分隔"
|
||||||
|
aria-label="标签"
|
||||||
|
onChange={(event) => update({ tags: event.target.value })}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={form.comment}
|
||||||
|
placeholder="一句话评价"
|
||||||
|
aria-label="一句话评价"
|
||||||
|
onChange={(event) => update({ comment: event.target.value })}
|
||||||
|
/>
|
||||||
|
<div className="admin-col-span">
|
||||||
|
<Textarea
|
||||||
|
rows={4}
|
||||||
|
value={form.body}
|
||||||
|
placeholder="正文(图文 / Markdown),可留空"
|
||||||
|
aria-label="正文"
|
||||||
|
onChange={(event) => update({ body: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------- 通用删除确认(页面级) ----------
|
// ---------- 通用删除确认(页面级) ----------
|
||||||
|
|
||||||
function ConfirmModal({
|
function ConfirmModal({
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export type EntryInput = {
|
|||||||
cover: string
|
cover: string
|
||||||
banner: string
|
banner: string
|
||||||
href: string
|
href: string
|
||||||
|
/** 编辑时传目标分区/分组,可移动条目 */
|
||||||
|
nodeKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemResult =
|
export type ItemResult =
|
||||||
|
|||||||
Reference in New Issue
Block a user