diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index e7f0e0c..05ba243 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -110,6 +110,21 @@ const MobileWithdrawalPage = lazy(() =>
default: module.MobileWithdrawalPage,
}))
)
+const MobileAnnouncementsPage = lazy(() =>
+ import('@/features/mobile/MobileAnnouncementsPage').then(module => ({
+ default: module.MobileAnnouncementsPage,
+ }))
+)
+const MobileNotificationsPage = lazy(() =>
+ import('@/features/mobile/MobileNotificationsPage').then(module => ({
+ default: module.MobileNotificationsPage,
+ }))
+)
+const MobileInvitationsPage = lazy(() =>
+ import('@/features/mobile/MobileInvitationsPage').then(module => ({
+ default: module.MobileInvitationsPage,
+ }))
+)
const MobileOrdersPage = lazy(() =>
import('@/features/mobile/MobileOrdersPage').then(module => ({
default: module.MobileOrdersPage,
@@ -430,6 +445,9 @@ export function AppRouter() {
} />
} />
} />
+ } />
+ } />
+ } />
} />
diff --git a/frontend/src/features/mobile/MobileAnnouncementsPage.tsx b/frontend/src/features/mobile/MobileAnnouncementsPage.tsx
new file mode 100644
index 0000000..a299825
--- /dev/null
+++ b/frontend/src/features/mobile/MobileAnnouncementsPage.tsx
@@ -0,0 +1,62 @@
+import { useState } from 'react'
+import { Card, Dialog, ErrorBlock, NavBar, Skeleton, Tag } from 'antd-mobile'
+import { useNavigate } from 'react-router-dom'
+
+import { useAnnouncements } from '@/features/announcements/api'
+import type { Announcement } from '@/features/announcements/types'
+import { formatDate, formatText } from '@/shared/utils/format'
+
+export function MobileAnnouncementsPage() {
+ const navigate = useNavigate()
+ const [page, setPage] = useState(1)
+ const query = useAnnouncements(page, 20)
+ const items = query.data?.items ?? []
+
+ const openDetail = async (item: Announcement) => {
+ await Dialog.alert({
+ title: item.title,
+ content: (
+
+ {item.content}
+
+ ),
+ confirmText: '知道了',
+ })
+ }
+
+ return (
+
+
navigate('/m/profile')}>公告
+
+ {query.isPending ? (
+
+
+
+ ) : items.length === 0 ? (
+
+ ) : (
+
+ {items.map(item => (
+
void openDetail(item)}>
+
+
+ {item.is_pinned ? 置顶 : null}
+ {item.is_important ? 重要 : null}
+ {item.title}
+
+
{formatText(item.content).slice(0, 80)}…
+
+ {formatDate(item.published_at || item.created_at)} · 阅读 {item.view_count}
+
+
+
+ ))}
+
+ )}
+
+ {query.data && items.length < (query.data.total ?? 0) ? (
+
setPage(current => current + 1)}>加载更多
+ ) : null}
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileInvitationsPage.tsx b/frontend/src/features/mobile/MobileInvitationsPage.tsx
new file mode 100644
index 0000000..0b18b59
--- /dev/null
+++ b/frontend/src/features/mobile/MobileInvitationsPage.tsx
@@ -0,0 +1,100 @@
+import { Button, Card, ErrorBlock, NavBar, Skeleton, Tag, Toast } from 'antd-mobile'
+import { useQuery } from '@tanstack/react-query'
+import { useNavigate } from 'react-router-dom'
+
+import { fetchInvitedRecords, useInvitationSummary } from '@/features/invitations/api'
+import { useSessionStore } from '@/stores/session'
+import { formatDate, formatMoney } from '@/shared/utils/format'
+
+const stageLabels: Record = {
+ first_month: '首月',
+ second_month: '次月',
+ subsequent: '后续',
+ unverified: '待实名',
+}
+
+export function MobileInvitationsPage() {
+ const navigate = useNavigate()
+ const user = useSessionStore(state => state.user)
+ const summaryQuery = useInvitationSummary(Boolean(user))
+ const recordsQuery = useQuery({
+ queryKey: ['invitation-records', 1],
+ queryFn: () => fetchInvitedRecords(1, 10),
+ enabled: Boolean(user),
+ })
+
+ const summary = summaryQuery.data
+ const records = recordsQuery.data?.items ?? []
+
+ const copyLink = async () => {
+ if (!summary?.invite_link) return
+ try {
+ await navigator.clipboard.writeText(summary.invite_link)
+ Toast.show({ icon: 'success', content: '邀请链接已复制' })
+ } catch {
+ Toast.show({ content: '复制失败,请手动复制' })
+ }
+ }
+
+ return (
+
+
navigate('/m/profile')}>邀请好友
+
+
+ {summaryQuery.isPending ? (
+
+ ) : (
+
+
累计返利
+
+ {formatMoney(summary?.total_rebate_cent ?? 0)}
+
+
+ 邀请码 {summary?.invite_code || '—'} · 已邀请 {summary?.total_invited_count ?? 0} 人
+
+
+ 首月 {Math.round((summary?.first_month_rate ?? 0) * 100)}% · 次月{' '}
+ {Math.round((summary?.second_month_rate ?? 0) * 100)}% · 后续{' '}
+ {Math.round((summary?.subsequent_rate ?? 0) * 100)}%
+
+
+
+ )}
+
+
+
+ {recordsQuery.isPending ? (
+
+ ) : records.length === 0 ? (
+
+ ) : (
+
+ {records.map(item => (
+
+
+ {item.nickname || item.masked_phone}
+
+ {item.status === 'active' ? '已生效' : '待实名'}
+
+ {stageLabels[item.cycle_stage] ?? item.cycle_stage}
+
+
+ 累计贡献 {formatMoney(item.total_earned_from_cent)} · 注册{' '}
+ {formatDate(item.registered_at)}
+
+
+ ))}
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileNotificationsPage.tsx b/frontend/src/features/mobile/MobileNotificationsPage.tsx
new file mode 100644
index 0000000..83e64fb
--- /dev/null
+++ b/frontend/src/features/mobile/MobileNotificationsPage.tsx
@@ -0,0 +1,83 @@
+import { Button, Card, ErrorBlock, NavBar, Skeleton, Tag, Toast } from 'antd-mobile'
+import { useQueryClient } from '@tanstack/react-query'
+import { useNavigate } from 'react-router-dom'
+
+import {
+ markAllNotificationsRead,
+ markNotificationRead,
+ useNotifications,
+} from '@/features/messages/api'
+import { useSessionStore } from '@/stores/session'
+import { formatDateTime } from '@/shared/utils/format'
+
+export function MobileNotificationsPage() {
+ const navigate = useNavigate()
+ const queryClient = useQueryClient()
+ const user = useSessionStore(state => state.user)
+ const query = useNotifications(Boolean(user))
+ const items = query.data?.pages.flatMap(page => page.items) ?? []
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ['notifications'] })
+
+ return (
+
+
navigate('/m/profile')}
+ right={
+
+ }
+ >
+ 通知
+
+
+ {query.isPending ? (
+
+
+
+ ) : items.length === 0 ? (
+
+ ) : (
+
+ {items.map(item => (
+
{
+ if (item.read_at) return
+ await markNotificationRead(item.id)
+ await invalidate()
+ }}
+ >
+
+
+ {item.title}
+ {item.read_at ? null : 未读}
+
+
{item.content}
+
{formatDateTime(item.created_at)}
+
+
+ ))}
+
+ )}
+
+ {query.hasNextPage ? (
+
+ ) : null}
+
+ )
+}