diff --git a/package.json b/package.json index 9be9c9f..596c9ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ec36ca..928647d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/app/distribution/orders/member-orders-client.tsx b/src/app/distribution/orders/member-orders-client.tsx new file mode 100644 index 0000000..740b328 --- /dev/null +++ b/src/app/distribution/orders/member-orders-client.tsx @@ -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([]) + 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 ( +
+
+
+

正在加载成员订单...

+
+
+ ) + } + + return ( +
+
+
+ +
+

成员订单

+

{memberName || '未知成员'}(ID: {memberId || '未提供'})

+
+
+
+ +
+ + + 订单统计 + + +
+
+
+ + 总佣金 +
+

¥{totalCommission.toFixed(1)}

+
+
+
+ + 订单数 +
+

{memberOrders.length}

+
+
+
+ + 总金额 +
+

¥{totalAmount.toFixed(1)}

+
+
+
+
+ + + + 订单列表 + + +
+ {memberOrders.map((order, index) => ( +
+
+ + {order.productName.slice(0,1)} + + +
+

{order.productName}

+
+ + {order.time} +
+
+
+
+ {order.status === 'completed' ? '已完成' : '待确认'} +

¥{order.amount}

+

佣金 ¥{order.commission}

+
+
+ ))} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/distribution/orders/page.tsx b/src/app/distribution/orders/page.tsx index 4a01fb5..4f1a76e 100644 --- a/src/app/distribution/orders/page.tsx +++ b/src/app/distribution/orders/page.tsx @@ -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([]) - 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 ( -
-
-
-

加载中...

-
-
- ) - } - return ( -
- {/* 优化后的头部区域 - 与主页面风格一致 */} -
- {/* 背景装饰 */} -
-
-
- -
- {/* 导航和用户信息 */} -
- -
- - - - {memberName?.charAt(0)} - - -
-

{memberName}

-

共 {memberOrders.length} 笔订单

-
-
-
- - {/* 统计概览卡片 */} -
-
-
-
- -
-

总佣金

-

¥{totalCommission.toFixed(1)}

-
-
-
- -
-

已完成

-

{completedOrders}

-
-
-
- -
-

总金额

-

¥{(totalAmount/1000).toFixed(1)}k

-
-
-
-
-
- -
- {/* 订单列表卡片 */} - - - - - 订单明细 - - - -
- {memberOrders.map((order, index) => ( -
-
-
- - {order.status === 'completed' ? '已完成' : '待确认'} - - #{order.id} -
-
- +¥{order.commission} -

佣金

-
-
- -

{order.productName}

- -
-
- - 订单金额: ¥{order.amount} - -
-
- - {order.time} -
-
-
- ))} -
-
-
- - {/* 空状态优化 */} - {memberOrders.length === 0 && ( - - -
- -
-

暂无订单记录

-

该成员还没有产生任何订单

-
-
- )} -
-
+ 页面加载中...}> + + ) -} \ No newline at end of file +} + +// 该页面需要动态渲染以避免静态预渲染时的 CSR Bailout 报错 +export const dynamic = 'force-dynamic' \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1e92ddc..29b53fd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - +
{children}
diff --git a/src/app/order/payment/page.tsx b/src/app/order/payment/page.tsx new file mode 100644 index 0000000..686712a --- /dev/null +++ b/src/app/order/payment/page.tsx @@ -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(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 ( +
+ + + 订单支付 + + + {order ? ( + <> + {/* 订单摘要 */} +
+ {order.product.image ? ( + // eslint-disable-next-line @next/next/no-img-element + {order.product.name} + ) : ( +
+ )} +
+
{order.product.name}
+
订单号:{order.orderId}
+
+
+
应付金额
+
¥{order.total.toFixed(2)}
+
+
+ + + + {/* 支付方式 Tabs */} + setMethod(val as any)} className="w-full"> + + 支付宝 + 微信支付 + 银行卡 + + + +
+
+
{qrText}
+ {/* 使用占位符生成二维码图片区域 */} + {/* eslint-disable-next-line @next/next/no-img-element */} + alipay-qrcode +
+
+

1. 打开支付宝,点击“扫一扫”。

+

2. 对准二维码完成支付。

+

3. 支付完成后点击下方“确认支付”。

+
+
+
+ + +
+
+
{qrText}
+ {/* eslint-disable-next-line @next/next/no-img-element */} + wechat-qrcode +
+
+

1. 打开微信,点击“扫一扫”。

+

2. 对准二维码完成支付。

+

3. 支付完成后点击下方“确认支付”。

+
+
+
+ + +
+
+ + setCardNumber(e.target.value)} /> +
+
+ + setCardName(e.target.value)} /> +
+
+ + setCardCvv(e.target.value)} /> +
+
提示:这是模拟支付,无需填写真实信息。
+
+
+
+ +
+ + +
+ + ) : ( +
未找到订单,请返回商品页重新下单。
+ )} + + +
+ ) +} \ No newline at end of file diff --git a/src/app/order/success/page.tsx b/src/app/order/success/page.tsx new file mode 100644 index 0000000..421bd55 --- /dev/null +++ b/src/app/order/success/page.tsx @@ -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(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 ( +
+ + + 下单成功 + + + {order ? ( + <> + {order.paid ? ( +
支付成功 {order.paymentTime ? `· ${new Date(order.paymentTime).toLocaleString()}` : ''}
+ ) : ( +
订单已创建,尚未支付
+ )} +
+ {order.product.image ? ( + // eslint-disable-next-line @next/next/no-img-element + {order.product.name} + ) : ( +
+ )} +
+
{order.product.name}
+
订单号:{order.orderId}
+
+
+
数量
+
{order.quantity}
+
+
+ + + +
+
+
收货人
+
{order.recipient.name}
+
{order.recipient.phone}
+
{order.recipient.address}
+
+
+
支付方式
+
{order.payment}
+
配送方式:{order.shipping}
+
+
+ + + +
+
+ 商品小计 + ¥{order.subtotal.toFixed(2)} +
+
+ 运费 + ¥{order.shippingFee.toFixed(2)} +
+
+ 实付金额 + ¥{order.total.toFixed(2)} +
+
+ + ) : ( +
未找到订单摘要,但下单已成功。
+ )} + +
+ + +
+ + +
+ ) +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 5c6af44..fef88c6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 (
+ {/* 首页快捷购买对话框 */} + {selectedProduct && ( + + )} {/* 优化后的顶部导航栏 */}
diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx index 6c2fe9a..48bc4be 100644 --- a/src/app/product/[id]/page.tsx +++ b/src/app/product/[id]/page.tsx @@ -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 (
+ {/* 购买流程对话框 */} + {product && ( + + )} {/* 顶部导航 */}
diff --git a/src/components/purchase-dialog.tsx b/src/components/purchase-dialog.tsx new file mode 100644 index 0000000..93e568b --- /dev/null +++ b/src/components/purchase-dialog.tsx @@ -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([]) + const [selectedAddressId, setSelectedAddressId] = useState("") + + // 配送与支付 + 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 ( + + + + 确认购买 + 请填写收货信息并选择支付方式,确认后为您创建订单。 + + + {/* 商品摘要 */} +
+ {product.image ? ( + // eslint-disable-next-line @next/next/no-img-element + {product.name} + ) : ( +
+ )} +
+
{product.name}
+
单价 ¥{product.price.toFixed(2)} · 数量 {quantity}
+
+
+
小计
+
¥{subtotal.toFixed(2)}
+
+
+ + + + {/* 收货信息 */} +
+
+ + setRecipientName(e.target.value)} /> +
+
+ + setPhone(e.target.value)} /> +
+
+ + setAddress(e.target.value)} /> +
+ {savedAddresses.length > 0 && ( +
+ + + {savedAddresses.map((addr) => ( + + ))} + +
+ )} +
+ +
+
+ + + + {/* 配送方式 */} +
+
配送方式
+ setShipping(val as "standard" | "express")} + className="grid grid-cols-2 gap-3"> + + + +
+ + {/* 支付方式 */} +
+
支付方式
+ setPayment(val as "alipay" | "wechat" | "card")} + className="grid grid-cols-3 gap-3"> + + + + +
+ + + + {/* 金额汇总 */} +
+
+ 商品小计 + ¥{subtotal.toFixed(2)} +
+
+ 运费 + ¥{shippingFee.toFixed(2)} +
+
+ 应付总额 + ¥{total.toFixed(2)} +
+
+ + + + + + +
+ ) +} \ No newline at end of file diff --git a/说明文档.md b/说明文档.md index 56cbfe3..0dc9e78 100644 --- a/说明文档.md +++ b/说明文档.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` 改为服务端页面,引入 `` 包裹新的客户端子组件 `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` 的服务端包装与 `` 客户端子组件结构。 + - 验证: + - `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/网关。