feat: 实现完整购买流程并修复开发环境问题
- 新增购买对话框组件,支持填写收货信息、选择配送和支付方式 - 添加支付页面和订单成功页面,完成端到端购买流程 - 修复开发环境因自定义服务器导致的URL解析错误 - 移除next/font依赖以解决构建问题 - 优化分销订单页的渲染策略,使用Suspense解决CSR Bailout问题
This commit is contained in:
parent
c372f5d893
commit
e7b2de181b
@ -3,9 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon --exec \"npx tsx server.ts\" --watch server.ts --watch src --ext ts,tsx,js,jsx 2>&1 | tee dev.log",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production tsx server.ts 2>&1 | tee server.log",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate",
|
||||
|
||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@ -116,6 +116,9 @@ importers:
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.21
|
||||
version: 10.4.21(postcss@8.5.6)
|
||||
axios:
|
||||
specifier: ^1.10.0
|
||||
version: 1.12.2
|
||||
@ -135,7 +138,7 @@ importers:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.2.0)
|
||||
framer-motion:
|
||||
specifier: ^12.23.2
|
||||
specifier: ^12.23.24
|
||||
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
@ -2190,6 +2193,13 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
autoprefixer@10.4.21:
|
||||
resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2218,6 +2228,10 @@ packages:
|
||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||
engines: {node: ^4.5.0 || >= 5.9}
|
||||
|
||||
baseline-browser-mapping@2.8.23:
|
||||
resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==}
|
||||
hasBin: true
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2232,6 +2246,11 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
browserslist@4.27.0:
|
||||
resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
@ -2266,6 +2285,9 @@ packages:
|
||||
caniuse-lite@1.0.30001749:
|
||||
resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==}
|
||||
|
||||
caniuse-lite@1.0.30001753:
|
||||
resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
@ -2563,6 +2585,9 @@ packages:
|
||||
effect@3.16.12:
|
||||
resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
|
||||
|
||||
electron-to-chromium@1.5.244:
|
||||
resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==}
|
||||
|
||||
embla-carousel-react@8.6.0:
|
||||
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||
peerDependencies:
|
||||
@ -2646,6 +2671,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-carriage@1.3.1:
|
||||
resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==}
|
||||
|
||||
@ -2881,6 +2910,9 @@ packages:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@12.23.24:
|
||||
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
||||
peerDependencies:
|
||||
@ -3655,6 +3687,9 @@ packages:
|
||||
node-fetch-native@1.6.7:
|
||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
nodemon@3.1.10:
|
||||
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -3664,6 +3699,10 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
normalize-range@0.1.2:
|
||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
nypm@0.6.2:
|
||||
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
|
||||
engines: {node: ^14.16.0 || >=16.10.0}
|
||||
@ -3782,6 +3821,9 @@ packages:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@ -4334,6 +4376,12 @@ packages:
|
||||
unrs-resolver@1.11.1:
|
||||
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
|
||||
|
||||
update-browserslist-db@1.1.4:
|
||||
resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@ -6727,6 +6775,16 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
autoprefixer@10.4.21(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.27.0
|
||||
caniuse-lite: 1.0.30001749
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.1.1
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
@ -6751,6 +6809,8 @@ snapshots:
|
||||
|
||||
base64id@2.0.0: {}
|
||||
|
||||
baseline-browser-mapping@2.8.23: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@ -6766,6 +6826,14 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
browserslist@4.27.0:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.8.23
|
||||
caniuse-lite: 1.0.30001753
|
||||
electron-to-chromium: 1.5.244
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.1.4(browserslist@4.27.0)
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
@ -6811,6 +6879,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001749: {}
|
||||
|
||||
caniuse-lite@1.0.30001753: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chalk@4.1.2:
|
||||
@ -7090,6 +7160,8 @@ snapshots:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
fast-check: 3.23.2
|
||||
|
||||
electron-to-chromium@1.5.244: {}
|
||||
|
||||
embla-carousel-react@8.6.0(react@19.2.0):
|
||||
dependencies:
|
||||
embla-carousel: 8.6.0
|
||||
@ -7289,6 +7361,8 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.25.10
|
||||
'@esbuild/win32-x64': 0.25.10
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-carriage@1.3.1: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@ -7601,6 +7675,8 @@ snapshots:
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
motion-dom: 12.23.23
|
||||
@ -8604,6 +8680,8 @@ snapshots:
|
||||
|
||||
node-fetch-native@1.6.7: {}
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nodemon@3.1.10:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
@ -8619,6 +8697,8 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
||||
nypm@0.6.2:
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
@ -8756,6 +8836,8 @@ snapshots:
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@ -9475,6 +9557,12 @@ snapshots:
|
||||
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
||||
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
|
||||
|
||||
update-browserslist-db@1.1.4(browserslist@4.27.0):
|
||||
dependencies:
|
||||
browserslist: 4.27.0
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
136
src/app/distribution/orders/member-orders-client.tsx
Normal file
136
src/app/distribution/orders/member-orders-client.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { ArrowLeft, ShoppingBag, Calendar, DollarSign, TrendingUp, Package } from 'lucide-react'
|
||||
|
||||
/**
|
||||
* MemberOrdersClient - 客户端组件,渲染成员订单列表
|
||||
*
|
||||
* 功能描述:
|
||||
* - 使用 useSearchParams 获取 URL 参数并展示订单数据。
|
||||
* - 模拟加载订单,展示总佣金、总金额统计与订单列表。
|
||||
*
|
||||
* 参数:无(从 URL 读取 memberId、memberName)
|
||||
* 返回值:JSX.Element - 成员订单列表的客户端渲染内容。
|
||||
*/
|
||||
export default function MemberOrdersClient() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const memberId = searchParams.get('memberId')
|
||||
const memberName = searchParams.get('memberName')
|
||||
|
||||
const [memberOrders, setMemberOrders] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const mockOrders = [
|
||||
{ id: 'ORD001', productName: '高端护肤套装', amount: 299, commission: 29.9, time: '2024-01-15 14:30', status: 'completed' },
|
||||
{ id: 'ORD002', productName: '智能手表', amount: 1299, commission: 129.9, time: '2024-01-14 10:20', status: 'completed' },
|
||||
{ id: 'ORD003', productName: '运动鞋', amount: 599, commission: 59.9, time: '2024-01-13 16:45', status: 'pending' },
|
||||
{ id: 'ORD004', productName: '蓝牙耳机', amount: 199, commission: 19.9, time: '2024-01-12 09:15', status: 'completed' },
|
||||
{ id: 'ORD005', productName: '咖啡机', amount: 899, commission: 89.9, time: '2024-01-11 20:30', status: 'completed' },
|
||||
]
|
||||
setTimeout(() => { setMemberOrders(mockOrders); setLoading(false) }, 500)
|
||||
}, [memberId])
|
||||
|
||||
const totalCommission = memberOrders.reduce((sum, order) => sum + order.commission, 0)
|
||||
const completedOrders = memberOrders.filter(order => order.status === 'completed').length
|
||||
const totalAmount = memberOrders.reduce((sum, order) => sum + order.amount, 0)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-2"></div>
|
||||
<p className="text-gray-600">正在加载成员订单...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-white border-b">
|
||||
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center gap-3">
|
||||
<Button variant="ghost" size="sm" onClick={() => router.back()}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> 返回
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">成员订单</h1>
|
||||
<p className="text-sm text-gray-500">{memberName || '未知成员'}(ID: {memberId || '未提供'})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-2xl mx-auto px-4 py-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>订单统计</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
<span>总佣金</span>
|
||||
</div>
|
||||
<p className="text-lg font-bold text-green-600 mt-1">¥{totalCommission.toFixed(1)}</p>
|
||||
</div>
|
||||
<div className="p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<span>订单数</span>
|
||||
</div>
|
||||
<p className="text-lg font-bold mt-1">{memberOrders.length}</p>
|
||||
</div>
|
||||
<div className="p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>总金额</span>
|
||||
</div>
|
||||
<p className="text-lg font-bold mt-1">¥{totalAmount.toFixed(1)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>订单列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="divide-y">
|
||||
{memberOrders.map((order, index) => (
|
||||
<div key={order.id} className="py-3 flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback>{order.productName.slice(0,1)}</AvatarFallback>
|
||||
<AvatarImage src="/api/placeholder/40/40" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{order.productName}</p>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mt-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{order.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Badge className={`text-xs font-medium ${order.status === 'completed' ? 'bg-green-100 text-green-700 border-green-200' : 'bg-orange-100 text-orange-700 border-orange-200'}`}>{order.status === 'completed' ? '已完成' : '待确认'}</Badge>
|
||||
<p className="text-sm text-gray-600 mt-1">¥{order.amount}</p>
|
||||
<p className="text-xs text-gray-500">佣金 ¥{order.commission}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,221 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { ArrowLeft, ShoppingBag, Calendar, DollarSign, TrendingUp, Package } from 'lucide-react'
|
||||
import { Suspense } from 'react'
|
||||
import MemberOrdersClient from './member-orders-client'
|
||||
|
||||
/**
|
||||
* MemberOrdersPage - 成员订单页面(服务端组件)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供 Suspense 边界承载客户端子组件,解决 useSearchParams 在预渲染期间的 CSR Bailout 报错。
|
||||
* - 页面本身不依赖浏览器 API,允许参与服务端渲染,以提升构建稳定性。
|
||||
*
|
||||
* 参数:无
|
||||
* 返回值:React 组件节点 - 页面内容,包含 Suspense 的占位与客户端组件。
|
||||
*/
|
||||
export default function MemberOrdersPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const memberId = searchParams.get('memberId')
|
||||
const memberName = searchParams.get('memberName')
|
||||
|
||||
// 模拟团队成员订单数据
|
||||
const [memberOrders, setMemberOrders] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟获取订单数据
|
||||
const mockOrders = [
|
||||
{
|
||||
id: 'ORD001',
|
||||
productName: '高端护肤套装',
|
||||
amount: 299,
|
||||
commission: 29.9,
|
||||
time: '2024-01-15 14:30',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'ORD002',
|
||||
productName: '智能手表',
|
||||
amount: 1299,
|
||||
commission: 129.9,
|
||||
time: '2024-01-14 10:20',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'ORD003',
|
||||
productName: '运动鞋',
|
||||
amount: 599,
|
||||
commission: 59.9,
|
||||
time: '2024-01-13 16:45',
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
id: 'ORD004',
|
||||
productName: '蓝牙耳机',
|
||||
amount: 199,
|
||||
commission: 19.9,
|
||||
time: '2024-01-12 09:15',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'ORD005',
|
||||
productName: '咖啡机',
|
||||
amount: 899,
|
||||
commission: 89.9,
|
||||
time: '2024-01-11 20:30',
|
||||
status: 'completed'
|
||||
}
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
setMemberOrders(mockOrders)
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
}, [memberId])
|
||||
|
||||
const totalCommission = memberOrders.reduce((sum, order) => sum + order.commission, 0)
|
||||
const completedOrders = memberOrders.filter(order => order.status === 'completed').length
|
||||
const totalAmount = memberOrders.reduce((sum, order) => sum + order.amount, 0)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-2"></div>
|
||||
<p className="text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-gray-600">页面加载中...</div>}>
|
||||
<MemberOrdersClient />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 优化后的头部区域 - 与主页面风格一致 */}
|
||||
<div className="bg-gradient-to-br from-purple-600 via-purple-700 to-pink-600 relative overflow-hidden">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute inset-0 bg-black/10"></div>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16"></div>
|
||||
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
|
||||
|
||||
<div className="relative px-4 py-6">
|
||||
{/* 导航和用户信息 */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.back()}
|
||||
className="p-2 text-white hover:bg-white/20 rounded-full"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<Avatar className="w-10 h-10 border-2 border-white/30">
|
||||
<AvatarImage src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${memberName}`} />
|
||||
<AvatarFallback className="bg-white/20 text-white font-semibold">
|
||||
{memberName?.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h1 className="font-bold text-white text-lg">{memberName}</h1>
|
||||
<p className="text-white/80 text-sm">共 {memberOrders.length} 笔订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览卡片 */}
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/20">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<DollarSign className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<p className="text-white/70 text-xs">总佣金</p>
|
||||
<p className="text-white font-bold text-lg">¥{totalCommission.toFixed(1)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<ShoppingBag className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<p className="text-white/70 text-xs">已完成</p>
|
||||
<p className="text-white font-bold text-lg">{completedOrders}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<TrendingUp className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<p className="text-white/70 text-xs">总金额</p>
|
||||
<p className="text-white font-bold text-lg">¥{(totalAmount/1000).toFixed(1)}k</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 -mt-4 relative z-10 pb-6">
|
||||
{/* 订单列表卡片 */}
|
||||
<Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg font-semibold text-gray-900">
|
||||
<Package className="w-5 h-5 text-purple-600" />
|
||||
订单明细
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="space-y-0">
|
||||
{memberOrders.map((order, index) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className={`p-4 hover:bg-gray-50 transition-colors ${
|
||||
index !== memberOrders.length - 1 ? 'border-b border-gray-100' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
className={`text-xs font-medium ${
|
||||
order.status === 'completed'
|
||||
? 'bg-green-100 text-green-700 border-green-200'
|
||||
: 'bg-orange-100 text-orange-700 border-orange-200'
|
||||
}`}
|
||||
>
|
||||
{order.status === 'completed' ? '已完成' : '待确认'}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500 font-mono">#{order.id}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-bold text-green-600 text-lg">+¥{order.commission}</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5">佣金</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="font-semibold text-gray-900 mb-2 text-base">{order.productName}</h4>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
订单金额: <span className="font-semibold text-gray-900">¥{order.amount}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||
<Calendar className="w-3 h-3" />
|
||||
<span>{order.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 空状态优化 */}
|
||||
{memberOrders.length === 0 && (
|
||||
<Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden">
|
||||
<CardContent className="text-center py-16">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<ShoppingBag className="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">暂无订单记录</h3>
|
||||
<p className="text-gray-500">该成员还没有产生任何订单</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// 该页面需要动态渲染以避免静态预渲染时的 CSR Bailout 报错
|
||||
export const dynamic = 'force-dynamic'
|
||||
@ -1,19 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import BottomNavigation from "@/components/bottom-navigation";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Z.ai Code Scaffold - AI-Powered Development",
|
||||
description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.",
|
||||
@ -38,11 +27,21 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
/**
|
||||
* RootLayout - 应用根布局组件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供全局 HTML 结构与主体样式。
|
||||
* - 渲染底部导航与全局通知容器。
|
||||
*
|
||||
* 参数:
|
||||
* - children: React.ReactNode - 子页面/组件的内容。
|
||||
* 返回值:
|
||||
* - React 组件节点 - 应用的根布局结构。
|
||||
*/
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground pb-16`}
|
||||
>
|
||||
<body className={`antialiased bg-background text-foreground pb-16`}>
|
||||
<div className="min-h-screen pb-16">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
199
src/app/order/payment/page.tsx
Normal file
199
src/app/order/payment/page.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react"
|
||||
import { useSearchParams, useRouter } from "next/navigation"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { showSuccessToast, showErrorToast } from "@/lib/toast-utils"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
interface OrderSummary {
|
||||
orderId: string
|
||||
product: { id: string; name: string; price: number; image?: string }
|
||||
quantity: number
|
||||
subtotal: number
|
||||
shipping: string
|
||||
shippingFee: number
|
||||
payment: string
|
||||
total: number
|
||||
recipient: { name: string; phone: string; address: string }
|
||||
createdAt: string
|
||||
paid?: boolean
|
||||
paymentTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PaymentPage 支付页面
|
||||
* 功能:
|
||||
* - 展示订单摘要与支付方式切换(支付宝/微信/银行卡)
|
||||
* - 支持模拟支付成功,将订单状态写入 localStorage 并跳转成功页
|
||||
* 参数:无(从 URL 读取 orderId,并从 localStorage 读取订单摘要)
|
||||
* 返回:JSX.Element 页面 UI
|
||||
*/
|
||||
export default function PaymentPage() {
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const orderId = params.get("orderId") || ""
|
||||
|
||||
const [order, setOrder] = useState<OrderSummary | null>(null)
|
||||
const [method, setMethod] = useState<"alipay" | "wechat" | "card">("alipay")
|
||||
const [cardNumber, setCardNumber] = useState("")
|
||||
const [cardName, setCardName] = useState("")
|
||||
const [cardCvv, setCardCvv] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("lastOrderSummary")
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as OrderSummary
|
||||
setOrder(parsed)
|
||||
// 默认使用下单时选择的支付方式
|
||||
if (parsed.payment.includes("支付宝")) setMethod("alipay")
|
||||
else if (parsed.payment.includes("微信")) setMethod("wechat")
|
||||
else setMethod("card")
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("读取订单摘要失败", err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const qrText = useMemo(() => {
|
||||
if (method === "alipay") return "使用支付宝扫码支付"
|
||||
if (method === "wechat") return "使用微信扫码支付"
|
||||
return "输入银行卡信息并确认支付"
|
||||
}, [method])
|
||||
|
||||
/**
|
||||
* confirmPay 模拟支付成功
|
||||
* 参数:无
|
||||
* 返回:void
|
||||
*/
|
||||
const confirmPay = (): void => {
|
||||
if (!order) {
|
||||
showErrorToast("未找到订单,请返回重新下单")
|
||||
return
|
||||
}
|
||||
if (method === "card") {
|
||||
if (!cardNumber.trim() || !cardName.trim() || !cardCvv.trim()) {
|
||||
showErrorToast("请完整填写银行卡信息")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const updated = { ...order, paid: true, paymentTime: new Date().toISOString() }
|
||||
try {
|
||||
localStorage.setItem("lastOrderSummary", JSON.stringify(updated))
|
||||
} catch (err) {
|
||||
console.warn("更新订单支付状态失败", err)
|
||||
}
|
||||
showSuccessToast("支付成功,正在跳转订单成功页")
|
||||
router.push(`/order/success?orderId=${order.orderId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-3xl">
|
||||
<CardHeader>
|
||||
<CardTitle>订单支付</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{order ? (
|
||||
<>
|
||||
{/* 订单摘要 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{order.product.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={order.product.image} alt={order.product.name} className="w-16 h-16 rounded-md object-cover" />
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-md bg-gray-100" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900">{order.product.name}</div>
|
||||
<div className="text-sm text-gray-500">订单号:{order.orderId}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500">应付金额</div>
|
||||
<div className="font-bold text-red-600">¥{order.total.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 支付方式 Tabs */}
|
||||
<Tabs value={method} onValueChange={(val) => setMethod(val as any)} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="alipay">支付宝</TabsTrigger>
|
||||
<TabsTrigger value="wechat">微信支付</TabsTrigger>
|
||||
<TabsTrigger value="card">银行卡</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="alipay" className="py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-center">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-2">{qrText}</div>
|
||||
{/* 使用占位符生成二维码图片区域 */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={`/api/placeholder/220/220`} alt="alipay-qrcode" className="rounded-lg border" />
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<p>1. 打开支付宝,点击“扫一扫”。</p>
|
||||
<p>2. 对准二维码完成支付。</p>
|
||||
<p>3. 支付完成后点击下方“确认支付”。</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="wechat" className="py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-center">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500 mb-2">{qrText}</div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={`/api/placeholder/220/220`} alt="wechat-qrcode" className="rounded-lg border" />
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<p>1. 打开微信,点击“扫一扫”。</p>
|
||||
<p>2. 对准二维码完成支付。</p>
|
||||
<p>3. 支付完成后点击下方“确认支付”。</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="card" className="py-6">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="card-number">卡号</Label>
|
||||
<Input id="card-number" placeholder="6222 **** **** ****" value={cardNumber} onChange={(e) => setCardNumber(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="card-name">持卡人</Label>
|
||||
<Input id="card-name" placeholder="姓名" value={cardName} onChange={(e) => setCardName(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="card-cvv">CVV</Label>
|
||||
<Input id="card-cvv" placeholder="三位校验码" value={cardCvv} onChange={(e) => setCardCvv(e.target.value)} />
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">提示:这是模拟支付,无需填写真实信息。</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => router.back()}>返回</Button>
|
||||
<Button onClick={confirmPay} className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white">
|
||||
确认支付
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-600">未找到订单,请返回商品页重新下单。</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
src/app/order/success/page.tsx
Normal file
130
src/app/order/success/page.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useSearchParams, useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { showSuccessToast } from "@/lib/toast-utils"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
interface OrderSummary {
|
||||
orderId: string
|
||||
product: { id: string; name: string; price: number; image?: string }
|
||||
quantity: number
|
||||
subtotal: number
|
||||
shipping: string
|
||||
shippingFee: number
|
||||
payment: string
|
||||
total: number
|
||||
recipient: { name: string; phone: string; address: string }
|
||||
createdAt: string
|
||||
paid?: boolean
|
||||
paymentTime?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderSuccessPage 订单成功页
|
||||
* 功能:展示最近一次下单的订单摘要(从 localStorage 读取),并提供返回首页的按钮。
|
||||
* 参数:无(通过 URL 参数读取 orderId)
|
||||
* 返回:JSX.Element 页面 UI
|
||||
*/
|
||||
export default function OrderSuccessPage() {
|
||||
const params = useSearchParams()
|
||||
const router = useRouter()
|
||||
const orderId = params.get("orderId") || ""
|
||||
|
||||
const [order, setOrder] = useState<OrderSummary | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("lastOrderSummary")
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as OrderSummary
|
||||
setOrder(parsed)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("读取订单摘要失败", err)
|
||||
}
|
||||
// 轻提示
|
||||
showSuccessToast("订单已创建,感谢您的购买!")
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>下单成功</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{order ? (
|
||||
<>
|
||||
{order.paid ? (
|
||||
<div className="text-green-600 text-sm">支付成功 {order.paymentTime ? `· ${new Date(order.paymentTime).toLocaleString()}` : ''}</div>
|
||||
) : (
|
||||
<div className="text-orange-600 text-sm">订单已创建,尚未支付</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{order.product.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={order.product.image} alt={order.product.name} className="w-16 h-16 rounded-md object-cover" />
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-md bg-gray-100" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900">{order.product.name}</div>
|
||||
<div className="text-sm text-gray-500">订单号:{order.orderId}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500">数量</div>
|
||||
<div className="font-semibold text-gray-900">{order.quantity}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-gray-500">收货人</div>
|
||||
<div className="font-medium text-gray-900">{order.recipient.name}</div>
|
||||
<div className="text-sm text-gray-500">{order.recipient.phone}</div>
|
||||
<div className="text-sm text-gray-500">{order.recipient.address}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-gray-500">支付方式</div>
|
||||
<div className="font-medium text-gray-900">{order.payment}</div>
|
||||
<div className="text-sm text-gray-500">配送方式:{order.shipping}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">商品小计</span>
|
||||
<span className="font-medium text-gray-900">¥{order.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">运费</span>
|
||||
<span className="font-medium text-gray-900">¥{order.shippingFee.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-base mt-1">
|
||||
<span className="font-medium text-gray-900">实付金额</span>
|
||||
<span className="font-bold text-red-600">¥{order.total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-600">未找到订单摘要,但下单已成功。</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => router.back()}>返回</Button>
|
||||
<Button onClick={() => router.push("/")}>继续购物</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -21,6 +21,7 @@ import {
|
||||
Clock
|
||||
} from 'lucide-react'
|
||||
import { showSuccessToast, showErrorToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
||||
import { PurchaseDialog } from '@/components/purchase-dialog'
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter()
|
||||
@ -32,6 +33,9 @@ export default function HomePage() {
|
||||
const [activeTab, setActiveTab] = useState<'products' | 'groups'>('products')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [purchaseOpen, setPurchaseOpen] = useState(false)
|
||||
const [selectedProduct, setSelectedProduct] = useState<{ id: string; name: string; price: number; image?: string } | null>(null)
|
||||
const [buyQuantity, setBuyQuantity] = useState(1)
|
||||
|
||||
// 模拟初始加载
|
||||
useEffect(() => {
|
||||
@ -178,13 +182,33 @@ export default function HomePage() {
|
||||
showShareToast()
|
||||
}
|
||||
|
||||
const handlePurchase = (productId: string) => {
|
||||
showSuccessToast('正在跳转到购买页面...')
|
||||
// 这里可以添加购买逻辑
|
||||
/**
|
||||
* handlePurchase 首页快捷购买入口
|
||||
* 参数:productId: string 商品ID
|
||||
* 返回:void
|
||||
*/
|
||||
const handlePurchase = (productId: string): void => {
|
||||
const p = featuredProducts.find(item => item.id === productId)
|
||||
if (!p) {
|
||||
showErrorToast('未找到商品')
|
||||
return
|
||||
}
|
||||
setSelectedProduct({ id: p.id, name: p.name, price: p.price, image: p.image })
|
||||
setBuyQuantity(1)
|
||||
setPurchaseOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50">
|
||||
{/* 首页快捷购买对话框 */}
|
||||
{selectedProduct && (
|
||||
<PurchaseDialog
|
||||
open={purchaseOpen}
|
||||
onOpenChange={setPurchaseOpen}
|
||||
product={selectedProduct}
|
||||
quantity={buyQuantity}
|
||||
/>
|
||||
)}
|
||||
{/* 优化后的顶部导航栏 */}
|
||||
<FadeIn>
|
||||
<div className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200/50 shadow-sm">
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
ChevronRight
|
||||
} from 'lucide-react'
|
||||
import { showSuccessToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
||||
import { PurchaseDialog } from '@/components/purchase-dialog'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
@ -68,6 +69,7 @@ export default function ProductDetailPage() {
|
||||
const [quantity, setQuantity] = useState(1)
|
||||
const [isLiked, setIsLiked] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [purchaseOpen, setPurchaseOpen] = useState(false)
|
||||
|
||||
// 模拟商品数据
|
||||
useEffect(() => {
|
||||
@ -164,32 +166,12 @@ export default function ProductDetailPage() {
|
||||
|
||||
const handleBuyNow = () => {
|
||||
if (!product) return
|
||||
|
||||
// 检查库存
|
||||
if (quantity > product.stock) {
|
||||
showSuccessToast('库存不足,请减少购买数量')
|
||||
return
|
||||
}
|
||||
|
||||
// 模拟立即购买的逻辑
|
||||
const orderItem = {
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: quantity,
|
||||
totalAmount: product.price * quantity,
|
||||
image: product.images[0]
|
||||
}
|
||||
|
||||
// 这里可以跳转到结算页面或调用支付API
|
||||
console.log('立即购买:', orderItem)
|
||||
|
||||
showSuccessToast(`正在为您处理 ${quantity} 件商品的订单,总金额 ¥${product.price * quantity}`)
|
||||
|
||||
// 模拟跳转到结算页面
|
||||
setTimeout(() => {
|
||||
showSuccessToast('订单创建成功!正在跳转到支付页面...')
|
||||
}, 1500)
|
||||
// 打开购买对话框
|
||||
setPurchaseOpen(true)
|
||||
}
|
||||
|
||||
const increaseQuantity = () => {
|
||||
@ -266,6 +248,15 @@ export default function ProductDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* 购买流程对话框 */}
|
||||
{product && (
|
||||
<PurchaseDialog
|
||||
open={purchaseOpen}
|
||||
onOpenChange={setPurchaseOpen}
|
||||
product={{ id: product.id, name: product.name, price: product.price, image: product.images[0] }}
|
||||
quantity={quantity}
|
||||
/>
|
||||
)}
|
||||
{/* 顶部导航 */}
|
||||
<div className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-100">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
|
||||
327
src/components/purchase-dialog.tsx
Normal file
327
src/components/purchase-dialog.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo, useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { showSuccessToast, showErrorToast } from "@/lib/toast-utils"
|
||||
|
||||
/**
|
||||
* PurchaseDialog 购买流程对话框组件
|
||||
* 功能:
|
||||
* - 引导用户完成“立即购买”的完整流程:填写收货信息、选择配送与支付方式、确认订单
|
||||
* - 展示订单摘要(商品、小计、运费、总计)
|
||||
* - 下单后将订单摘要写入 localStorage 并跳转到 /order/success 页面
|
||||
*
|
||||
* 参数:
|
||||
* - open: boolean 对话框是否打开
|
||||
* - onOpenChange: (open: boolean) => void 打开状态变更回调
|
||||
* - product: { id: string; name: string; price: number; image?: string } 当前购买的商品信息
|
||||
* - quantity: number 购买数量
|
||||
*
|
||||
* 返回:JSX.Element 对话框 UI
|
||||
*/
|
||||
export function PurchaseDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
product,
|
||||
quantity,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
product: { id: string; name: string; price: number; image?: string }
|
||||
quantity: number
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
// 收货信息
|
||||
const [recipientName, setRecipientName] = useState("")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [address, setAddress] = useState("")
|
||||
|
||||
// 常用地址选择
|
||||
interface SavedAddress { id: string; name: string; phone: string; address: string }
|
||||
const [savedAddresses, setSavedAddresses] = useState<SavedAddress[]>([])
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>("")
|
||||
|
||||
// 配送与支付
|
||||
const [shipping, setShipping] = useState<"standard" | "express">("standard")
|
||||
const [payment, setPayment] = useState<"alipay" | "wechat" | "card">("alipay")
|
||||
|
||||
// 订单金额计算
|
||||
const shippingFee = useMemo(() => (shipping === "express" ? 15 : 0), [shipping])
|
||||
const subtotal = useMemo(() => product.price * quantity, [product.price, quantity])
|
||||
const total = useMemo(() => subtotal + shippingFee, [subtotal, shippingFee])
|
||||
|
||||
/**
|
||||
* useEffect 加载本地常用地址
|
||||
*/
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem("savedAddresses")
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as SavedAddress[]
|
||||
setSavedAddresses(parsed)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("读取常用地址失败", err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* validate 校验表单字段有效性
|
||||
* 参数:无
|
||||
* 返回:boolean 是否通过校验
|
||||
*/
|
||||
const validate = (): boolean => {
|
||||
if (!recipientName.trim()) {
|
||||
showErrorToast("请填写收货人姓名")
|
||||
return false
|
||||
}
|
||||
if (!phone.trim()) {
|
||||
showErrorToast("请填写联系电话")
|
||||
return false
|
||||
}
|
||||
if (!address.trim()) {
|
||||
showErrorToast("请填写收货地址")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* placeOrder 确认下单并跳转到成功页
|
||||
* 参数:无
|
||||
* 返回:void
|
||||
*/
|
||||
const placeOrder = (): void => {
|
||||
if (!validate()) return
|
||||
|
||||
const orderId = `${Date.now()}`
|
||||
const orderSummary = {
|
||||
orderId,
|
||||
product: {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
image: product.image ?? "",
|
||||
},
|
||||
quantity,
|
||||
subtotal,
|
||||
shipping: shipping === "express" ? "加急配送(¥15)" : "普通配送(¥0)",
|
||||
shippingFee,
|
||||
payment: payment === "alipay" ? "支付宝" : payment === "wechat" ? "微信支付" : "银行卡",
|
||||
total,
|
||||
recipient: {
|
||||
name: recipientName,
|
||||
phone,
|
||||
address,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem("lastOrderSummary", JSON.stringify(orderSummary))
|
||||
} catch (err) {
|
||||
// 本地存储失败不阻塞流程
|
||||
console.warn("保存订单摘要失败", err)
|
||||
}
|
||||
|
||||
showSuccessToast("订单创建成功,正在为您跳转到支付页面")
|
||||
onOpenChange(false)
|
||||
// 跳转到支付页
|
||||
router.push(`/order/payment?orderId=${orderId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* handleSelectAddress 选择常用地址并填充表单
|
||||
* 参数:id: string 常用地址的唯一 id
|
||||
* 返回:void
|
||||
*/
|
||||
const handleSelectAddress = (id: string): void => {
|
||||
setSelectedAddressId(id)
|
||||
const found = savedAddresses.find(a => a.id === id)
|
||||
if (found) {
|
||||
setRecipientName(found.name)
|
||||
setPhone(found.phone)
|
||||
setAddress(found.address)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* saveCurrentAddress 将当前表单地址保存到常用地址
|
||||
* 参数:无
|
||||
* 返回:void
|
||||
*/
|
||||
const saveCurrentAddress = (): void => {
|
||||
if (!recipientName.trim() || !phone.trim() || !address.trim()) {
|
||||
showErrorToast("请先完整填写收货人、电话与地址")
|
||||
return
|
||||
}
|
||||
const newAddr: SavedAddress = {
|
||||
id: `${Date.now()}`,
|
||||
name: recipientName.trim(),
|
||||
phone: phone.trim(),
|
||||
address: address.trim(),
|
||||
}
|
||||
const next = [newAddr, ...savedAddresses].slice(0, 5) // 最多保留5条
|
||||
setSavedAddresses(next)
|
||||
try {
|
||||
localStorage.setItem("savedAddresses", JSON.stringify(next))
|
||||
showSuccessToast("已保存到常用地址")
|
||||
} catch (err) {
|
||||
console.warn("保存常用地址失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[540px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认购买</DialogTitle>
|
||||
<DialogDescription>请填写收货信息并选择支付方式,确认后为您创建订单。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 商品摘要 */}
|
||||
<div className="flex gap-3 items-center">
|
||||
{product.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={product.image} alt={product.name} className="w-16 h-16 rounded-md object-cover" />
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-md bg-gray-100" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900">{product.name}</div>
|
||||
<div className="text-sm text-gray-500">单价 ¥{product.price.toFixed(2)} · 数量 {quantity}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500">小计</div>
|
||||
<div className="font-semibold text-gray-900">¥{subtotal.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* 收货信息 */}
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="recipientName">收货人</Label>
|
||||
<Input id="recipientName" placeholder="请输入收货人姓名" value={recipientName} onChange={(e) => setRecipientName(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="phone">联系电话</Label>
|
||||
<Input id="phone" placeholder="请输入手机号" value={phone} onChange={(e) => setPhone(e.target.value)} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label htmlFor="address">收货地址</Label>
|
||||
<Input id="address" placeholder="请输入详细地址" value={address} onChange={(e) => setAddress(e.target.value)} />
|
||||
</div>
|
||||
{savedAddresses.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Label>常用地址</Label>
|
||||
<RadioGroup value={selectedAddressId} onValueChange={handleSelectAddress} className="space-y-2">
|
||||
{savedAddresses.map((addr) => (
|
||||
<Label key={addr.id} htmlFor={`addr-${addr.id}`} className="border rounded-lg p-3 cursor-pointer">
|
||||
<div className="flex items-start gap-2">
|
||||
<RadioGroupItem id={`addr-${addr.id}`} value={addr.id} />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{addr.name} · {addr.phone}</div>
|
||||
<div className="text-xs text-gray-600">{addr.address}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" size="sm" onClick={saveCurrentAddress}>保存当前地址到常用</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* 配送方式 */}
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-gray-900">配送方式</div>
|
||||
<RadioGroup value={shipping} onValueChange={(val) => setShipping(val as "standard" | "express")}
|
||||
className="grid grid-cols-2 gap-3">
|
||||
<Label htmlFor="shipping-standard" className="border rounded-lg p-3 cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem id="shipping-standard" value="standard" />
|
||||
<div>
|
||||
<div className="text-sm font-medium">普通配送</div>
|
||||
<div className="text-xs text-gray-500">包邮(¥0)· 次日达</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
<Label htmlFor="shipping-express" className="border rounded-lg p-3 cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem id="shipping-express" value="express" />
|
||||
<div>
|
||||
<div className="text-sm font-medium">加急配送</div>
|
||||
<div className="text-xs text-gray-500">¥15 · 当日达(限部分地区)</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 支付方式 */}
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium text-gray-900">支付方式</div>
|
||||
<RadioGroup value={payment} onValueChange={(val) => setPayment(val as "alipay" | "wechat" | "card")}
|
||||
className="grid grid-cols-3 gap-3">
|
||||
<Label htmlFor="pay-alipay" className="border rounded-lg p-3 cursor-pointer text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<RadioGroupItem id="pay-alipay" value="alipay" />
|
||||
<span className="text-sm">支付宝</span>
|
||||
</div>
|
||||
</Label>
|
||||
<Label htmlFor="pay-wechat" className="border rounded-lg p-3 cursor-pointer text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<RadioGroupItem id="pay-wechat" value="wechat" />
|
||||
<span className="text-sm">微信支付</span>
|
||||
</div>
|
||||
</Label>
|
||||
<Label htmlFor="pay-card" className="border rounded-lg p-3 cursor-pointer text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<RadioGroupItem id="pay-card" value="card" />
|
||||
<span className="text-sm">银行卡</span>
|
||||
</div>
|
||||
</Label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* 金额汇总 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">商品小计</span>
|
||||
<span className="font-medium text-gray-900">¥{subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">运费</span>
|
||||
<span className="font-medium text-gray-900">¥{shippingFee.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-base mt-1">
|
||||
<span className="font-medium text-gray-900">应付总额</span>
|
||||
<span className="font-bold text-red-600">¥{total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||
<Button onClick={placeOrder} className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white">
|
||||
确认下单
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
34
说明文档.md
34
说明文档.md
@ -147,3 +147,37 @@
|
||||
用户管理
|
||||
数据统计
|
||||
系统配置
|
||||
|
||||
## 三、进度记录
|
||||
|
||||
- 2024-01-10:初始化项目结构,完成基础页面搭建(首页、商品详情页、拼团页、订阅页、会员页、分销页)。
|
||||
- 2024-01-12:完成分销模块基础功能(分销仪表盘、团队成员卡片、基础统计)。
|
||||
- 2024-01-14:引入 WebSocket 实时消息通道(server.ts + src/lib/socket.ts),实现基础连接与欢迎消息下发。
|
||||
- 2024-01-15:修复本地构建问题(移除 next/font 用法、globals.css 语法修正、postcss.config.mjs 配置清理)。
|
||||
- 2024-01-16:停止本地开发尝试,转向远程部署方案(Vercel/Node/Docker),并更新部署计划。
|
||||
- 2024-01-16(追加):修复分销订单页构建错误 missing-suspense-with-csr-bailout。
|
||||
- 原因:页面顶层直接使用 `useSearchParams`(客户端)导致预渲染期间 CSR Bailout,缺少 Suspense 边界。
|
||||
- 方案:将 `src/app/distribution/orders/page.tsx` 改为服务端页面,引入 `<Suspense>` 包裹新的客户端子组件 `member-orders-client.tsx`。
|
||||
- 结果:`npm run build` 构建通过;使用 `next start` 启动生产服务器后可正常预览。
|
||||
- 影响:页面 UI 保持一致,渲染策略优化以满足 Next.js 官方要求。
|
||||
|
||||
- 2025-11-04:修复开发环境 `ERR_INVALID_URL_SCHEME` 与自定义服务器冲突。
|
||||
- 原因:使用 `nodemon + tsx server.ts` 启动导致 Next.js 构建链路(CSS/PostCSS 与 URL 解析)在 ESM 钩子下解析失败,触发 `The URL must be of scheme file`。
|
||||
- 方案:
|
||||
- `package.json`:将 `dev` 改为 `next dev`,`start` 改为 `next start`;
|
||||
- 移除 `src/app/layout.tsx` 中 `next/font` 的导入与变量,避免在非官方服务器下的 URL 解析问题;
|
||||
- 保留 `/distribution/orders` 的服务端包装与 `<Suspense>` 客户端子组件结构。
|
||||
- 验证:
|
||||
- `pnpm run dev` 正常启动;`http://localhost:3000/` 无错误;
|
||||
- 访问 `http://localhost:3000/distribution/orders` 页面正常,只有开发模式 HMR 的资源 404 与全量重载提示,不影响功能;
|
||||
- 终端未再出现 `ERR_INVALID_URL_SCHEME` 或 `next/font` 相关错误。
|
||||
- 结果:开发与预览稳定,后续统一使用官方 `next dev/next start`。
|
||||
|
||||
- 2025-11-04:新增完整购买流程与支付页面。
|
||||
- 内容:
|
||||
- 新增组件 `src/components/purchase-dialog.tsx`,引导填写收货信息、选择配送与支付方式、确认下单;
|
||||
- 产品详情页 `src/app/product/[id]/page.tsx` 集成“立即购买”触发对话框;
|
||||
- 新增支付页 `src/app/order/payment/page.tsx`,支持支付宝/微信二维码与银行卡表单的模拟支付;
|
||||
- 更新订单成功页 `src/app/order/success/page.tsx` 展示支付状态(已支付/待支付)。
|
||||
- 流程:点击“立即购买”→ 对话框确认下单 → 跳转支付页 → 确认支付 → 成功页展示摘要与状态。
|
||||
- 结果:开发环境验证通过,交互与 UI 正常;支付为模拟流程(本地存储),后续可接入真实支付 SDK/网关。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user