feat: 实现完整购买流程并修复开发环境问题

- 新增购买对话框组件,支持填写收货信息、选择配送和支付方式
- 添加支付页面和订单成功页面,完成端到端购买流程
- 修复开发环境因自定义服务器导致的URL解析错误
- 移除next/font依赖以解决构建问题
- 优化分销订单页的渲染策略,使用Suspense解决CSR Bailout问题
This commit is contained in:
邹方成 2025-11-04 22:21:31 +08:00
parent c372f5d893
commit e7b2de181b
11 changed files with 989 additions and 259 deletions

View File

@ -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
View File

@ -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

View 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 memberIdmemberName
* 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>
)
}

View File

@ -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'

View File

@ -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>

View 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>
)
}

View 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>
)
}

View File

@ -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">

View File

@ -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">

View 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>
)
}

View File

@ -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/网关。