first commit

This commit is contained in:
邹方成 2025-11-03 21:45:23 +08:00
parent dfae1b91e4
commit c372f5d893
20 changed files with 2730 additions and 795 deletions

159
package-lock.json generated
View File

@ -44,6 +44,7 @@
"@reactuses/core": "^6.0.5", "@reactuses/core": "^6.0.5",
"@tanstack/react-query": "^5.82.0", "@tanstack/react-query": "^5.82.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"autoprefixer": "^10.4.21",
"axios": "^1.10.0", "axios": "^1.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -5657,6 +5658,42 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"browserslist": "^4.24.4",
"caniuse-lite": "^1.0.30001702",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -5750,6 +5787,14 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/baseline-browser-mapping": {
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -5787,6 +5832,38 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browserslist": {
"version": "4.26.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
"electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@ -5938,9 +6015,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001739", "version": "1.0.30001750",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
"integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -5954,8 +6031,7 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ]
"license": "CC-BY-4.0"
}, },
"node_modules/ccount": { "node_modules/ccount": {
"version": "2.0.1", "version": "2.0.1",
@ -6756,6 +6832,11 @@
"fast-check": "^3.23.1" "fast-check": "^3.23.1"
} }
}, },
"node_modules/electron-to-chromium": {
"version": "1.5.234",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz",
"integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg=="
},
"node_modules/embla-carousel": { "node_modules/embla-carousel": {
"version": "8.6.0", "version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
@ -7144,6 +7225,14 @@
"@esbuild/win32-x64": "0.25.9" "@esbuild/win32-x64": "0.25.9"
} }
}, },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-carriage": { "node_modules/escape-carriage": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz", "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
@ -7904,6 +7993,18 @@
"node": ">=0.4.x" "node": ">=0.4.x"
} }
}, },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "12.23.24", "version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
@ -10917,6 +11018,11 @@
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-releases": {
"version": "2.0.23",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@ -10979,6 +11085,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nypm": { "node_modules/nypm": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
@ -11360,7 +11474,6 @@
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -11385,6 +11498,11 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/preact": { "node_modules/preact": {
"version": "10.27.1", "version": "10.27.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz",
@ -13397,6 +13515,35 @@
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1" "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
} }
}, },
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/uri-js": { "node_modules/uri-js": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -49,6 +49,7 @@
"@reactuses/core": "^6.0.5", "@reactuses/core": "^6.0.5",
"@tanstack/react-query": "^5.82.0", "@tanstack/react-query": "^5.82.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"autoprefixer": "^10.4.21",
"axios": "^1.10.0", "axios": "^1.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

137
src/app/create/page.tsx Normal file
View File

