| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- 'use client';
- import { useRouter } from 'next/navigation';
- import { Calendar, ExternalLink, AlertCircle, CheckCircle, Hourglass, Activity, History } from 'lucide-react';
- import TimeAgo from '@/components/common/TimeAgo';
- import { useLanguage } from '@/lib/i18n/LanguageContext';
- export interface SlotSnapshot {
- id: number;
- country: string;
- city: string;
- visa_type: string;
- availability_status: 'None' | 'Available' | 'Waitlist';
- earliest_date: string | null;
- snapshot_at: string;
- website?: string;
- last_check_at?: string | null;
- routing_key?: string;
- slot_snapshot?: {
- routing_key?: string;
- [key: string]: any;
- };
- }
- export default function CitySlotCard({ data, isBlacklisted = false }: { data: SlotSnapshot; isBlacklisted?: boolean }) {
- const { t, lang } = useLanguage();
- const router = useRouter();
-
- // === 1. 时间计算与解析 ===
- const now = new Date().getTime();
- const THRESHOLD = 6 * 60 * 1000; // 5分钟阈值
- const parseTime = (timeStr: string | null | undefined): number => {
- if (!timeStr) return 0;
- let t = timeStr;
- if (!t.endsWith('Z') && !t.includes('+')) t += 'Z';
- return new Date(t).getTime();
- };
- const lastCheckTime = parseTime(data.last_check_at);
- const snapshotTime = parseTime(data.snapshot_at);
- // === 2. 状态判定逻辑 ===
- const isMonitorStale = (now - lastCheckTime) > THRESHOLD;
- const isSnapshotStale = (now - snapshotTime) > THRESHOLD;
- const isDataStale = isMonitorStale || isSnapshotStale; // 统称数据过期
- const rawStatus = data.availability_status;
- // 定义“有号”:包括具体的 Available 和 Waitlist
- const hasSlot = rawStatus === 'Available' || rawStatus === 'Waitlist';
- // 1. 真正的有号 (Live Slots) -> 数据新鲜且有号
- const isLiveSlot = hasSlot && !isDataStale;
- // 2. 历史记录 (History) -> 曾经有号,但数据旧了
- const isHistorySlot = hasSlot && isDataStale;
- // 格式化日期
- const formatDate = (dateStr: string | null) => {
- if (!dateStr) return 'N/A';
- const d = new Date(dateStr);
- return d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
- month: 'short', day: 'numeric', weekday: 'long'
- });
- };
- // === 3. 动态样式配置 ===
- const getStyleConfig = () => {
-
- // 情况 A: 历史快照 (有号但过期) -> 橙色
- if (isHistorySlot) {
- return {
- bg: 'bg-orange-50/40 border-orange-200',
- badgeBg: 'bg-orange-100 text-orange-700',
- icon: <History size={14} />,
- label: lang === 'zh' ? '历史快照' : 'History/Stale', // 依然提示数据旧
- contentColor: 'text-slate-700 opacity-70',
- isStale: true
- };
- }
- // 情况 B: 真正的有号 (Available OR Waitlist) -> 绿色
- // *修改点:Waitlist 现在完全共享 Available 的绿色样式*
- if (isLiveSlot) {
- return {
- bg: 'bg-green-50/60 border-green-200',
- badgeBg: 'bg-green-100 text-green-700',
- icon: <CheckCircle size={14} />,
- label: t('slots.status_available') || 'Available', // 统称 Available
- contentColor: 'text-green-700',
- isStale: false
- };
- }
- // 情况 C: 监控过期 (Monitor Offline) 且原本就没号
- if (isMonitorStale) {
- return {
- bg: 'bg-slate-50 border-slate-200',
- badgeBg: 'bg-slate-200 text-slate-500',
- icon: <Activity size={14} className="text-slate-400" />,
- label: lang === 'zh' ? '监控离线' : 'Monitor Offline',
- contentColor: 'text-slate-400',
- isStale: true
- };
- }
- // 情况 D: 正常无号 (None) -> 灰色
- return {
- bg: 'bg-white border-slate-200 opacity-90',
- badgeBg: 'bg-slate-100 text-slate-500',
- icon: <AlertCircle size={14} />,
- label: t('slots.status_unavailable') || 'None',
- contentColor: 'text-slate-400',
- isStale: false
- };
- };
- const style = getStyleConfig();
- const slotRoutingKey = data.slot_snapshot?.routing_key || data.routing_key;
- const slotCity = data.city;
- const slotCountry = data.country;
- const slotVisaType = data.visa_type;
- const canSubscribe = Boolean(slotRoutingKey) && !isBlacklisted;
- const handleSubscribe = () => {
- if (!slotRoutingKey) return;
- const params = new URLSearchParams();
- params.set('slot_routing_key', slotRoutingKey);
- if (slotCity) params.set('slot_city', slotCity);
- if (slotCountry) params.set('slot_country', slotCountry);
- if (slotVisaType) params.set('slot_visa_type', slotVisaType);
- router.push(`/create-order/16?${params.toString()}`);
- };
- // 决定中间显示什么内容
- const renderCenterContent = () => {
-
- // 如果是 Waitlist (无论是否过期)
- if (rawStatus === 'Waitlist') {
- return (
- <>
- <Hourglass size={18} />
- <span>Waitlist</span>
- {style.isStale && (
- <span className="text-xs font-normal ml-2 text-orange-600/70 border border-orange-200 px-1 rounded">Exp?</span>
- )}
- </>
- );
- }
- // 如果是 Available (有日期)
- if (rawStatus === 'Available') {
- return (
- <>
- <Calendar size={18} />
- {formatDate(data.earliest_date)}
- {style.isStale && (
- <span className="text-xs font-normal ml-2 text-orange-600/70 border border-orange-200 px-1 rounded">Exp?</span>
- )}
- </>
- );
- }
- // 既不是 Available 也不是 Waitlist (即 None)
- if (style.isStale) {
- return <span className="text-base font-normal text-slate-400">{lang === 'zh' ? '数据已过期' : 'Data Stale'}</span>;
- }
- return <span className="text-base font-normal">暂无名额</span>;
- };
- return (
- <div className={`relative overflow-hidden rounded-xl border p-5 transition-all hover:shadow-md ${style.bg} flex flex-col h-full`}>
-
- {/* 右上角状态标签 */}
- <div className="absolute top-4 right-4">
- <span className={`flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full ${style.badgeBg}`}>
- {style.icon} {style.label}
- </span>
- </div>
- {/* 标题 */}
- <div className="mb-4 pr-24">
- <h3 className="font-bold text-slate-900 text-lg flex items-center gap-2">
- <span className="w-8 h-8 rounded-lg bg-white border border-slate-100 flex items-center justify-center text-sm font-bold shadow-sm">
- {data.country.substring(0, 2).toUpperCase()}
- </span>
- {data.country}
- </h3>
- <div className="flex items-center gap-1.5 text-xs text-slate-500 mt-1.5 pl-1">
- <span className="bg-white/50 px-1.5 py-0.5 rounded border border-slate-200/50">
- {data.visa_type}
- </span>
- </div>
- </div>
- {/* 核心指标区域 */}
- <div className="bg-white/80 rounded-lg p-3 border border-black/5 mb-4 shadow-sm backdrop-blur-sm flex-1 flex flex-col justify-center">
- <p className="text-[10px] text-slate-400 font-bold uppercase tracking-wider mb-1">
- {/* 这里文案也统一,如果是 Waitlist 也可以算作 Slot Info */}
- {rawStatus === 'Waitlist' ? 'Slot Status' : (t('slots.earliest_date') || 'Earliest Slot')}
- </p>
- <div className={`flex items-center gap-2 text-lg font-bold ${style.contentColor}`}>
- {renderCenterContent()}
- </div>
- </div>
- {/* 底部:监控状态 & 链接 */}
- <div className="pt-3 border-t border-slate-200/50 space-y-1.5">
-
- <div className="flex items-center justify-between text-xs">
- <span className="text-slate-400 flex items-center gap-1">
- <Activity size={12} className={!isMonitorStale ? "text-green-500" : "text-slate-300"} />
- {lang === 'zh' ? '监控状态' : 'Monitor'}
- </span>
- <div className="flex items-center gap-1">
- {/*
- 只要是有号(日期或Waitlist)且新鲜,就闪烁绿灯
- */}
- {isLiveSlot && (
- <span className="relative flex h-2 w-2 mr-1">
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
- <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
- </span>
- )}
- <span className={`font-medium ${!isMonitorStale ? 'text-green-700' : 'text-slate-400'}`}>
- <TimeAgo date={data.last_check_at} />
- </span>
- </div>
- </div>
-
- {/* 调试用:历史快照时间 */}
- {isHistorySlot && (
- <div className="flex items-center justify-between text-[10px] text-orange-400 px-1">
- <span>Last seen:</span>
- <TimeAgo date={data.snapshot_at} />
- </div>
- )}
- {data.website && (
- <div className="pt-2 mt-2 border-t border-dashed border-slate-200">
- <a
- href={data.website}
- target="_blank"
- rel="noopener noreferrer"
- className="w-full flex items-center justify-center gap-1 text-blue-600 hover:text-blue-800 text-xs font-bold transition bg-blue-50/50 py-1.5 rounded-md hover:bg-blue-100"
- >
- {t('common.book_now') || '前往预约'} <ExternalLink size={12} />
- </a>
- </div>
- )}
- <div className="pt-2 mt-2 border-t border-dashed border-slate-200">
- <button
- onClick={handleSubscribe}
- disabled={!canSubscribe}
- className={`w-full px-3 py-2 rounded-lg text-sm font-bold transition ${canSubscribe ? 'bg-slate-900 text-white hover:bg-slate-800' : 'bg-slate-100 text-slate-500 cursor-not-allowed'}`}
- >
- {t('slots.subscribe_notification') || 'Subscribe for updates'}
- </button>
- {!canSubscribe && !isBlacklisted && (
- <p className="text-xs text-slate-400 mt-1">
- {t('slots.subscribe_unavailable') || 'Routing info unavailable'}
- </p>
- )}
- </div>
- </div>
- </div>
- );
- }
|