feat: upgrade deps; fix lint for test pages
This commit is contained in:
@@ -3,18 +3,19 @@ import reactDom from 'eslint-plugin-react-dom'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import reactX from 'eslint-plugin-react-x'
|
import reactX from 'eslint-plugin-react-x'
|
||||||
import { globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
export default tseslint.config([
|
export default defineConfig([
|
||||||
globalIgnores(['dist', 'test-pages']),
|
globalIgnores(['dist', 'test-pages']),
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
reactHooks.configs['recommended-latest'],
|
// reactHooks.configs['recommended-latest'],
|
||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
|||||||
1259
package-lock.json
generated
1259
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -50,46 +50,43 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^2.0.22",
|
"@ai-sdk/openai": "^2.0.49",
|
||||||
"ai": "^5.0.26",
|
"ai": "^5.0.68",
|
||||||
"ai-motion": "^0.4.7",
|
"ai-motion": "^0.4.7",
|
||||||
"chalk": "^5.6.0",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.1.3"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.1.0",
|
"@commitlint/cli": "^20.1.0",
|
||||||
"@commitlint/config-conventional": "^20.0.0",
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
"@eslint/js": "^9.30.1",
|
"@eslint/js": "^9.37.0",
|
||||||
"@microsoft/api-extractor": "^7.52.13",
|
"@microsoft/api-extractor": "^7.53.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^9.31.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-dom": "^2.0.2",
|
"eslint-plugin-react-dom": "^2.0.6",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.23",
|
||||||
"eslint-plugin-react-x": "^2.0.2",
|
"eslint-plugin-react-x": "^2.0.6",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.1.2",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.2.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.14",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.39.0",
|
"typescript-eslint": "^8.46.0",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.1.9",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||||
"wouter": "^3.7.1"
|
"wouter": "^3.7.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"zod": "^4.1.3"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||||
"npx prettier --write --ignore-unknown",
|
"npx prettier --write --ignore-unknown",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Link } from 'wouter'
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
@@ -17,7 +17,7 @@ const generateProducts = (count: number): Product[] => {
|
|||||||
const categories = ['手机', '电脑', '平板', '耳机', '手表', '相机']
|
const categories = ['手机', '电脑', '平板', '耳机', '手表', '相机']
|
||||||
const brands = ['Apple', 'Samsung', 'Huawei', 'Xiaomi', 'Sony', 'Dell']
|
const brands = ['Apple', 'Samsung', 'Huawei', 'Xiaomi', 'Sony', 'Dell']
|
||||||
const adjectives = ['Pro', 'Max', 'Ultra', 'Plus', 'Air', 'Mini']
|
const adjectives = ['Pro', 'Max', 'Ultra', 'Plus', 'Air', 'Mini']
|
||||||
|
|
||||||
return Array.from({ length: count }, (_, i) => ({
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
name: `${brands[i % brands.length]} ${categories[i % categories.length]} ${adjectives[i % adjectives.length]}`,
|
name: `${brands[i % brands.length]} ${categories[i % categories.length]} ${adjectives[i % adjectives.length]}`,
|
||||||
@@ -27,13 +27,177 @@ const generateProducts = (count: number): Product[] => {
|
|||||||
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
|
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
|
||||||
image: `https://picsum.photos/200/200?random=${i}`,
|
image: `https://picsum.photos/200/200?random=${i}`,
|
||||||
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
|
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
|
||||||
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1)
|
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loading skeleton component
|
||||||
|
const LoadingSkeleton = () => (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
{Array.from({ length: 12 }, (_, i) => `skeleton-item-${i}`).map((id) => (
|
||||||
|
<div key={id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
|
||||||
|
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
|
||||||
|
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
|
||||||
|
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Product card component
|
||||||
|
const ProductCard = ({ product }: { product: Product }) => (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-48 object-cover rounded-lg"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
|
||||||
|
{product.tags.map((tag) => (
|
||||||
|
<span key={tag} className="bg-red-500 text-white text-xs px-2 py-1 rounded-full">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
¥{product.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-400">★</span>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">{product.rating}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">库存: {product.stock}</span>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">{product.category}</span>
|
||||||
|
</div>
|
||||||
|
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
||||||
|
加入购物车
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Product list item component
|
||||||
|
const ProductListItem = ({ product }: { product: Product }) => (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-20 h-20 object-cover rounded-lg flex-shrink-0"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{product.name}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<span>{product.category}</span>
|
||||||
|
<span>库存: {product.stock}</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="text-yellow-400">★</span>
|
||||||
|
<span className="ml-1">{product.rating}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
¥{product.price.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
||||||
|
加入购物车
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pagination component
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
startIndex: number
|
||||||
|
endIndex: number
|
||||||
|
totalItems: number
|
||||||
|
onPageChange: (page: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination = ({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
startIndex,
|
||||||
|
endIndex,
|
||||||
|
totalItems,
|
||||||
|
onPageChange,
|
||||||
|
}: PaginationProps) => {
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pages = []
|
||||||
|
const maxVisible = 5
|
||||||
|
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
||||||
|
const end = Math.min(totalPages, start + maxVisible - 1)
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mt-8">
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
显示 {startIndex + 1}-{Math.min(endIndex, totalItems)} 条, 共 {totalItems} 条结果
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
{getPageNumbers().map((page) => (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => onPageChange(page)}
|
||||||
|
className={`px-3 py-2 text-sm font-medium rounded-md ${
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ListTestPage() {
|
export default function ListTestPage() {
|
||||||
const [products, setProducts] = useState<Product[]>([])
|
const [products, setProducts] = useState<Product[]>([])
|
||||||
const [filteredProducts, setFilteredProducts] = useState<Product[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [selectedCategory, setSelectedCategory] = useState('全部')
|
const [selectedCategory, setSelectedCategory] = useState('全部')
|
||||||
@@ -45,34 +209,55 @@ export default function ListTestPage() {
|
|||||||
|
|
||||||
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
|
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
|
||||||
|
|
||||||
|
// Helper to set filters and reset page
|
||||||
|
const handleSearchChange = (term: string) => {
|
||||||
|
setSearchTerm(term)
|
||||||
|
setCurrentPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCategoryChange = (category: string) => {
|
||||||
|
setSelectedCategory(category)
|
||||||
|
setCurrentPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortChange = (sort: string) => {
|
||||||
|
setSortBy(sort)
|
||||||
|
setCurrentPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSortOrderChange = (order: 'asc' | 'desc') => {
|
||||||
|
setSortOrder(order)
|
||||||
|
setCurrentPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
// 模拟数据加载
|
// 模拟数据加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||||
const data = generateProducts(150)
|
const data = generateProducts(150)
|
||||||
setProducts(data)
|
setProducts(data)
|
||||||
setFilteredProducts(data)
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
loadData()
|
loadData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 搜索和过滤
|
// 搜索和过滤 - Use useMemo to compute filtered products
|
||||||
useEffect(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
let filtered = products
|
let filtered = [...products]
|
||||||
|
|
||||||
// 按类别过滤
|
// 按类别过滤
|
||||||
if (selectedCategory !== '全部') {
|
if (selectedCategory !== '全部') {
|
||||||
filtered = filtered.filter(product => product.category === selectedCategory)
|
filtered = filtered.filter((product) => product.category === selectedCategory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按搜索词过滤
|
// 按搜索词过滤
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
filtered = filtered.filter(product =>
|
filtered = filtered.filter(
|
||||||
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(product) =>
|
||||||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
|
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
product.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +278,7 @@ export default function ListTestPage() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setFilteredProducts(filtered)
|
return filtered
|
||||||
setCurrentPage(1) // 重置到第一页
|
|
||||||
}, [products, searchTerm, selectedCategory, sortBy, sortOrder])
|
}, [products, searchTerm, selectedCategory, sortBy, sortOrder])
|
||||||
|
|
||||||
// 分页计算
|
// 分页计算
|
||||||
@@ -109,173 +293,12 @@ export default function ListTestPage() {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingSkeleton = () => (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
||||||
{Array.from({ length: 12 }).map((_, i) => (
|
|
||||||
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
|
|
||||||
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ProductCard = ({ product }: { product: Product }) => (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
|
|
||||||
<div className="relative mb-4">
|
|
||||||
<img
|
|
||||||
src={product.image}
|
|
||||||
alt={product.name}
|
|
||||||
className="w-full h-48 object-cover rounded-lg"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
|
|
||||||
{product.tags.map((tag, index) => (
|
|
||||||
<span
|
|
||||||
key={index}
|
|
||||||
className="bg-red-500 text-white text-xs px-2 py-1 rounded-full"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
|
|
||||||
{product.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
|
|
||||||
{product.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
¥{product.price.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-yellow-400">★</span>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">
|
|
||||||
{product.rating}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
库存: {product.stock}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{product.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
|
||||||
加入购物车
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ProductListItem = ({ product }: { product: Product }) => (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
|
|
||||||
<img
|
|
||||||
src={product.image}
|
|
||||||
alt={product.name}
|
|
||||||
className="w-20 h-20 object-cover rounded-lg flex-shrink-0"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
|
|
||||||
{product.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
|
|
||||||
{product.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<span>{product.category}</span>
|
|
||||||
<span>库存: {product.stock}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="text-yellow-400">★</span>
|
|
||||||
<span className="ml-1">{product.rating}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
¥{product.price.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
|
|
||||||
加入购物车
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Pagination = () => {
|
|
||||||
const getPageNumbers = () => {
|
|
||||||
const pages = []
|
|
||||||
const maxVisible = 5
|
|
||||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
|
||||||
const end = Math.min(totalPages, start + maxVisible - 1)
|
|
||||||
|
|
||||||
if (end - start + 1 < maxVisible) {
|
|
||||||
start = Math.max(1, end - maxVisible + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
pages.push(i)
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between mt-8">
|
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
显示 {startIndex + 1}-{Math.min(endIndex, filteredProducts.length)} 条,
|
|
||||||
共 {filteredProducts.length} 条结果
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
上一页
|
|
||||||
</button>
|
|
||||||
{getPageNumbers().map(page => (
|
|
||||||
<button
|
|
||||||
key={page}
|
|
||||||
onClick={() => handlePageChange(page)}
|
|
||||||
className={`px-3 py-2 text-sm font-medium rounded-md ${
|
|
||||||
page === currentPage
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
下一页
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">产品列表测试</h1>
|
||||||
产品列表测试
|
<p className="text-gray-600 dark:text-gray-300">测试搜索、过滤、排序、分页和滚动功能</p>
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
测试搜索、过滤、排序、分页和滚动功能
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 搜索和过滤栏 */}
|
{/* 搜索和过滤栏 */}
|
||||||
@@ -289,7 +312,7 @@ export default function ListTestPage() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
placeholder="输入产品名称或描述..."
|
placeholder="输入产品名称或描述..."
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
/>
|
/>
|
||||||
@@ -302,11 +325,13 @@ export default function ListTestPage() {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
onChange={(e) => handleCategoryChange(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
>
|
>
|
||||||
{categories.map(category => (
|
{categories.map((category) => (
|
||||||
<option key={category} value={category}>{category}</option>
|
<option key={category} value={category}>
|
||||||
|
{category}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +343,7 @@ export default function ListTestPage() {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
onChange={(e) => handleSortChange(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
>
|
>
|
||||||
<option value="name">名称</option>
|
<option value="name">名称</option>
|
||||||
@@ -335,7 +360,7 @@ export default function ListTestPage() {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={sortOrder}
|
value={sortOrder}
|
||||||
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
|
onChange={(e) => handleSortOrderChange(e.target.value as 'asc' | 'desc')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||||
>
|
>
|
||||||
<option value="asc">升序</option>
|
<option value="asc">升序</option>
|
||||||
@@ -362,9 +387,7 @@ export default function ListTestPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">视图:</span>
|
||||||
视图:
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('grid')}
|
||||||
className={`p-2 rounded-md ${
|
className={`p-2 rounded-md ${
|
||||||
@@ -402,27 +425,32 @@ export default function ListTestPage() {
|
|||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
没有找到匹配的产品
|
没有找到匹配的产品
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<p className="text-gray-600 dark:text-gray-300">请尝试调整搜索条件或过滤器</p>
|
||||||
请尝试调整搜索条件或过滤器
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{viewMode === 'grid' ? (
|
{viewMode === 'grid' ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
{currentProducts.map(product => (
|
{currentProducts.map((product: Product) => (
|
||||||
<ProductCard key={product.id} product={product} />
|
<ProductCard key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{currentProducts.map(product => (
|
{currentProducts.map((product: Product) => (
|
||||||
<ProductListItem key={product.id} product={product} />
|
<ProductListItem key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Pagination />
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
startIndex={startIndex}
|
||||||
|
endIndex={endIndex}
|
||||||
|
totalItems={filteredProducts.length}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -432,7 +460,12 @@ export default function ListTestPage() {
|
|||||||
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
|
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M5 10l7-7m0 0l7 7m-7-7v18"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user