|
@@ -2,13 +2,22 @@
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
|
import { useState } from 'react';
|
|
|
import api from '@/lib/api';
|
|
import api from '@/lib/api';
|
|
|
-import { Search, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react';
|
|
|
|
|
|
|
+import { Search, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle, ExternalLink } from 'lucide-react';
|
|
|
import { useLanguage } from '@/lib/i18n/LanguageContext';
|
|
import { useLanguage } from '@/lib/i18n/LanguageContext';
|
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
|
|
|
|
|
|
-// === 类型定义 (保持不变) ===
|
|
|
|
|
-interface TimeSlot { time: string; label?: string; }
|
|
|
|
|
-interface DayAvailability { date: string; times: TimeSlot[]; }
|
|
|
|
|
|
|
+// === 类型定义 ===
|
|
|
|
|
+
|
|
|
|
|
+interface TimeSlot {
|
|
|
|
|
+ time: string;
|
|
|
|
|
+ label?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DayAvailability {
|
|
|
|
|
+ date: string;
|
|
|
|
|
+ times?: TimeSlot[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
interface SlotSnapshot {
|
|
interface SlotSnapshot {
|
|
|
id: number;
|
|
id: number;
|
|
|
country: string;
|
|
country: string;
|
|
@@ -17,6 +26,8 @@ interface SlotSnapshot {
|
|
|
availability_status: 'None' | 'Available' | 'Waitlist';
|
|
availability_status: 'None' | 'Available' | 'Waitlist';
|
|
|
earliest_date: string | null;
|
|
earliest_date: string | null;
|
|
|
snapshot_at: string;
|
|
snapshot_at: string;
|
|
|
|
|
+ // 新增 website 字段
|
|
|
|
|
+ website?: string;
|
|
|
availability: DayAvailability[];
|
|
availability: DayAvailability[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -29,7 +40,7 @@ export default function SlotQueryPage() {
|
|
|
const [city, setCity] = useState('Dublin');
|
|
const [city, setCity] = useState('Dublin');
|
|
|
const [visaType, setVisaType] = useState('Tourist');
|
|
const [visaType, setVisaType] = useState('Tourist');
|
|
|
|
|
|
|
|
- // ... (options 和 fetchSlots 保持不变) ...
|
|
|
|
|
|
|
+ // ... (options 保持不变)
|
|
|
const options = {
|
|
const options = {
|
|
|
countries: ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'],
|
|
countries: ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'],
|
|
|
cities: ['Dublin','Edinburgh','London','Manchester','Melbourne','Montreal','Singapore','Sydney','Toronto'],
|
|
cities: ['Dublin','Edinburgh','London','Manchester','Melbourne','Montreal','Singapore','Sydney','Toronto'],
|
|
@@ -60,7 +71,6 @@ export default function SlotQueryPage() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- // 调整 1: 移动端 Padding
|
|
|
|
|
<div className="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-6">
|
|
<div className="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-6">
|
|
|
<div className="max-w-5xl mx-auto">
|
|
<div className="max-w-5xl mx-auto">
|
|
|
|
|
|
|
@@ -72,7 +82,6 @@ export default function SlotQueryPage() {
|
|
|
|
|
|
|
|
{/* 筛选区 */}
|
|
{/* 筛选区 */}
|
|
|
<div className="bg-white p-5 md:p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
|
|
<div className="bg-white p-5 md:p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
|
|
|
- {/* 调整 2: 移动端单列布局 */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
<div>
|
|
<div>
|
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.country')}</label>
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.country')}</label>
|
|
@@ -104,7 +113,6 @@ export default function SlotQueryPage() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="mt-6 flex justify-end">
|
|
<div className="mt-6 flex justify-end">
|
|
|
- {/* 调整 3: 移动端按钮全宽 */}
|
|
|
|
|
<button
|
|
<button
|
|
|
onClick={fetchSlots}
|
|
onClick={fetchSlots}
|
|
|
disabled={loading}
|
|
disabled={loading}
|
|
@@ -148,34 +156,71 @@ export default function SlotQueryPage() {
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 具体的 Slot 列表 */}
|
|
|
|
|
|
|
+ {/*
|
|
|
|
|
+ === 列表渲染逻辑优化 ===
|
|
|
|
|
+ 如果没有具体时间段,不要显示空网格,而是显示行动按钮或紧凑状态
|
|
|
|
|
+ */}
|
|
|
{snapshot.availability && snapshot.availability.length > 0 && (
|
|
{snapshot.availability && snapshot.availability.length > 0 && (
|
|
|
- // 调整 4: 移动端单列卡片
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
- {snapshot.availability.map((day, idx) => (
|
|
|
|
|
- <div key={idx} className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition">
|
|
|
|
|
- <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex justify-between items-center">
|
|
|
|
|
- <div className="flex items-center gap-2 font-bold text-slate-700">
|
|
|
|
|
- <Calendar size={16} className="text-blue-500" />
|
|
|
|
|
- {formatDate(day.date)}
|
|
|
|
|
- </div>
|
|
|
|
|
- <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">
|
|
|
|
|
- {day.times.length} {t('slots.slots_count')}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="p-4 grid grid-cols-2 gap-2">
|
|
|
|
|
- {day.times.map((slot, tIdx) => (
|
|
|
|
|
- <div key={tIdx} className="text-sm border border-slate-100 rounded p-2 text-center hover:border-blue-300 hover:bg-blue-50 transition cursor-default">
|
|
|
|
|
- <div className="font-mono font-bold text-slate-800">{slot.time}</div>
|
|
|
|
|
- {slot.label && (
|
|
|
|
|
- <div className="text-[10px] text-orange-500 font-medium mt-0.5">{slot.label}</div>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {snapshot.availability.map((day, idx) => {
|
|
|
|
|
+ const times = day.times || [];
|
|
|
|
|
+ const hasTimes = times.length > 0;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={idx} className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition flex flex-col">
|
|
|
|
|
+
|
|
|
|
|
+ {/* Card Header */}
|
|
|
|
|
+ <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex justify-between items-center">
|
|
|
|
|
+ <div className="flex items-center gap-2 font-bold text-slate-700">
|
|
|
|
|
+ <Calendar size={16} className="text-blue-500" />
|
|
|
|
|
+ {formatDate(day.date)}
|
|
|
</div>
|
|
</div>
|
|
|
- ))}
|
|
|
|
|
|
|
+ {/* 优化徽章:有时间显示数量,没时间显示 Available */}
|
|
|
|
|
+ <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
|
|
|
|
+ hasTimes ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {hasTimes ? `${times.length} ${t('slots.slots_count')}` : t('slots.status_available')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Card Body */}
|
|
|
|
|
+ <div className="p-4 flex-1 flex flex-col justify-center">
|
|
|
|
|
+ {hasTimes ? (
|
|
|
|
|
+ // 场景 A: 有具体时间段
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-2">
|
|
|
|
|
+ {times.map((slot, tIdx) => (
|
|
|
|
|
+ <div key={tIdx} className="text-sm border border-slate-100 rounded p-2 text-center hover:border-blue-300 hover:bg-blue-50 transition cursor-default">
|
|
|
|
|
+ <div className="font-mono font-bold text-slate-800">{slot.time}</div>
|
|
|
|
|
+ {slot.label && (
|
|
|
|
|
+ <div className="text-[10px] text-orange-500 font-medium mt-0.5">{slot.label}</div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ // 场景 B: 无具体时间段 (Date Only)
|
|
|
|
|
+ // 显示更友好的提示或跳转按钮
|
|
|
|
|
+ <div className="text-center space-y-3">
|
|
|
|
|
+ <p className="text-xs text-slate-500">
|
|
|
|
|
+ {/* 翻译:该日期已开放预约,具体时间请前往官网查看 */}
|
|
|
|
|
+ {lang === 'zh' ? '该日期已开放预约,具体时间请前往官网查看' : 'Slots available. Please check details on the official website.'}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ {snapshot.website && (
|
|
|
|
|
+ <a
|
|
|
|
|
+ href={snapshot.website}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ rel="noopener noreferrer"
|
|
|
|
|
+ className="flex items-center justify-center gap-2 w-full py-2 bg-blue-600 text-white rounded-lg text-sm font-bold hover:bg-blue-700 transition"
|
|
|
|
|
+ >
|
|
|
|
|
+ {lang === 'zh' ? '前往官网预约' : 'Book on Website'} <ExternalLink size={14} />
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|