From d46636c3d25ba4ba7930a2392dc4a8ce694c040b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 13:25:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=96=B0=E5=A2=9E=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=85=AC=E5=91=8A=E7=AE=A1=E7=90=86=E4=B8=8E=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Phase1-验收报告.md | 4 +- docs/Phase2-公告管理验收.md | 66 +++ docs/项目计划.md | 2 +- frontend/src/app/AppRouter.tsx | 7 +- .../features/admin/AdminAnnouncementsPage.tsx | 461 ++++++++++++++++++ frontend/src/features/admin/api.ts | 60 +++ frontend/src/features/admin/labels.ts | 21 + frontend/src/features/admin/types.ts | 44 ++ 8 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 docs/Phase2-公告管理验收.md create mode 100644 frontend/src/features/admin/AdminAnnouncementsPage.tsx diff --git a/docs/Phase1-验收报告.md b/docs/Phase1-验收报告.md index a37f949..79b9893 100644 --- a/docs/Phase1-验收报告.md +++ b/docs/Phase1-验收报告.md @@ -194,9 +194,9 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 5. 已知限制 1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。 -2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板,其余页面仍为统一占位。 +2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理,其余页面仍为统一占位。 3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。 -4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`;其余页面仍需核对。 +4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`;其余页面仍需核对。 5. SVG 中文尚未转曲。 6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。 7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理与审核在本批次提交。 diff --git a/docs/Phase2-公告管理验收.md b/docs/Phase2-公告管理验收.md new file mode 100644 index 0000000..8538728 --- /dev/null +++ b/docs/Phase2-公告管理验收.md @@ -0,0 +1,66 @@ +# Phase 2 验收报告(后台公告管理) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:`/console-jy8k3m/announcements` +> 结论:**本切片通过,公告列表、编辑、发布、归档与删除已接真实接口** + +--- + +## 1. 接口与权限 + +| 能力 | 接口 | 权限码 | +| --- | --- | --- | +| 公告列表 | `GET /api/admin/announcements` | `announcement:view` | +| 公告详情 | `GET /api/admin/announcements/:id` | `announcement:view` | +| 新建 | `POST /api/admin/announcements` | `announcement:manage` | +| 编辑 | `PUT /api/admin/announcements/:id` | `announcement:manage` | +| 发布 | `POST /api/admin/announcements/:id/publish` | `announcement:manage` | +| 归档 | `POST /api/admin/announcements/:id/archive` | `announcement:manage` | +| 取消归档 | `POST /api/admin/announcements/:id/unarchive` | `announcement:manage` | +| 删除 | `DELETE /api/admin/announcements/:id` | `announcement:manage` | + +## 2. 页面 + +`frontend/src/features/admin/AdminAnnouncementsPage.tsx` + +- 筛选:分类(通知/教程/规则/常见问题)、状态(草稿/已发布/已归档) +- 列:公告(标题/置顶/重要/弹窗/优先级)、分类、站点、状态、浏览量、发布时间、更新时间、操作 +- 分页 10 / 20 / 50 +- 操作按状态与权限显示: + - 草稿:编辑、发布、删除 + - 已发布:编辑、归档、删除 + - 已归档:编辑、取消归档、删除 +- 发布/归档/取消归档/删除均二次确认;删除提示优先使用归档 + +## 3. 编辑弹窗 + +- 字段:标题、分类、优先级、归属站点(仅新建)、正文、置顶、重要、弹窗 +- 新建默认保存为草稿,发布后前台展示 +- 弹窗公告提示「每天只弹一次,需用户手动关闭」 +- 正文按纯文本编辑,支持换行 + +## 4. 验证结果 + +```text +npx tsc --noEmit 通过 +npm run build 通过(入口 496.93 kB,gzip 161.32 kB) +GET /api/admin/announcements?page=1&page_size=5 ok,返回 1 条种子公告 +GET /console-jy8k3m/announcements 200 +``` + +列表读取已实测;新建、编辑、发布、归档、删除未执行,避免改动种子公告与前台展示。 + +## 5. 已知限制 + +1. 归属分站使用数字 ID 输入,未接分站选择器。 +2. 正文无 Markdown 预览,前台渲染效果需发布后核对。 +3. 未做公告置顶排序拖拽或批量归档。 +4. 未接分站代发公告的 `site_id` 筛选入口(接口已支持)。 + +## 6. 下一步建议 + +1. 后台系统配置与审计日志。 +2. 分站公告代发入口。 +3. 线下出款/运营支出登记。 +4. 移动端下单、支付与交接/归还/结账。 diff --git a/docs/项目计划.md b/docs/项目计划.md index 4b4a65e..a332077 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -395,7 +395,7 @@ hfb_jysh/ ### Phase 4:管理后台核心(4–5 周) -> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉与财务看板;线下出款/运营支出、系统配置与审计日志待迁移。 +> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板与公告管理;线下出款/运营支出、系统配置与审计日志待迁移。 - 后台登录、工作台、订单列表、订单详情。 - 商品列表、商品详情、审核。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 9f771d9..887c0f0 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -98,6 +98,11 @@ const AdminFinancePage = lazy(() => default: module.AdminFinancePage, })) ) +const AdminAnnouncementsPage = lazy(() => + import('@/features/admin/AdminAnnouncementsPage').then(module => ({ + default: module.AdminAnnouncementsPage, + })) +) const AdminLayout = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout })) ) @@ -107,7 +112,6 @@ const AdminSectionPlaceholder = lazy(() => const adminSections: Array<{ path: string; title: string; description: string }> = [ { path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' }, - { path: 'announcements', title: '公告管理', description: '维护主站与分站公告。' }, { path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, { path: 'audit-logs', title: '审计日志', description: '查看高风险操作与变更记录。' }, ] @@ -149,6 +153,7 @@ export function AppRouter() { } /> } /> } /> + } /> {adminSections.map(section => ( () + const [draft, setDraft] = useState(emptyFilters) + const [applied, setApplied] = useState(emptyFilters) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + const [editorOpen, setEditorOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [submitting, setSubmitting] = useState(false) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canManage = Boolean( + permissions?.includes('*') || permissions?.includes('announcement:manage') + ) + + const { data, isPending, isFetching, isError, error, refetch } = useQuery({ + queryKey: ['admin-announcements', applied, page, pageSize], + queryFn: () => fetchAdminAnnouncements({ ...applied, page, page_size: pageSize }), + placeholderData: keepPreviousData, + }) + + const openCreate = () => { + setEditing(null) + form.setFieldsValue({ + distribution_site_id: 0, + title: '', + content: '', + category: 'notice', + priority: 0, + is_pinned: false, + is_important: false, + is_popup: false, + }) + setEditorOpen(true) + } + + const openEdit = (item: AdminAnnouncement) => { + setEditing(item) + form.setFieldsValue({ + distribution_site_id: item.distribution_site_id, + title: item.title, + content: item.content, + category: item.category, + priority: item.priority, + is_pinned: item.is_pinned, + is_important: item.is_important, + is_popup: item.is_popup, + }) + setEditorOpen(true) + } + + const save = async () => { + const values = await form.validateFields() + setSubmitting(true) + try { + const payload: AdminAnnouncementPayload = { + ...values, + distribution_site_id: Number(values.distribution_site_id ?? 0), + } + if (editing) { + await updateAdminAnnouncement(editing.id, payload) + message.success('公告已更新') + } else { + await createAdminAnnouncement(payload) + message.success('公告已创建为草稿') + } + setEditorOpen(false) + await queryClient.invalidateQueries({ queryKey: ['admin-announcements'] }) + } catch (err) { + const text = + err && typeof err === 'object' && 'response' in err + ? ((err as { response?: { data?: { message?: string } } }).response?.data?.message ?? + '保存失败') + : err instanceof Error + ? err.message + : '保存失败' + message.error(text) + } finally { + setSubmitting(false) + } + } + + const runAction = (title: string, content: string, action: () => Promise) => { + modal.confirm({ + title, + content, + okText: '确认', + cancelText: '取消', + onOk: async () => { + try { + await action() + message.success('操作成功') + await queryClient.invalidateQueries({ queryKey: ['admin-announcements'] }) + } catch (err) { + message.error(err instanceof Error ? err.message : '操作失败') + } + }, + }) + } + + const columns = useMemo>( + () => [ + { + title: '公告', + dataIndex: 'title', + width: 320, + fixed: 'left', + render: (_, row) => ( +
+ {row.title} + + {row.is_pinned ? 置顶 : null} + {row.is_important ? 重要 : null} + {row.is_popup ? 弹窗 : null} + {row.priority !== 0 ? 优先级 {row.priority} : null} + + #{row.id} +
+ ), + }, + { + title: '分类', + dataIndex: 'category', + width: 110, + render: value => announcementCategoryMap[value] ?? value, + }, + { + title: '站点', + key: 'site', + width: 150, + render: (_, row) => + row.distribution_site_id > 0 + ? row.distribution_site_name || `分站 #${row.distribution_site_id}` + : '主站', + }, + { + title: '状态', + dataIndex: 'status', + width: 110, + render: value => , + }, + { + title: '浏览', + dataIndex: 'view_count', + width: 90, + align: 'right', + }, + { + title: '发布时间', + dataIndex: 'published_at', + width: 170, + render: value => formatDateTime(value), + }, + { + title: '更新时间', + dataIndex: 'updated_at', + width: 170, + render: value => formatDateTime(value), + }, + { + title: '操作', + key: 'actions', + width: 260, + fixed: 'right', + render: (_, row) => ( + + {canManage ? ( + + ) : null} + {row.status === 'draft' && canManage ? ( + + ) : null} + {row.status === 'published' && canManage ? ( + + ) : null} + {row.status === 'archived' && canManage ? ( + + ) : null} + {canManage ? ( + + ) : null} + + ), + }, + ], + [canManage] + ) + + const patchFilter = (patch: Partial) => { + setDraft(current => ({ ...current, ...patch })) + } + + const applyFilters = () => { + setApplied({ ...draft }) + setPage(1) + } + + const resetFilters = () => { + setDraft(emptyFilters) + setApplied(emptyFilters) + setPage(1) + } + + const hasActiveFilters = Boolean(draft.category) || Boolean(draft.status) + const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。' + + return ( +
+ + 共 {data?.total ?? 0} 条 + {isFetching && !isPending ? 正在更新 : null} + + } + extra={ + + + {canManage ? ( + + ) : null} + + } + /> + + + + patchFilter({ status: value ?? '' })} + /> + + + + {isError ? ( + void refetch()}> + 重试 + + } + /> + ) : null} + + + title={公告列表} + extra={ + + {data ? `第 ${data.page} 页 · 当前 ${data.items.length} 条` : '加载中'} + + } + rowKey="id" + columns={columns} + dataSource={data?.items ?? []} + loading={isFetching} + total={data?.total ?? 0} + page={data?.page ?? page} + pageSize={data?.page_size ?? pageSize} + onPageChange={(nextPage, nextPageSize) => { + setPage(nextPageSize !== pageSize ? 1 : nextPage) + setPageSize(nextPageSize) + }} + emptyDescription="还没有公告" + /> + + void save()} + onCancel={() => { + if (!submitting) setEditorOpen(false) + }} + > +
+ + + + + +