diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index a5329ce..22cf81b 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -60,6 +60,21 @@ const PaymentAccountsPage = lazy(() =>
const WithdrawalPage = lazy(() =>
import('@/features/wallet/WithdrawalPage').then(module => ({ default: module.WithdrawalPage }))
)
+const AnnouncementsPage = lazy(() =>
+ import('@/features/announcements/AnnouncementsPage').then(module => ({
+ default: module.AnnouncementsPage,
+ }))
+)
+const AnnouncementDetailPage = lazy(() =>
+ import('@/features/announcements/AnnouncementDetailPage').then(module => ({
+ default: module.AnnouncementDetailPage,
+ }))
+)
+const NotificationsPage = lazy(() =>
+ import('@/features/notifications/NotificationsPage').then(module => ({
+ default: module.NotificationsPage,
+ }))
+)
const MessagesPage = lazy(() =>
import('@/features/messages/MessagesPage').then(module => ({ default: module.MessagesPage }))
)
@@ -377,6 +392,9 @@ export function AppRouter() {
} />
} />
} />
+ } />
+ } />
+ } />
} />
} />
} />
diff --git a/frontend/src/features/announcements/AnnouncementDetailPage.tsx b/frontend/src/features/announcements/AnnouncementDetailPage.tsx
new file mode 100644
index 0000000..01290ee
--- /dev/null
+++ b/frontend/src/features/announcements/AnnouncementDetailPage.tsx
@@ -0,0 +1,50 @@
+import { Alert, Button, Card, Skeleton, Space, Tag, Typography } from 'antd'
+import { ArrowLeftOutlined } from '@ant-design/icons'
+import { useNavigate, useParams } from 'react-router-dom'
+
+import { useAnnouncement } from '@/features/announcements/api'
+import { formatDateTime, formatText } from '@/shared/utils/format'
+
+export function AnnouncementDetailPage() {
+ const { id } = useParams()
+ const navigate = useNavigate()
+ const query = useAnnouncement(id)
+ const item = query.data
+
+ return (
+
+
+ } onClick={() => navigate('/announcements')}>
+ 返回公告列表
+
+
+
+ {query.isError ? (
+
+ ) : null}
+
+
+ {query.isPending ? (
+
+ ) : !item ? null : (
+ <>
+
+ {item.title}
+
+
+ {item.is_pinned ? 置顶 : null}
+ {item.is_important ? 重要 : null}
+ {formatText(item.category)}
+
+ {formatDateTime(item.published_at || item.created_at)} · 阅读 {item.view_count}
+
+
+
+ {item.content}
+
+ >
+ )}
+
+
+ )
+}
diff --git a/frontend/src/features/announcements/AnnouncementsPage.tsx b/frontend/src/features/announcements/AnnouncementsPage.tsx
new file mode 100644
index 0000000..fc1a159
--- /dev/null
+++ b/frontend/src/features/announcements/AnnouncementsPage.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react'
+import { Alert, Card, Pagination, Segmented, Space, Tag, Typography } from 'antd'
+import { Link } from 'react-router-dom'
+
+import { useAnnouncements } from '@/features/announcements/api'
+import type { Announcement } from '@/features/announcements/types'
+import { formatDateTime, formatText } from '@/shared/utils/format'
+
+const categoryOptions = [
+ { value: '', label: '全部' },
+ { value: 'system', label: '系统公告' },
+ { value: 'order', label: '订单规则' },
+ { value: 'activity', label: '活动' },
+]
+
+export function AnnouncementsPage() {
+ const [category, setCategory] = useState('')
+ const [page, setPage] = useState(1)
+ const query = useAnnouncements(page, 10, category)
+ const items = query.data?.items ?? []
+
+ return (
+
+
+
+ JIEYOU · 公告
+
+ 公告中心
+
+
+ 平台规则、重要提醒与活动信息;置顶与重要公告会优先展示。
+
+
+
+ 共
+ {query.data?.total ?? 0}
+ 条公告
+
+
+
+
{
+ setCategory(String(value))
+ setPage(1)
+ }}
+ />
+
+ {query.isError ? (
+
+ ) : null}
+
+
+ {items.length === 0 && !query.isPending ? (
+
+ 暂无公告
+
+ ) : null}
+
+ {items.map((item: Announcement) => (
+
+
+
+ {item.is_pinned ? 置顶 : null}
+ {item.is_important ? 重要 : null}
+ {formatText(item.category)}
+
+ {item.title}
+
+
+
+ {item.content}
+
+
+ {formatDateTime(item.published_at || item.created_at)} · 阅读 {item.view_count}
+
+
+
+ ))}
+
+
+ {(query.data?.total ?? 0) > 10 ? (
+ setPage(next)}
+ style={{ textAlign: 'center' }}
+ />
+ ) : null}
+
+ )
+}
diff --git a/frontend/src/features/announcements/api.ts b/frontend/src/features/announcements/api.ts
new file mode 100644
index 0000000..c454656
--- /dev/null
+++ b/frontend/src/features/announcements/api.ts
@@ -0,0 +1,38 @@
+import { useQuery } from '@tanstack/react-query'
+
+import { apiClient, unwrapData } from '@/shared/api/client'
+import type { ApiResponse } from '@/shared/api/types'
+import type { Announcement, AnnouncementPage } from '@/features/announcements/types'
+
+const PAGE_SIZE = 20
+
+export function fetchAnnouncements(page = 1, pageSize = PAGE_SIZE, category = '') {
+ return unwrapData>(
+ apiClient.get>>('/announcements', {
+ params: { page, page_size: pageSize, category: category || undefined },
+ })
+ ).then(data => ({
+ items: (data.items ?? []) as Announcement[],
+ total: data.total ?? 0,
+ page: data.page ?? page,
+ page_size: data.page_size ?? pageSize,
+ }))
+}
+
+export function useAnnouncements(page: number, pageSize = PAGE_SIZE, category = '') {
+ return useQuery({
+ queryKey: ['announcements', page, pageSize, category],
+ queryFn: () => fetchAnnouncements(page, pageSize, category),
+ })
+}
+
+export function useAnnouncement(id?: string) {
+ return useQuery({
+ queryKey: ['announcement', id],
+ queryFn: () =>
+ unwrapData(
+ apiClient.get>(`/announcements/${id}`)
+ ),
+ enabled: Boolean(id),
+ })
+}
diff --git a/frontend/src/features/announcements/types.ts b/frontend/src/features/announcements/types.ts
new file mode 100644
index 0000000..22d74d4
--- /dev/null
+++ b/frontend/src/features/announcements/types.ts
@@ -0,0 +1,24 @@
+export interface Announcement {
+ id: number
+ distribution_site_id: number
+ distribution_site_name?: string
+ title: string
+ content: string
+ category: string
+ priority: number
+ is_pinned: boolean
+ is_important: boolean
+ is_popup: boolean
+ view_count: number
+ status: string
+ published_at?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AnnouncementPage {
+ items: Announcement[]
+ total: number
+ page: number
+ page_size: number
+}
diff --git a/frontend/src/features/notifications/NotificationsPage.tsx b/frontend/src/features/notifications/NotificationsPage.tsx
new file mode 100644
index 0000000..d9f8951
--- /dev/null
+++ b/frontend/src/features/notifications/NotificationsPage.tsx
@@ -0,0 +1,120 @@
+import { useState } from 'react'
+import { useQueryClient } from '@tanstack/react-query'
+import { Alert, Button, Card, Empty, Space, Tag, Typography } from 'antd'
+import { ReloadOutlined } from '@ant-design/icons'
+
+import {
+ markAllNotificationsRead,
+ markNotificationRead,
+ useNotifications,
+} from '@/features/messages/api'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime } from '@/shared/utils/format'
+
+export function NotificationsPage() {
+ const queryClient = useQueryClient()
+ const user = useSessionStore(state => state.user)
+ const [page, setPage] = useState(1)
+ const notificationsQuery = useNotifications(Boolean(user))
+ const notifications = notificationsQuery.data?.pages.flatMap(item => item.items) ?? []
+
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['notifications'] })
+
+ return (
+
+
+
+ JIEYOU · 消息
+
+ 我的通知
+
+
+ 订单、资金与系统通知;未读通知会在消息入口展示红点。
+
+
+
+ 未读
+ {notifications.filter(item => !item.read_at).length}
+
+ }
+ loading={notificationsQuery.isFetching}
+ onClick={() => void notificationsQuery.refetch()}
+ >
+ 刷新
+
+
+
+
+
+
+ {notificationsQuery.isError ? (
+
+ ) : null}
+
+
+ {notifications.length === 0 && !notificationsQuery.isPending ? (
+
+
+
+ ) : null}
+
+ {notifications.map(item => (
+
+
+ {item.title}
+ {item.read_at ? (
+ 已读
+ ) : (
+
+ 未读
+
+ )}
+
+ {formatDateTime(item.created_at)}
+
+
+
+ {item.content}
+
+ {item.read_at ? null : (
+
+ )}
+
+ ))}
+
+
+ {notificationsQuery.hasNextPage ? (
+
+ ) : null}
+
+ {page}
+
+
+ )
+}
diff --git a/frontend/src/layouts/StorefrontLayout.tsx b/frontend/src/layouts/StorefrontLayout.tsx
index b6ac846..125d9ab 100644
--- a/frontend/src/layouts/StorefrontLayout.tsx
+++ b/frontend/src/layouts/StorefrontLayout.tsx
@@ -32,6 +32,7 @@ export function StorefrontLayout() {
卖家中心
+ 公告
流程
规则
@@ -52,6 +53,16 @@ export function StorefrontLayout() {
label: '我的消息',
onClick: () => navigate('/messages'),
},
+ {
+ key: 'notifications',
+ label: '我的通知',
+ onClick: () => navigate('/notifications'),
+ },
+ {
+ key: 'announcements',
+ label: '公告中心',
+ onClick: () => navigate('/announcements'),
+ },
{
key: 'wallet',
label: '我的钱包',