295 lines
9.8 KiB
TypeScript
295 lines
9.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { ComponentCard } from '@/components/ComponentCard'
|
|
import { Search, Filter } from 'lucide-react'
|
|
|
|
interface Component {
|
|
id: string
|
|
name: string
|
|
brand: string
|
|
model: string
|
|
price: number
|
|
description?: string
|
|
imageUrl?: string
|
|
stock: number
|
|
componentType: {
|
|
id: string
|
|
name: string
|
|
}
|
|
}
|
|
|
|
interface ComponentType {
|
|
id: string
|
|
name: string
|
|
_count: {
|
|
components: number
|
|
}
|
|
}
|
|
|
|
interface Pagination {
|
|
page: number
|
|
limit: number
|
|
total: number
|
|
totalPages: number
|
|
hasNext: boolean
|
|
hasPrev: boolean
|
|
}
|
|
|
|
export default function ComponentsPage() {
|
|
const [components, setComponents] = useState<Component[]>([])
|
|
const [componentTypes, setComponentTypes] = useState<ComponentType[]>([])
|
|
const [pagination, setPagination] = useState<Pagination>({
|
|
page: 1,
|
|
limit: 12,
|
|
total: 0,
|
|
totalPages: 0,
|
|
hasNext: false,
|
|
hasPrev: false
|
|
})
|
|
const [filters, setFilters] = useState({
|
|
search: '',
|
|
type: '',
|
|
brand: '',
|
|
minPrice: '',
|
|
maxPrice: ''
|
|
})
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [showFilters, setShowFilters] = useState(false)
|
|
|
|
useEffect(() => {
|
|
fetchComponentTypes()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchComponents()
|
|
}, [filters, pagination.page])
|
|
|
|
const fetchComponentTypes = async () => {
|
|
try {
|
|
const response = await fetch('/api/component-types')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setComponentTypes(data)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch component types:', error)
|
|
}
|
|
}
|
|
|
|
const fetchComponents = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const searchParams = new URLSearchParams()
|
|
if (filters.search) searchParams.set('search', filters.search)
|
|
if (filters.type) searchParams.set('type', filters.type)
|
|
if (filters.brand) searchParams.set('brand', filters.brand)
|
|
if (filters.minPrice) searchParams.set('minPrice', filters.minPrice)
|
|
if (filters.maxPrice) searchParams.set('maxPrice', filters.maxPrice)
|
|
searchParams.set('page', pagination.page.toString())
|
|
searchParams.set('limit', pagination.limit.toString())
|
|
|
|
const response = await fetch(`/api/components?${searchParams}`)
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setComponents(data.components)
|
|
setPagination(data.pagination)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch components:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleFilterChange = (key: string, value: string) => {
|
|
setFilters({ ...filters, [key]: value })
|
|
setPagination({ ...pagination, page: 1 })
|
|
}
|
|
|
|
const handlePageChange = (newPage: number) => {
|
|
setPagination({ ...pagination, page: newPage })
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
const resetFilters = () => {
|
|
setFilters({
|
|
search: '',
|
|
type: '',
|
|
brand: '',
|
|
minPrice: '',
|
|
maxPrice: ''
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">电脑配件商城</h1>
|
|
|
|
{/* Search Bar */}
|
|
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
|
<input
|
|
type="text"
|
|
placeholder="搜索配件名称、品牌、型号..."
|
|
value={filters.search}
|
|
onChange={(e) => handleFilterChange('search', e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
>
|
|
<Filter className="h-5 w-5" />
|
|
筛选
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
{showFilters && (
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border mb-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
配件类型
|
|
</label>
|
|
<select
|
|
value={filters.type}
|
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="">全部类型</option>
|
|
{componentTypes.map((type) => (
|
|
<option key={type.id} value={type.id}>
|
|
{type.name} ({type._count.components})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
品牌
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="品牌名称"
|
|
value={filters.brand}
|
|
onChange={(e) => handleFilterChange('brand', e.target.value)}
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
最低价格
|
|
</label>
|
|
<input
|
|
type="number"
|
|
placeholder="0"
|
|
value={filters.minPrice}
|
|
onChange={(e) => handleFilterChange('minPrice', e.target.value)}
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
最高价格
|
|
</label>
|
|
<input
|
|
type="number"
|
|
placeholder="10000"
|
|
value={filters.maxPrice}
|
|
onChange={(e) => handleFilterChange('maxPrice', e.target.value)}
|
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 flex justify-end">
|
|
<button
|
|
onClick={resetFilters}
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
|
>
|
|
重置筛选
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Results */}
|
|
<div className="mb-6">
|
|
<p className="text-gray-600">
|
|
共找到 {pagination.total} 个配件
|
|
</p>
|
|
</div>
|
|
|
|
{/* Loading */}
|
|
{isLoading ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Components Grid */}
|
|
{components.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-8">
|
|
{components.map((component) => (
|
|
<ComponentCard key={component.id} component={component} />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500 text-lg">没有找到符合条件的配件</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{pagination.totalPages > 1 && (
|
|
<div className="flex justify-center items-center space-x-2">
|
|
<button
|
|
onClick={() => handlePageChange(pagination.page - 1)}
|
|
disabled={!pagination.hasPrev}
|
|
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
|
>
|
|
上一页
|
|
</button>
|
|
|
|
<div className="flex space-x-1">
|
|
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => (
|
|
<button
|
|
key={page}
|
|
onClick={() => handlePageChange(page)}
|
|
className={`px-3 py-2 border rounded-md ${
|
|
page === pagination.page
|
|
? 'bg-blue-600 text-white border-blue-600'
|
|
: 'border-gray-300 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{page}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handlePageChange(pagination.page + 1)}
|
|
disabled={!pagination.hasNext}
|
|
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
|
>
|
|
下一页
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|