diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx
index 05ba243..69322f2 100644
--- a/frontend/src/app/AppRouter.tsx
+++ b/frontend/src/app/AppRouter.tsx
@@ -80,6 +80,11 @@ const InvitationsPage = lazy(() =>
default: module.InvitationsPage,
}))
)
+const InviteLandingPage = lazy(() =>
+ import('@/features/invitations/InviteLandingPage').then(module => ({
+ default: module.InviteLandingPage,
+ }))
+)
const MessagesPage = lazy(() =>
import('@/features/messages/MessagesPage').then(module => ({ default: module.MessagesPage }))
)
@@ -125,6 +130,16 @@ const MobileInvitationsPage = lazy(() =>
default: module.MobileInvitationsPage,
}))
)
+const MobileRegisterPage = lazy(() =>
+ import('@/features/mobile/MobileRegisterPage').then(module => ({
+ default: module.MobileRegisterPage,
+ }))
+)
+const MobileAddHomeGuidePage = lazy(() =>
+ import('@/features/mobile/MobileAddHomeGuidePage').then(module => ({
+ default: module.MobileAddHomeGuidePage,
+ }))
+)
const MobileOrdersPage = lazy(() =>
import('@/features/mobile/MobileOrdersPage').then(module => ({
default: module.MobileOrdersPage,
@@ -428,6 +443,7 @@ export function AppRouter() {
} />
+ } />
} />
}>
@@ -448,6 +464,8 @@ export function AppRouter() {
} />
} />
} />
+ } />
+ } />
} />
diff --git a/frontend/src/features/invitations/InviteLandingPage.tsx b/frontend/src/features/invitations/InviteLandingPage.tsx
new file mode 100644
index 0000000..d5d0d20
--- /dev/null
+++ b/frontend/src/features/invitations/InviteLandingPage.tsx
@@ -0,0 +1,81 @@
+import { useEffect } from 'react'
+import { useQuery } from '@tanstack/react-query'
+import { Alert, Button, Card, Skeleton, Space, Tag, Typography } from 'antd'
+import { GiftOutlined } from '@ant-design/icons'
+import { useNavigate, useParams } from 'react-router-dom'
+
+import { fetchInviteCodeInfo } from '@/features/invitations/api'
+import { BrandLogo } from '@/components/BrandLogo'
+
+const INVITE_STORAGE_KEY = 'jieyou_invite_code'
+
+export function InviteLandingPage() {
+ const { code } = useParams()
+ const navigate = useNavigate()
+ const infoQuery = useQuery({
+ queryKey: ['invite-code-info', code],
+ queryFn: () => fetchInviteCodeInfo(code as string),
+ enabled: Boolean(code),
+ retry: false,
+ })
+
+ useEffect(() => {
+ if (code) {
+ try {
+ sessionStorage.setItem(INVITE_STORAGE_KEY, code)
+ } catch {
+ // 忽略存储失败
+ }
+ }
+ }, [code])
+
+ const info = infoQuery.data
+
+ return (
+
+
+
+ JIEYOU · 邀请
+
+ 解忧商行邀请你加入
+
+
+ 注册并完成实名后即可租号、出租账号;邀请你的好友还能获得首月 / 次月 / 后续返利。
+
+
+
+
+
+
+
+
+ {infoQuery.isPending ? (
+
+ ) : infoQuery.isError || !info?.valid ? (
+
+ ) : (
+
+ }>
+ 邀请码 {info.invite_code}
+
+
+ 邀请人:{info.inviter_nickname || '解忧商行用户'}
+
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx b/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx
new file mode 100644
index 0000000..4c4f2c6
--- /dev/null
+++ b/frontend/src/features/mobile/MobileAddHomeGuidePage.tsx
@@ -0,0 +1,32 @@
+import { Button, Card, NavBar, Steps } from 'antd-mobile'
+import { useNavigate } from 'react-router-dom'
+
+const { Step } = Steps
+
+export function MobileAddHomeGuidePage() {
+ const navigate = useNavigate()
+
+ return (
+
+
navigate('/m/profile')}>添加到桌面
+
+
+
+ 添加后可以像 App 一样从桌面直接进入解忧商行,下单、查订单、联系客服都更快。
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/mobile/MobileRegisterPage.tsx b/frontend/src/features/mobile/MobileRegisterPage.tsx
new file mode 100644
index 0000000..273fe2b
--- /dev/null
+++ b/frontend/src/features/mobile/MobileRegisterPage.tsx
@@ -0,0 +1,215 @@
+import { useEffect, useState } from 'react'
+import { useQuery } from '@tanstack/react-query'
+import { Button, Card, Form, Image, Input, NavBar, Toast } from 'antd-mobile'
+import { useNavigate, useSearchParams } from 'react-router-dom'
+
+import { apiClient } from '@/shared/api/client'
+import type { ApiResponse } from '@/shared/api/types'
+import { useSessionStore } from '@/stores/session'
+
+const INVITE_STORAGE_KEY = 'jieyou_invite_code'
+
+interface CaptchaData {
+ captcha_id: string
+ image: string
+}
+
+interface RegisterFormValues {
+ phone: string
+ captcha: string
+ code: string
+ password: string
+}
+
+function errorText(error: unknown) {
+ const response = (error as { response?: { data?: { message?: string } } })?.response
+ return response?.data?.message || (error instanceof Error ? error.message : '请求失败')
+}
+
+export function MobileRegisterPage() {
+ const navigate = useNavigate()
+ const [params] = useSearchParams()
+ const [form] = Form.useForm()
+ const [captchaCode, setCaptchaCode] = useState('')
+ const [countdown, setCountdown] = useState(0)
+ const [sending, setSending] = useState(false)
+ const [submitting, setSubmitting] = useState(false)
+ const [inviteCode, setInviteCode] = useState('')
+ const login = useSessionStore(state => state.login)
+
+ const captchaQuery = useQuery({
+ queryKey: ['auth-captcha', captchaCode],
+ queryFn: () =>
+ apiClient
+ .get>('/auth/captcha', { skipErrorHandler: true } as never)
+ .then(response => response.data.data),
+ staleTime: 0,
+ })
+
+ useEffect(() => {
+ const fromQuery = params.get('invite')
+ let stored = ''
+ try {
+ stored = sessionStorage.getItem(INVITE_STORAGE_KEY) || ''
+ } catch {
+ stored = ''
+ }
+ setInviteCode(fromQuery || stored)
+ }, [params])
+
+ useEffect(() => {
+ if (countdown <= 0) return
+ const timer = window.setTimeout(() => setCountdown(current => current - 1), 1000)
+ return () => window.clearTimeout(timer)
+ }, [countdown])
+
+ const sendSMS = async () => {
+ const phone = form.getFieldValue('phone') as string
+ const captcha = form.getFieldValue('captcha') as string
+ if (!phone) {
+ Toast.show({ content: '请先填写手机号' })
+ return
+ }
+ if (!captcha) {
+ Toast.show({ content: '请先填写图形验证码' })
+ return
+ }
+ if (!captchaQuery.data?.captcha_id) {
+ Toast.show({ content: '验证码加载失败,请点击图片刷新' })
+ return
+ }
+ setSending(true)
+ try {
+ await apiClient.post(
+ '/auth/sms/send',
+ {
+ phone,
+ captcha_id: captchaQuery.data.captcha_id,
+ captcha_code: captcha,
+ },
+ { skipErrorHandler: true } as never
+ )
+ Toast.show({ icon: 'success', content: '验证码已发送' })
+ setCountdown(60)
+ setCaptchaCode(`${Date.now()}`)
+ form.setFieldValue('captcha', '')
+ } catch (error) {
+ Toast.show({ content: errorText(error) })
+ setCaptchaCode(`${Date.now()}`)
+ } finally {
+ setSending(false)
+ }
+ }
+
+ const submit = async () => {
+ const values = await form.validateFields()
+ setSubmitting(true)
+ try {
+ await apiClient.post(
+ '/auth/password/register',
+ {
+ phone: values.phone.trim(),
+ code: values.code.trim(),
+ password: values.password,
+ invite_code: inviteCode || undefined,
+ },
+ { skipErrorHandler: true } as never
+ )
+ await login(values.phone.trim(), values.password)
+ Toast.show({ icon: 'success', content: '注册成功' })
+ navigate('/m', { replace: true })
+ } catch (error) {
+ Toast.show({ content: errorText(error) })
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ return (
+
+ navigate('/m/login')}>注册账号
+
+
+
+
+
+
+
+ {captchaQuery.data?.image ? (
+ setCaptchaCode(`${Date.now()}`)}
+ style={{ borderRadius: 6 }}
+ />
+ ) : null}
+ 点击图片刷新
+
+ }
+ >
+
+
+
+ 0}
+ onClick={() => void sendSMS()}
+ style={{ marginTop: 6 }}
+ >
+ {countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
+
+ }
+ >
+
+
+
+
+
+
+
+
+ {inviteCode ? (
+ 邀请码:{inviteCode}
+ ) : null}
+
+
+
+
+
+
+
+ )
+}