@ -0,0 +1,137 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Plus,
ArrowRight,
Droplets,
Package,
Sparkles
} from 'lucide-react'
import BottomNavigation from '@/components/bottom-navigation'
import { FadeIn, SlideIn, StaggerContainer, StaggerItem } from '@/components/animations'
export default function CreatePage() {
const [selectedType, setSelectedType] = useState<string | null>(null)
const createOptions = [
{
id: 'perfume-group',
title: '香水团购',
description: '发起香水团购活动,人数越多折扣越大',
icon: Droplets,
color: 'bg-pink-500',
gradient: 'from-pink-500 to-rose-600',
features: ['阶梯折扣', '正品保证', '快速成团']
},
{
id: 'perfume-split',
title: '香水分装',
description: '发起香水分装服务,小容量更实惠',
icon: Package,
color: 'bg-indigo-500',
gradient: 'from-indigo-500 to-purple-600',
features: ['小容量', '超值价格', '多款选择']
}
]
const handleCreateClick = (type: string) => {
setSelectedType(type)
// 根据类型跳转到不同的创建页面
if (type === 'perfume-group') {
// 跳转到香水团购创建页面
window.location.href = '/create/perfume-group'
} else if (type === 'perfume-split') {
// 跳转到香水分装创建页面
window.location.href = '/create/perfume-split'
} else {
console.log(`创建 ${type}`)
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 pb-20">
{/* 优化后的顶部标题 */}
<FadeIn>
<div className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200/50 shadow-sm">
<div className="px-4 py-6">
<SlideIn direction="left">
<div className="flex items-center gap-3">
<Sparkles className="w-6 h-6 text-purple-600" />
<div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
</h1>
<p className="text-gray-600 text-sm mt-1"></p>
</div>
</div>
</SlideIn>
</div>
</div>
</FadeIn>
{/* 创建选项 */}
<div className="px-4 py-8">
<StaggerContainer className="space-y-6">
{createOptions.map((option, index) => {
const IconComponent = option.icon
return (
<StaggerItem key={option.id}>
<div
className={`bg-white/70 backdrop-blur-sm rounded-3xl p-6 shadow-lg border border-white/50 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] ${
selectedType === option.id ? 'ring-2 ring-purple-400 ring-opacity-50 shadow-purple-200/50' : ''
}`}
onClick={() => setSelectedType(option.id)}
>
<div className="flex items-start gap-5">
{/* 优化后的图标 */}
<div className={`w-16 h-16 rounded-2xl bg-gradient-to-r ${option.gradient} flex items-center justify-center flex-shrink-0 shadow-lg`}>
<IconComponent className="w-8 h-8 text-white" />
</div>
{/* 内容 */}
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-900 mb-2">{option.title}</h3>
<p className="text-gray-600 text-sm mb-4 leading-relaxed">{option.description}</p>
{/* 特性标签 */}
<div className="flex flex-wrap gap-2 mb-6">
{option.features.map((feature, index) => (
<Badge
key={index}
variant="outline"
className="text-xs bg-white/50 border-gray-200 text-gray-700 hover:bg-purple-50 hover:border-purple-200 transition-colors"
>
{feature}
</Badge>
))}
</div>
{/* 创建按钮 */}
<Button
className={`w-full bg-gradient-to-r ${option.gradient} hover:opacity-90 text-white shadow-lg hover:shadow-xl transition-all duration-300 rounded-2xl py-3 text-base font-semibold`}
onClick={(e) => {
e.stopPropagation()
handleCreateClick(option.id)
}}
>
<Plus className="w-5 h-5 mr-2" />
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
</div>
</div>
</div>
</StaggerItem>
)
})}
</StaggerContainer>
</div>
{/* 底部导航 */}
<BottomNavigation />
</div>
)
}

View File

@ -0,0 +1,190 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ProductSelector } from '@/components/product-selector'
import {
ArrowLeft,
Droplets,
Users,
Clock,
Calendar
} from 'lucide-react'
interface Product {
id: string
name: string
brand: string
price: number
originalPrice: number
image: string
category: string
tags: string[]
rating: number
reviews: number
stock: number
description: string
volume?: string
}
export default function CreatePerfumeGroupPage() {
const router = useRouter()
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
const [formData, setFormData] = useState({
duration: '7', // 团购持续天数
minParticipants: '3',
maxParticipants: '50'
})
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleSubmit = () => {
// 这里处理表单提交逻辑
console.log('创建香水团购:', { selectedProduct, formData })
// 可以跳转到团购详情页或返回列表页
router.push('/create')
}
return (
<div className="min-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-gradient-to-br from-pink-500 via-pink-600 to-rose-600 relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10 px-4 py-6">
<div className="flex items-center gap-3 mb-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="text-white hover:bg-white/20 p-2"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<h1 className="text-xl font-bold text-white"></h1>
<p className="text-pink-100 text-sm"></p>
</div>
</div>
</div>
</div>
{/* 表单内容 */}
<div className="px-4 py-6 space-y-6 max-w-2xl mx-auto">
{/* 商品选择 */}
<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">
<Droplets className="w-5 h-5 text-pink-600" />
</CardTitle>
</CardHeader>
<CardContent>
<ProductSelector
selectedProduct={selectedProduct}
onProductSelect={setSelectedProduct}
/>
</CardContent>
</Card>
{/* 团购设置 */}
<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">
<Users className="w-5 h-5 text-blue-600" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div>
<Label htmlFor="duration" className="text-sm font-medium text-gray-700"></Label>
<Input
id="duration"
type="number"
value={formData.duration}
onChange={(e) => handleInputChange('duration', e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="minParticipants" className="text-sm font-medium text-gray-700"></Label>
<Input
id="minParticipants"
type="number"
value={formData.minParticipants}
onChange={(e) => handleInputChange('minParticipants', e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="maxParticipants" className="text-sm font-medium text-gray-700"></Label>
<Input
id="maxParticipants"
type="number"
value={formData.maxParticipants}
onChange={(e) => handleInputChange('maxParticipants', e.target.value)}
className="mt-1"
/>
</div>
</div>
</CardContent>
</Card>
{/* 商品预览 */}
{selectedProduct && (
<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">
<Calendar className="w-5 h-5 text-pink-600" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-gradient-to-br from-pink-50 via-white to-rose-50 rounded-xl p-4 border border-pink-100">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-pink-100 rounded-lg flex items-center justify-center">
<Droplets className="w-8 h-8 text-pink-600" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{selectedProduct.name}</h3>
<p className="text-sm text-gray-600">{selectedProduct.brand}</p>
<div className="mt-2 flex items-center gap-4">
<div className="text-lg font-bold text-pink-600">¥{selectedProduct.price}</div>
<div className="text-sm text-gray-500">
{formData.minParticipants}-{formData.maxParticipants}
</div>
</div>
</div>
</div>
<div className="mt-4 p-3 bg-white rounded-lg border border-pink-100">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600"></span>
<span className="font-medium">{formData.duration}</span>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* 创建按钮 */}
<div className="pt-4">
<Button
onClick={handleSubmit}
className="w-full bg-gradient-to-r from-pink-500 to-rose-600 hover:opacity-90 text-white py-3 text-lg font-semibold"
disabled={!selectedProduct}
>
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,289 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { ProductSelector } from '@/components/product-selector'
import {
ArrowLeft,
Package,
Droplets,
Plus,
Minus,
Beaker,
Clock
} from 'lucide-react'
interface SplitOption {
volume: number // ml
price: number
quantity: number // 可分装数量
}
interface Product {
id: string
name: string
brand: string
price: number
originalPrice: number
image: string
category: string
tags: string[]
rating: number
reviews: number
stock: number
description: string
volume?: string
}
export default function CreatePerfumeSplitPage() {
const router = useRouter()
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
const [formData, setFormData] = useState({
originalVolume: '100', // 原装容量 ml
duration: '14', // 分装活动持续天数
})
const [splitOptions, setSplitOptions] = useState<SplitOption[]>([
{ volume: 5, price: 0, quantity: 10 },
{ volume: 10, price: 0, quantity: 8 },
{ volume: 20, price: 0, quantity: 4 }
])
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const addSplitOption = () => {
const lastOption = splitOptions[splitOptions.length - 1]
setSplitOptions(prev => [...prev, {
volume: lastOption.volume + 5,
price: 0,
quantity: 1
}])
}
const removeSplitOption = (index: number) => {
if (splitOptions.length > 1) {
setSplitOptions(prev => prev.filter((_, i) => i !== index))
}
}
const updateSplitOption = (index: number, field: keyof SplitOption, value: number) => {
setSplitOptions(prev => prev.map((option, i) =>
i === index ? { ...option, [field]: value } : option
))
}
const handleSubmit = () => {
console.log('创建香水分装:', { selectedProduct, formData, splitOptions })
router.push('/create')
}
return (
<div className="min-h-screen bg-gray-50">
{/* 头部 */}
<div className="bg-gradient-to-br from-indigo-500 via-purple-600 to-pink-600 relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10 px-4 py-6">
<div className="flex items-center gap-3 mb-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="text-white hover:bg-white/20 p-2"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div>
<h1 className="text-xl font-bold text-white"></h1>
<p className="text-purple-100 text-sm"></p>
</div>
</div>
</div>
</div>
{/* 表单内容 */}
<div className="px-4 py-6 space-y-6 max-w-2xl mx-auto">
{/* 商品选择 */}
<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">
<Droplets className="w-5 h-5 text-purple-600" />
</CardTitle>
</CardHeader>
<CardContent>
<ProductSelector
selectedProduct={selectedProduct}
onProductSelect={setSelectedProduct}
/>
</CardContent>
</Card>
{/* 分装设置 */}
<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">
<Beaker className="w-5 h-5 text-indigo-600" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="originalVolume" className="text-sm font-medium text-gray-700"> (ml)</Label>
<Input
id="originalVolume"
type="number"
value={formData.originalVolume}
onChange={(e) => handleInputChange('originalVolume', e.target.value)}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="duration" className="text-sm font-medium text-gray-700"></Label>
<Input
id="duration"
type="number"
value={formData.duration}
onChange={(e) => handleInputChange('duration', e.target.value)}
className="mt-1"
/>
</div>
</div>
</CardContent>
</Card>
{/* 分装规格 */}
<Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg font-semibold text-gray-900">
<Package className="w-5 h-5 text-purple-600" />
</CardTitle>
<Button
variant="outline"
size="sm"
onClick={addSplitOption}
className="text-purple-600 border-purple-200 hover:bg-purple-50"
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">
{splitOptions.map((option, index) => (
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div className="flex-1 grid grid-cols-3 gap-3">
<div>
<Label className="text-xs text-gray-600"> (ml)</Label>
<Input
type="number"
value={option.volume}
onChange={(e) => updateSplitOption(index, 'volume', parseInt(e.target.value) || 0)}
className="mt-1 h-8"
/>
</div>
<div>
<Label className="text-xs text-gray-600"> (¥)</Label>
<Input
type="number"
value={option.price}
onChange={(e) => updateSplitOption(index, 'price', parseInt(e.target.value) || 0)}
className="mt-1 h-8"
/>
</div>
<div>
<Label className="text-xs text-gray-600"></Label>
<Input
type="number"
value={option.quantity}
onChange={(e) => updateSplitOption(index, 'quantity', parseInt(e.target.value) || 0)}
className="mt-1 h-8"
/>
</div>
</div>
{splitOptions.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => removeSplitOption(index)}
className="p-1 text-red-500 hover:bg-red-50"
>
<Minus className="w-4 h-4" />
</Button>
)}
</div>
))}
</CardContent>
</Card>
{/* 分装预览 */}
{selectedProduct && splitOptions.some(opt => opt.price > 0) && (
<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>
<div className="bg-gradient-to-br from-purple-50 via-white to-indigo-50 rounded-xl p-4 border border-purple-100">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-purple-100 rounded-lg flex items-center justify-center">
<Droplets className="w-8 h-8 text-purple-600" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{selectedProduct.name}</h3>
<p className="text-sm text-gray-600">{selectedProduct.brand}</p>
<div className="mt-1 text-sm text-gray-500">
{formData.originalVolume}ml · {formData.duration}
</div>
</div>
</div>
<div className="space-y-2">
{splitOptions.filter(opt => opt.price > 0).map((option, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-white rounded-lg border border-purple-100">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<Droplets className="w-4 h-4 text-indigo-600" />
</div>
<div>
<div className="font-medium text-gray-900">{option.volume}ml</div>
<div className="text-xs text-gray-500"> {option.quantity} </div>
</div>
</div>
<div className="text-right">
<div className="font-bold text-purple-600">¥{option.price}</div>
<div className="text-xs text-gray-500">¥{(option.price / option.volume).toFixed(1)}/ml</div>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
)}
{/* 创建按钮 */}
<div className="pt-4">
<Button
onClick={handleSubmit}
className="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:opacity-90 text-white py-3 text-lg font-semibold"
disabled={!selectedProduct}
>
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,221 @@
'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'
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 (
<div className="min-h-screen bg-gray-50">
{/* 优化后的头部区域 - 与主页面风格一致 */}
<div className="bg-gradient-to-br from-purple-600 via-purple-700 to-pink-600 relative overflow-hidden">
{/* 背景装饰 */}
<div className="absolute inset-0 bg-black/10"></div>
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
<div className="relative px-4 py-6">
{/* 导航和用户信息 */}
<div className="flex items-center gap-3 mb-4">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="p-2 text-white hover:bg-white/20 rounded-full"
>
<ArrowLeft className="w-5 h-5" />
</Button>
<div className="flex items-center gap-3 flex-1">
<Avatar className="w-10 h-10 border-2 border-white/30">
<AvatarImage src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${memberName}`} />
<AvatarFallback className="bg-white/20 text-white font-semibold">
{memberName?.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<h1 className="font-bold text-white text-lg">{memberName}</h1>
<p className="text-white/80 text-sm"> {memberOrders.length} </p>
</div>
</div>
</div>
{/* 统计概览卡片 */}
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/20">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="flex items-center justify-center gap-1 mb-1">
<DollarSign className="w-4 h-4 text-white" />
</div>
<p className="text-white/70 text-xs"></p>
<p className="text-white font-bold text-lg">¥{totalCommission.toFixed(1)}</p>
</div>
<div>
<div className="flex items-center justify-center gap-1 mb-1">
<ShoppingBag className="w-4 h-4 text-white" />
</div>
<p className="text-white/70 text-xs"></p>
<p className="text-white font-bold text-lg">{completedOrders}</p>
</div>
<div>
<div className="flex items-center justify-center gap-1 mb-1">
<TrendingUp className="w-4 h-4 text-white" />
</div>
<p className="text-white/70 text-xs"></p>
<p className="text-white font-bold text-lg">¥{(totalAmount/1000).toFixed(1)}k</p>
</div>
</div>
</div>
</div>
</div>
<div className="px-4 -mt-4 relative z-10 pb-6">
{/* 订单列表卡片 */}
<Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg font-semibold text-gray-900">
<Package className="w-5 h-5 text-purple-600" />
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="space-y-0">
{memberOrders.map((order, index) => (
<div
key={order.id}
className={`p-4 hover:bg-gray-50 transition-colors ${
index !== memberOrders.length - 1 ? 'border-b border-gray-100' : ''
}`}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<Badge
className={`text-xs font-medium ${
order.status === 'completed'
? 'bg-green-100 text-green-700 border-green-200'
: 'bg-orange-100 text-orange-700 border-orange-200'
}`}
>
{order.status === 'completed' ? '已完成' : '待确认'}
</Badge>
<span className="text-xs text-gray-500 font-mono">#{order.id}</span>
</div>
<div className="text-right">
<span className="font-bold text-green-600 text-lg">+¥{order.commission}</span>
<p className="text-xs text-gray-500 mt-0.5"></p>
</div>
</div>
<h4 className="font-semibold text-gray-900 mb-2 text-base">{order.productName}</h4>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
: <span className="font-semibold text-gray-900">¥{order.amount}</span>
</span>
</div>
<div className="flex items-center gap-1 text-sm text-gray-500">
<Calendar className="w-3 h-3" />
<span>{order.time}</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 空状态优化 */}
{memberOrders.length === 0 && (
<Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden">
<CardContent className="text-center py-16">
<div className="w-16 h-16 mx-auto mb-4 bg-purple-100 rounded-full flex items-center justify-center">
<ShoppingBag className="w-8 h-8 text-purple-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-500"></p>
</CardContent>
</Card>
)}
</div>
</div>
)
}

View File

@ -1,13 +1,14 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Separator } from '@/components/ui/separator' import BottomNavigation from '@/components/bottom-navigation'
import { import {
Users, Users,
TrendingUp, TrendingUp,
@ -20,17 +21,22 @@ import {
MessageCircle, MessageCircle,
Download, Download,
Star, Star,
Zap,
ArrowUpRight, ArrowUpRight,
Calendar, Calendar,
ShoppingBag, ShoppingBag,
Share2, Eye,
ShoppingCart, Wallet,
Home UserPlus,
BarChart3,
Sparkles,
Info,
Clock,
CreditCard
} from 'lucide-react' } from 'lucide-react'
export default function DistributionPage() { export default function DistributionPage() {
const [activeTab, setActiveTab] = useState('overview') const [activeTab, setActiveTab] = useState('overview')
const router = useRouter()
// 模拟数据 // 模拟数据
const stats = { const stats = {
@ -41,7 +47,9 @@ export default function DistributionPage() {
level: '品鉴分销员', level: '品鉴分销员',
nextLevelProgress: 65, nextLevelProgress: 65,
monthlyOrders: 45, monthlyOrders: 45,
monthlyRevenue: 8950.00 monthlyRevenue: 8950.00,
weeklyGrowth: 12.5,
conversionRate: 8.3
} }
const teamMembers = [ const teamMembers = [
@ -50,36 +58,45 @@ export default function DistributionPage() {
name: '小美', name: '小美',
avatar: '小', avatar: '小',
level: '探索分销员', level: '探索分销员',
joinTime: '2025-03-15',
totalOrders: 15, totalOrders: 15,
commission: 234.50, commission: 234.50,
status: 'active', avatarUrl: 'https://picsum.photos/40/40?random=30',
monthlyOrders: 8, growth: '+15%',
avatarUrl: '/api/placeholder/40/40' orders: [
{ id: 'ORD001', productName: '经典玫瑰香水30ml', amount: 199.00, commission: 19.90, time: '2025-04-01 10:30', status: 'completed' },
{ id: 'ORD002', productName: '薰衣草精油礼盒', amount: 299.00, commission: 29.90, time: '2025-04-02 14:20', status: 'completed' },
{ id: 'ORD003', productName: '茉莉花香水50ml', amount: 399.00, commission: 39.90, time: '2025-04-03 16:45', status: 'pending' }
]
}, },
{ {
id: '2', id: '2',
name: '香香公主', name: '香香公主',
avatar: '香', avatar: '香',
level: '品鉴分销员', level: '品鉴分销员',
joinTime: '2025-03-10',
totalOrders: 28, totalOrders: 28,
commission: 456.80, commission: 456.80,
status: 'active', avatarUrl: 'https://picsum.photos/40/40?random=31',
monthlyOrders: 12, growth: '+23%',
avatarUrl: '/api/placeholder/40/40' orders: [
{ id: 'ORD004', productName: '白茶香水礼盒装', amount: 599.00, commission: 59.90, time: '2025-04-01 09:15', status: 'completed' },
{ id: 'ORD005', productName: '柑橘调香水30ml', amount: 259.00, commission: 25.90, time: '2025-04-02 11:30', status: 'completed' },
{ id: 'ORD006', productName: '木质调香水50ml', amount: 459.00, commission: 45.90, time: '2025-04-03 13:20', status: 'completed' }
]
}, },
{ {
id: '3', id: '3',
name: '花仙子', name: '花仙子',
avatar: '花', avatar: '花',
level: '调香分销员', level: '调香分销员',
joinTime: '2025-03-01',
totalOrders: 42, totalOrders: 42,
commission: 789.20, commission: 789.20,
status: 'active', avatarUrl: 'https://picsum.photos/40/40?random=32',
monthlyOrders: 18, growth: '+8%',
avatarUrl: '/api/placeholder/40/40' orders: [
{ id: 'ORD007', productName: '限量版香水套装', amount: 899.00, commission: 89.90, time: '2025-04-01 15:45', status: 'completed' },
{ id: 'ORD008', productName: '花香调香水30ml', amount: 329.00, commission: 32.90, time: '2025-04-02 17:10', status: 'completed' },
{ id: 'ORD009', productName: '东方调香水礼盒', amount: 699.00, commission: 69.90, time: '2025-04-03 19:30', status: 'pending' }
]
} }
] ]
@ -91,7 +108,6 @@ export default function DistributionPage() {
productName: '经典玫瑰香水30ml', productName: '经典玫瑰香水30ml',
type: 'direct', type: 'direct',
amount: 19.90, amount: 19.90,
rate: 0.10,
status: 'settled', status: 'settled',
time: '2025-04-02 14:30', time: '2025-04-02 14:30',
orderAmount: 199.00 orderAmount: 199.00
@ -103,96 +119,31 @@ export default function DistributionPage() {
productName: '薰衣草精油礼盒', productName: '薰衣草精油礼盒',
type: 'indirect', type: 'indirect',
amount: 14.95, amount: 14.95,
rate: 0.05,
status: 'pending', status: 'pending',
time: '2025-04-02 13:15', time: '2025-04-02 13:15',
orderAmount: 299.00 orderAmount: 299.00
},
{
id: '3',
orderNo: 'ORD20250402003',
customerName: '王小姐',
productName: '小样体验包',
type: 'direct',
amount: 0.10,
rate: 0.10,
status: 'settled',
time: '2025-04-02 10:45',
orderAmount: 1.00
} }
] ]
const materials = [ const achievements = [
{
id: '1',
title: '宝妈话术模板',
content: '自用省50分享赚100香氛好物自用省钱分享赚钱',
category: '话术',
usage: 1250,
rating: 4.8,
image: '/api/placeholder/200/150'
},
{
id: '2',
title: '产品种草文案',
content: '这款玫瑰香水太绝了!留香一整天,闺蜜都问我要链接',
category: '文案',
usage: 890,
rating: 4.6,
image: '/api/placeholder/200/150'
},
{
id: '3',
title: '拼团分享海报',
content: '精美海报模板,一键生成分享图',
category: '海报',
usage: 2100,
rating: 4.9,
image: '/api/placeholder/200/150'
},
{
id: '4',
title: '产品介绍视频',
content: '专业拍摄的产品展示视频,适合短视频平台',
category: '视频',
usage: 650,
rating: 4.7,
image: '/api/placeholder/200/150'
}
]
const tasks = [
{ {
id: '1', id: '1',
title: '邀请3位好友', title: '邀请3位好友',
description: '成功邀请3位好友注册并完成首单', description: '成功邀请3位好友注册并完成首单',
reward: '解锁高佣金商品推广权',
progress: 2, progress: 2,
target: 3, target: 3,
status: 'in_progress', reward: '解锁高佣金商品推广权',
icon: Users, icon: UserPlus,
color: 'blue' color: 'blue'
}, },
{ {
id: '2', id: '2',
title: '月销10单', title: '月销售额达到5000元',
description: '本月完成10笔有效订单', description: '单月团队销售额突破5000元',
reward: '获得限量香氛礼品', progress: 4200,
progress: 7, target: 5000,
target: 10,
status: 'in_progress',
icon: ShoppingBag,
color: 'green'
},
{
id: '3',
title: '团队建设',
description: '团队人数达到20人',
reward: '晋升为调香分销员', reward: '晋升为调香分销员',
progress: 23, icon: Target,
target: 20,
status: 'completed',
icon: Crown,
color: 'purple' color: 'purple'
} }
] ]
@ -209,361 +160,335 @@ export default function DistributionPage() {
} }
} }
const copyMaterial = async (content: string) => {
try {
await navigator.clipboard.writeText(content)
alert('内容已复制到剪贴板')
} catch (error) {
console.error('复制失败')
}
}
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 pb-20">
{/* Header */} {/* 优化后的紧凑顶部区域 */}
<div className="bg-gradient-to-r from-purple-600 to-pink-600 text-white"> <div className="bg-gradient-to-br from-purple-600 via-purple-700 to-pink-600 relative overflow-hidden">
<div className="max-w-6xl mx-auto px-4 py-8"> {/* 背景装饰 */}
<div className="flex items-center justify-between"> <div className="absolute inset-0 bg-black/10"></div>
<div className="flex items-center gap-4"> <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="w-16 h-16 bg-white/20 backdrop-blur rounded-full flex items-center justify-center"> <div className="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
<Crown className="w-8 h-8 text-white" />
<div className="relative px-4 py-6">
{/* 紧凑的用户信息区域 */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white/20 backdrop-blur rounded-xl flex items-center justify-center">
<Crown className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<h1 className="text-3xl font-bold"></h1> <h1 className="text-xl font-bold text-white"></h1>
<p className="text-purple-100 mt-1">{stats.level} · {stats.teamSize} </p> <div className="flex items-center gap-2 mt-1">
<Badge className="bg-white/20 text-white border-white/30 text-xs">
{stats.level}
</Badge>
<span className="text-white/80 text-xs"> {stats.teamSize} </span>
</div>
</div> </div>
</div> </div>
<div className="text-right">
<p className="text-sm text-purple-100"></p>
<p className="text-3xl font-bold">¥{stats.totalCommission.toFixed(2)}</p>
<Button <Button
onClick={copyInviteCode} onClick={copyInviteCode}
className="mt-2 bg-white text-purple-600 hover:bg-purple-50" size="sm"
className="bg-white text-purple-600 hover:bg-white/90 rounded-full px-4"
> >
<Copy className="w-4 h-4 mr-2" /> <UserPlus className="w-4 h-4 mr-1" />
</Button> </Button>
</div> </div>
{/* 累计佣金展示 */}
<div className="text-center bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/20">
<p className="text-white/80 text-sm mb-1"></p>
<p className="text-3xl font-bold text-white">¥{stats.totalCommission.toFixed(2)}</p>
{/* 等级进度条 */}
<div className="mt-3">
<div className="flex items-center justify-between mb-1">
<span className="text-white/70 text-xs"></span>
<span className="text-white text-xs font-medium">{stats.nextLevelProgress}%</span>
</div>
<Progress value={stats.nextLevelProgress} className="h-1.5 bg-white/20" />
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="max-w-6xl mx-auto px-4 py-6"> <div className="px-4 -mt-4 relative z-10">
{/* 统计卡片 */} {/* 核心数据概览卡片 - 合并设计 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> <Card className="bg-white shadow-lg border-0 rounded-2xl overflow-hidden mb-4">
<Card className="bg-gradient-to-br from-green-50 to-green-100 border-green-200"> <CardHeader className="pb-3">
<CardContent className="p-6"> <CardTitle className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<div className="flex items-center justify-between"> <BarChart3 className="w-5 h-5 text-purple-600" />
<div>
<p className="text-sm text-green-600"></p> </CardTitle>
<p className="text-2xl font-bold text-green-700">¥{stats.todayCommission.toFixed(2)}</p>
<p className="text-xs text-green-600 mt-1">+12.5% </p>
</div>
<div className="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-blue-50 to-blue-100 border-blue-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-blue-600"></p>
<p className="text-2xl font-bold text-blue-700">{stats.teamSize}</p>
<p className="text-xs text-blue-600 mt-1"> {stats.activeMembers} </p>
</div>
<div className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-purple-600"></p>
<p className="text-2xl font-bold text-purple-700">{stats.monthlyOrders}</p>
<p className="text-xs text-purple-600 mt-1">¥{stats.monthlyRevenue.toFixed(0)}</p>
</div>
<div className="w-12 h-12 bg-purple-500 rounded-full flex items-center justify-center">
<ShoppingBag className="w-6 h-6 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-orange-50 to-orange-100 border-orange-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-orange-600"></p>
<p className="text-2xl font-bold text-orange-700">{stats.nextLevelProgress}%</p>
<Progress value={stats.nextLevelProgress} className="h-1 mt-2" />
</div>
<div className="w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center">
<Award className="w-6 h-6 text-white" />
</div>
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="team"></TabsTrigger>
<TabsTrigger value="materials"></TabsTrigger>
<TabsTrigger value="tasks"></TabsTrigger>
</TabsList>
{/* 数据概览 */}
<TabsContent value="overview" className="mt-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 佣金明细 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle></CardTitle>
<Button variant="outline" size="sm">
<Calendar className="w-4 h-4 mr-2" />
</Button>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="pt-0">
<div className="space-y-4"> {/* 主要指标网格 */}
{commissions.map((commission) => ( <div className="grid grid-cols-2 gap-3 mb-3">
<div key={commission.id} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"> <div className="text-center p-3 bg-green-50 rounded-xl">
<div className="flex-1"> <div className="flex items-center justify-center gap-1 mb-2">
<div className="flex items-center gap-2 mb-1"> <Wallet className="w-4 h-4 text-green-600" />
<span className="font-medium">{commission.customerName}</span> <span className="text-sm text-green-700 font-medium"></span>
<Badge variant={commission.type === 'direct' ? 'default' : 'secondary'}> </div>
{commission.type === 'direct' ? '直推' : '间推'} <p className="text-2xl font-bold text-green-600">¥{stats.todayCommission.toFixed(0)}</p>
</Badge> <Badge className="bg-green-100 text-green-700 text-xs mt-1">
<Badge variant={commission.status === 'settled' ? 'default' : 'outline'}> +{stats.weeklyGrowth}%
{commission.status === 'settled' ? '已结算' : '待结算'}
</Badge> </Badge>
</div> </div>
<p className="text-sm text-gray-600">{commission.productName}</p>
<p className="text-xs text-gray-500">{commission.orderNo} · {commission.time}</p> <div className="text-center p-3 bg-blue-50 rounded-xl">
<div className="flex items-center justify-center gap-1 mb-2">
<Users className="w-4 h-4 text-blue-600" />
<span className="text-sm text-blue-700 font-medium"></span>
</div>
<p className="text-2xl font-bold text-blue-600">{stats.teamSize}</p>
<Badge className="bg-blue-100 text-blue-700 text-xs mt-1">
{stats.activeMembers}
</Badge>
</div>
</div>
{/* 次要指标 */}
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<ShoppingBag className="w-4 h-4 text-purple-600" />
</div>
<div>
<p className="text-xs text-gray-600"></p>
<p className="text-lg font-bold text-gray-900">{stats.monthlyOrders}</p>
</div>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-orange-100 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-orange-600" />
</div>
<div>
<p className="text-xs text-gray-600"></p>
<p className="text-lg font-bold text-gray-900">¥{(stats.monthlyRevenue/1000).toFixed(1)}K</p>
</div>
</div>
</div>
</div>
{/* 转化率指标 */}
<div className="mt-3 p-3 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<Target className="w-4 h-4 text-purple-600" />
</div>
<span className="text-sm font-medium text-gray-700"></span>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-lg font-bold text-green-600">¥{commission.amount.toFixed(2)}</p> <p className="text-xl font-bold text-purple-600">{stats.conversionRate}%</p>
<p className="text-xs text-gray-500">{(commission.rate * 100).toFixed(0)}%</p> <p className="text-xs text-gray-500"></p>
<p className="text-xs text-gray-400">¥{commission.orderAmount}</p>
</div> </div>
</div> </div>
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* 业绩趋势 */} {/* 标签页内容 */}
<Card> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<CardHeader> <TabsList className="grid w-full grid-cols-3 bg-white rounded-2xl p-1 shadow-sm mb-3">
<CardTitle></CardTitle> <TabsTrigger value="overview" className="rounded-xl data-[state=active]:bg-purple-100 data-[state=active]:text-purple-700"></TabsTrigger>
<TabsTrigger value="team" className="rounded-xl data-[state=active]:bg-purple-100 data-[state=active]:text-purple-700"></TabsTrigger>
<TabsTrigger value="earnings" className="rounded-xl data-[state=active]:bg-purple-100 data-[state=active]:text-purple-700"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-0 space-y-3">
{/* 成就系统 - 优化布局 */}
<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">
<Sparkles className="w-5 h-5 text-purple-600" />
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="space-y-3">
<div className="space-y-4"> {achievements.map((achievement) => {
<div className="grid grid-cols-2 gap-4"> const IconComponent = achievement.icon
<div className="text-center p-4 bg-blue-50 rounded-lg"> const progressPercentage = (achievement.progress / achievement.target) * 100
<p className="text-sm text-blue-600"></p>
<p className="text-xl font-bold text-blue-700">¥568.00</p>
<p className="text-xs text-blue-600">+8.2%</p>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg">
<p className="text-sm text-green-600"></p>
<p className="text-xl font-bold text-green-700">5</p>
<p className="text-xs text-green-600">+25%</p>
</div>
</div>
<div className="h-48 bg-gray-100 rounded-lg flex items-center justify-center"> return (
<p className="text-gray-500"></p> <article key={achievement.id} className="p-3 bg-gray-50 rounded-xl">
<div className="flex items-start gap-3">
<div className={`w-10 h-10 bg-${achievement.color}-100 rounded-xl flex items-center justify-center flex-shrink-0`}>
<IconComponent className={`w-5 h-5 text-${achievement.color}-600`} />
</div> </div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-gray-900 mb-1 text-sm">{achievement.title}</h4>
<p className="text-xs text-gray-600 mb-2">{achievement.description}</p>
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-gray-500">
{achievement.progress} / {achievement.target}
</span>
<span className="text-xs font-medium text-gray-900">
{Math.round(progressPercentage)}%
</span>
</div> </div>
</CardContent> <Progress value={progressPercentage} className="h-1.5" />
</Card>
</div> </div>
</TabsContent>
{/* 团队管理 */}
<TabsContent value="team" className="mt-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle> ({teamMembers.length})</CardTitle>
<Button variant="outline" size="sm">
<Users className="w-4 h-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{teamMembers.map((member) => (
<div key={member.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<Avatar className="w-10 h-10">
<AvatarImage src={member.avatarUrl} />
<AvatarFallback>{member.avatar}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{member.name}</p>
<p className="text-sm text-gray-600">{member.level}</p>
</div>
</div>
<div className="text-center">
<p className="text-sm text-gray-600"></p>
<p className="font-bold">{member.totalOrders}</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-600"></p>
<p className="font-bold text-blue-600">{member.monthlyOrders}</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-600"></p>
<p className="font-bold text-green-600">¥{member.commission.toFixed(2)}</p>
</div>
<div className="text-center">
<p className="text-sm text-gray-600"></p>
<p className="text-sm">{member.joinTime}</p>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${member.status === 'active' ? 'bg-green-500' : 'bg-gray-400'}`} />
<span className="text-sm">{member.status === 'active' ? '活跃' : '离线'}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 素材库 */}
<TabsContent value="materials" className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
{materials.map((material) => (
<Card key={material.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-video bg-gray-100">
<img
src={material.image}
alt={material.title}
className="w-full h-full object-cover"
/>
</div>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold">{material.title}</h3>
<Badge variant="outline">{material.category}</Badge>
</div>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{material.content}</p>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-500" /> <Gift className="w-3 h-3 text-orange-500" />
<span className="text-sm">{material.rating}</span> <span className="text-xs text-orange-600 font-medium">{achievement.reward}</span>
</div> </div>
<span className="text-sm text-gray-500">使 {material.usage} </span>
</div> </div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="flex-1"
onClick={() => copyMaterial(material.content)}
>
<Copy className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" variant="outline" className="flex-1">
<Download className="w-4 h-4 mr-1" />
</Button>
<Button size="sm" variant="outline">
<Share2 className="w-4 h-4" />
</Button>
</div> </div>
</article>
)
})}
</CardContent> </CardContent>
</Card> </Card>
))}
</div>
</TabsContent> </TabsContent>
{/* 任务体系 */} {/* 团队标签页 - 优化布局 */}
<TabsContent value="tasks" className="mt-6"> <TabsContent value="team" className="mt-0 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <Card className="bg-white shadow-lg border-0 rounded-2xl">
{tasks.map((task) => ( <CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
<Card key={task.id} className={`relative ${task.status === 'completed' ? 'bg-green-50 border-green-200' : ''}`}> <CardTitle className="text-lg font-semibold text-gray-800"></CardTitle>
<CardContent className="p-6"> <Badge className="bg-blue-100 text-blue-700 text-sm">
<div className="flex items-center gap-3 mb-4"> {teamMembers.length}
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${ </Badge>
task.status === 'completed' ? 'bg-green-100' : `${task.color}-100` </CardHeader>
}`}> <CardContent className="pt-0">
{task.status === 'completed' ? ( <ul className="divide-y divide-gray-100" role="list">
<Award className="w-5 h-5 text-green-600" /> {teamMembers.map((member) => (
) : ( <li key={member.id} className="flex items-center gap-3 py-4">
<task.icon className={`w-5 h-5 text-${task.color}-600`} /> <Avatar className="w-12 h-12 flex-shrink-0">
)} <AvatarImage src={member.avatarUrl} />
</div> <AvatarFallback className="bg-purple-100 text-purple-600 font-semibold">
<div className="flex-1"> {member.avatar}
<h3 className="font-semibold">{task.title}</h3> </AvatarFallback>
<p className="text-sm text-gray-600">{task.description}</p> </Avatar>
</div> <div className="flex-1 min-w-0">
<Badge variant={task.status === 'completed' ? 'default' : 'secondary'}> <div className="flex items-center gap-2 mb-1">
{task.status === 'completed' ? '已完成' : '进行中'} <h4 className="font-medium text-gray-900 truncate">{member.name}</h4>
<Badge variant="outline" className="text-xs flex-shrink-0">
{member.level}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<div className="space-y-2 mb-4"> <span> {member.totalOrders}</span>
<div className="flex items-center justify-between text-sm"> <span> ¥{member.commission}</span>
<span></span> <Badge className="bg-green-100 text-green-700 text-xs">
<span className="font-medium">{task.progress}/{task.target}</span> {member.growth}
</Badge>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="flex-shrink-0 w-10 h-10 p-0 hover:bg-purple-50"
aria-label="查看订单明细"
onClick={() => {
router.push(`/distribution/orders?memberId=${member.id}&memberName=${encodeURIComponent(member.name)}`)
}}
>
<ArrowUpRight className="w-4 h-4" />
</Button>
</li>
))}
</ul>
</CardContent>
</Card>
</TabsContent>
{/* 收益标签页 - 优化布局 */}
<TabsContent value="earnings" className="mt-0 space-y-3">
{/* 佣金结算说明 */}
<Card className="bg-white shadow-lg border-0 rounded-2xl">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Info className="w-5 h-5 text-blue-600" />
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-4">
<div className="grid grid-cols-1 gap-4">
{/* 结算周期 */}
<div className="flex items-start gap-3 p-3 bg-blue-50 rounded-xl">
<Clock className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1"></h4>
<p className="text-sm text-gray-600">1T+7</p>
</div> </div>
<Progress
value={(task.progress / task.target) * 100}
className="h-2"
/>
</div> </div>
<div className="p-3 bg-yellow-50 rounded-lg"> {/* 佣金比例 */}
<div className="flex items-center gap-2"> <div className="flex items-start gap-3 p-3 bg-green-50 rounded-xl">
<Gift className="w-4 h-4 text-yellow-600" /> <DollarSign className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<span className="text-sm font-medium text-yellow-800">{task.reward}</span> <div>
<h4 className="font-semibold text-gray-900 mb-1"></h4>
<p className="text-sm text-gray-600">10%5%</p>
</div>
</div>
{/* 提现规则 */}
<div className="flex items-start gap-3 p-3 bg-purple-50 rounded-xl">
<CreditCard className="w-5 h-5 text-purple-600 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1"></h4>
<p className="text-sm text-gray-600">10022/</p>
</div>
</div>
{/* 冻结说明 */}
<div className="flex items-start gap-3 p-3 bg-orange-50 rounded-xl">
<Eye className="w-5 h-5 text-orange-600 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-semibold text-gray-900 mb-1"></h4>
<p className="text-sm text-gray-600">15</p>
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
))}
<Card className="bg-white shadow-lg border-0 rounded-2xl">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold text-gray-800"></CardTitle>
</CardHeader>
<CardContent className="pt-0">
<ul className="divide-y divide-gray-100" role="list">
{commissions.map((commission) => (
<li key={commission.id} className="py-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 min-w-0">
<Badge
className={`text-xs ${
commission.type === 'direct'
? 'bg-green-100 text-green-700'
: 'bg-blue-100 text-blue-700'
}`}
>
{commission.type === 'direct' ? '直推' : '团队'}
</Badge>
<span className="font-medium text-gray-900 truncate">{commission.productName}</span>
</div> </div>
<span className="font-semibold text-green-600 flex-shrink-0">+¥{commission.amount}</span>
</div>
<div className="flex items-center justify-between text-sm text-gray-600">
<span>: {commission.customerName}</span>
<span>{commission.time}</span>
</div>
</li>
))}
</ul>
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
{/* 小程序风格底部导航栏 */} {/* 底部导航 */}
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 safe-area-inset-bottom"> <BottomNavigation />
<div className="flex items-center justify-around py-2">
<a href="/" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<Home className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/group" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<ShoppingCart className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/distribution" className="flex flex-col items-center gap-1 py-1 text-red-500 active:bg-red-50 rounded-lg px-3">
<TrendingUp className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/profile" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<Users className="w-5 h-5" />
<span className="text-xs"></span>
</a>
</div>
</nav>
</div> </div>
) )
} }

View File

@ -1,9 +1,65 @@
@import "tailwindcss"; @import "tailwindcss";
@layer base { @layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
* { * {
border-color: hsl(var(--border)); border-color: hsl(var(--border));
} }
body { body {
background-color: hsl(var(--background)); background-color: hsl(var(--background));
color: hsl(var(--foreground)); color: hsl(var(--foreground));

View File

@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress' import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import BottomNavigation from '@/components/bottom-navigation'
import { import {
Users, Users,
Clock, Clock,
@ -31,7 +32,7 @@ export default function GroupListPage() {
id: '1', id: '1',
title: '3人拼团 玫瑰香水盲盒', title: '3人拼团 玫瑰香水盲盒',
description: '经典玫瑰香调,优雅女神范', description: '经典玫瑰香调,优雅女神范',
image: '/api/placeholder/300/200', image: 'https://picsum.photos/300/200?random=20',
originalPrice: 199, originalPrice: 199,
groupPrice: 139, groupPrice: 139,
groupSize: 3, groupSize: 3,
@ -48,79 +49,79 @@ export default function GroupListPage() {
}, },
{ {
id: '2', id: '2',
title: '5人拼团 薰衣草精油套装', title: '5人拼团 薰衣草助眠套装',
description: '纯天然薰衣草精油,助眠神器', description: '天然薰衣草精油,助眠好伴侣',
image: '/api/placeholder/300/200', image: 'https://picsum.photos/300/200?random=21',
originalPrice: 299, originalPrice: 299,
groupPrice: 199, groupPrice: 199,
groupSize: 5, groupSize: 5,
currentMembers: 3, currentMembers: 4,
timeLeft: '1天5小时', timeLeft: '5小时12分',
status: 'active', status: 'active',
host: { host: {
name: '精油达人', name: '花仙子',
avatar: '', avatar: '',
rating: 4.8 rating: 4.8
}, },
category: '精油', category: '护理',
tags: ['新品'] tags: ['新品', '热销']
}, },
{ {
id: '3', id: '3',
title: '2人拼团 茉莉香氛蜡烛', title: '2人拼团 柠檬清香车载香薰',
description: '手工制作,天然大豆蜡', description: '清新柠檬香,净化车内空气',
image: '/api/placeholder/300/200', image: 'https://picsum.photos/300/200?random=22',
originalPrice: 89, originalPrice: 89,
groupPrice: 59, groupPrice: 59,
groupSize: 2, groupSize: 2,
currentMembers: 1, currentMembers: 1,
timeLeft: '6小时12分', timeLeft: '1天8小时',
status: 'active', status: 'active',
host: { host: {
name: '蜡烛工坊', name: '清风徐来',
avatar: '', avatar: '',
rating: 4.7 rating: 4.7
}, },
category: '香薰', category: '车载',
tags: ['手工', '天然'] tags: ['清新']
}, },
{ {
id: '4', id: '4',
title: '4人拼团 小样体验包', title: '4人拼团 茉莉花茶香氛礼盒',
description: '10款热门香水小样超值体验', description: '优雅茉莉花香,送礼首选',
image: '/api/placeholder/300/200', image: 'https://picsum.photos/300/200?random=23',
originalPrice: 99, originalPrice: 399,
groupPrice: 29, groupPrice: 299,
groupSize: 4, groupSize: 4,
currentMembers: 4, currentMembers: 3,
timeLeft: '已成团', timeLeft: '3小时45分',
status: 'success', status: 'active',
host: { host: {
name: '小样控', name: '茉莉小姐',
avatar: '', avatar: '',
rating: 4.9 rating: 5.0
}, },
category: '小样', category: '礼盒',
tags: ['超值', '体验'] tags: ['礼品', '高端']
}, },
{ {
id: '5', id: '5',
title: '3人拼团 沉香木手串', title: '6人拼团 混合香调体验装',
description: '天然沉香木,淡雅香韵', description: '多种香调小样,发现你的专属香味',
image: '/api/placeholder/300/200', image: 'https://picsum.photos/300/200?random=24',
originalPrice: 399, originalPrice: 159,
groupPrice: 299, groupPrice: 99,
groupSize: 3, groupSize: 6,
currentMembers: 1, currentMembers: 5,
timeLeft: '3天2小时', timeLeft: '30分钟',
status: 'active', status: 'urgent',
host: { host: {
name: '香木匠人', name: '香调师',
avatar: '', avatar: '',
rating: 4.6 rating: 4.9
}, },
category: '香木', category: '体验',
tags: ['天然', '收藏'] tags: ['体验装', '紧急']
} }
] ]
@ -132,228 +133,180 @@ export default function GroupListPage() {
) )
} }
const getFilteredGroups = () => { const getProgressPercentage = (current: number, total: number) => {
switch (activeTab) { return (current / total) * 100
}
const getStatusColor = (status: string) => {
switch (status) {
case 'urgent':
return 'text-red-600 bg-red-50'
case 'active': case 'active':
return groupBuys.filter(group => group.status === 'active') return 'text-green-600 bg-green-50'
case 'success':
return groupBuys.filter(group => group.status === 'success')
case 'hot':
return groupBuys.filter(group => group.tags.includes('热门'))
default: default:
return groupBuys return 'text-gray-600 bg-gray-50'
} }
} }
const getStatusBadge = (group: any) => { const filteredGroups = groupBuys.filter(group => {
if (group.status === 'success') { if (activeTab === 'all') return true
return <Badge className="bg-green-500 text-white text-xs px-2 py-1"></Badge> if (activeTab === 'urgent') return group.status === 'urgent'
} if (activeTab === 'new') return group.tags.includes('新品')
if (group.currentMembers === group.groupSize - 1) { return true
return <Badge className="bg-orange-500 text-white text-xs px-2 py-1">1</Badge> })
}
return <Badge className="bg-red-500 text-white text-xs px-2 py-1"></Badge>
}
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 pb-20">
{/* 小程序风格顶部导航 */} {/* 顶部导航 */}
<div className="sticky top-0 z-50 bg-white border-b border-gray-100"> <div className="bg-white border-b border-gray-100 sticky top-0 z-40">
<div className="px-4 py-3"> <div className="px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between mb-4">
<h1 className="text-lg font-bold text-gray-800"></h1> <h1 className="text-xl font-bold text-gray-900"></h1>
<div className="flex items-center gap-2"> <Button variant="ghost" size="sm">
<div className="relative"> <Search className="w-5 h-5" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> </Button>
<input
type="text"
placeholder="搜索商品"
className="w-48 pl-10 pr-4 py-2 text-sm bg-gray-100 rounded-full focus:outline-none focus:bg-white focus:ring-1 focus:ring-purple-300"
/>
</div>
</div>
</div>
</div>
</div> </div>
<div className="px-4 pb-20"> {/* 标签切换 */}
{/* 小程序风格统计卡片 */} <div className="flex gap-2">
<div className="grid grid-cols-3 gap-3 py-4"> <Button
<div className="bg-gradient-to-br from-red-400 to-red-500 text-white p-3 rounded-xl text-center shadow-sm"> variant={activeTab === 'all' ? 'default' : 'ghost'}
<div className="text-xl font-bold">156</div> size="sm"
<div className="text-xs opacity-90"></div> onClick={() => setActiveTab('all')}
</div> className="rounded-full"
<div className="bg-gradient-to-br from-green-400 to-green-500 text-white p-3 rounded-xl text-center shadow-sm">
<div className="text-xl font-bold">89</div>
<div className="text-xs opacity-90"></div>
</div>
<div className="bg-gradient-to-br from-orange-400 to-orange-500 text-white p-3 rounded-xl text-center shadow-sm">
<div className="text-xl font-bold">2.3k</div>
<div className="text-xs opacity-90"></div>
</div>
</div>
{/* 小程序风格分类标签 */}
<div className="flex gap-2 mb-4 overflow-x-auto scrollbar-hide">
{[
{ key: 'all', label: '全部' },
{ key: 'active', label: '进行中' },
{ key: 'hot', label: '热门' },
{ key: 'success', label: '已成团' }
].map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-all ${
activeTab === tab.key
? 'bg-red-500 text-white shadow-md'
: 'bg-white text-gray-600 border border-gray-200'
}`}
> >
{tab.label}
</button> </Button>
))} <Button
variant={activeTab === 'urgent' ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab('urgent')}
className="rounded-full"
>
<Zap className="w-4 h-4 mr-1" />
</Button>
<Button
variant={activeTab === 'new' ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab('new')}
className="rounded-full"
>
<Star className="w-4 h-4 mr-1" />
</Button>
</div>
</div>
</div> </div>
{/* 拼团商品列表 - 小程序风格 */} {/* 统计信息 */}
<div className="space-y-3"> <div className="px-4 py-4">
{getFilteredGroups().map((group) => ( <div className="grid grid-cols-3 gap-4">
<Card key={group.id} className="overflow-hidden bg-white rounded-xl shadow-sm border-0"> <div className="bg-white rounded-xl p-4 text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">{groupBuys.length}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-xl p-4 text-center">
<div className="text-2xl font-bold text-green-600 mb-1">156</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-white rounded-xl p-4 text-center">
<div className="text-2xl font-bold text-purple-600 mb-1">89</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</div>
{/* 拼团列表 */}
<div className="px-4">
<div className="space-y-4">
{filteredGroups.map((group) => (
<Card key={group.id} className="overflow-hidden border-0 shadow-sm">
<CardContent className="p-0">
<Link href={`/group/${group.id}`}>
<div className="flex"> <div className="flex">
{/* 商品图片 */} {/* 商品图片 */}
<div className="relative w-28 h-28 flex-shrink-0"> <div className="w-24 h-24 flex-shrink-0">
<img <img
src={group.image} src={group.image}
alt={group.title} alt={group.title}
className="w-full h-full object-cover rounded-l-xl" className="w-full h-full object-cover"
/> />
<div className="absolute top-2 left-2">
{getStatusBadge(group)}
</div>
{group.tags.map((tag, index) => (
<Badge
key={index}
className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-1.5 py-0.5"
>
{tag}
</Badge>
))}
</div> </div>
{/* 商品信息 */} {/* 内容区域 */}
<div className="flex-1 p-3"> <div className="flex-1 p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex items-start justify-between mb-2">
<h3 className="font-medium text-sm text-gray-800 line-clamp-2 flex-1 mr-2"> <div className="flex-1">
<h3 className="font-semibold text-gray-900 text-sm line-clamp-1 mb-1">
{group.title} {group.title}
</h3> </h3>
<button <p className="text-xs text-gray-600 line-clamp-1 mb-2">
onClick={() => handleLike(group.id)} {group.description}
className="p-1" </p>
</div>
<Button
variant="ghost"
size="sm"
className="p-1 ml-2"
onClick={(e) => {
e.preventDefault()
handleLike(group.id)
}}
> >
<Heart className={`w-4 h-4 ${likedGroups.includes(group.id) ? 'fill-red-500 text-red-500' : 'text-gray-400'}`} /> <Heart className={`w-4 h-4 ${likedGroups.includes(group.id) ? 'fill-red-500 text-red-500' : 'text-gray-400'}`} />
</button> </Button>
</div> </div>
{/* 价格信息 */} {/* 价格和进度 */}
<div className="flex items-baseline gap-2 mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-lg font-bold text-red-500">¥{group.groupPrice}</span> <div className="flex items-baseline gap-2">
<span className="text-xs text-gray-400 line-through">¥{group.originalPrice}</span> <span className="text-red-600 font-bold text-lg">¥{group.groupPrice}</span>
<span className="text-gray-400 line-through text-sm">¥{group.originalPrice}</span>
</div> </div>
<div className="text-xs text-gray-500">
{/* 拼团进度 */}
<div className="mb-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1">
<Users className="w-3 h-3 text-red-500" />
<span className="text-xs text-gray-600"></span>
</div>
<span className="text-xs font-medium text-red-500">
{group.currentMembers}/{group.groupSize} {group.currentMembers}/{group.groupSize}
</span>
</div> </div>
</div>
{/* 进度条 */}
<div className="mb-2">
<Progress <Progress
value={(group.currentMembers / group.groupSize) * 100} value={getProgressPercentage(group.currentMembers, group.groupSize)}
className="h-1.5" className="h-2"
/> />
</div> </div>
{/* 底部信息 */} {/* 底部信息 */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<div className="relative"> <Avatar className="w-5 h-5">
<Avatar className="w-4 h-4"> <AvatarFallback className="text-xs bg-purple-100 text-purple-600">
<AvatarFallback className="text-xs bg-gray-200">{group.host.avatar}</AvatarFallback> {group.host.avatar}
</AvatarFallback>
</Avatar> </Avatar>
<Crown className="w-2 h-2 text-yellow-500 absolute -top-0.5 -right-0.5" /> <span className="text-xs text-gray-600">{group.host.name}</span>
<div className="flex items-center gap-1">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="text-xs text-gray-600">{group.host.rating}</span>
</div> </div>
<span className="text-xs text-gray-500">{group.host.name}</span>
<Star className="w-2.5 h-2.5 text-yellow-500 fill-current" />
<span className="text-xs text-gray-500">{group.host.rating}</span>
</div> </div>
<div className="flex items-center gap-1 text-orange-500"> <div className={`px-2 py-1 rounded-full text-xs ${getStatusColor(group.status)}`}>
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3 inline mr-1" />
<span className="text-xs">{group.timeLeft}</span> {group.timeLeft}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* 操作按钮 */}
<div className="px-3 pb-3">
<Link href={`/group/${group.id}`} className="block">
<Button
className="w-full h-8 bg-gradient-to-r from-red-400 to-red-500 hover:from-red-500 hover:to-red-600 text-white text-sm rounded-full"
disabled={group.status === 'success'}
>
{group.status === 'success' ? (
<>
<Gift className="w-3 h-3 mr-1" />
</>
) : (
<>
<Users className="w-3 h-3 mr-1" />
</>
)}
</Button>
</Link> </Link>
</div> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
{/* 空状态 */}
{getFilteredGroups().length === 0 && (
<div className="text-center py-12">
<Users className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-base font-medium text-gray-600 mb-2"></h3>
<p className="text-sm text-gray-500"></p>
</div>
)}
</div> </div>
{/* 小程序风格底部导航栏 */} {/* 底部导航 */}
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 safe-area-inset-bottom"> <BottomNavigation />
<div className="flex items-center justify-around py-2">
<Link href="/" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<Home className="h-5 w-5" />
<span className="text-xs"></span>
</Link>
<Link href="/group" className="flex flex-col items-center gap-1 py-1 text-red-500 active:bg-red-50 rounded-lg px-3">
<ShoppingCart className="h-5 w-5" />
<span className="text-xs"></span>
</Link>
<Link href="/distribution" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<TrendingUp className="h-5 w-5" />
<span className="text-xs"></span>
</Link>
<Link href="/profile" className="flex flex-col items-center gap-1 py-1 text-gray-600 active:bg-gray-100 rounded-lg px-3">
<Users className="h-5 w-5" />
<span className="text-xs"></span>
</Link>
</div>
</nav>
</div> </div>
) )
} }

View File

@ -64,7 +64,7 @@ export default function HomePage() {
{ {
id: '1', id: '1',
name: '玫瑰香水盲盒', name: '玫瑰香水盲盒',
image: '/api/placeholder/400/400', image: 'https://picsum.photos/400/400?random=1',
price: 139, price: 139,
originalPrice: 199, originalPrice: 199,
discount: '7折', discount: '7折',
@ -82,7 +82,7 @@ export default function HomePage() {
{ {
id: '2', id: '2',
name: '薰衣草助眠香氛', name: '薰衣草助眠香氛',
image: '/api/placeholder/400/400', image: 'https://picsum.photos/400/400?random=2',
price: 89, price: 89,
originalPrice: 129, originalPrice: 129,
discount: '69折', discount: '69折',
@ -104,7 +104,7 @@ export default function HomePage() {
{ {
id: '1', id: '1',
title: '3人拼团 玫瑰香水', title: '3人拼团 玫瑰香水',
image: '/api/placeholder/400/400', image: 'https://picsum.photos/400/400?random=3',
originalPrice: 199, originalPrice: 199,
groupPrice: 139, groupPrice: 139,
groupSize: 3, groupSize: 3,
@ -120,7 +120,7 @@ export default function HomePage() {
{ {
id: '2', id: '2',
title: '5人拼团 薰衣草套装', title: '5人拼团 薰衣草套装',
image: '/api/placeholder/400/400', image: 'https://picsum.photos/400/400?random=4',
originalPrice: 299, originalPrice: 299,
groupPrice: 199, groupPrice: 199,
groupSize: 5, groupSize: 5,

View File

@ -0,0 +1,562 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Heart,
Share2,
ShoppingCart,
Star,
ArrowLeft,
Plus,
Minus,
Shield,
Truck,
RotateCcw,
ChevronLeft,
ChevronRight
} from 'lucide-react'
import { showSuccessToast, showLikeToast, showShareToast } from '@/lib/toast-utils'
interface Product {
id: string
name: string
images: string[]
price: number
originalPrice: number
discount: string
rating: number
reviews: number
likes: number
comments: number
description: string
tags: string[]
stock: number
category: string
isHot: boolean
isNew: boolean
specifications: {
brand: string
capacity: string
fragrance: string
duration: string
origin: string
}
details: {
ingredients: string[]
usage: string
storage: string
notes: {
top: string[]
middle: string[]
base: string[]
}
}
}
export default function ProductDetailPage() {
const router = useRouter()
const params = useParams()
const productId = params.id as string
const scrollContainerRef = useRef<HTMLDivElement>(null)
const [product, setProduct] = useState<Product | null>(null)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [quantity, setQuantity] = useState(1)
const [isLiked, setIsLiked] = useState(false)
const [isLoading, setIsLoading] = useState(true)
// 模拟商品数据
useEffect(() => {
const mockProduct: Product = {
id: productId,
name: '玫瑰香水盲盒',
images: [
'https://picsum.photos/600/600?random=10',
'https://picsum.photos/600/600?random=11',
'https://picsum.photos/600/600?random=12',
'https://picsum.photos/600/600?random=13',
'https://picsum.photos/600/600?random=14',
'https://picsum.photos/600/600?random=15'
],
price: 139,
originalPrice: 199,
discount: '7折',
rating: 4.8,
reviews: 234,
likes: 567,
comments: 89,
description: '经典玫瑰香调优雅女神范持久留香8小时。采用法国进口玫瑰精油层次丰富前调清新中调浓郁后调温暖。适合日常使用也是送礼的绝佳选择。',
tags: ['热销', '限量', '法国进口'],
stock: 15,
category: 'floral',
isHot: true,
isNew: false,
specifications: {
brand: 'Elegant Rose',
capacity: '50ml',
fragrance: '花香调',
duration: '6-8小时',
origin: '法国'
},
details: {
ingredients: ['玫瑰精油', '茉莉花精华', '白麝香', '雪松木', '香草'],
usage: '喷洒在脉搏处,如手腕、耳后、颈部等部位,避免直接接触衣物。',
storage: '请存放在阴凉干燥处,避免阳光直射和高温环境。',
notes: {
top: ['柠檬', '佛手柑', '粉红胡椒'],
middle: ['玫瑰', '茉莉', '牡丹'],
base: ['白麝香', '雪松', '香草']
}
}
}
setTimeout(() => {
setProduct(mockProduct)
setIsLoading(false)
}, 1000)
}, [productId])
const handleLike = () => {
setIsLiked(!isLiked)
showLikeToast()
}
const handleShare = () => {
if (navigator.share) {
navigator.share({
title: product?.name,
text: `来看看这个超棒的香氛产品!`,
url: window.location.href
})
} else {
navigator.clipboard.writeText(window.location.href)
showShareToast()
}
}
const handleAddToCart = () => {
if (!product) return
// 检查库存
if (quantity > product.stock) {
showSuccessToast('库存不足,请减少购买数量')
return
}
// 模拟添加到购物车的逻辑
const cartItem = {
productId: product.id,
name: product.name,
price: product.price,
quantity: quantity,
image: product.images[0]
}
// 这里可以调用实际的购物车API
console.log('添加到购物车:', cartItem)
showSuccessToast(`已添加 ${quantity} 件 "${product.name}" 到购物车`)
}
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)
}
const increaseQuantity = () => {
if (product && quantity < product.stock) {
setQuantity(quantity + 1)
}
}
const decreaseQuantity = () => {
if (quantity > 1) {
setQuantity(quantity - 1)
}
}
// 图片滑动功能
const scrollToImage = (index: number) => {
setCurrentImageIndex(index)
if (scrollContainerRef.current) {
const container = scrollContainerRef.current
const imageWidth = container.clientWidth
container.scrollTo({
left: imageWidth * index,
behavior: 'smooth'
})
}
}
const handlePrevImage = () => {
if (!product) return
const prevIndex = currentImageIndex > 0 ? currentImageIndex - 1 : product.images.length - 1
scrollToImage(prevIndex)
}
const handleNextImage = () => {
if (!product) return
const nextIndex = currentImageIndex < product.images.length - 1 ? currentImageIndex + 1 : 0
scrollToImage(nextIndex)
}
// 监听滑动事件
const handleScroll = () => {
if (scrollContainerRef.current && product) {
const container = scrollContainerRef.current
const imageWidth = container.clientWidth
const scrollLeft = container.scrollLeft
const newIndex = Math.round(scrollLeft / imageWidth)
if (newIndex !== currentImageIndex && newIndex >= 0 && newIndex < product.images.length) {
setCurrentImageIndex(newIndex)
}
}
}
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-purple-200 border-t-purple-600 rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600">...</p>
</div>
</div>
)
}
if (!product) {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 flex items-center justify-center">
<div className="text-center">
<p className="text-gray-600 mb-4"></p>
<Button onClick={() => router.back()}></Button>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部导航 */}
<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">
<Button variant="ghost" size="sm" onClick={() => router.back()} className="p-2">
<ArrowLeft className="w-5 h-5" />
</Button>
<h1 className="font-medium text-gray-900"></h1>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={handleLike} className="p-2">
<Heart className={`w-5 h-5 ${isLiked ? 'fill-red-500 text-red-500' : 'text-gray-600'}`} />
</Button>
<Button variant="ghost" size="sm" onClick={handleShare} className="p-2">
<Share2 className="w-5 h-5 text-gray-600" />
</Button>
</div>
</div>
</div>
<div className="pb-24">
{/* 商品图片轮播区域 */}
<div className="relative bg-white">
{/* 主图片滑动容器 */}
<div
ref={scrollContainerRef}
className="flex overflow-x-auto snap-x snap-mandatory scrollbar-hide"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
onScroll={handleScroll}
>
{product.images.map((image, index) => (
<div key={index} className="w-full flex-shrink-0 snap-center">
<div className="aspect-square relative">
<img
src={image}
alt={`${product.name} - 图片 ${index + 1}`}
className="w-full h-full object-cover"
/>
{/* 标签 */}
{index === 0 && (
<div className="absolute top-4 left-4 flex flex-col gap-2">
{product.isHot && (
<Badge className="bg-red-500 text-white shadow-lg"></Badge>
)}
{product.isNew && (
<Badge className="bg-green-500 text-white shadow-lg"></Badge>
)}
</div>
)}
</div>
</div>
))}
</div>
{/* 左右切换按钮 */}
<button
onClick={handlePrevImage}
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 backdrop-blur-sm rounded-full shadow-lg flex items-center justify-center hover:bg-white transition-colors"
>
<ChevronLeft className="w-5 h-5 text-gray-700" />
</button>
<button
onClick={handleNextImage}
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 backdrop-blur-sm rounded-full shadow-lg flex items-center justify-center hover:bg-white transition-colors"
>
<ChevronRight className="w-5 h-5 text-gray-700" />
</button>
{/* 图片指示器 */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
{product.images.map((_, index) => (
<button
key={index}
onClick={() => scrollToImage(index)}
className={`w-2 h-2 rounded-full transition-all ${
currentImageIndex === index
? 'bg-white w-6'
: 'bg-white/50'
}`}
/>
))}
</div>
</div>
{/* 商品基本信息 */}
<div className="bg-white px-4 py-6 mb-2">
{/* 标签 */}
<div className="flex flex-wrap gap-2 mb-3">
{product.tags.map((tag, index) => (
<Badge key={index} variant="outline" className="text-purple-600 border-purple-200 bg-purple-50">
{tag}
</Badge>
))}
</div>
{/* 商品名称 */}
<h1 className="text-xl font-bold text-gray-900 mb-3 leading-tight">{product.name}</h1>
{/* 评分和评价 */}
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="font-medium text-gray-900">{product.rating}</span>
<span className="text-gray-500 text-sm">({product.reviews})</span>
</div>
<div className="text-gray-500 text-sm">
{product.likes}
</div>
</div>
{/* 价格 */}
<div className="flex items-baseline gap-3 mb-4">
<span className="text-2xl font-bold text-red-600">¥{product.price}</span>
<span className="text-base text-gray-400 line-through">¥{product.originalPrice}</span>
<Badge variant="destructive" className="text-xs">{product.discount}</Badge>
</div>
{/* 商品描述 */}
<p className="text-gray-600 text-sm leading-relaxed">{product.description}</p>
</div>
{/* 购买选项 */}
<div className="bg-white px-4 py-6 mb-2">
{/* 数量选择 */}
<div className="flex items-center justify-between mb-6">
<span className="font-medium text-gray-900"></span>
<div className="flex items-center gap-3">
<Button
variant="outline"
size="sm"
onClick={decreaseQuantity}
disabled={quantity <= 1}
className="h-9 w-9 p-0 rounded-full"
>
<Minus className="w-4 h-4" />
</Button>
<div className="flex items-center justify-center w-12 h-9 bg-gray-50 rounded-lg">
<span className="font-medium">{quantity}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={increaseQuantity}
disabled={quantity >= product.stock}
className="h-9 w-9 p-0 rounded-full"
>
<Plus className="w-4 h-4" />
</Button>
</div>
</div>
{/* 库存和小计 */}
<div className="flex items-center justify-between text-sm text-gray-500 mb-6">
<span> {product.stock} </span>
<span className="font-medium text-gray-900"> ¥{(product.price * quantity).toFixed(2)}</span>
</div>
{/* 库存状态提示 */}
{product.stock === 0 && (
<div className="text-center py-3 bg-red-50 rounded-lg mb-4">
<span className="text-red-600 font-medium"></span>
</div>
)}
{product.stock > 0 && product.stock <= 5 && (
<div className="text-center py-2 bg-orange-50 rounded-lg mb-4">
<span className="text-orange-600 text-sm"> {product.stock} </span>
</div>
)}
</div>
{/* 商品详情 */}
<div className="bg-white mb-2">
<Tabs defaultValue="details" className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-gray-50 mx-4 rounded-lg">
<TabsTrigger value="details" className="text-sm"></TabsTrigger>
<TabsTrigger value="specs" className="text-sm"></TabsTrigger>
<TabsTrigger value="reviews" className="text-sm"></TabsTrigger>
</TabsList>
<TabsContent value="details" className="px-4 py-6">
<div className="space-y-6">
<div>
<h3 className="font-semibold text-gray-900 mb-3"></h3>
<div className="space-y-3">
<div className="flex gap-3">
<span className="w-12 text-sm text-gray-500 flex-shrink-0"></span>
<span className="text-gray-900 text-sm">{product.details.notes.top.join('、')}</span>
</div>
<div className="flex gap-3">
<span className="w-12 text-sm text-gray-500 flex-shrink-0"></span>
<span className="text-gray-900 text-sm">{product.details.notes.middle.join('、')}</span>
</div>
<div className="flex gap-3">
<span className="w-12 text-sm text-gray-500 flex-shrink-0"></span>
<span className="text-gray-900 text-sm">{product.details.notes.base.join('、')}</span>
</div>
</div>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm">{product.details.ingredients.join('、')}</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-3">使</h3>
<p className="text-gray-600 text-sm leading-relaxed">{product.details.usage}</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-3"></h3>
<p className="text-gray-600 text-sm leading-relaxed">{product.details.storage}</p>
</div>
</div>
</TabsContent>
<TabsContent value="specs" className="px-4 py-6">
<div className="space-y-4">
{Object.entries(product.specifications).map(([key, value]) => (
<div key={key} className="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
<span className="text-gray-500 text-sm">
{key === 'brand' ? '品牌' :
key === 'capacity' ? '容量' :
key === 'fragrance' ? '香调' :
key === 'duration' ? '持香时间' :
key === 'origin' ? '产地' : key}
</span>
<span className="font-medium text-gray-900 text-sm">{value}</span>
</div>
))}
</div>
</TabsContent>
<TabsContent value="reviews" className="px-4 py-6">
<div className="text-center py-12">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Star className="w-8 h-8 text-gray-400" />
</div>
<p className="text-gray-500 mb-2"></p>
<p className="text-sm text-gray-400"></p>
</div>
</TabsContent>
</Tabs>
</div>
{/* 服务保障 */}
<div className="bg-white px-4 py-6">
<h3 className="font-semibold text-gray-900 mb-4"></h3>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 bg-green-50 rounded-full flex items-center justify-center mb-2">
<Shield className="w-6 h-6 text-green-500" />
</div>
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 bg-blue-50 rounded-full flex items-center justify-center mb-2">
<Truck className="w-6 h-6 text-blue-500" />
</div>
<span className="text-xs text-gray-600"></span>
</div>
<div className="flex flex-col items-center text-center">
<div className="w-12 h-12 bg-purple-50 rounded-full flex items-center justify-center mb-2">
<RotateCcw className="w-6 h-6 text-purple-500" />
</div>
<span className="text-xs text-gray-600">7退</span>
</div>
</div>
</div>
</div>
{/* 底部固定购买栏 */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 z-40">
<div className="flex items-center gap-3">
<Button
variant="outline"
className="flex-1 h-12 border-gray-300"
onClick={handleAddToCart}
disabled={product.stock === 0}
>
<ShoppingCart className="w-5 h-5 mr-2" />
</Button>
<Button
className="flex-1 h-12 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white font-medium"
onClick={handleBuyNow}
disabled={product.stock === 0}
>
</Button>
</div>
</div>
</div>
)
}

View File

@ -12,6 +12,7 @@ import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import BottomNavigation from '@/components/bottom-navigation'
import { import {
Crown, Crown,
Star, Star,
@ -671,29 +672,8 @@ export default function SubscriptionPage() {
</Dialog> </Dialog>
</div> </div>
{/* Bottom Navigation */} {/* 底部导航 */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t"> <BottomNavigation />
<div className="max-w-6xl mx-auto px-4">
<div className="grid grid-cols-4 py-2">
<a href="/" className="flex flex-col items-center gap-1 py-2 text-gray-500 hover:text-purple-500 transition-colors">
<Home className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/subscription" className="flex flex-col items-center gap-1 py-2 text-gray-500 hover:text-purple-500 transition-colors">
<ShoppingCart className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/distribution" className="flex flex-col items-center gap-1 py-2 text-gray-500 hover:text-purple-500 transition-colors">
<TrendingUp className="w-5 h-5" />
<span className="text-xs"></span>
</a>
<a href="/profile" className="flex flex-col items-center gap-1 py-2 text-gray-500 hover:text-purple-500 transition-colors">
<Users className="w-5 h-5" />
<span className="text-xs"></span>
</a>
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -5,9 +5,9 @@ import { useRouter, usePathname } from 'next/navigation'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Home, Home,
Search, TrendingUp,
PlusSquare, PlusSquare,
ShoppingBag, Bell,
User User
} from 'lucide-react' } from 'lucide-react'
@ -18,9 +18,9 @@ export default function BottomNavigation() {
// 根据当前路径确定活跃标签 // 根据当前路径确定活跃标签
const getActiveTab = () => { const getActiveTab = () => {
if (pathname === '/') return 'home' if (pathname === '/') return 'home'
if (pathname.startsWith('/discover')) return 'discover' if (pathname.startsWith('/distribution')) return 'distribution'
if (pathname.startsWith('/create')) return 'create' if (pathname.startsWith('/create')) return 'create'
if (pathname.startsWith('/orders')) return 'orders' if (pathname.startsWith('/subscription')) return 'subscription'
if (pathname.startsWith('/profile')) return 'profile' if (pathname.startsWith('/profile')) return 'profile'
return 'home' return 'home'
} }
@ -48,11 +48,11 @@ export default function BottomNavigation() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className={`flex flex-col items-center gap-1 p-2 h-auto ${activeTab === 'discover' ? 'text-purple-600' : 'text-gray-500'}`} className={`flex flex-col items-center gap-1 p-2 h-auto ${activeTab === 'distribution' ? 'text-purple-600' : 'text-gray-500'}`}
onClick={() => handleNavigation('discover', '/discover')} onClick={() => handleNavigation('distribution', '/distribution')}
> >
<Search className="w-5 h-5" /> <TrendingUp className="w-5 h-5" />
<span className="text-xs"></span> <span className="text-xs"></span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@ -66,11 +66,11 @@ export default function BottomNavigation() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className={`flex flex-col items-center gap-1 p-2 h-auto ${activeTab === 'orders' ? 'text-purple-600' : 'text-gray-500'}`} className={`flex flex-col items-center gap-1 p-2 h-auto ${activeTab === 'subscription' ? 'text-purple-600' : 'text-gray-500'}`}
onClick={() => handleNavigation('orders', '/orders')} onClick={() => handleNavigation('subscription', '/subscription')}
> >
<ShoppingBag className="w-5 h-5" /> <Bell className="w-5 h-5" />
<span className="text-xs"></span> <span className="text-xs"></span>
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@ -52,17 +53,23 @@ export function ProductCard({
isBookmarked, isBookmarked,
variant = 'default' variant = 'default'
}: ProductCardProps) { }: ProductCardProps) {
const router = useRouter()
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const discountPercentage = Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100) const discountPercentage = Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)
const handleCardClick = () => {
router.push(`/product/${product.id}`)
}
if (variant === 'compact') { if (variant === 'compact') {
return ( return (
<Card <Card
className="group border-0 shadow-md hover:shadow-xl transition-all duration-300 rounded-2xl overflow-hidden cursor-pointer transform hover:scale-[1.02]" className="group border-0 shadow-md hover:shadow-xl transition-all duration-300 rounded-2xl overflow-hidden cursor-pointer transform hover:scale-[1.02]"
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
onClick={handleCardClick}
> >
<div className="relative"> <div className="relative">
<div className="aspect-square overflow-hidden bg-gray-100"> <div className="aspect-square overflow-hidden bg-gray-100">
@ -170,6 +177,7 @@ export function ProductCard({
className="group border-0 shadow-lg hover:shadow-2xl transition-all duration-500 rounded-3xl overflow-hidden cursor-pointer transform hover:scale-[1.01]" className="group border-0 shadow-lg hover:shadow-2xl transition-all duration-500 rounded-3xl overflow-hidden cursor-pointer transform hover:scale-[1.01]"
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
onClick={handleCardClick}
> >
{/* 图片区域 */} {/* 图片区域 */}
<div className="relative"> <div className="relative">
@ -208,13 +216,13 @@ export function ProductCard({
</div> </div>
{/* 快捷操作按钮 */} {/* 快捷操作按钮 */}
<div className={`absolute top-4 right-4 flex gap-2 transition-all duration-300 ${ <div className={`absolute top-4 right-4 flex flex-col gap-3 transition-all duration-300 ${
isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2' isHovered ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-4'
}`}> }`}>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="bg-white/20 backdrop-blur-md text-white p-2.5 rounded-full shadow-lg hover:bg-white/30 transition-all duration-200" className="bg-white/90 backdrop-blur-sm text-gray-700 p-3 rounded-full shadow-lg hover:bg-white hover:scale-110 transition-all duration-200"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onLike(product.id) onLike(product.id)
@ -225,7 +233,7 @@ export function ProductCard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="bg-white/20 backdrop-blur-md text-white p-2.5 rounded-full shadow-lg hover:bg-white/30 transition-all duration-200" className="bg-white/90 backdrop-blur-sm text-gray-700 p-3 rounded-full shadow-lg hover:bg-white hover:scale-110 transition-all duration-200"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onBookmark(product.id) onBookmark(product.id)
@ -233,69 +241,77 @@ export function ProductCard({
> >
<Bookmark className={`w-5 h-5 ${isBookmarked ? 'fill-current text-blue-400' : ''}`} /> <Bookmark className={`w-5 h-5 ${isBookmarked ? 'fill-current text-blue-400' : ''}`} />
</Button> </Button>
<Button
variant="ghost"
size="sm"
className="bg-white/90 backdrop-blur-sm text-gray-700 p-3 rounded-full shadow-lg hover:bg-white hover:scale-110 transition-all duration-200"
onClick={(e) => {
e.stopPropagation()
onShare(product.name)
}}
>
<Send className="w-5 h-5" />
</Button>
</div> </div>
{/* 库存提醒 */} {/* 库存警告 */}
{product.stock <= 5 && ( {product.stock <= 5 && (
<div className="absolute bottom-4 left-4"> <div className="absolute bottom-4 left-4">
<Badge className="bg-red-500/90 backdrop-blur-sm text-white text-xs px-3 py-1 rounded-full shadow-lg animate-pulse"> <Badge className="bg-red-500/90 backdrop-blur-sm text-white text-sm px-3 py-1 rounded-full shadow-lg">
{product.stock} {product.stock}
</Badge> </Badge>
</div> </div>
)} )}
{/* 查看详情按钮 */} {/* 查看详情提示 */}
<div className={`absolute bottom-4 right-4 transition-all duration-300 ${ <div className={`absolute inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center transition-all duration-300 ${
isHovered ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2' isHovered ? 'opacity-100' : 'opacity-0'
}`}> }`}>
<Button <div className="bg-white/90 backdrop-blur-sm rounded-full px-6 py-3 flex items-center gap-2 shadow-lg transform transition-all duration-300 hover:scale-105">
variant="ghost" <Eye className="w-5 h-5 text-purple-600" />
size="sm" <span className="text-purple-600 font-medium"></span>
className="bg-white/20 backdrop-blur-md text-white p-2.5 rounded-full shadow-lg hover:bg-white/30" </div>
>
<Eye className="w-5 h-5" />
</Button>
</div> </div>
</div> </div>
{/* 内容区域 */}
<CardContent className="p-6"> <CardContent className="p-6">
<div className="space-y-4"> <div className="flex flex-wrap gap-2 mb-3">
{/* 标题和描述 */} {product.tags.slice(0, 2).map((tag, index) => (
<div> <Badge key={index} variant="outline" className="text-purple-600 border-purple-200 text-xs">
{tag}
</Badge>
))}
</div>
<h3 className="font-bold text-xl mb-2 line-clamp-2 leading-tight">{product.name}</h3> <h3 className="font-bold text-xl mb-2 line-clamp-2 leading-tight">{product.name}</h3>
<p className="text-sm text-gray-600 line-clamp-2 leading-relaxed">{product.description}</p> <p className="text-sm text-gray-600 line-clamp-2 leading-relaxed">{product.description}</p>
</div>
{/* 评分和评价 */} <div className="flex items-center gap-4 my-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-500 fill-current" /> <Star className="w-4 h-4 text-yellow-500 fill-current" />
<span className="font-semibold text-sm">{product.rating}</span> <span className="font-medium text-gray-900">{product.rating}</span>
<span className="text-sm text-gray-500">({product.reviews})</span>
</div>
<div className="text-sm text-gray-500">
{product.likes}
</div> </div>
<span className="text-sm text-gray-500">{product.reviews} </span>
<span className="text-sm text-gray-500"> {product.stock}</span>
</div> </div>
{/* 价格区域 */}
<div className="flex items-baseline gap-3"> <div className="flex items-baseline gap-3">
<span className="text-3xl font-bold text-red-500">¥{product.price}</span> <span className="text-3xl font-bold text-red-600">¥{product.price}</span>
{product.originalPrice > product.price && ( {product.originalPrice > product.price && (
<span className="text-lg text-gray-400 line-through">¥{product.originalPrice}</span> <span className="text-lg text-gray-400 line-through">¥{product.originalPrice}</span>
)} )}
{discountPercentage > 0 && ( <Badge variant="destructive" className="ml-auto">
<Badge variant="secondary" className="text-sm font-semibold"> {product.discount}
¥{product.originalPrice - product.price}
</Badge> </Badge>
)}
</div> </div>
{/* 互动区域 */} <div className="flex gap-3 mt-6">
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div className="flex items-center gap-6">
<Button <Button
variant="ghost" variant="outline"
size="sm" className="flex-1"
className="flex items-center gap-2 text-gray-600 hover:text-red-500 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onLike(product.id) onLike(product.id)
@ -305,31 +321,28 @@ export function ProductCard({
<span className="text-sm font-medium">{product.likes}</span> <span className="text-sm font-medium">{product.likes}</span>
</Button> </Button>
<Button <Button
variant="ghost" variant="outline"
size="sm"
className="flex items-center gap-2 text-gray-600 hover:text-blue-500 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onShare(product.name) onShare(product.name)
}} }}
> >
<Send className="w-5 h-5" /> <Send className="w-5 h-5" />
<span className="text-sm font-medium">{product.comments}</span>
</Button> </Button>
</div>
<Button <Button
className="px-8 py-2.5 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105" className="flex-1 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onPurchase(product.id) onPurchase(product.id)
}} }}
> >
<ShoppingCart className="w-4 h-4 mr-2" /> <ShoppingCart className="w-5 h-5 mr-2" />
</Button> </Button>
</div> </div>
</div>
</CardContent> </CardContent>
</Card> </Card>
) )
} }
export default ProductCard

View File

@ -0,0 +1,302 @@
'use client'
import { useState, useEffect } from 'react'
import { Search, Check, Package, Star, Tag } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
interface Product {
id: string
name: string
brand: string
price: number
originalPrice: number
image: string
category: string
tags: string[]
rating: number
reviews: number
stock: number
description: string
volume?: string
}
interface ProductSelectorProps {
selectedProduct?: Product | null
onProductSelect: (product: Product) => void
placeholder?: string
disabled?: boolean
}
export function ProductSelector({
selectedProduct,
onProductSelect,
placeholder = "选择商品",
disabled = false
}: ProductSelectorProps) {
const [isOpen, setIsOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const [products, setProducts] = useState<Product[]>([])
// 模拟商品数据
useEffect(() => {
const mockProducts: Product[] = [
{
id: '1',
name: '迪奥小姐香水',
brand: 'Dior',
price: 899,
originalPrice: 1299,
image: 'https://picsum.photos/200/200?random=1',
category: 'floral',
tags: ['热销', '经典'],
rating: 4.8,
reviews: 1234,
stock: 50,
description: '经典花香调,优雅迷人',
volume: '100ml'
},
{
id: '2',
name: '香奈儿五号香水',
brand: 'Chanel',
price: 1299,
originalPrice: 1599,
image: 'https://picsum.photos/200/200?random=2',
category: 'floral',
tags: ['经典', '限量'],
rating: 4.9,
reviews: 2156,
stock: 30,
description: '永恒经典,女性魅力象征',
volume: '100ml'
},
{
id: '3',
name: '爱马仕大地香水',
brand: 'Hermès',
price: 1199,
originalPrice: 1499,
image: 'https://picsum.photos/200/200?random=3',
category: 'woody',
tags: ['男士', '木质'],
rating: 4.7,
reviews: 856,
stock: 25,
description: '木质调香水,成熟男士首选',
volume: '100ml'
},
{
id: '4',
name: '祖马龙英国梨香水',
brand: 'Jo Malone',
price: 799,
originalPrice: 999,
image: 'https://picsum.photos/200/200?random=4',
category: 'fruity',
tags: ['清新', '果香'],
rating: 4.6,
reviews: 678,
stock: 40,
description: '清新果香,日常必备',
volume: '100ml'
},
{
id: '5',
name: '汤姆福特黑兰花香水',
brand: 'Tom Ford',
price: 1599,
originalPrice: 1899,
image: 'https://picsum.photos/200/200?random=5',
category: 'oriental',
tags: ['奢华', '东方调'],
rating: 4.8,
reviews: 432,
stock: 15,
description: '神秘东方调,奢华体验',
volume: '100ml'
},
{
id: '6',
name: '宝格丽紫水晶香水',
brand: 'Bulgari',
price: 699,
originalPrice: 899,
image: 'https://picsum.photos/200/200?random=6',
category: 'floral',
tags: ['女士', '花香'],
rating: 4.5,
reviews: 789,
stock: 35,
description: '优雅花香,女性魅力',
volume: '65ml'
}
]
setProducts(mockProducts)
}, [])
const categories = [
{ id: 'all', name: '全部' },
{ id: 'floral', name: '花香调' },
{ id: 'woody', name: '木质调' },
{ id: 'fruity', name: '果香调' },
{ id: 'oriental', name: '东方调' }
]
const filteredProducts = products.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
product.brand.toLowerCase().includes(searchQuery.toLowerCase())
const matchesCategory = selectedCategory === 'all' || product.category === selectedCategory
return matchesSearch && matchesCategory
})
const handleProductSelect = (product: Product) => {
onProductSelect(product)
setIsOpen(false)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
className="w-full justify-start h-auto p-4 border-2 border-dashed border-gray-300 hover:border-purple-400 transition-colors"
disabled={disabled}
>
{selectedProduct ? (
<div className="flex items-center gap-3 w-full">
<img
src={selectedProduct.image}
alt={selectedProduct.name}
className="w-12 h-12 rounded-lg object-cover"
/>
<div className="flex-1 text-left">
<div className="font-semibold text-gray-900">{selectedProduct.name}</div>
<div className="text-sm text-gray-500">{selectedProduct.brand} · {selectedProduct.volume}</div>
<div className="text-sm font-medium text-purple-600">¥{selectedProduct.price}</div>
</div>
<Check className="w-5 h-5 text-green-500" />
</div>
) : (
<div className="flex items-center gap-2 text-gray-500">
<Package className="w-5 h-5" />
<span>{placeholder}</span>
</div>
)}
</Button>
</DialogTrigger>
<DialogContent className="!max-w-4xl max-h-[80vh] overflow-hidden w-[95vw] sm:w-[90vw] md:w-[80vw] lg:w-[70vw]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="w-5 h-5 text-purple-600" />
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* 搜索栏 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
placeholder="搜索商品名称或品牌..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* 分类筛选 */}
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<Button
key={category.id}
variant={selectedCategory === category.id ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category.id)}
className={selectedCategory === category.id ? "bg-purple-600 hover:bg-purple-700" : ""}
>
{category.name}
</Button>
))}
</div>
{/* 商品列表 */}
<div className="max-h-96 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredProducts.map((product) => (
<Card
key={product.id}
className="cursor-pointer hover:shadow-lg transition-all duration-200 hover:scale-[1.02]"
onClick={() => handleProductSelect(product)}
>
<CardContent className="p-4">
<div className="flex gap-4">
<img
src={product.image}
alt={product.name}
className="w-20 h-20 rounded-lg object-cover flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-semibold text-gray-900 truncate">{product.name}</h3>
<p className="text-sm text-gray-500">{product.brand} · {product.volume}</p>
</div>
{selectedProduct?.id === product.id && (
<Check className="w-5 h-5 text-green-500 flex-shrink-0" />
)}
</div>
<div className="flex items-center gap-2 mb-2">
<div className="flex items-center gap-1">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="text-xs text-gray-600">{product.rating}</span>
<span className="text-xs text-gray-400">({product.reviews})</span>
</div>
<Badge variant="outline" className="text-xs">
{product.stock}
</Badge>
</div>
<div className="flex items-center justify-between">
<div className="flex items-baseline gap-2">
<span className="text-lg font-bold text-purple-600">¥{product.price}</span>
{product.originalPrice > product.price && (
<span className="text-xs text-gray-400 line-through">¥{product.originalPrice}</span>
)}
</div>
<div className="flex gap-1">
{product.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
<p className="text-xs text-gray-500 mt-2 line-clamp-2">{product.description}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{filteredProducts.length === 0 && (
<div className="text-center py-12">
<Package className="w-12 h-12 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500"></p>
<p className="text-sm text-gray-400 mt-1"></p>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@ -25,13 +25,13 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-lg border p-4 pr-6 shadow-lg backdrop-blur-sm transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{ {
variants: { variants: {
variant: { variant: {
default: "border bg-background text-foreground", default: "border-gray-200 bg-white/95 text-foreground shadow-xl",
destructive: destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground", "destructive group border-red-200 bg-red-50/95 text-red-900 shadow-xl shadow-red-100/50",
}, },
}, },
defaultVariants: { defaultVariants: {
@ -77,7 +77,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close <ToastPrimitives.Close
ref={ref} ref={ref}
className={cn( className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className className
)} )}
toast-close="" toast-close=""
@ -94,7 +94,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Title <ToastPrimitives.Title
ref={ref} ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)} className={cn("text-sm font-semibold leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ))
@ -106,7 +106,7 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<ToastPrimitives.Description <ToastPrimitives.Description
ref={ref} ref={ref}
className={cn("text-sm opacity-90", className)} className={cn("text-sm opacity-90 mt-1", className)}
{...props} {...props}
/> />
)) ))

View File

@ -36,7 +36,7 @@ function TooltipTrigger({
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, sideOffset = 8,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@ -46,13 +46,20 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", // 优化后的现代化 tooltip 样式
"bg-gray-900/95 text-white backdrop-blur-sm border border-gray-700/50",
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin)",
"rounded-lg px-3 py-2 text-sm font-medium leading-relaxed",
"shadow-lg shadow-black/20",
"text-balance break-words",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="fill-gray-900/95 z-50 size-2 translate-y-[calc(-50%_-_1px)] rotate-45" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) )

View File

@ -1,3 +1,4 @@
import React from 'react'
import { toast } from '@/hooks/use-toast' import { toast } from '@/hooks/use-toast'
export const showSuccessToast = (message: string) => { export const showSuccessToast = (message: string) => {
@ -17,8 +18,9 @@ export const showErrorToast = (message: string) => {
export const showLikeToast = () => { export const showLikeToast = () => {
toast({ toast({
title: "已点赞", title: "点赞成功 ❤️",
description: "您已为这个内容点赞", description: "感谢您的喜欢!",
className: "border-pink-200 bg-gradient-to-r from-pink-50 to-red-50 shadow-lg shadow-pink-100/50",
}) })
} }

View File

@ -7,6 +7,7 @@ const config: Config = {
"./pages/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
extend: { extend: {

149
说明文档.md Normal file
View File

@ -0,0 +1,149 @@
## 一、核心功能模块
1.1 商品管理系统
1.1.1 商品基础信息
2.1商品属性2.1: 名称、描述、价格、原价、图片、分类、标签
2.1库存管理2.1: 库存数量、销量统计、商品状态
2.1分类体系2.1: 花香调(floral)、草本调(herbal)等香调分类
1.1.2 商品展示
2.1多视图模式2.1: 网格视图、列表视图切换
2.1商品卡片2.1: 包含价格、折扣、评分、评论数、点赞数
2.1商品详情2.1: 详细描述、用户评价、相关推荐
1.2 拼团购买系统
1.2.1 拼团配置
2.1阶梯拼团2.1:
3人团7折优惠团长获得200积分
5人团5折优惠团长获得300积分
2.1时间限制2.1: 默认24小时拼团时间
2.1每日限量2.1: 支持设置每日拼团限量
1.2.2 拼团流程
2.1发起拼团2.1: 用户选择商品和拼团规模
2.1邀请参团2.1: 分享拼团链接邀请好友
2.1实时进度2.1: 显示当前参团人数和剩余时间
2.1自动成团2.1: 达到目标人数自动成团
2.1溯源码2.1: 成功拼团生成唯一溯源码
1.2.3 秒杀功能
2.1秒杀配置2.1: 特定时间点(如10:00)开启秒杀
2.1限量抢购2.1: 设置秒杀库存和价格
2.1防刷机制2.1: 避免恶意刷单
1.3 订阅盲盒系统
1.3.1 订阅计划
2.1月度订阅2.1: 每月配送香水盲盒
2.1季度订阅2.1: 每季度配送,享受更多优惠
2.1配送设置2.1: 用户可选择配送频率和日期
1.3.2 盲盒机制
2.1香调排除2.1: 用户可排除不喜欢的香调
2.1解锁系统2.1: 逐步解锁新香调
2.1个性化推荐2.1: 基于用户偏好推荐
1.3.3 会员折扣
2.1探索会员2.1: 95折优惠
2.1品鉴会员2.1: 85折优惠
2.1调香会员2.1: 专属折扣和服务
1.4 会员积分体系
1.4.1 积分获取
2.1购买获得2.1: 消费金额的一定比例
2.1拼团奖励2.1: 团长额外积分奖励
2.1分销奖励2.1: 推广订单获得积分
2.1UGC内容2.1: 发布评价、教程等获得积分
1.4.2 会员等级
2.1探索会员2.1: 无门槛,享受基础权益
2.1品鉴会员2.1: 累计消费满500元享受更多优惠
2.1调香会员2.1: 累计消费满2000元享受VIP服务
1.4.3 积分使用
2.1商品兑换2.1: 积分兑换商品或优惠券
2.1线下体验2.1: 积分兑换线下调香体验
2.1会员升级2.1: 积分加速会员等级提升
1.5 分销推广系统
1.5.1 分销员体系
2.1等级制度2.1: 探索、品鉴、调香三级分销员
2.1佣金比例2.1:
直接推广: 10%佣金
间接推广: 5%佣金
2.1团队管理2.1: 查看下级分销员和业绩
1.5.2 推广工具
2.1分享链接2.1: 生成专属推广链接
2.1素材库2.1: 提供推广图片和文案
2.1数据统计2.1: 实时查看推广效果
1.5.3 佣金结算
2.1结算周期2.1: 订单完成后进入结算流程
2.1提现功能2.1: 支持佣金提现到账户
2.1税务处理2.1: 符合相关税务规定
1.6 用户生成内容系统
1.6.1 内容类型
2.1商品评价2.1: 用户购买后的使用体验分享
2.1使用教程2.1: 香水使用技巧和搭配指南
2.1香水故事2.1: 个人香水使用故事和心得
1.6.2 激励机制
2.1积分奖励2.1: 发布优质内容获得积分奖励
2.1精选推荐2.1: 优质内容获得平台首页推荐
2.1专家认证2.1: 优秀创作者获得专家标识
1.6.3 内容管理
2.1审核机制2.1: 内容发布前进行质量审核
2.1互动功能2.1: 支持点赞、评论、分享功能
2.1数据统计2.1: 查看内容浏览量和互动数据
1.7 订单管理系统
1.7.1 订单流程
2.1下单2.1: 选择商品、数量、配送信息
2.1支付2.1: 支持多种支付方式
2.1发货2.1: 商家发货并提供物流信息
2.1收货2.1: 用户确认收货完成交易
1.7.2 订单状态
2.1待支付2.1: 订单创建等待支付
2.1已支付2.1: 支付完成等待发货
2.1已发货2.1: 商品已发出在途中
2.1已完成2.1: 用户确认收货
2.1已取消2.1: 订单被取消
1.7.3 售后服务
2.1退换货2.1: 支持7天无理由退换货
2.1客服支持2.1: 在线客服解决问题
2.1投诉处理2.1: 完善的投诉处理机制
## 二、用户角色与权限
2.1 普通用户
浏览商品、参与拼团
购买商品、管理订单
参与订阅服务
发布UGC内容
查看积分和会员信息
2.2 分销员
普通用户所有权限
生成推广链接
查看推广数据
管理下级分销员
佣金提现
2.3 管理员
商品管理
订单管理
用户管理
数据统计
系统配置