Compare commits
1 Commits
main
...
rollback_c
| Author | SHA1 | Date | |
|---|---|---|---|
| e7b2de181b |
@ -3,9 +3,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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",
|
"build": "next build",
|
||||||
"start": "NODE_ENV=production tsx server.ts 2>&1 | tee server.log",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
|
|||||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@ -116,6 +116,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.3
|
specifier: ^8.21.3
|
||||||
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
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:
|
axios:
|
||||||
specifier: ^1.10.0
|
specifier: ^1.10.0
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
@ -135,7 +138,7 @@ importers:
|
|||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.2.0)
|
version: 8.6.0(react@19.2.0)
|
||||||
framer-motion:
|
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)
|
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
input-otp:
|
input-otp:
|
||||||
specifier: ^1.4.2
|
specifier: ^1.4.2
|
||||||
@ -2190,6 +2193,13 @@ packages:
|
|||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
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:
|
available-typed-arrays@1.0.7:
|
||||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2218,6 +2228,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||||
engines: {node: ^4.5.0 || >= 5.9}
|
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:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2232,6 +2246,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
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:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
@ -2266,6 +2285,9 @@ packages:
|
|||||||
caniuse-lite@1.0.30001749:
|
caniuse-lite@1.0.30001749:
|
||||||
resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==}
|
resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==}
|
||||||
|
|
||||||
|
caniuse-lite@1.0.30001753:
|
||||||
|
resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==}
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
@ -2563,6 +2585,9 @@ packages:
|
|||||||
effect@3.16.12:
|
effect@3.16.12:
|
||||||
resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
|
resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==}
|
||||||
|
|
||||||
|
electron-to-chromium@1.5.244:
|
||||||
|
resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==}
|
||||||
|
|
||||||
embla-carousel-react@8.6.0:
|
embla-carousel-react@8.6.0:
|
||||||
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2646,6 +2671,10 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
escalade@3.2.0:
|
||||||
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
escape-carriage@1.3.1:
|
escape-carriage@1.3.1:
|
||||||
resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==}
|
resolution: {integrity: sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==}
|
||||||
|
|
||||||
@ -2881,6 +2910,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
engines: {node: '>=0.4.x'}
|
||||||
|
|
||||||
|
fraction.js@4.3.7:
|
||||||
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
framer-motion@12.23.24:
|
framer-motion@12.23.24:
|
||||||
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3655,6 +3687,9 @@ packages:
|
|||||||
node-fetch-native@1.6.7:
|
node-fetch-native@1.6.7:
|
||||||
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
|
||||||
|
|
||||||
|
node-releases@2.0.27:
|
||||||
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
nodemon@3.1.10:
|
nodemon@3.1.10:
|
||||||
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3664,6 +3699,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
nypm@0.6.2:
|
||||||
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
|
resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
|
||||||
engines: {node: ^14.16.0 || >=16.10.0}
|
engines: {node: ^14.16.0 || >=16.10.0}
|
||||||
@ -3782,6 +3821,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
postcss-value-parser@4.2.0:
|
||||||
|
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@ -4334,6 +4376,12 @@ packages:
|
|||||||
unrs-resolver@1.11.1:
|
unrs-resolver@1.11.1:
|
||||||
resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
|
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:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
@ -6727,6 +6775,16 @@ snapshots:
|
|||||||
|
|
||||||
asynckit@0.4.0: {}
|
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:
|
available-typed-arrays@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
possible-typed-array-names: 1.1.0
|
possible-typed-array-names: 1.1.0
|
||||||
@ -6751,6 +6809,8 @@ snapshots:
|
|||||||
|
|
||||||
base64id@2.0.0: {}
|
base64id@2.0.0: {}
|
||||||
|
|
||||||
|
baseline-browser-mapping@2.8.23: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
brace-expansion@1.1.12:
|
brace-expansion@1.1.12:
|
||||||
@ -6766,6 +6826,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
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:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@ -6811,6 +6879,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001749: {}
|
caniuse-lite@1.0.30001749: {}
|
||||||
|
|
||||||
|
caniuse-lite@1.0.30001753: {}
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
@ -7090,6 +7160,8 @@ snapshots:
|
|||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
fast-check: 3.23.2
|
fast-check: 3.23.2
|
||||||
|
|
||||||
|
electron-to-chromium@1.5.244: {}
|
||||||
|
|
||||||
embla-carousel-react@8.6.0(react@19.2.0):
|
embla-carousel-react@8.6.0(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
embla-carousel: 8.6.0
|
embla-carousel: 8.6.0
|
||||||
@ -7289,6 +7361,8 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.25.10
|
'@esbuild/win32-ia32': 0.25.10
|
||||||
'@esbuild/win32-x64': 0.25.10
|
'@esbuild/win32-x64': 0.25.10
|
||||||
|
|
||||||
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-carriage@1.3.1: {}
|
escape-carriage@1.3.1: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
@ -7601,6 +7675,8 @@ snapshots:
|
|||||||
|
|
||||||
format@0.2.2: {}
|
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):
|
framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 12.23.23
|
motion-dom: 12.23.23
|
||||||
@ -8604,6 +8680,8 @@ snapshots:
|
|||||||
|
|
||||||
node-fetch-native@1.6.7: {}
|
node-fetch-native@1.6.7: {}
|
||||||
|
|
||||||
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
nodemon@3.1.10:
|
nodemon@3.1.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
@ -8619,6 +8697,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
|
normalize-range@0.1.2: {}
|
||||||
|
|
||||||
nypm@0.6.2:
|
nypm@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
@ -8756,6 +8836,8 @@ snapshots:
|
|||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
|
postcss-value-parser@4.2.0: {}
|
||||||
|
|
||||||
postcss@8.4.31:
|
postcss@8.4.31:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
@ -9475,6 +9557,12 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
|
||||||
'@unrs/resolver-binding-win32-x64-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:
|
uri-js@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
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 { Suspense } from 'react'
|
||||||
|
import MemberOrdersClient from './member-orders-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'
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MemberOrdersPage - 成员订单页面(服务端组件)
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供 Suspense 边界承载客户端子组件,解决 useSearchParams 在预渲染期间的 CSR Bailout 报错。
|
||||||
|
* - 页面本身不依赖浏览器 API,允许参与服务端渲染,以提升构建稳定性。
|
||||||
|
*
|
||||||
|
* 参数:无
|
||||||
|
* 返回值:React 组件节点 - 页面内容,包含 Suspense 的占位与客户端组件。
|
||||||
|
*/
|
||||||
export default function MemberOrdersPage() {
|
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-gray-600">页面加载中...</div>}>
|
||||||
{/* 优化后的头部区域 - 与主页面风格一致 */}
|
<MemberOrdersClient />
|
||||||
<div className="bg-gradient-to-br from-purple-600 via-purple-700 to-pink-600 relative overflow-hidden">
|
</Suspense>
|
||||||
{/* 背景装饰 */}
|
|
||||||
<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 type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import BottomNavigation from "@/components/bottom-navigation";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Z.ai Code Scaffold - AI-Powered Development",
|
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.",
|
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<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
/**
|
||||||
|
* RootLayout - 应用根布局组件
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供全局 HTML 结构与主体样式。
|
||||||
|
* - 渲染底部导航与全局通知容器。
|
||||||
|
*
|
||||||
|
* 参数:
|
||||||
|
* - children: React.ReactNode - 子页面/组件的内容。
|
||||||
|
* 返回值:
|
||||||
|
* - React 组件节点 - 应用的根布局结构。
|
||||||
|
*/
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body className={`antialiased bg-background text-foreground pb-16`}>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground pb-16`}
|
|
||||||
>
|
|
||||||
<div className="min-h-screen pb-16">
|
<div className="min-h-screen pb-16">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</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
|
Clock
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { showSuccessToast, showErrorToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
import { showSuccessToast, showErrorToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
||||||
|
import { PurchaseDialog } from '@/components/purchase-dialog'
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -32,6 +33,9 @@ export default function HomePage() {
|
|||||||
const [activeTab, setActiveTab] = useState<'products' | 'groups'>('products')
|
const [activeTab, setActiveTab] = useState<'products' | 'groups'>('products')
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
@ -178,13 +182,33 @@ export default function HomePage() {
|
|||||||
showShareToast()
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50">
|
<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>
|
<FadeIn>
|
||||||
<div className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200/50 shadow-sm">
|
<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
|
ChevronRight
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { showSuccessToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
import { showSuccessToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
|
||||||
|
import { PurchaseDialog } from '@/components/purchase-dialog'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
id: string
|
id: string
|
||||||
@ -68,6 +69,7 @@ export default function ProductDetailPage() {
|
|||||||
const [quantity, setQuantity] = useState(1)
|
const [quantity, setQuantity] = useState(1)
|
||||||
const [isLiked, setIsLiked] = useState(false)
|
const [isLiked, setIsLiked] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [purchaseOpen, setPurchaseOpen] = useState(false)
|
||||||
|
|
||||||
// 模拟商品数据
|
// 模拟商品数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -164,32 +166,12 @@ export default function ProductDetailPage() {
|
|||||||
|
|
||||||
const handleBuyNow = () => {
|
const handleBuyNow = () => {
|
||||||
if (!product) return
|
if (!product) return
|
||||||
|
|
||||||
// 检查库存
|
|
||||||
if (quantity > product.stock) {
|
if (quantity > product.stock) {
|
||||||
showSuccessToast('库存不足,请减少购买数量')
|
showSuccessToast('库存不足,请减少购买数量')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 打开购买对话框
|
||||||
// 模拟立即购买的逻辑
|
setPurchaseOpen(true)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const increaseQuantity = () => {
|
const increaseQuantity = () => {
|
||||||
@ -266,6 +248,15 @@ export default function ProductDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<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="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">
|
<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