From d10419d325e868c771d537074ce64ffcab8571b3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 17:30:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(crash):=20=E6=96=B0=E5=A2=9E=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E6=91=B8=E7=BA=A2=E5=95=86=E5=9F=8E=E4=B8=89?= =?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 +++ .../features/mobile/MobileCrashDetailPage.tsx | 84 ++++++++++++ .../features/mobile/MobileCrashListPage.tsx | 76 +++++++++++ .../features/mobile/MobileCrashOrdersPage.tsx | 120 ++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 frontend/src/features/mobile/MobileCrashDetailPage.tsx create mode 100644 frontend/src/features/mobile/MobileCrashListPage.tsx create mode 100644 frontend/src/features/mobile/MobileCrashOrdersPage.tsx diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 2a1a147..81a4478 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -149,6 +149,21 @@ const MobileAddHomeGuidePage = lazy(() => default: module.MobileAddHomeGuidePage, })) ) +const MobileCrashListPage = lazy(() => + import('@/features/mobile/MobileCrashListPage').then(module => ({ + default: module.MobileCrashListPage, + })) +) +const MobileCrashDetailPage = lazy(() => + import('@/features/mobile/MobileCrashDetailPage').then(module => ({ + default: module.MobileCrashDetailPage, + })) +) +const MobileCrashOrdersPage = lazy(() => + import('@/features/mobile/MobileCrashOrdersPage').then(module => ({ + default: module.MobileCrashOrdersPage, + })) +) const MobileOrdersPage = lazy(() => import('@/features/mobile/MobileOrdersPage').then(module => ({ default: module.MobileOrdersPage, @@ -503,6 +518,9 @@ export function AppRouter() { } /> } /> } /> + } /> + } /> + } /> } /> diff --git a/frontend/src/features/mobile/MobileCrashDetailPage.tsx b/frontend/src/features/mobile/MobileCrashDetailPage.tsx new file mode 100644 index 0000000..591fba1 --- /dev/null +++ b/frontend/src/features/mobile/MobileCrashDetailPage.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react' +import { Button, Card, Dialog, Image, NavBar, Stepper, Toast } from 'antd-mobile' +import { useNavigate, useParams } from 'react-router-dom' + +import { createCrashOrder, startCrashPayment, useCrashProduct } from '@/features/crash/api' +import { formatMoney } from '@/shared/utils/format' + +function errorText(error: unknown) { + const response = (error as { response?: { data?: { message?: string } } })?.response + return response?.data?.message || (error instanceof Error ? error.message : '下单失败') +} + +export function MobileCrashDetailPage() { + const { id } = useParams() + const navigate = useNavigate() + const [quantity, setQuantity] = useState(1) + const [submitting, setSubmitting] = useState(false) + const productQuery = useCrashProduct(id) + const product = productQuery.data + + const submit = async () => { + if (!product) return + setSubmitting(true) + try { + const order = await createCrashOrder({ product_id: product.id, quantity }) + try { + const payment = await startCrashPayment(order.id) + if (payment?.pay_url) { + window.location.href = payment.pay_url + return + } + } catch { + // 支付未配置时稍后在订单页继续 + } + Toast.show({ icon: 'success', content: '下单成功' }) + navigate('/m/crash/orders') + } catch (error) { + await Dialog.alert({ content: errorText(error), confirmText: '知道了' }) + } finally { + setSubmitting(false) + } + } + + return ( +
+ navigate('/m/crash')} + right={ + + } + > + 商品详情 + + + {product ? ( + <> + {product.cover_url ? ( + + ) : null} + +
+ {product.title} +
+ {formatMoney(product.price_cent)} · {product.category_name || '未分类'} +
+
{product.description}
+
+ 数量 + setQuantity(value)} /> + 合计 {formatMoney(product.price_cent * quantity)} +
+
+
+ +
下单需登录并完成实名;支付成功后由客服接单处理。
+ + ) : null} +
+ ) +} diff --git a/frontend/src/features/mobile/MobileCrashListPage.tsx b/frontend/src/features/mobile/MobileCrashListPage.tsx new file mode 100644 index 0000000..11f3dd3 --- /dev/null +++ b/frontend/src/features/mobile/MobileCrashListPage.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react' +import { Button, Card, ErrorBlock, Image, NavBar, Skeleton, Tag } from 'antd-mobile' +import { useNavigate } from 'react-router-dom' + +import { useCrashCategories, useCrashProducts } from '@/features/crash/api' +import { formatMoney } from '@/shared/utils/format' + +export function MobileCrashListPage() { + const navigate = useNavigate() + const [categoryId, setCategoryId] = useState() + const categoriesQuery = useCrashCategories() + const productsQuery = useCrashProducts({ category_id: categoryId, page: 1, page_size: 30 }) + const items = productsQuery.data?.items ?? [] + + return ( +
+ navigate('/m')} + right={ + + } + > + 摸红商城 + + +
+ setCategoryId(undefined)} + > + 全部 + + {(categoriesQuery.data ?? []).map(item => ( + setCategoryId(item.id)} + > + {item.name} + + ))} +
+ + {productsQuery.isPending ? ( + + + + ) : items.length === 0 ? ( + + ) : ( +
+ {items.map(item => ( + navigate(`/m/crash/${item.id}`)}> +
+ {item.cover_url ? ( + + ) : null} +
+ {item.title} +
+ {formatMoney(item.price_cent)} · {item.category_name || '未分类'} +
+ + {item.stock < 0 ? '不限量' : `库存 ${item.stock}`} + +
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/src/features/mobile/MobileCrashOrdersPage.tsx b/frontend/src/features/mobile/MobileCrashOrdersPage.tsx new file mode 100644 index 0000000..1a41ab9 --- /dev/null +++ b/frontend/src/features/mobile/MobileCrashOrdersPage.tsx @@ -0,0 +1,120 @@ +import { Button, Card, Dialog, ErrorBlock, Image, NavBar, Skeleton, Tag, Toast } from 'antd-mobile' +import { useNavigate } from 'react-router-dom' +import { useQueryClient } from '@tanstack/react-query' + +import { cancelCrashOrder, startCrashPayment, useMyCrashOrders } from '@/features/crash/api' +import { useSessionStore } from '@/stores/session' +import { formatDateTime, formatMoney } from '@/shared/utils/format' + +const statusMeta: Record = { + pending_payment: { label: '待支付', color: 'warning' }, + paid: { label: '已支付', color: 'primary' }, + received: { label: '已接单', color: 'primary' }, + completed: { label: '已完成', color: 'success' }, + cancelled: { label: '已取消', color: 'default' }, + refunded: { label: '已退款', color: 'danger' }, +} + +function errorText(error: unknown) { + const response = (error as { response?: { data?: { message?: string } } })?.response + return response?.data?.message || (error instanceof Error ? error.message : '操作失败') +} + +export function MobileCrashOrdersPage() { + const navigate = useNavigate() + const queryClient = useQueryClient() + const user = useSessionStore(state => state.user) + const ordersQuery = useMyCrashOrders(1, 30) + const items = ordersQuery.data?.items ?? [] + const invalidate = () => queryClient.invalidateQueries({ queryKey: ['crash-orders'] }) + + return ( +
+ navigate('/m/crash')}>我的撞车订单 + + {ordersQuery.isPending ? ( + + + + ) : items.length === 0 ? ( + + ) : ( +
+ {items.map(order => { + const meta = statusMeta[order.status] ?? { label: order.status, color: 'default' as const } + return ( + +
+ {order.product_cover_url ? ( + + ) : null} +
+
+ {order.product_title || order.order_no} + {meta.label} +
+
{order.order_no}
+
+ {formatMoney(order.amount_cent)} · 数量 {order.quantity} +
+
{formatDateTime(order.created_at)}
+ {order.status === 'pending_payment' ? ( +
+ + +
+ ) : null} +
+
+
+ ) + })} +
+ )} + + {!user ?
登录后可查看自己的撞车订单。
: null} +
+ ) +}