feat(distribution): 公告中心支持分站自定义
公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。 本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。 数据与口径: - 新增 announcements.distribution_site_id(0=主站),前台按站点隔离 - 新增 distribution_sites.announcement_mode:inherit(默认,主站+本站)/ own_only(仅本站) 后端: - 公告模块新增 ResolvePublicSiteScope:主站 [0]、inherit [0, siteID]、own_only [siteID], 站点编码无效/已停用回落主站;列表与详情都按该范围过滤,详情不再允许跨站按 ID 读取 - 管理端公告列表支持按站点筛选(site_id:不传=全部,0=主站,>0=分站)并带出站点名 - 分销模块新增本站公告 CRUD(distribution/announcement.go):强制 distribution_site_id=本站, 先过 ensureSiteAccess,跨站更新/删除/上下架一律 ErrAnnouncementNotFound - UpdateSiteBranding 支持 announcement_mode 切换(非法值报错,读侧非法历史值回落 inherit) - 新增权限 distribution:announcement_view|manage 并授予 super_admin/ops/finance, 站长开通时继承 ops 角色即自动获得 - 路由:站长侧 /api/distribution/announcements*,平台侧 /api/admin/distribution/announcements* 前端: - 分销后台新增「公告中心」页:来源开关(继承主站+本站 / 仅本站)、本站公告列表与 新建/编辑/发布/下架/归档/恢复/删除,空态按来源模式给出引导 - 平台后台公告页新增「站点」列与筛选,新建公告可选择归属站点(主站或指定分站代发) - 前台 PC/移动无需改动:请求已带 X-Distribution-Site-Code,后端按站点隔离,页面已有空态 测试: - 公告模块:主站/继承/仅本站三种可见范围、跨站详情拦截、无效站点回落、管理端站点筛选 - 分销模块:本站公告 CRUD 与跨站防护、公告来源开关校验与非法值容错
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
OrderedListOutlined,
|
||||
SettingOutlined,
|
||||
ShopOutlined,
|
||||
SoundOutlined,
|
||||
SkinOutlined,
|
||||
UserOutlined,
|
||||
WalletOutlined,
|
||||
@@ -26,6 +27,7 @@ import Accounts from './pages/Accounts'
|
||||
import Settlements from './pages/Settlements'
|
||||
import Orders from './pages/Orders'
|
||||
import Wallet from './pages/Wallet'
|
||||
import Announcements from './pages/Announcements'
|
||||
import SiteSettings from './pages/SiteSettings'
|
||||
import Login from './pages/Login'
|
||||
import Storefront from './pages/Storefront'
|
||||
@@ -44,6 +46,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
{ key: '/sold', icon: <FileSearchOutlined />, label: <Link to="/sold">售出账号订单</Link> },
|
||||
{ key: '/receive', icon: <AccountBookOutlined />, label: <Link to="/receive">分销收号管理</Link> },
|
||||
{ key: '/accounts', icon: <ShopOutlined />, label: <Link to="/accounts">商品货架与加价</Link> },
|
||||
{ key: '/announcements', icon: <SoundOutlined />, label: <Link to="/announcements">公告中心</Link> },
|
||||
{ key: '/wallet', icon: <WalletOutlined />, label: <Link to="/wallet">资金与提现</Link> },
|
||||
{
|
||||
key: '/settlements',
|
||||
@@ -153,6 +156,7 @@ function MainLayout({ member, onLogout }: { member: CurrentMember; onLogout: ()
|
||||
<Route path="/payments" element={<Payments />} />
|
||||
<Route path="/accounts" element={<Accounts />} />
|
||||
<Route path="/wallet" element={<Wallet />} />
|
||||
<Route path="/announcements" element={<Announcements />} />
|
||||
<Route path="/settings" element={<SiteSettings />} />
|
||||
<Route path="/settlements" element={<Settlements />} />
|
||||
<Route path="/sold" element={<Orders kind="sold" />} />
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface Site {
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
storefront_text_json?: string
|
||||
announcement_mode?: string
|
||||
listing_qrcode_url?: string
|
||||
receive_ratio_delta?: number
|
||||
receive_cost_delta?: number
|
||||
@@ -653,9 +654,97 @@ export interface SiteBrandingPayload {
|
||||
aw_recycle_json?: string
|
||||
cooperation_feedback_json?: string
|
||||
storefront_text_json?: string
|
||||
/** 公告中心来源:inherit=继承主站+本站,own_only=仅本站 */
|
||||
announcement_mode?: string
|
||||
listing_qrcode_url?: string
|
||||
}
|
||||
|
||||
// ---------------- 分站公告中心(站长自助维护本站公告) ----------------
|
||||
|
||||
export interface SiteAnnouncement {
|
||||
id: number
|
||||
distribution_site_id: number
|
||||
title: string
|
||||
content: string
|
||||
category: string
|
||||
priority: number
|
||||
is_pinned: boolean
|
||||
is_important: boolean
|
||||
view_count: number
|
||||
status: string
|
||||
published_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface SiteAnnouncementPage {
|
||||
items: SiteAnnouncement[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface SiteAnnouncementPayload {
|
||||
title: string
|
||||
content: string
|
||||
category: string
|
||||
priority: number
|
||||
is_pinned: boolean
|
||||
is_important: boolean
|
||||
}
|
||||
|
||||
export async function listSiteAnnouncements(
|
||||
siteID: number,
|
||||
params: { status?: string; page?: number; page_size?: number } = {},
|
||||
): Promise<SiteAnnouncementPage> {
|
||||
const resp = await api.get<ApiResponse<SiteAnnouncementPage>>('/admin/distribution/announcements', {
|
||||
params: { site_id: siteID, ...params },
|
||||
})
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function createSiteAnnouncement(
|
||||
siteID: number,
|
||||
payload: SiteAnnouncementPayload,
|
||||
): Promise<SiteAnnouncement> {
|
||||
const resp = await api.post<ApiResponse<SiteAnnouncement>>(
|
||||
'/admin/distribution/announcements',
|
||||
payload,
|
||||
{ params: { site_id: siteID } },
|
||||
)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function updateSiteAnnouncement(
|
||||
siteID: number,
|
||||
id: number,
|
||||
payload: SiteAnnouncementPayload,
|
||||
): Promise<SiteAnnouncement> {
|
||||
const resp = await api.put<ApiResponse<SiteAnnouncement>>(
|
||||
`/admin/distribution/announcements/${id}`,
|
||||
payload,
|
||||
{ params: { site_id: siteID } },
|
||||
)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function updateSiteAnnouncementStatus(
|
||||
siteID: number,
|
||||
id: number,
|
||||
action: 'publish' | 'archive' | 'unarchive',
|
||||
): Promise<SiteAnnouncement> {
|
||||
const resp = await api.post<ApiResponse<SiteAnnouncement>>(
|
||||
`/admin/distribution/announcements/${id}/${action}`,
|
||||
{},
|
||||
{ params: { site_id: siteID } },
|
||||
)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function deleteSiteAnnouncement(siteID: number, id: number): Promise<void> {
|
||||
await api.delete(`/admin/distribution/announcements/${id}`, { params: { site_id: siteID } })
|
||||
}
|
||||
|
||||
export async function uploadSiteImage(file: File, scene: string): Promise<string> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import {
|
||||
BellOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
PushpinOutlined,
|
||||
ReloadOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
createSiteAnnouncement,
|
||||
deleteSiteAnnouncement,
|
||||
updateSiteAnnouncement,
|
||||
updateSiteAnnouncementStatus,
|
||||
updateSiteBranding,
|
||||
listSiteAnnouncements,
|
||||
listSites,
|
||||
getMe,
|
||||
type Site,
|
||||
type SiteAnnouncement,
|
||||
type SiteAnnouncementPayload,
|
||||
} from '../api'
|
||||
import { useMember } from '../MemberContext'
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
{ value: 'notice', label: '通知公告' },
|
||||
{ value: 'tutorial', label: '使用教程' },
|
||||
{ value: 'rule', label: '规则说明' },
|
||||
{ value: 'faq', label: '常见问题' },
|
||||
]
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'published', label: '已发布' },
|
||||
{ value: 'archived', label: '已归档' },
|
||||
]
|
||||
|
||||
const STATUS_META: Record<string, { label: string; color: string }> = {
|
||||
draft: { label: '草稿', color: 'default' },
|
||||
published: { label: '已发布', color: 'green' },
|
||||
archived: { label: '已归档', color: 'orange' },
|
||||
}
|
||||
|
||||
const EMPTY_FORM: SiteAnnouncementPayload = {
|
||||
title: '',
|
||||
content: '',
|
||||
category: 'notice',
|
||||
priority: 0,
|
||||
is_pinned: false,
|
||||
is_important: false,
|
||||
}
|
||||
|
||||
function formatTime(value?: string | null) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 分站公告中心:站长自助维护本站公告。
|
||||
*
|
||||
* 展示来源由本站 announcement_mode 控制:
|
||||
* inherit(默认)= 主站公告 + 本站公告;own_only = 仅本站公告。
|
||||
*/
|
||||
export default function Announcements() {
|
||||
const member = useMember()
|
||||
const [siteID, setSiteID] = useState<number | undefined>(member?.site_id)
|
||||
const [sites, setSites] = useState<Site[]>([])
|
||||
const [announcementMode, setAnnouncementMode] = useState<'inherit' | 'own_only'>('inherit')
|
||||
const [modeSaving, setModeSaving] = useState(false)
|
||||
|
||||
const [items, setItems] = useState<SiteAnnouncement[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [status, setStatus] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<SiteAnnouncement | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form] = Form.useForm<SiteAnnouncementPayload>()
|
||||
|
||||
const load = useCallback(
|
||||
async (targetSiteID?: number, nextPage = page, nextPageSize = pageSize, nextStatus = status) => {
|
||||
if (!targetSiteID) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await listSiteAnnouncements(targetSiteID, {
|
||||
status: nextStatus || undefined,
|
||||
page: nextPage,
|
||||
page_size: nextPageSize,
|
||||
})
|
||||
setItems(result.items)
|
||||
setTotal(result.total)
|
||||
} catch {
|
||||
message.error('公告加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[page, pageSize, status],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
let currentMember = member
|
||||
if (!currentMember) {
|
||||
try {
|
||||
currentMember = await getMe()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
const activeSiteID = currentMember?.site_id
|
||||
if (!activeSiteID) return
|
||||
setSiteID(activeSiteID)
|
||||
try {
|
||||
const rows = await listSites()
|
||||
setSites(rows)
|
||||
const current = rows.find(row => row.id === activeSiteID)
|
||||
setAnnouncementMode(current?.announcement_mode === 'own_only' ? 'own_only' : 'inherit')
|
||||
} catch {
|
||||
// 站点信息读取失败时保留默认口径
|
||||
}
|
||||
void load(activeSiteID, 1, pageSize, '')
|
||||
}
|
||||
void init()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [member])
|
||||
|
||||
async function handleModeChange(next: 'inherit' | 'own_only') {
|
||||
if (!siteID || next === announcementMode) return
|
||||
setModeSaving(true)
|
||||
try {
|
||||
await updateSiteBranding(siteID, { announcement_mode: next })
|
||||
setAnnouncementMode(next)
|
||||
message.success(next === 'own_only' ? '已切换为「仅本站公告」' : '已切换为「继承主站 + 本站公告」')
|
||||
} catch {
|
||||
message.error('公告来源切换失败')
|
||||
} finally {
|
||||
setModeSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
setEditing(null)
|
||||
form.setFieldsValue({ ...EMPTY_FORM })
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
function openEdit(row: SiteAnnouncement) {
|
||||
setEditing(row)
|
||||
form.setFieldsValue({
|
||||
title: row.title,
|
||||
content: row.content,
|
||||
category: row.category,
|
||||
priority: row.priority,
|
||||
is_pinned: row.is_pinned,
|
||||
is_important: row.is_important,
|
||||
})
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!siteID) return
|
||||
const values = await form.validateFields()
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editing) {
|
||||
await updateSiteAnnouncement(siteID, editing.id, values)
|
||||
message.success('公告已保存')
|
||||
} else {
|
||||
await createSiteAnnouncement(siteID, values)
|
||||
message.success('公告已创建为草稿,发布后才会在前台展示')
|
||||
}
|
||||
setModalOpen(false)
|
||||
void load(siteID)
|
||||
} catch {
|
||||
message.error('公告保存失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStatusAction(row: SiteAnnouncement, action: 'publish' | 'archive' | 'unarchive') {
|
||||
if (!siteID) return
|
||||
try {
|
||||
await updateSiteAnnouncementStatus(siteID, row.id, action)
|
||||
message.success(action === 'publish' ? '公告已发布' : action === 'archive' ? '公告已归档' : '公告已取消归档')
|
||||
void load(siteID)
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: SiteAnnouncement) {
|
||||
if (!siteID) return
|
||||
try {
|
||||
await deleteSiteAnnouncement(siteID, row.id)
|
||||
message.success('公告已删除')
|
||||
void load(siteID)
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Space align="center" size={8}>
|
||||
<BellOutlined style={{ color: '#1677ff' }} />
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
公告中心
|
||||
</Typography.Title>
|
||||
</Space>
|
||||
<Typography.Text type="secondary">
|
||||
维护本站公告(列表 / 详情页展示)。公告发布后立即在本站前台生效。
|
||||
</Typography.Text>
|
||||
<Space direction="vertical" size={6} style={{ marginTop: 8 }}>
|
||||
<Typography.Text strong>公告来源</Typography.Text>
|
||||
<Radio.Group
|
||||
value={announcementMode}
|
||||
onChange={e => void handleModeChange(e.target.value)}
|
||||
disabled={modeSaving}
|
||||
>
|
||||
<Radio.Button value="inherit">继承主站 + 本站公告</Radio.Button>
|
||||
<Radio.Button value="own_only">仅本站公告</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
「继承」会同时展示主站公告与本站公告;「仅本站」只展示本站公告,主站公告不再下发(本站未发布公告时公告中心为空)。
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }} wrap>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={status}
|
||||
options={STATUS_OPTIONS}
|
||||
style={{ width: 140 }}
|
||||
onChange={value => {
|
||||
setStatus(value)
|
||||
setPage(1)
|
||||
void load(siteID, 1, pageSize, value)
|
||||
}}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void load(siteID)}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建公告
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table<SiteAnnouncement>
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={items}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty
|
||||
description={
|
||||
announcementMode === 'own_only'
|
||||
? '本站还没有公告。仅本站模式下公告中心会保持为空,先新建并发布一条公告。'
|
||||
: '本站还没有自己的公告。当前继承主站公告,也可以新建本站专属公告。'
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
void load(siteID, nextPage, nextPageSize, status)
|
||||
},
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
render: (_: string, row) => (
|
||||
<Space size={6} wrap>
|
||||
<Typography.Text strong>{row.title}</Typography.Text>
|
||||
{row.is_pinned && (
|
||||
<Tag color="gold" icon={<PushpinOutlined />}>
|
||||
置顶
|
||||
</Tag>
|
||||
)}
|
||||
{row.is_important && (
|
||||
<Tag color="red" icon={<WarningOutlined />}>
|
||||
重要
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
width: 120,
|
||||
render: (value: string) =>
|
||||
CATEGORY_OPTIONS.find(item => item.value === value)?.label || value,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (value: string) => (
|
||||
<Tag color={STATUS_META[value]?.color || 'default'}>
|
||||
{STATUS_META[value]?.label || value}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '优先级', dataIndex: 'priority', width: 90, align: 'center' },
|
||||
{ title: '阅读', dataIndex: 'view_count', width: 80, align: 'center' },
|
||||
{
|
||||
title: '发布时间',
|
||||
dataIndex: 'published_at',
|
||||
width: 160,
|
||||
render: (value: string | null) => formatTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 260,
|
||||
render: (_: unknown, row) => (
|
||||
<Space size={4} wrap>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
{row.status === 'published' ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => void handleStatusAction(row, 'archive')}
|
||||
>
|
||||
下架
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
void handleStatusAction(row, row.status === 'archived' ? 'unarchive' : 'publish')
|
||||
}
|
||||
>
|
||||
{row.status === 'archived' ? '恢复发布' : '发布'}
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确认删除这条公告?"
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
onConfirm={() => void handleDelete(row)}
|
||||
>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑公告' : '新建公告'}
|
||||
open={modalOpen}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={() => void handleSubmit()}
|
||||
confirmLoading={saving}
|
||||
okText={editing ? '保存' : '创建草稿'}
|
||||
width={680}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={EMPTY_FORM}>
|
||||
<Form.Item
|
||||
label="公告标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入公告标题' }, { max: 255, message: '标题不超过 255 字' }]}
|
||||
>
|
||||
<Input placeholder="例如:本站春节期间发货安排" maxLength={255} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="公告分类"
|
||||
name="category"
|
||||
rules={[{ required: true, message: '请选择公告分类' }]}
|
||||
>
|
||||
<Select options={CATEGORY_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="公告内容"
|
||||
name="content"
|
||||
rules={[{ required: true, message: '请输入公告内容' }]}
|
||||
>
|
||||
<Input.TextArea rows={8} placeholder="支持 Markdown 语法" />
|
||||
</Form.Item>
|
||||
<Space size="large" align="start" wrap>
|
||||
<Form.Item label="优先级" name="priority" tooltip="数值越大越靠前">
|
||||
<InputNumber min={0} max={9999} />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_pinned" valuePropName="checked">
|
||||
<Checkbox>置顶</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="is_important" valuePropName="checked">
|
||||
<Checkbox>标记重要</Checkbox>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
{!editing && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="新建的公告是草稿,需要在列表中点击「发布」才会在本站前台展示。"
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user