From 2f0490c270745ab10665dcd662e86232f0eae474 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 17:18:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E6=96=B0=E5=A2=9E=E5=85=AC?= =?UTF-8?q?=E5=91=8A=E3=80=81=E9=80=9A=E7=9F=A5=E4=B8=8E=E9=82=80=E8=AF=B7?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/AppRouter.tsx | 18 ++++ .../mobile/MobileAnnouncementsPage.tsx | 62 +++++++++++ .../features/mobile/MobileInvitationsPage.tsx | 100 ++++++++++++++++++ .../mobile/MobileNotificationsPage.tsx | 83 +++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 frontend/src/features/mobile/MobileAnnouncementsPage.tsx create mode 100644 frontend/src/features/mobile/MobileInvitationsPage.tsx create mode 100644 frontend/src/features/mobile/MobileNotificationsPage.tsx 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} +
+ ) +}