| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 'use client';
- import { useEffect, useState } from 'react';
- import { useRouter } from 'next/navigation';
- import api from '@/lib/api';
- import { Loader2, Search, MapPin, Filter, X, Globe } from 'lucide-react';
- import Pagination from '@/components/common/Pagination';
- import { useLanguage } from '@/lib/i18n/LanguageContext';
- interface Product {
- id: number;
- title: string;
- price_amount: number;
- price_currency: string;
- description: string;
- country: string;
- city: string;
- visa_type: string;
- provider: string;
- }
- export default function ServiceList() {
- const router = useRouter();
- const { t } = useLanguage();
-
- const [products, setProducts] = useState<Product[]>([]);
- const [loading, setLoading] = useState<boolean>(true);
- const [error, setError] = useState<string>('');
- const [keyword, setKeyword] = useState('');
- const [selectedCountry, setSelectedCountry] = useState('');
- const [selectedType, setSelectedType] = useState('');
-
- const [page, setPage] = useState(1);
- const [pageSize] = useState(9);
- const [total, setTotal] = useState(0);
- const countries = ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'];
- const visaTypes = ['Tourist','Business','Family','Student','Work','Transit','e-Visa'];
- useEffect(() => {
- fetchProducts(1);
- }, []);
- const fetchProducts = async (targetPage: number) => {
- setLoading(true);
- setError('');
-
- try {
- const res = await api.get('/api/vas/product/list', {
- params: {
- page: targetPage,
- size: pageSize,
- keyword: keyword,
- country: selectedCountry,
- visa_type: selectedType
- }
- });
- const data = res.data.data || {};
-
- if (Array.isArray(data)) {
- let filtered = data;
- if (keyword) {
- const lowerKey = keyword.toLowerCase();
- filtered = filtered.filter((p: any) =>
- p.title.toLowerCase().includes(lowerKey) ||
- p.city?.toLowerCase().includes(lowerKey) ||
- p.country?.toLowerCase().includes(lowerKey)
- );
- }
- if (selectedCountry) filtered = filtered.filter((p: any) => p.country === selectedCountry);
- if (selectedType) filtered = filtered.filter((p: any) => p.visa_type === selectedType);
-
- setProducts(filtered);
- setTotal(filtered.length);
- } else {
- setProducts(data.items || []);
- setTotal(data.total || 0);
- }
-
- setPage(targetPage);
- } catch (err) {
- console.warn("API Error, using mock data");
- setProducts([]);
- setTotal(0);
- } finally {
- setLoading(false);
- }
- };
- const handleSearch = () => fetchProducts(1);
- const handleReset = () => {
- setKeyword('');
- setSelectedCountry('');
- setSelectedType('');
- fetchProducts(1);
- };
- const handleOrderClick = (id: number) => {
- router.push(`/create-order/${id}`);
- };
- return (
- <div className="space-y-8">
-
- {/* === 筛选工具栏:响应式调整 === */}
- <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
-
- {/* 搜索框 */}
- <div className="relative md:col-span-1">
- <input
- type="text"
- placeholder={t('services.search_placeholder')}
- className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
- value={keyword}
- onChange={(e) => setKeyword(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
- />
- <Search size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
- </div>
- {/* 国家筛选 */}
- <div className="relative md:col-span-1">
- <select
- className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
- value={selectedCountry}
- onChange={(e) => setSelectedCountry(e.target.value)}
- >
- <option value="">{t('services.all_countries')}</option>
- {countries.map(c => <option key={c} value={c}>{c}</option>)}
- </select>
- <Globe size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
- </div>
- {/* 类型筛选 */}
- <div className="relative md:col-span-1">
- <select
- className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
- value={selectedType}
- onChange={(e) => setSelectedType(e.target.value)}
- >
- <option value="">{t('services.all_types')}</option>
- {visaTypes.map(t => <option key={t} value={t}>{t}</option>)}
- </select>
- <Filter size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
- </div>
- {/* 按钮组 */}
- <div className="flex gap-2 md:col-span-1">
- <button
- onClick={handleSearch}
- className="flex-1 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition shadow-sm flex items-center justify-center gap-2 py-3 md:py-2.5 active:scale-95"
- >
- <Search size={16} /> {t('common.search')}
- </button>
- {(keyword || selectedCountry || selectedType) && (
- <button
- onClick={handleReset}
- className="px-4 border border-slate-300 text-slate-500 rounded-lg hover:bg-slate-50 hover:text-red-500 transition py-3 md:py-2.5 active:scale-95"
- title={t('services.reset_filter')}
- >
- <X size={18} />
- </button>
- )}
- </div>
- </div>
- </div>
- {/* === 商品列表:响应式调整 === */}
- {loading ? (
- <div className="flex justify-center p-20">
- <Loader2 className="animate-spin text-blue-600 w-8 h-8" />
- </div>
- ) : error ? (
- <div className="text-center text-red-500 p-10 bg-red-50 rounded-xl border border-red-100">{error}</div>
- ) : products.length === 0 ? (
- <div className="text-center py-20 bg-white rounded-xl border border-dashed border-slate-200">
- <div className="mx-auto w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
- <Search className="text-slate-300" size={32} />
- </div>
- <h3 className="text-slate-900 font-bold mb-1">{t('services.no_result_title')}</h3>
- <p className="text-slate-500 text-sm">{t('services.no_result_desc')}</p>
- </div>
- ) : (
- // 修改:移动端 grid-cols-1,平板 grid-cols-2,桌面 grid-cols-3
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
- {products.map((item) => (
- <div key={item.id} className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:border-blue-300 hover:shadow-md transition flex flex-col group h-full">
-
- <div className="flex justify-between items-start mb-4">
- <div className="flex flex-wrap gap-2">
- <div className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded text-xs font-bold flex items-center gap-1.5 border border-blue-100">
- <MapPin size={12} className="flex-shrink-0" />
- <span>{item.country}</span>
- {item.city && (
- <>
- <span className="text-blue-300">/</span>
- <span className="text-blue-800">{item.city}</span>
- </>
- )}
- </div>
- <div className="bg-slate-100 text-slate-600 px-2.5 py-1 rounded text-xs font-medium border border-slate-200">
- {item.visa_type}
- </div>
- </div>
- </div>
-
- <h2 className="text-lg font-bold mb-2 text-slate-900 group-hover:text-blue-600 transition-colors line-clamp-2 leading-snug" title={item.title}>
- {item.title}
- </h2>
-
- <p className="text-slate-500 mb-6 text-sm flex-grow line-clamp-2">
- {item.description || t('services.no_desc')}
- </p>
-
- <div className="flex items-center justify-between mt-auto pt-4 border-t border-slate-50">
- <div className="flex flex-col">
- <span className="text-xs text-slate-400 font-medium">{t('services.service_fee')}</span>
- <span className="text-lg font-bold text-slate-900 leading-none">
- <span className="text-xs font-normal mr-0.5">{item.price_currency === 'CNY' ? '¥' : item.price_currency}</span>
- {(item.price_amount / 100).toLocaleString()}
- </span>
- </div>
- <button
- onClick={() => handleOrderClick(item.id)}
- className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-bold text-sm shadow-sm shadow-blue-200 active:scale-95"
- >
- {t('services.apply_now')}
- </button>
- </div>
- </div>
- ))}
- </div>
- )}
- <Pagination
- currentPage={page}
- total={total}
- pageSize={pageSize}
- onPageChange={(p) => fetchProducts(p)}
- />
- </div>
- );
- }
|