feat(crash): 新增移动端摸红商城三页
This commit is contained in:
@@ -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() {
|
||||
<Route path="invitations" element={<MobileInvitationsPage />} />
|
||||
<Route path="register" element={<MobileRegisterPage />} />
|
||||
<Route path="add-home-guide" element={<MobileAddHomeGuidePage />} />
|
||||
<Route path="crash" element={<MobileCrashListPage />} />
|
||||
<Route path="crash/orders" element={<MobileCrashOrdersPage />} />
|
||||
<Route path="crash/:id" element={<MobileCrashDetailPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path={`${adminBase}/login`} element={<AdminLoginPage />} />
|
||||
|
||||
@@ -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 (
|
||||
<div className="mobile-wallet">
|
||||
<NavBar
|
||||
onBack={() => navigate('/m/crash')}
|
||||
right={
|
||||
<Button size="mini" fill="none" onClick={() => navigate('/m/crash/orders')}>
|
||||
我的订单
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
商品详情
|
||||
</NavBar>
|
||||
|
||||
{product ? (
|
||||
<>
|
||||
{product.cover_url ? (
|
||||
<Image src={product.cover_url} width="100%" height={200} fit="cover" style={{ borderRadius: 10 }} />
|
||||
) : null}
|
||||
<Card>
|
||||
<div className="admin-stack" style={{ gap: 8 }}>
|
||||
<strong style={{ fontSize: 18 }}>{product.title}</strong>
|
||||
<div className="mobile-wallet__sub">
|
||||
{formatMoney(product.price_cent)} · {product.category_name || '未分类'}
|
||||
</div>
|
||||
<div className="mobile-wallet__sub">{product.description}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span>数量</span>
|
||||
<Stepper min={1} max={99} value={quantity} onChange={value => setQuantity(value)} />
|
||||
<strong>合计 {formatMoney(product.price_cent * quantity)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Button block color="primary" loading={submitting} onClick={() => void submit()}>
|
||||
立即下单
|
||||
</Button>
|
||||
<div className="mobile-wallet__hint">下单需登录并完成实名;支付成功后由客服接单处理。</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<number | undefined>()
|
||||
const categoriesQuery = useCrashCategories()
|
||||
const productsQuery = useCrashProducts({ category_id: categoryId, page: 1, page_size: 30 })
|
||||
const items = productsQuery.data?.items ?? []
|
||||
|
||||
return (
|
||||
<div className="mobile-wallet">
|
||||
<NavBar
|
||||
onBack={() => navigate('/m')}
|
||||
right={
|
||||
<Button size="mini" fill="none" onClick={() => navigate('/m/crash/orders')}>
|
||||
我的订单
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
摸红商城
|
||||
</NavBar>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, overflowX: 'auto', padding: '4px 0 8px' }}>
|
||||
<Tag
|
||||
color={categoryId ? 'default' : 'primary'}
|
||||
onClick={() => setCategoryId(undefined)}
|
||||
>
|
||||
全部
|
||||
</Tag>
|
||||
{(categoriesQuery.data ?? []).map(item => (
|
||||
<Tag
|
||||
key={item.id}
|
||||
color={categoryId === item.id ? 'primary' : 'default'}
|
||||
onClick={() => setCategoryId(item.id)}
|
||||
>
|
||||
{item.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{productsQuery.isPending ? (
|
||||
<Card>
|
||||
<Skeleton.Paragraph lineCount={4} animated />
|
||||
</Card>
|
||||
) : items.length === 0 ? (
|
||||
<ErrorBlock status="empty" title="暂无商品" />
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
{items.map(item => (
|
||||
<Card key={item.id} onClick={() => navigate(`/m/crash/${item.id}`)}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{item.cover_url ? (
|
||||
<Image src={item.cover_url} width={78} height={78} fit="cover" style={{ borderRadius: 8 }} />
|
||||
) : null}
|
||||
<div className="admin-stack" style={{ flex: 1, gap: 4 }}>
|
||||
<strong>{item.title}</strong>
|
||||
<div className="mobile-wallet__sub">
|
||||
{formatMoney(item.price_cent)} · {item.category_name || '未分类'}
|
||||
</div>
|
||||
<Tag color={item.stock < 0 ? 'success' : 'primary'}>
|
||||
{item.stock < 0 ? '不限量' : `库存 ${item.stock}`}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<string, { label: string; color: 'primary' | 'success' | 'warning' | 'danger' | 'default' }> = {
|
||||
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 (
|
||||
<div className="mobile-wallet">
|
||||
<NavBar onBack={() => navigate('/m/crash')}>我的撞车订单</NavBar>
|
||||
|
||||
{ordersQuery.isPending ? (
|
||||
<Card>
|
||||
<Skeleton.Paragraph lineCount={4} animated />
|
||||
</Card>
|
||||
) : items.length === 0 ? (
|
||||
<ErrorBlock status="empty" title="还没有撞车订单" />
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: 10 }}>
|
||||
{items.map(order => {
|
||||
const meta = statusMeta[order.status] ?? { label: order.status, color: 'default' as const }
|
||||
return (
|
||||
<Card key={order.id}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{order.product_cover_url ? (
|
||||
<Image
|
||||
src={order.product_cover_url}
|
||||
width={72}
|
||||
height={72}
|
||||
fit="cover"
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="admin-stack" style={{ flex: 1, gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<strong>{order.product_title || order.order_no}</strong>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
</div>
|
||||
<div className="mobile-wallet__sub">{order.order_no}</div>
|
||||
<div className="mobile-wallet__sub">
|
||||
{formatMoney(order.amount_cent)} · 数量 {order.quantity}
|
||||
</div>
|
||||
<div className="mobile-wallet__sub">{formatDateTime(order.created_at)}</div>
|
||||
{order.status === 'pending_payment' ? (
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
||||
<Button
|
||||
size="mini"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const payment = await startCrashPayment(order.id)
|
||||
if (payment?.pay_url) {
|
||||
window.location.href = payment.pay_url
|
||||
} else {
|
||||
Toast.show({ content: '支付已发起,请在支付渠道完成付款' })
|
||||
}
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
Toast.show({ content: errorText(error) })
|
||||
}
|
||||
}}
|
||||
>
|
||||
去支付
|
||||
</Button>
|
||||
<Button
|
||||
size="mini"
|
||||
onClick={async () => {
|
||||
const confirmed = await Dialog.confirm({
|
||||
content: '确认取消该订单?',
|
||||
confirmText: '取消订单',
|
||||
})
|
||||
if (!confirmed) return
|
||||
try {
|
||||
await cancelCrashOrder(order.id)
|
||||
Toast.show({ icon: 'success', content: '订单已取消' })
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
Toast.show({ content: errorText(error) })
|
||||
}
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!user ? <div className="mobile-wallet__hint">登录后可查看自己的撞车订单。</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user