jerry 3 hónapja
szülő
commit
518c51083f

+ 16 - 1
src/app/admin/orders/page.tsx

@@ -3,11 +3,12 @@
 import { useState, useEffect } from 'react';
 import { useRouter } from 'next/navigation';
 import api from '@/lib/api';
-import { RefreshCw, Search, Plus } from 'lucide-react';
+import { RefreshCw, Search, Plus, DollarSign} from 'lucide-react';
 import OrderTable from '@/components/admin/orders/OrderTable';
 import OrderDetailModal, { OrderDetail } from '@/components/admin/orders/OrderDetailModal';
 import OrderEditModal from '@/components/admin/orders/OrderEditModal';
 import OrderPaymentModal from '@/components/admin/orders/OrderPaymentModal'; // 1. 引入支付管理弹窗
+import OrderPriceAdjustModal from '@/components/admin/orders/OrderPriceAdjustModal';
 import Pagination from '@/components/common/Pagination';
 
 export default function AdminOrdersPage() {
@@ -29,6 +30,7 @@ export default function AdminOrdersPage() {
   const [isDetailOpen, setIsDetailOpen] = useState(false);
   const [isEditOpen, setIsEditOpen] = useState(false);
   const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false); // 2. 新增支付弹窗状态
+  const [isPriceModalOpen, setIsPriceModalOpen] = useState(false);
 
   const [editingOrder, setEditingOrder] = useState<OrderDetail | null>(null);
 
@@ -86,6 +88,11 @@ export default function AdminOrdersPage() {
     }
   };
 
+  const handleAdjustPrice = (order: OrderDetail) => {
+    setSelectedOrder(order);
+    setIsPriceModalOpen(true);
+  };
+
   const handleViewDetail = (order: OrderDetail) => {
     setSelectedOrder(order);
     setIsDetailOpen(true);
@@ -162,6 +169,7 @@ export default function AdminOrdersPage() {
         onViewDetail={handleViewDetail} 
         onEdit={handleEditOrder}
         onCheckPayments={handleCheckPayments} // <--- 关键修复点
+        onAdjustPrice={handleAdjustPrice} 
       />
 
       <div className="mt-4">
@@ -195,6 +203,13 @@ export default function AdminOrdersPage() {
         order={selectedOrder}
         onSuccess={handlePaymentSuccess}
       />
+
+      <OrderPriceAdjustModal 
+        isOpen={isPriceModalOpen}
+        onClose={() => setIsPriceModalOpen(false)}
+        order={selectedOrder}
+        onSuccess={() => fetchOrders(page)}
+      />
     </div>
   );
 }

+ 92 - 83
src/app/admin/page.tsx

@@ -2,18 +2,38 @@
 
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
-import DataStats from '@/components/admin/DataStats'; // 复用之前的组件
-import OverviewCharts from '@/components/admin/dashboard/OverviewCharts'; // 新组件
+import DataStats from '@/components/admin/DataStats'; // 请确保此路径正确
+import OverviewCharts from '@/components/admin/dashboard/OverviewCharts'; // 请确保此路径正确
 import { ShoppingBag, Users, AlertCircle, CheckCircle, Wallet, ArrowRight } from 'lucide-react';
 import Link from 'next/link';
 
+// 定义接口以匹配后端 API 返回的数据结构
+interface DashboardStats {
+  totalOrders: number;
+  totalOrdersTrend: number;   // 动态增长率
+  totalRevenue: number;
+  totalRevenueTrend: number;  // 动态增长率
+  activeUsers: number;
+  pendingTickets: number;
+  successRate: string;
+}
+
+interface ActivityItem {
+  id: string;
+  text: string;
+  time: string;
+  type: 'order' | 'money' | 'ticket' | 'system';
+}
+
 export default function AdminDashboard() {
   const [loading, setLoading] = useState(true);
   
   // 状态数据
-  const [stats, setStats] = useState({
+  const [stats, setStats] = useState<DashboardStats>({
     totalOrders: 0,
+    totalOrdersTrend: 0,
     totalRevenue: 0,
+    totalRevenueTrend: 0,
     activeUsers: 0,
     pendingTickets: 0,
     successRate: '0%'
@@ -25,7 +45,7 @@ export default function AdminDashboard() {
     products: any[]
   }>({ revenue: [], products: [] });
 
-  const [recentActivities, setRecentActivities] = useState<any[]>([]);
+  const [recentActivities, setRecentActivities] = useState<ActivityItem[]>([]);
 
   useEffect(() => {
     fetchDashboardData();
@@ -34,67 +54,21 @@ export default function AdminDashboard() {
   const fetchDashboardData = async () => {
     setLoading(true);
     try {
-      // -------------------------------------------------------------
-      // TODO: 后端 API: GET /api/vas/statistics/overview
-      // -------------------------------------------------------------
+      // 调用后端 API
       const res = await api.get('/api/vas/statistics/overview');
+      // 假设后端返回结构为: { code: 200, data: { stats: ..., revenue_trend: ... } }
       const data = res.data.data;
 
-      setStats(data.stats);
+      if (data) {
+        setStats(data.stats);
 
-      // 注意这里后端返回的是 revenue_trend 和 product_dist
-      setChartData({ 
-        revenue: data.revenue_trend, 
-        products: data.product_dist 
-      });
+        setChartData({ 
+          revenue: data.revenue_trend || [], 
+          products: data.product_dist || [] 
+        });
 
-      setRecentActivities(data.recent_activities);
-      
-      // === 暂用 Mock Data 模拟真实业务场景 ===
-      
-      // // 1. 顶部卡片数据
-      // setStats({
-      //   totalOrders: 128,
-      //   totalRevenue: 384000, // 单位分
-      //   activeUsers: 45,
-      //   pendingTickets: 3,
-      //   successRate: '94.5%'
-      // });
-
-      // // 2. 模拟近 7 天营收
-      // const revenueMock = [];
-      // const now = new Date();
-      // for (let i = 6; i >= 0; i--) {
-      //   const d = new Date();
-      //   d.setDate(now.getDate() - i);
-      //   revenueMock.push({
-      //     date: d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }),
-      //     amount: Math.floor(Math.random() * 5000) + 2000, // 2000~7000 元
-      //     orders: Math.floor(Math.random() * 10) + 1
-      //   });
-      // }
-
-      // // 3. 模拟商品销量分布
-      // const productMock = [
-      //   { name: '日本旅游签证', value: 45 },
-      //   { name: '法国申根签', value: 32 },
-      //   { name: '泰国电子签', value: 24 },
-      //   { name: '美国 B1/B2', value: 15 },
-      //   { name: '其他', value: 12 },
-      // ];
-
-      // setChartData({
-      //   revenue: revenueMock,
-      //   products: productMock
-      // });
-
-      // // 4. 模拟最新动态
-      // setRecentActivities([
-      //   { id: 1, text: '用户 Zhang San 提交了新的日本签证申请', time: '10分钟前', type: 'order' },
-      //   { id: 2, text: '系统自动抓取了 "fr_visabot" 的预约号', time: '35分钟前', type: 'system' },
-      //   { id: 3, text: '收到一笔新的 Stripe 支付 ¥800.00', time: '1小时前', type: 'money' },
-      //   { id: 4, text: '客服处理了 "退款申请" 工单 #102', time: '2小时前', type: 'ticket' },
-      // ]);
+        setRecentActivities(data.recent_activities || []);
+      }
 
     } catch (error) {
       console.error("Failed to load dashboard data", error);
@@ -103,28 +77,51 @@ export default function AdminDashboard() {
     }
   };
 
+  /**
+   * 格式化趋势百分比
+   * 例如: 12.5 -> "+12.5%", -5 -> "-5.0%"
+   */
+  const formatTrend = (val: number) => {
+    // 处理 NaN 或 null 情况
+    if (val === undefined || val === null || isNaN(val)) return "0.0%";
+    
+    const absVal = Math.abs(val);
+    const sign = val >= 0 ? '+' : '-';
+    return `${sign}${absVal.toFixed(1)}%`;
+  };
+
   return (
     <div>
       <h1 className="text-2xl font-bold mb-6 text-slate-800">系统概览</h1>
       
-      {/* 1. 核心指标卡片 */}
+      {/* 1. 核心指标卡片区域 */}
       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+        
+        {/* 卡片1: 总营收 (本月) */}
         <DataStats 
           title="总营收 (本月)" 
+          // 后端返回的是分,除以100转为元
           value={`¥${(stats.totalRevenue / 100).toLocaleString()}`} 
           icon={Wallet} 
-          trend="+12.5%" 
-          trendUp={true} 
+          // 动态设置趋势文本
+          trend={formatTrend(stats.totalRevenueTrend)} 
+          // 动态判断颜色方向 (大于等于0为上升绿色/蓝色,小于0为红色)
+          // 注意:DataStats组件内部通常根据 trendUp=true 显示绿色/上升箭头
+          trendUp={stats.totalRevenueTrend >= 0} 
           color="blue"
         />
+        
+        {/* 卡片2: 活跃订单 (本月) */}
         <DataStats 
-          title="活跃订单数" 
+          title="本月订单数" 
           value={stats.totalOrders} 
           icon={ShoppingBag} 
-          trend="+8%" 
-          trendUp={true} 
+          trend={formatTrend(stats.totalOrdersTrend)} 
+          trendUp={stats.totalOrdersTrend >= 0} 
           color="purple"
         />
+        
+        {/* 卡片3: 待处理工单 (无环比概念,仅根据数量预警) */}
         <DataStats 
           title="待处理工单" 
           value={stats.pendingTickets} 
@@ -133,15 +130,19 @@ export default function AdminDashboard() {
           trendUp={stats.pendingTickets === 0}
           color={stats.pendingTickets > 0 ? "red" : "green"}
         />
+        
+        {/* 卡片4: 机器人成功率 */}
         <DataStats 
           title="机器人成功率" 
           value={stats.successRate} 
           icon={CheckCircle} 
+          trend="全自动"
+          trendUp={true}
           color="green"
         />
       </div>
 
-      {/* 2. 图表区域 (核心新增) */}
+      {/* 2. 图表区域 */}
       <OverviewCharts 
         revenueData={chartData.revenue} 
         productData={chartData.products} 
@@ -150,30 +151,38 @@ export default function AdminDashboard() {
       {/* 3. 底部:最新动态与快捷入口 */}
       <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
         
-        {/* 最新动态列表 */}
+        {/* 左侧:最新动态列表 */}
         <div className="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-slate-100">
           <div className="flex justify-between items-center mb-4">
             <h3 className="font-bold text-slate-800">最新系统动态</h3>
             <button className="text-xs text-blue-600 hover:underline">查看全部日志</button>
           </div>
-          <div className="space-y-4">
-            {recentActivities.map((item) => (
-              <div key={item.id} className="flex items-start gap-3 pb-3 border-b border-slate-50 last:border-0 last:pb-0">
-                <div className={`mt-1 w-2 h-2 rounded-full flex-shrink-0 
-                  ${item.type === 'order' ? 'bg-blue-500' : 
-                    item.type === 'money' ? 'bg-green-500' : 
-                    item.type === 'ticket' ? 'bg-orange-500' : 'bg-gray-400'}`} 
-                />
-                <div className="flex-1">
-                  <p className="text-sm text-slate-700">{item.text}</p>
-                  <p className="text-xs text-slate-400 mt-0.5">{item.time}</p>
+          
+          {loading && recentActivities.length === 0 ? (
+            <div className="py-10 text-center text-slate-400">加载中...</div>
+          ) : recentActivities.length === 0 ? (
+            <div className="py-10 text-center text-slate-400">暂无最新动态</div>
+          ) : (
+            <div className="space-y-4">
+              {recentActivities.map((item) => (
+                <div key={item.id} className="flex items-start gap-3 pb-3 border-b border-slate-50 last:border-0 last:pb-0">
+                  {/* 根据类型显示不同颜色的圆点 */}
+                  <div className={`mt-1 w-2 h-2 rounded-full flex-shrink-0 
+                    ${item.type === 'order' ? 'bg-blue-500' : 
+                      item.type === 'money' ? 'bg-green-500' : 
+                      item.type === 'ticket' ? 'bg-orange-500' : 'bg-gray-400'}`} 
+                  />
+                  <div className="flex-1">
+                    <p className="text-sm text-slate-700">{item.text}</p>
+                    <p className="text-xs text-slate-400 mt-0.5">{item.time}</p>
+                  </div>
                 </div>
-              </div>
-            ))}
-          </div>
+              ))}
+            </div>
+          )}
         </div>
 
-        {/* 快捷操作区 */}
+        {/* 右侧:快捷操作区 */}
         <div className="bg-slate-900 text-white p-6 rounded-xl shadow-lg flex flex-col justify-between">
           <div>
             <h3 className="font-bold text-lg mb-2">管理快捷入口</h3>

+ 25 - 6
src/app/admin/payments/page.tsx

@@ -2,10 +2,11 @@
 
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
-import { Plus, RefreshCw } from 'lucide-react';
+import { Plus, RefreshCw, Banknote } from 'lucide-react'; // 引入 Banknote 图标
 import ProviderList from '@/components/admin/payments/ProviderList';
 import ProviderModal from '@/components/admin/payments/ProviderModal';
 import QrManager from '@/components/admin/payments/QrManager';
+import ExchangeRateModal from '@/components/admin/payments/ExchangeRateModal'; // 引入新组件
 
 export default function AdminPaymentsPage() {
   const [providers, setProviders] = useState<any[]>([]);
@@ -14,6 +15,9 @@ export default function AdminPaymentsPage() {
   // Modal States
   const [isProviderModalOpen, setProviderModalOpen] = useState(false);
   const [isQrManagerOpen, setQrManagerOpen] = useState(false);
+  // 新增:汇率弹窗状态
+  const [isExchangeModalOpen, setExchangeModalOpen] = useState(false);
+  
   const [selectedProvider, setSelectedProvider] = useState<any>(null);
 
   const fetchProviders = async () => {
@@ -96,24 +100,33 @@ export default function AdminPaymentsPage() {
           <p className="text-sm text-slate-500 mt-1">管理支付渠道 (Stripe, WeChat, Alipay) 及其参数</p>
         </div>
 
-        {/* 操作按钮组:移动端全宽 + 平分 */}
-        <div className="flex gap-3 w-full md:w-auto">
+        {/* 操作按钮组 */}
+        <div className="flex flex-wrap md:flex-nowrap gap-3 w-full md:w-auto">
+          {/* 新增:汇率管理按钮 */}
+          <button 
+            onClick={() => setExchangeModalOpen(true)}
+            className="flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium transition active:scale-95 whitespace-nowrap"
+          >
+            <Banknote size={16} className="text-blue-600"/> 汇率管理
+          </button>
+
           <button 
             onClick={fetchProviders} 
-            className="flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-white border rounded-lg hover:bg-slate-50 text-slate-700 font-medium transition active:scale-95"
+            className="flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium transition active:scale-95"
           >
             <RefreshCw size={16} /> 刷新
           </button>
+          
           <button 
             onClick={openCreate} 
-            className="flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition active:scale-95"
+            className="flex-1 md:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition active:scale-95 whitespace-nowrap"
           >
             <Plus size={16} /> 添加服务商
           </button>
         </div>
       </div>
 
-      {/* 列表组件 (内部已做 Table/Card 响应式切换) */}
+      {/* 列表组件 */}
       <ProviderList 
         providers={providers} 
         loading={loading} 
@@ -136,6 +149,12 @@ export default function AdminPaymentsPage() {
         providerId={selectedProvider?.id}
         providerName={selectedProvider?.name}
       />
+
+      {/* 新增:汇率管理弹窗 */}
+      <ExchangeRateModal 
+        isOpen={isExchangeModalOpen}
+        onClose={() => setExchangeModalOpen(false)}
+      />
     </div>
   );
 }

+ 4 - 3
src/app/refund-policy/page.tsx

@@ -20,8 +20,9 @@ export default function RefundPolicyPage() {
   const { t } = useLanguage();
 
   // 请在这里填入你的真实账号
-  const TELEGRAM_USERNAME = "your_telegram_id"; // 例如: visafly_support
-  const WHATSAPP_NUMBER = "your_phone_number";  // 例如: 1234567890 (不需要加 + 号)
+  const TELEGRAM_USERNAME = "Visafly Support"
+  const TELEGRAM_NUMBER = "+8617386033451"; // 例如: visafly_support
+  const WHATSAPP_NUMBER = "353892125284";  // 例如: 1234567890 (不需要加 + 号)
 
   return (
     <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
@@ -118,7 +119,7 @@ export default function RefundPolicyPage() {
 
               {/* Telegram Button */}
               <a 
-                href={`https://t.me/${TELEGRAM_USERNAME}`}
+                href={`https://t.me/${TELEGRAM_NUMBER}`}
                 target="_blank"
                 rel="noopener noreferrer"
                 className="flex items-center gap-3 p-4 rounded-xl border border-slate-200 bg-white hover:border-sky-500 hover:bg-sky-50 transition-all group"

+ 176 - 193
src/app/slots/page.tsx

@@ -1,236 +1,219 @@
 'use client';
 
-import { useState } from 'react';
+import { useState, useEffect, useMemo } from 'react';
 import api from '@/lib/api';
-import { Search, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle, ExternalLink } from 'lucide-react';
+import { RefreshCw, MapPin, Info, Globe, Search } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-import LocalTime from '@/components/common/LocalTime';
-
-// === 类型定义 ===
-
-interface TimeSlot {
-  time: string;
-  label?: string;
-}
-
-interface DayAvailability {
-  date: string;
-  times?: TimeSlot[]; 
-}
-
-interface SlotSnapshot {
-  id: number;
-  country: string;
-  city: string;
-  visa_type: string;
-  availability_status: 'None' | 'Available' | 'Waitlist';
-  earliest_date: string | null;
-  snapshot_at: string;
-  // 新增 website 字段
-  website?: string; 
-  availability: DayAvailability[]; 
-}
-
-export default function SlotQueryPage() {
-  const [loading, setLoading] = useState(false);
-  const [snapshot, setSnapshot] = useState<SlotSnapshot | null>(null);
-  const { t, lang } = useLanguage();
+import CitySlotCard, { SlotSnapshot } from '@/components/slots/CitySlotCard';
+
+const LOCATIONS = [
+  { code: 'IE', name: 'Ireland', flag: '🇮🇪', cities: ['Dublin'] },
+  { code: 'GB', name: 'United Kingdom', flag: '🇬🇧', cities: ['London', 'Manchester'] },
+  { code: 'SG', name: 'Singapore', flag: '🇸🇬', cities: ['Singapore'] }
+];
+
+export default function SlotDashboardPage() {
+  const { t } = useLanguage();
+  const [loading, setLoading] = useState(true);
+  const [snapshots, setSnapshots] = useState<SlotSnapshot[]>([]);
   
-  const [country, setCountry] = useState('France');
-  const [city, setCity] = useState('Dublin');
-  const [visaType, setVisaType] = useState('Tourist');
-
-  // ... (options 保持不变)
-  const options = {
-    countries: ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'],
-    cities: ['Dublin','Edinburgh','London','Manchester','Melbourne','Montreal','Singapore','Sydney','Toronto'],
-    types: ['Tourist','Business','Family','Student','Work','Transit','e-Visa']
+  const [selectedCountryCode, setSelectedCountryCode] = useState('IE');
+  const [selectedCity, setSelectedCity] = useState('Dublin');
+
+  const currentCities = useMemo(() => {
+    return LOCATIONS.find(l => l.code === selectedCountryCode)?.cities || [];
+  }, [selectedCountryCode]);
+
+  const handleCountryChange = (code: string) => {
+    setSelectedCountryCode(code);
+    const countryData = LOCATIONS.find(l => l.code === code);
+    if (countryData && countryData.cities.length > 0) {
+      setSelectedCity(countryData.cities[0]);
+    } else {
+      setSelectedCity('');
+    }
   };
 
-  const fetchSlots = async () => {
+  useEffect(() => {
+    if (selectedCity) {
+      fetchDashboard();
+    }
+  }, [selectedCity]);
+
+  const fetchDashboard = async () => {
     setLoading(true);
     try {
-      const res = await api.get('/api/slots/latest', {
-        params: { country, city, visa_type: visaType }
+      // 暂时注释掉真实 API,使用下方的 Mock 数据
+      const res = await api.get('/api/slots/overview', { params: { city: selectedCity } }); 
+      let data = res.data.data || [];
+      
+      // === 模拟网络延迟 ===
+      await new Promise(resolve => setTimeout(resolve, 800));
+
+      // === 构造 Mock 数据 ===
+      const now = new Date();
+      const subMins = (m: number) => new Date(now.getTime() - m * 60000).toISOString();
+      const subHours = (h: number) => new Date(now.getTime() - h * 3600000).toISOString();
+
+      // const mockData: SlotSnapshot[] = [
+      //   { 
+      //     id: 1, 
+      //     country: 'France', 
+      //     city: 'Dublin', 
+      //     visa_type: 'Short Stay', 
+      //     availability_status: 'Available', 
+      //     earliest_date: '2026-02-14', 
+      //     website: 'https://google.com',
+      //     snapshot_at: subMins(10),       // 数据是10分钟前更新的
+      //     last_check_at: subMins(1)       // 爬虫刚刚(1分钟前)还在跑 -> 健康 (显示呼吸灯)
+      //   },
+      //   { 
+      //     id: 2, 
+      //     country: 'Spain', 
+      //     city: 'Dublin', 
+      //     visa_type: 'Tourist', 
+      //     availability_status: 'Waitlist', 
+      //     earliest_date: null, 
+      //     website: 'https://google.com',
+      //     snapshot_at: subHours(2), 
+      //     last_check_at: subMins(5)       // 5分钟前跑过 -> 健康 (显示呼吸灯)
+      //   },
+      //   { 
+      //     id: 3, 
+      //     country: 'Italy', 
+      //     city: 'Dublin', 
+      //     visa_type: 'Tourism', 
+      //     availability_status: 'None', 
+      //     earliest_date: null, 
+      //     snapshot_at: subHours(5), 
+      //     last_check_at: subMins(2)       // 重点:虽然无号,但2分钟前刚查过 -> 增加用户信任
+      //   },
+      //   { 
+      //     id: 4, 
+      //     country: 'Germany', 
+      //     city: 'Dublin', 
+      //     visa_type: 'Business', 
+      //     availability_status: 'None', 
+      //     earliest_date: null, 
+      //     snapshot_at: subHours(10), 
+      //     last_check_at: subHours(8)      // 8小时前查的 -> 离线/过期 (无呼吸灯,提示数据旧)
+      //   }
+      // ];
+
+      // 简单过滤一下城市,模拟真实效果
+      const filtered = data.filter(i => i.city === selectedCity);
+      
+      // 排序
+      filtered.sort((a, b) => {
+        const score = (s: string) => s === 'Available' ? 3 : s === 'Waitlist' ? 2 : 1;
+        return score(b.availability_status) - score(a.availability_status);
       });
-      const data = res.data.data;
-      if (data) setSnapshot(data);
-      else setSnapshot(null);
+
+      setSnapshots(filtered);
+
     } catch (e) {
-      console.warn("API Error");
-      setSnapshot(null);
+      console.warn("Error", e);
+      setSnapshots([]);
     } finally {
       setLoading(false);
     }
   };
 
-  const formatDate = (dateStr: string) => {
-    const date = new Date(dateStr);
-    const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
-    return date.toLocaleDateString(locale, { month: 'long', day: 'numeric', weekday: 'short' });
-  };
-
   return (
-    <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="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-8">
+      <div className="max-w-7xl mx-auto">
         
-        {/* 标题区 */}
-        <div className="text-center mb-8 md:mb-10">
+        {/* Header */}
+        <div className="mb-8">
           <h1 className="text-2xl md:text-3xl font-bold text-slate-900">{t('slots.title')}</h1>
-          <p className="text-sm md:text-base text-slate-500 mt-2">{t('slots.subtitle')}</p>
+          <p className="text-sm text-slate-500 mt-2 flex items-center gap-1">
+            <Info size={14} />
+            {t('slots.subtitle') || '查看各使馆最新名额状态 (实时更新)'}
+          </p>
         </div>
 
-        {/* 筛选区 */}
-        <div className="bg-white p-5 md:p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
-          <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
-            <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.country')}</label>
-              <select 
-                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
-                value={country} onChange={e => setCountry(e.target.value)}
-              >
-                {options.countries.map(c => <option key={c} value={c}>{c}</option>)}
-              </select>
-            </div>
-            <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.city')}</label>
+        {/* Filter Bar */}
+        <div className="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 mb-8 flex flex-col md:flex-row gap-4 items-center">
+          
+          {/* Country */}
+          <div className="w-full md:w-auto flex-1 max-w-xs relative">
+            <label className="text-xs font-bold text-slate-400 uppercase ml-1 mb-1 block">
+              Application Location (Country)
+            </label>
+            <div className="relative">
               <select 
-                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
-                value={city} onChange={e => setCity(e.target.value)}
+                className="w-full pl-10 pr-8 py-3 border border-slate-200 rounded-xl bg-slate-50 text-sm font-bold text-slate-800 outline-none focus:ring-2 focus:ring-blue-500 appearance-none cursor-pointer"
+                value={selectedCountryCode}
+                onChange={(e) => handleCountryChange(e.target.value)}
               >
-                {options.cities.map(c => <option key={c} value={c}>{c}</option>)}
+                {LOCATIONS.map(loc => (
+                  <option key={loc.code} value={loc.code}>
+                    {loc.flag} {loc.name}
+                  </option>
+                ))}
               </select>
+              <Globe size={18} className="absolute left-3 top-3.5 text-slate-400 pointer-events-none" />
+              <div className="absolute right-3 top-4 pointer-events-none text-slate-400">
+                <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
+              </div>
             </div>
-            <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.visa_type')}</label>
+          </div>
+
+          {/* City */}
+          <div className="w-full md:w-auto flex-1 max-w-xs relative">
+            <label className="text-xs font-bold text-slate-400 uppercase ml-1 mb-1 block">
+              Application City
+            </label>
+            <div className="relative">
               <select 
-                className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
-                value={visaType} onChange={e => setVisaType(e.target.value)}
+                className="w-full pl-10 pr-8 py-3 border border-slate-200 rounded-xl bg-slate-50 text-sm font-bold text-slate-800 outline-none focus:ring-2 focus:ring-blue-500 appearance-none cursor-pointer"
+                value={selectedCity}
+                onChange={(e) => setSelectedCity(e.target.value)}
               >
-                {options.types.map(c => <option key={c} value={c}>{c}</option>)}
+                {currentCities.map(city => (
+                  <option key={city} value={city}>{city}</option>
+                ))}
               </select>
+              <MapPin size={18} className="absolute left-3 top-3.5 text-slate-400 pointer-events-none" />
+              <div className="absolute right-3 top-4 pointer-events-none text-slate-400">
+                <svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L5 5L9 1" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
+              </div>
             </div>
           </div>
-          
-          <div className="mt-6 flex justify-end">
+
+          {/* Refresh */}
+          <div className="w-full md:w-auto pt-5">
             <button 
-              onClick={fetchSlots}
+              onClick={fetchDashboard} 
               disabled={loading}
-              className="w-full md:w-auto flex justify-center items-center gap-2 bg-slate-900 text-white px-8 py-3 md:py-2.5 rounded-lg font-bold hover:bg-slate-800 transition disabled:opacity-70 shadow-lg shadow-slate-200 active:scale-95"
+              className="w-full md:w-auto flex items-center justify-center gap-2 px-6 py-3 bg-slate-900 text-white rounded-xl hover:bg-slate-800 transition disabled:opacity-70 shadow-sm active:scale-95"
             >
-              {loading ? <RefreshCw className="animate-spin" size={18} /> : <Search size={18} />}
-              {t('common.search')}
+              <RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
+              <span className="font-bold text-sm">{t('common.refresh') || 'Refresh'}</span>
             </button>
           </div>
         </div>
 
-        {/* 结果展示区 */}
-        {snapshot ? (
-          <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
-            
-            {/* 概览 Banner */}
-            <div className={`p-5 md:p-6 rounded-xl border flex flex-col md:flex-row items-start md:items-center justify-between gap-4 md:gap-6
-              ${snapshot.availability_status === 'Available' ? 'bg-green-50 border-green-200' : 'bg-gray-50 border-gray-200'}
-            `}>
-              <div className="flex items-start gap-4">
-                <div className={`p-3 rounded-full flex-shrink-0 ${snapshot.availability_status === 'Available' ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-500'}`}>
-                  {snapshot.availability_status === 'Available' ? <CheckCircle size={28} /> : <AlertCircle size={28} />}
-                </div>
-                <div>
-                  <h3 className={`text-lg font-bold ${snapshot.availability_status === 'Available' ? 'text-green-800' : 'text-gray-700'}`}>
-                    {snapshot.availability_status === 'Available' ? t('slots.status_available') : t('slots.status_unavailable')}
-                  </h3>
-                  <p className="text-sm opacity-80 flex flex-wrap items-center gap-1 mt-1">
-                    <Clock size={12} className="flex-shrink-0" /> 
-                    <span>{t('slots.updated_at')}:</span>
-                    <LocalTime date={snapshot.snapshot_at} />
-                  </p>
-                </div>
-              </div>
-
-              {snapshot.earliest_date && (
-                <div className="w-full md:w-auto bg-white/60 px-5 py-3 rounded-lg border border-black/5 text-center md:text-right">
-                  <p className="text-xs font-bold uppercase tracking-wider opacity-60">{t('slots.earliest_date')}</p>
-                  <p className="text-2xl font-bold text-slate-800">{snapshot.earliest_date}</p>
-                </div>
-              )}
+        {/* Grid List */}
+        {loading && snapshots.length === 0 ? (
+          <div className="text-center py-24 text-slate-400 bg-white rounded-2xl border border-dashed">
+            <RefreshCw size={32} className="animate-spin mx-auto mb-3 opacity-50" />
+            <p>正在获取 {selectedCity} 的最新数据...</p>
+          </div>
+        ) : snapshots.length === 0 ? (
+          <div className="text-center py-24 bg-white rounded-2xl 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>
-
-            {/* 
-               === 列表渲染逻辑优化 === 
-               如果没有具体时间段,不要显示空网格,而是显示行动按钮或紧凑状态
-            */}
-            {snapshot.availability && snapshot.availability.length > 0 && (
-              <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
-                {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>
-                        {/* 优化徽章:有时间显示数量,没时间显示 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>
-            )}
+            <h3 className="text-slate-900 font-bold mb-1">暂无监控数据</h3>
+            <p className="text-slate-500 text-sm">当前城市没有被监控的签证服务</p>
           </div>
         ) : (
-          !loading && (
-            <div className="text-center py-20 text-slate-400">
-              <Search size={48} className="mx-auto mb-4 opacity-20" />
-              <p>{t('slots.select_hint')}</p>
-            </div>
-          )
+          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
+            {snapshots.map((slot) => (
+              <CitySlotCard key={slot.id} data={slot} />
+            ))}
+          </div>
         )}
 
       </div>

+ 188 - 105
src/components/CreateOrderForm.tsx

@@ -3,7 +3,14 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
-import { Loader2, Info } from 'lucide-react';
+import { 
+  Loader2, 
+  Info, 
+  Check, 
+  FileText, 
+  CheckCircle2, 
+  ShieldCheck 
+} from 'lucide-react'; 
 import BindEmailModal from '@/components/BindEmailModal';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import ConfirmModal from '@/components/common/ConfirmModal';
@@ -124,7 +131,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
                 if (prop.default !== undefined) {
                   initialValues[key] = prop.default;
                 } else if (prop.type === 'boolean') {
-                   // boolean 类型如果没有默认值,初始化为 false
                    initialValues[key] = false;
                 } else {
                   initialValues[key] = ''; 
@@ -288,58 +294,102 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     setIsBindEmailOpen(false);
   };
 
+  // --- 核心优化:渲染描述 ---
+  // 解决描述文本“一大坨”和换行符丢失的问题
+  const renderProductDescription = (text: string) => {
+    if (!text) return null;
+
+    // 1. 按换行符拆分,过滤空行
+    const lines = text.split('\n').filter(line => line.trim().length > 0);
+
+    // 2. 如果只有一行,直接显示(保留 pre-wrap 以防万一)
+    if (lines.length <= 1) {
+      return (
+        <p className="text-slate-600 text-sm leading-relaxed whitespace-pre-wrap">
+          {text}
+        </p>
+      );
+    }
+
+    // 3. 如果有多行,渲染为漂亮的权益列表
+    return (
+      <div className="bg-white rounded-lg border border-slate-100 p-4 mt-3 shadow-sm">
+        <ul className="space-y-3">
+          {lines.map((line, index) => (
+            <li key={index} className="flex items-start gap-3 text-sm text-slate-700">
+              <CheckCircle2 size={18} className="text-green-500 mt-0.5 flex-shrink-0" />
+              <span className="leading-snug">{line.replace(/^[•\-\*]\s*/, '')}</span>
+            </li>
+          ))}
+        </ul>
+      </div>
+    );
+  };
+
+  // --- Render Fields ---
   const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
     const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-base md:text-sm bg-white min-h-[46px]";
     const label = fieldSchema.title || key;
     const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
     const currentValue = formValues[key];
 
-    // ----------------------------------------------------------------
-    // 1. Boolean 类型 (标准的 Checkbox)
-    // ----------------------------------------------------------------
+    // Boolean 类型优化
     if (fieldSchema.type === 'boolean') {
       const isChecked = currentValue === true;
       return (
-        <div className="flex items-center h-[46px] px-1" key={key}>
-            <input
-                id={`field-${key}`}
-                type="checkbox"
-                // Boolean 不应该有 HTML required 属性,因为 unchecked (false) 也是有效值
-                // 除非你特意需要 "Must agree" 的逻辑
-                checked={isChecked}
-                onChange={(e) => handleInputChange(key, e.target.checked)}
-                className="w-5 h-5 text-blue-600 rounded border-gray-300 focus:ring-blue-500 cursor-pointer accent-blue-600"
-            />
+        <div className="flex items-center h-[46px]" key={key}>
             <label 
+                className="flex items-center cursor-pointer select-none group"
                 htmlFor={`field-${key}`} 
-                className="ml-3 text-gray-700 text-sm cursor-pointer select-none font-medium"
             >
-                {/* 如果选中显示 Yes,否则显示 No,或者你可以只显示固定的 "Enabled/Yes" */}
-                {isChecked ? 'Yes' : 'No'}
+                <div className={`
+                    w-12 h-6 rounded-full border flex items-center transition-colors relative
+                    ${isChecked ? 'bg-blue-600 border-blue-600' : 'bg-gray-200 border-gray-200'}
+                `}>
+                    <div className={`
+                        w-4 h-4 rounded-full bg-white shadow-sm transform transition-transform absolute top-1
+                        ${isChecked ? 'translate-x-7' : 'translate-x-1'}
+                    `}/>
+                </div>
+                <input
+                    id={`field-${key}`}
+                    type="checkbox"
+                    className="hidden"
+                    checked={isChecked}
+                    onChange={(e) => handleInputChange(key, e.target.checked)}
+                />
+                <span className={`ml-3 text-sm font-medium ${isChecked ? 'text-gray-900' : 'text-gray-600'}`}>
+                    {isChecked ? (t('common.yes') || 'Yes') : (t('common.no') || 'No')}
+                </span>
             </label>
         </div>
       );
     }
 
-    // 2. 枚举类型 (Select)
+    // 枚举类型
     if (fieldSchema.enum && fieldSchema.enum.length > 0) {
       return (
-        <select
-          key={key}
-          required={required}
-          className={`${commonClasses} appearance-none`}
-          value={currentValue || ''}
-          onChange={(e) => handleInputChange(key, e.target.value)}
-        >
-          <option value="" disabled>{t('common.select')} {label}</option>
-          {fieldSchema.enum.map((option: string | number) => (
-            <option key={option} value={option}>{option}</option>
-          ))}
-        </select>
+        <div className="relative">
+          <select
+            key={key}
+            required={required}
+            className={`${commonClasses} appearance-none cursor-pointer`}
+            value={currentValue || ''}
+            onChange={(e) => handleInputChange(key, e.target.value)}
+          >
+            <option value="" disabled>{t('common.select')} {label}</option>
+            {fieldSchema.enum.map((option: string | number) => (
+              <option key={option} value={option}>{option}</option>
+            ))}
+          </select>
+          <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
+             <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path></svg>
+          </div>
+        </div>
       );
     }
 
-    // 3. 日期/时间类型
+    // 日期/时间
     if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
       const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
       return (
@@ -355,6 +405,8 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
             style={{ lineHeight: 'normal' }}
             value={currentValue || ''}
             onChange={(e) => handleInputChange(key, e.target.value)}
+            onFocus={(e) => e.target.classList.remove('text-transparent')}
+            onBlur={(e) => !e.target.value && e.target.classList.add('text-transparent')}
           />
           {!currentValue && (
             <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base md:text-sm pointer-events-none truncate pr-8 w-full h-fit transition-opacity peer-focus:opacity-0">
@@ -365,7 +417,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       );
     }
 
-    // 4. 其他 Input 类型
+    // 其他
     let inputType = 'text';
     if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
     if (fieldSchema.format === 'email') inputType = 'email';
@@ -386,11 +438,9 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
   const getSortedKeys = () => {
     const properties = formSchema?.properties || {};
     const keys = Object.keys(properties);
-
     if (Array.isArray(formSchema?.['ui:order'])) {
       return formSchema!['ui:order'];
     }
-
     return keys.sort((a, b) => {
       const propA = properties[a];
       const propB = properties[b];
@@ -400,9 +450,9 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     });
   };
 
-  // --- Render ---
+  // --- Render Main ---
 
-  if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
+  if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600 w-8 h-8"/></div>;
   if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
 
   const properties = formSchema?.properties || {};
@@ -411,91 +461,124 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
   const hasFields = sortedKeys.length > 0;
 
   return (
-    <div className="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
+    <div className="max-w-3xl mx-auto">
       
-      {/* Header */}
-      <div className="mb-6 md:mb-8 pb-4 md:pb-6 border-b border-gray-100">
-        <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2 md:gap-4">
-          <h1 className="text-xl md:text-2xl font-bold text-gray-900 leading-tight">
-            {product.title || productName}
-          </h1>
-          <span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
-             {(product.price_amount / 100).toFixed(2)} {product.price_currency}
-          </span>
-        </div>
-        <p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
-        
-        <div className="mt-4 flex items-start gap-2 text-xs text-amber-700 bg-amber-50 px-3 py-2.5 rounded-lg w-full md:w-fit border border-amber-100">
-          <Info size={16} className="flex-shrink-0 mt-0.5" />
-          <span className="leading-snug">{t('order.fill_form_hint')}</span>
-        </div>
-      </div>
-
-      {/* Form Fields */}
-      <form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
-        {!hasFields && (
-          <div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
-            {t('order.no_extra_info_needed')}
+      {/* 
+        核心修改:头部商品信息卡片 
+        1. 独立背景色 (bg-slate-50) 
+        2. 更好的边距 (p-6)
+        3. 描述文本优化 (renderProductDescription)
+      */}
+      <div className="bg-slate-50 p-6 md:p-8 rounded-2xl border border-slate-100 mb-8 relative overflow-hidden">
+        {/* 装饰背景 */}
+        <div className="absolute top-0 right-0 w-40 h-40 bg-blue-100 rounded-full blur-3xl opacity-30 -translate-y-1/2 translate-x-1/2 pointer-events-none"></div>
+
+        <div className="relative z-10">
+          <div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
+            <h1 className="text-2xl font-bold text-slate-900 leading-snug flex-1">
+              {product.title || productName}
+            </h1>
+            <div className="bg-white px-4 py-2 rounded-lg shadow-sm border border-slate-100 flex items-baseline gap-1 shrink-0">
+               <span className="text-sm font-semibold text-slate-500">{product.price_currency === 'CNY' ? '¥' : product.price_currency}</span>
+               <span className="text-2xl font-extrabold text-slate-900">
+                 {(product.price_amount / 100).toLocaleString()}
+               </span>
+            </div>
           </div>
-        )}
 
-        {sortedKeys.map((key) => (
-          <div key={key}>
-            <label className="block text-sm font-bold text-gray-700 mb-1.5">
-              {properties[key].title || key}
-              {requiredFields.includes(key) && <span className="text-red-500 ml-1">*</span>}
-            </label>
-            {renderField(key, properties[key], requiredFields.includes(key))}
-            {properties[key].description && (
-              <p className="text-xs text-gray-400 mt-1.5 leading-snug">{properties[key].description}</p>
-            )}
+          <div className="mt-4">
+            {/* 渲染优化后的描述 */}
+            {renderProductDescription(product.description)}
           </div>
-        ))}
 
-        <div className="pt-4 md:pt-6 border-t border-gray-50 mt-8">
-          <button
-            type="submit"
-            disabled={submitting}
-            className="w-full bg-blue-600 text-white py-3.5 rounded-xl font-bold hover:bg-blue-700 transition disabled:opacity-70 disabled:cursor-not-allowed flex justify-center items-center shadow-lg shadow-blue-200 active:scale-[0.98] text-sm md:text-base"
-          >
-            {submitting ? (
-              <>
-                <Loader2 className="animate-spin mr-2 w-5 h-5"/> {t('common.processing')}
-              </>
-            ) : (
-              <span>
-                {t('order.submit_and_pay')} 
-                <span className="ml-1 opacity-90 text-blue-100 font-normal">
-                  ({(product.price_amount / 100).toFixed(2)} {product.price_currency})
-                </span>
-              </span>
-            )}
-          </button>
-          <p className="text-center text-xs text-gray-400 mt-3">
-            {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
-          </p>
+          <div className="flex items-center gap-2 mt-4 text-xs text-slate-500">
+            <ShieldCheck size={14} className="text-green-500"/>
+            <span>{t('order.official_fast_secure') || "官方保障 · 极速处理 · 隐私加密"}</span>
+          </div>
         </div>
-      </form>
+      </div>
 
-      {/* === 全局弹窗挂载 === */}
+      {/* 表单区域 */}
+      <div className="bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-slate-200">
+        <h2 className="text-lg font-bold text-slate-800 mb-6 flex items-center gap-2">
+            <FileText className="text-blue-600" size={20}/>
+            {t('order.fill_form_title') || "填写申请信息"}
+        </h2>
+
+        <form onSubmit={handleSubmit} className="space-y-6">
+          {!hasFields ? (
+            <div className="text-center py-12 bg-slate-50 rounded-xl border border-dashed border-slate-200">
+              <div className="w-14 h-14 bg-white rounded-full flex items-center justify-center mx-auto mb-4 shadow-sm text-green-500">
+                  <Check size={28} />
+              </div>
+              <p className="text-slate-900 font-medium mb-1">
+                 {t('order.quick_checkout') || "极速下单"}
+              </p>
+              <p className="text-slate-500 text-sm">
+                  {t('order.no_extra_info_needed') || "该服务无需填写额外资料,请直接提交支付"}
+              </p>
+            </div>
+          ) : (
+            <div className="grid grid-cols-1 gap-6">
+              {sortedKeys.map((key) => (
+              <div key={key} className="group">
+                  <label className="block text-sm font-bold text-slate-700 mb-2 flex items-center">
+                      {properties[key].title || key}
+                      {requiredFields.includes(key) && <span className="text-red-500 ml-1" title="Required">*</span>}
+                  </label>
+                  
+                  {renderField(key, properties[key], requiredFields.includes(key))}
+                  
+                  {properties[key].description && (
+                   <div className="flex items-start gap-1.5 mt-2 text-xs text-slate-500 bg-slate-50 p-2 rounded border border-slate-100">
+                     <Info size={12} className="mt-0.5 shrink-0 text-slate-400"/>
+                     <span className="leading-relaxed">{properties[key].description}</span>
+                   </div>
+                  )}
+              </div>
+              ))}
+            </div>
+          )}
+
+          <div className="pt-8 mt-8 border-t border-slate-100">
+            <button
+              type="submit"
+              disabled={submitting}
+              className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold hover:bg-slate-800 transition-all disabled:opacity-70 disabled:cursor-not-allowed flex justify-center items-center shadow-lg shadow-slate-200 active:scale-[0.99] text-base"
+            >
+              {submitting ? (
+                <>
+                  <Loader2 className="animate-spin mr-2 w-5 h-5"/> {t('common.processing')}
+                </>
+              ) : (
+                <span className="flex items-center gap-1">
+                  {t('order.submit_and_pay')} 
+                  <span className="ml-1 opacity-80 font-normal">
+                    · {(product.price_amount / 100).toLocaleString()} {product.price_currency === 'CNY' ? '¥' : product.price_currency}
+                  </span>
+                </span>
+              )}
+            </button>
+            <p className="text-center text-xs text-slate-400 mt-4">
+              {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
+            </p>
+          </div>
+        </form>
+      </div>
 
+      {/* === 全局弹窗 === */}
       <BindEmailModal 
         isOpen={isBindEmailOpen}
         onClose={() => setIsBindEmailOpen(false)}
         onSuccess={handleBindSuccess}
       />
-
       <ConfirmModal 
         isOpen={showConfirmModal}
-        title={t('common.confirm_title') || "请确认您的操作"}
-        message={t('order.confirm_submit_msg') || "请核对信息无误后,点击确认提交。"}
+        title={t('common.confirm_title')}
+        message={t('order.confirm_submit_msg')}
         onConfirm={handleConfirmSubmit}
-        onCancel={() => {
-            setShowConfirmModal(false);
-            setSubmitting(false); 
-        }}
+        onCancel={() => { setShowConfirmModal(false); setSubmitting(false); }}
       />
-
       <MessageModal 
         isOpen={msgModal.isOpen}
         title={msgModal.title}

+ 134 - 35
src/components/PaymentProcessor.tsx

@@ -3,16 +3,60 @@
 import { useEffect, useState } from 'react';
 import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
-import { Loader2, ArrowLeft, Sparkles, ExternalLink, Clock, ArrowRightLeft, CheckCircle2, AlertCircle } from 'lucide-react';
+import { 
+  Loader2, 
+  ArrowLeft, 
+  Sparkles, 
+  ExternalLink, 
+  Clock, 
+  ArrowRightLeft, 
+  CheckCircle2, 
+  AlertCircle,
+  TrendingUp,
+  TrendingDown
+} from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import LocalTime from '@/components/common/LocalTime';
-// 1. 引入消息弹窗
 import MessageModal from '@/components/common/MessageModal';
 
-// ... 接口定义保持不变 ...
-interface PaymentProcessorProps { orderId: string; }
-interface PaymentProvider { id: number | string; name: string; currency?: string; icon?: string; title?: string; }
-interface PaymentResult { id: number; status: string; channel: 'online_link' | 'qr_static' | string; provider: string; qr_id?: number; payment_url?: string; expire_at: string; base_amount: number; base_currency: string; amount: number; currency: string; random_offset: number; exchange_rate: number; [key: string]: any; }
+// === 类型定义 ===
+
+interface PaymentProcessorProps {
+  orderId: string;
+}
+
+interface PaymentProvider {
+  id: number | string;
+  name: string;
+  currency?: string;
+  icon?: string;
+  title?: string;
+}
+
+interface PaymentResult {
+  id: number; // payment_id
+  status: string;
+  channel: 'online_link' | 'qr_static' | string;
+  provider: string; 
+  qr_id?: number;   
+  payment_url?: string;
+  expire_at: string;
+  
+  // 金额相关
+  base_amount: number;
+  base_currency: string;
+  amount: number;
+  currency: string;
+  random_offset: number;
+  exchange_rate: number;
+
+  // 新增:人工调价金额 (分)
+  adjustment_delta?: number;
+  
+  [key: string]: any;
+}
+
+// === 组件逻辑 ===
 
 export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
   const router = useRouter();
@@ -26,7 +70,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
   const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
   const [qrCode, setQrCode] = useState<string>('');
 
-  // 2. 消息弹窗状态
+  // 消息弹窗状态
   const [msgModal, setMsgModal] = useState({
     isOpen: false,
     title: '',
@@ -51,16 +95,21 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
     if (callback) callback();
   };
 
-  useEffect(() => { fetchProviders(); }, []);
+  useEffect(() => {
+    fetchProviders();
+  }, []);
 
   const fetchProviders = async () => {
     try {
       const res = await api.get('/api/vas/payment_provider/list_enabled');
       const list = Array.isArray(res.data) ? res.data : (res.data.data || []);
       setProviders(list);
-    } catch (error) { console.error("Failed to load providers", error); }
+    } catch (error) {
+      console.error("Failed to load providers", error);
+    }
   };
 
+  // 发起支付 (Step 1 -> Step 2)
   const handlePay = async (providerName: string) => {
     setLoading(true);
     try {
@@ -69,35 +118,63 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
       else if (providerCode.includes('ali')) providerCode = 'alipay';
       else if (providerCode.includes('stripe') || providerCode.includes('card')) providerCode = 'stripe';
 
-      const payRes = await api.post('/api/vas/payment/create', { order_id: String(orderId), provider: providerCode });
+      // 1. 创建支付单
+      const payRes = await api.post('/api/vas/payment/create', {
+        order_id: String(orderId),
+        provider: providerCode
+      });
+      
       const data: PaymentResult = payRes.data.data || payRes.data;
       if (!data?.id) throw new Error("Payment creation failed");
 
       setPaymentData(data); 
 
+      // 2. 根据渠道处理后续逻辑
       if (data.channel === 'online_link') {
-        if (data.payment_url) setStep(2);
-        else showMessage(t('payment.link_gen_failed'), 'error');
-      } else if (data.channel === 'qr_static') {
-        const qrRes = await api.get('/api/vas/payment_qr/qrcode', { params: { id: data.qr_id } });
+        if (data.payment_url) {
+          setStep(2);
+        } else {
+          showMessage(t('payment.link_gen_failed'), 'error');
+        }
+      } 
+      else if (data.channel === 'qr_static') {
+        // 获取二维码
+        const qrRes = await api.get('/api/vas/payment_qr/qrcode', {
+          params: { id: data.qr_id }
+        });
         const qrData = qrRes.data.data || qrRes.data;
         const qrUrl = qrData?.qr_code || qrData?.qrcode_url;
-        if (qrUrl) { setQrCode(qrUrl); setStep(2); } 
-        else showMessage(t('payment.qr_gen_failed'), 'error');
+
+        if (qrUrl) {
+          setQrCode(qrUrl);
+          setStep(2);
+        } else {
+          showMessage(t('payment.qr_gen_failed'), 'error');
+        }
       } else {
         showMessage(`${t('payment.unsupported_channel')}: ${data.channel}`, 'error');
       }
+
     } catch (error: any) {
+      console.error(error);
       const errorMsg = error.response?.data?.message || error.response?.data?.detail || "";
-      if (errorMsg.includes("active payment")) showMessage(t('payment.active_payment_exists'), 'info');
-      else showMessage(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')), 'error');
-    } finally { setLoading(false); }
+      if (errorMsg.includes("active payment")) {
+        showMessage(t('payment.active_payment_exists'), 'info');
+      } else {
+        showMessage(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')), 'error');
+      }
+    } finally {
+      setLoading(false);
+    }
   };
 
+  // === 用户点击“我已完成支付” ===
   const handleUserConfirm = async () => {
     if (!paymentData) return;
+    
     setConfirming(true);
     try {
+      // 构造 Payload
       const payload = {
         payment_id: paymentData.id,
         amount: paymentData.amount,
@@ -105,17 +182,22 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
         random_offset: paymentData.random_offset,
         confirmed_at: new Date().toISOString()
       };
+
+      // 发送请求
       await api.post('/api/vas/payment/confirm_by_user', payload);
-      
-      // 成功后弹窗,点击确定跳转 Dashboard
+
+      // 成功提示并跳转
       showMessage(t('payment.confirm_success_alert'), 'success', () => {
         router.push('/dashboard');
       });
-      
+
     } catch (error: any) {
+      console.error(error);
       const msg = error.response?.data?.message || t('common.unknown_error');
       showMessage(`${t('payment.confirm_failed')}: ${msg}`, 'error');
-    } finally { setConfirming(false); }
+    } finally {
+      setConfirming(false);
+    }
   };
 
   const handleBack = () => {
@@ -124,19 +206,14 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
     setQrCode('');
   };
 
-  const formatMoney = (amount: number, currency: string) => `${(amount / 100).toFixed(2)} ${currency}`;
-
-  // ... formatTime ...
-  const formatTime = (isoString: string) => {
-    const date = new Date(isoString);
-    const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
-    return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
+  const formatMoney = (amount: number, currency: string) => {
+    return `${(amount / 100).toFixed(2)} ${currency}`;
   };
 
   return (
     <div className="bg-white p-6 md:p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
       
-      {/* ... Step 1 (保持不变) ... */}
+      {/* === Step 1: 选择支付方式 === */}
       {step === 1 && (
         <>
           <h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
@@ -176,11 +253,11 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
         </>
       )}
 
-      {/* ... Step 2 (保持不变) ... */}
+      {/* === Step 2: 支付详情 & 确认 === */}
       {step === 2 && paymentData && (
         <div className="animate-in fade-in zoom-in duration-300 text-left">
           
-          {/* Top Nav */}
+          {/* 顶部导航 */}
           <div className="flex items-center justify-between mb-6">
             <button 
               onClick={handleBack}
@@ -202,11 +279,14 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
           {/* Amount Card */}
           <div className="bg-slate-50 rounded-xl p-4 md:p-5 mb-6 border border-slate-100 space-y-3">
+            
+            {/* 1. 原始金额 */}
             <div className="flex justify-between text-sm text-gray-600">
               <span>{t('payment.original_amount')}</span>
               <span className="font-medium text-gray-900">{formatMoney(paymentData.base_amount, paymentData.base_currency)}</span>
             </div>
 
+            {/* 2. 汇率 */}
             {paymentData.currency !== paymentData.base_currency && (
               <div className="flex justify-between text-xs text-gray-400 items-center">
                 <span className="flex items-center gap-1"><ArrowRightLeft size={12}/> {t('payment.exchange_rate')}</span>
@@ -214,20 +294,39 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
               </div>
             )}
 
+            {/* 4. 人工调价 (New) */}
+            {paymentData.adjustment_delta && paymentData.adjustment_delta !== 0 ? (
+              <div className={`flex justify-between text-sm font-medium items-center p-2 rounded-lg border 
+                ${paymentData.adjustment_delta < 0 
+                  ? 'bg-green-50 text-green-700 border-green-100' // 降价
+                  : 'bg-amber-50 text-amber-700 border-amber-100' // 加价
+                }`}>
+                <span className="flex items-center gap-1">
+                  {paymentData.adjustment_delta < 0 ? <TrendingDown size={14}/> : <TrendingUp size={14}/>}
+                  {t('payment.manual_adjustment')}
+                </span>
+                <span>
+                  {paymentData.adjustment_delta > 0 ? '+' : ''}
+                  {formatMoney(paymentData.adjustment_delta, paymentData.base_currency)}
+                </span>
+              </div>
+            ) : null}
+
+            {/* 3. 随机立减 */}
             {paymentData.random_offset !== 0 && (
               <div className="flex justify-between text-sm text-red-500 font-medium items-center bg-red-50 p-2 rounded-lg border border-red-100">
                 <span className="flex items-center gap-1">
                   <Sparkles size={14} className="fill-red-500" /> {t('payment.random_discount')}
                 </span>
                 <span>
-                  {paymentData.random_offset > 0 ? '+' : ''} 
-                  {formatMoney(paymentData.random_offset, paymentData.currency)}
+                  -{formatMoney(paymentData.random_offset, paymentData.currency)}
                 </span>
               </div>
             )}
 
             <div className="border-t border-slate-200 my-2"></div>
 
+            {/* 5. 实际需付 */}
             <div className="flex justify-between items-end">
               <span className="text-gray-600 font-medium pb-1">{t('payment.actual_pay')}</span>
               <span className="text-2xl md:text-3xl font-bold text-blue-600">

+ 120 - 46
src/components/ServiceList.tsx

@@ -3,7 +3,18 @@
 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 { 
+  Loader2, 
+  Search, 
+  MapPin, 
+  Filter, 
+  X, 
+  Globe, 
+  CheckCircle2, // 新增
+  ArrowRight,   // 新增
+  Plane,        // 新增
+  FileText      // 新增
+} from 'lucide-react';
 import Pagination from '@/components/common/Pagination';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
@@ -103,10 +114,49 @@ export default function ServiceList() {
     router.push(`/create-order/${id}`);
   };
 
+  // === 核心辅助函数:解析描述文本 ===
+  const renderDescription = (text: string) => {
+    if (!text) {
+      return (
+        <div className="flex items-center gap-2 text-slate-400 text-sm mt-2">
+          <FileText size={14} />
+          <span>{t('services.no_desc') || '暂无描述'}</span>
+        </div>
+      );
+    }
+
+    // 1. 按换行符分割
+    // 2. 过滤掉空行
+    // 3. 只取前 4 行,防止卡片无限拉长
+    const lines = text.split('\n').filter(line => line.trim().length > 0).slice(0, 4);
+
+    // 如果只有一行文字(没有换行符),或者处理后只剩一行,就显示为普通段落
+    if (lines.length <= 1) {
+      return (
+        <p className="text-slate-500 text-sm mt-2 line-clamp-3 leading-relaxed">
+          {text}
+        </p>
+      );
+    }
+
+    // 如果有多行,渲染为带图标的列表
+    return (
+      <ul className="space-y-2 mt-3">
+        {lines.map((line, index) => (
+          <li key={index} className="flex items-start gap-2.5 text-sm text-slate-600">
+            {/* 蓝色对勾图标 */}
+            <CheckCircle2 size={16} className="text-blue-500 mt-0.5 flex-shrink-0" />
+            <span className="leading-snug">{line.replace(/^[•\-\*]\s*/, '')}</span> 
+          </li>
+        ))}
+      </ul>
+    );
+  };
+
   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">
           
@@ -114,7 +164,7 @@ export default function ServiceList() {
           <div className="relative md:col-span-1">
             <input 
               type="text" 
-              placeholder={t('services.search_placeholder')} 
+              placeholder={t('services.search_placeholder') || 'Search services...'} 
               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)}
@@ -130,7 +180,7 @@ export default function ServiceList() {
               value={selectedCountry}
               onChange={(e) => setSelectedCountry(e.target.value)}
             >
-              <option value="">{t('services.all_countries')}</option>
+              <option value="">{t('services.all_countries') || '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" />
@@ -143,7 +193,7 @@ export default function ServiceList() {
               value={selectedType}
               onChange={(e) => setSelectedType(e.target.value)}
             >
-              <option value="">{t('services.all_types')}</option>
+              <option value="">{t('services.all_types') || '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" />
@@ -155,13 +205,13 @@ export default function ServiceList() {
               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')}
+              <Search size={16} /> {t('common.search') || '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')}
+                title={t('services.reset_filter') || 'Reset'}
               >
                 <X size={18} />
               </button>
@@ -170,7 +220,7 @@ export default function ServiceList() {
         </div>
       </div>
 
-      {/* === 商品列表:响应式调整 === */}
+      {/* === 商品列表 === */}
       {loading ? (
         <div className="flex justify-center p-20">
           <Loader2 className="animate-spin text-blue-600 w-8 h-8" />
@@ -182,54 +232,78 @@ export default function ServiceList() {
           <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>
+          <h3 className="text-slate-900 font-bold mb-1">{t('services.no_result_title') || 'No results found'}</h3>
+          <p className="text-slate-500 text-sm">{t('services.no_result_desc') || 'Try adjusting your search criteria'}</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">
+        /* 修改 Grid 布局:大屏幕使用 2 列 (lg:grid-cols-2),给予长文本更多空间 */
+        <div className="grid grid-cols-1 lg:grid-cols-2 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 
+              key={item.id} 
+              className="group bg-white rounded-2xl border border-slate-200 shadow-[0_2px_8px_rgba(0,0,0,0.04)] hover:shadow-[0_8px_24px_rgba(0,0,0,0.08)] hover:border-blue-500/30 transition-all duration-300 flex flex-col overflow-hidden relative"
+            >
+              {/* 装饰背景:右上角的淡色圆圈,增加层次感 */}
+              <div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-blue-50 to-transparent rounded-bl-[100px] -z-0 opacity-40 transition-opacity group-hover:opacity-100 pointer-events-none"></div>
+
+              {/* --- 卡片内容主体 --- */}
+              <div className="p-6 flex-grow flex flex-col z-10">
+                
+                {/* 1. 顶部元数据:国家 & 签证类型 */}
+                <div className="flex items-center justify-between mb-4">
+                  <div className="flex items-center gap-3">
+                    {/* 图标容器:模仿国旗或地点图标,增加视觉锚点 */}
+                    <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center text-blue-600 border border-blue-100 shadow-sm shrink-0">
+                      <Plane size={20} className="-rotate-45" /> 
+                    </div>
+                    
+                    <div>
+                      <h3 className="text-sm font-bold text-slate-800 flex items-center gap-1.5">
+                        {item.country}
+                        {item.city && <span className="font-normal text-slate-400">| {item.city}</span>}
+                      </h3>
+                      <span className="text-xs font-medium text-slate-500 bg-slate-100 px-2 py-0.5 rounded mt-1 inline-block border border-slate-200">
+                        {item.visa_type}
+                      </span>
+                    </div>
                   </div>
                 </div>
+
+                {/* 2. 服务标题 (限制高度,防止错位) */}
+                <h2 
+                  className="text-lg font-bold text-slate-900 mb-4 leading-snug group-hover:text-blue-600 transition-colors line-clamp-2 min-h-[3.5rem]"
+                  title={item.title}
+                >
+                  {item.title}
+                </h2>
+
+                {/* 3. 极细分割线 */}
+                <div className="h-px w-full bg-slate-100 mb-4"></div>
+
+                {/* 4. 描述信息:核心卖点列表 */}
+                <div className="flex-grow">
+                  {renderDescription(item.description)}
+                </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 className="px-6 py-4 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
+                <div>
+                  <p className="text-xs text-slate-400 mb-0.5">{t('services.service_fee') || 'Service Fee'}</p>
+                  <div className="flex items-baseline gap-1">
+                    <span className="text-sm font-semibold text-slate-900">{item.price_currency === 'CNY' ? '¥' : item.price_currency}</span>
+                    <span className="text-2xl font-bold text-slate-900 tracking-tight">
+                      {(item.price_amount / 100).toLocaleString()}
+                    </span>
+                  </div>
                 </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"
+                  className="flex items-center gap-2 px-5 py-2.5 bg-slate-900 text-white rounded-lg font-semibold text-sm shadow hover:bg-blue-600 hover:shadow-blue-200 hover:-translate-y-0.5 transition-all active:scale-95"
                 >
-                  {t('services.apply_now')}
+                  {t('services.apply_now') || 'Apply Now'}
+                  <ArrowRight size={16} />
                 </button>
               </div>
             </div>

+ 124 - 0
src/components/admin/orders/OrderPriceAdjustModal.tsx

@@ -0,0 +1,124 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { X, DollarSign, Loader2, Save } from 'lucide-react';
+import api from '@/lib/api';
+
+interface OrderPriceAdjustModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  order: any;
+  onSuccess: () => void;
+}
+
+export default function OrderPriceAdjustModal({ isOpen, onClose, order, onSuccess }: OrderPriceAdjustModalProps) {
+  const [loading, setLoading] = useState(false);
+  
+  // 修改 1: 将 adjustment_delta 初始化为空字符串,方便处理输入逻辑
+  const [formData, setFormData] = useState({
+    adjustment_delta: '', // 改为 string
+    reason: '人工调价'
+  });
+
+  useEffect(() => {
+    if (isOpen) {
+      setFormData({ adjustment_delta: '', reason: '人工调价' });
+    }
+  }, [isOpen]);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!order?.id) return;
+    
+    // 修改 2: 提交前进行校验和转换
+    const deltaValue = parseFloat(formData.adjustment_delta);
+    if (isNaN(deltaValue)) {
+      alert("请输入有效的金额");
+      return;
+    }
+    if (deltaValue === 0) {
+      alert("调整金额不能为 0");
+      return;
+    }
+
+    setLoading(true);
+    try {
+      // 转换为分 (Assuming backend needs cents)
+      await api.post('/api/vas/order/adjust-price', 
+        { 
+          adjustment_delta: Math.round(deltaValue * 100), 
+          reason: formData.reason 
+        }, 
+        { params: { order_id: order.id } }
+      );
+
+      alert('价格调整成功');
+      onSuccess();
+      onClose();
+    } catch (error: any) {
+      alert('调整失败: ' + (error.response?.data?.message || '未知错误'));
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen || !order) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className="font-bold text-gray-900 flex items-center gap-2">
+            <DollarSign size={18} className="text-amber-500" /> 调整订单价格
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <form onSubmit={handleSubmit} className="p-6 space-y-4">
+          <div className="bg-blue-50 p-3 rounded-lg text-xs text-blue-700 space-y-1">
+            <p>订单号: <span className="font-mono font-bold">{order.id}</span></p>
+            <p>原价格: <span className="font-bold">{(order.base_amount / 100).toFixed(2)} {order.base_currency}</span></p>
+          </div>
+
+          <div>
+            <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">调整金额 (元)</label>
+            <input
+              type="number"
+              required
+              className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+              placeholder="例如: -20 表示减免20元"
+              // 修改 3: 直接绑定字符串值
+              value={formData.adjustment_delta}
+              // 修改 4: 移除 parseFloat,直接保存用户输入的字符串
+              onChange={e => setFormData({ ...formData, adjustment_delta: e.target.value })}
+            />
+            <p className="text-[10px] text-slate-400 mt-1">输入负数(如 -20)表示降价,输入正数(如 20)表示加价</p>
+          </div>
+
+          <div>
+            <label className="block text-xs font-bold text-slate-500 mb-1 uppercase">调价原因</label>
+            <input
+              type="text"
+              required
+              className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+              value={formData.reason}
+              onChange={e => setFormData({ ...formData, reason: e.target.value })}
+            />
+          </div>
+
+          <div className="pt-4 flex gap-3">
+            <button type="button" onClick={onClose} className="flex-1 px-4 py-2 border rounded-lg hover:bg-gray-50 text-sm font-medium">取消</button>
+            <button 
+              type="submit" 
+              disabled={loading}
+              className="flex-1 bg-slate-900 text-white rounded-lg py-2 text-sm font-bold hover:bg-slate-800 transition flex items-center justify-center gap-2 disabled:opacity-50"
+            >
+              {loading ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
+              确认提交
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}

+ 74 - 37
src/components/admin/orders/OrderTable.tsx

@@ -1,16 +1,23 @@
 'use client';
 
-import { Eye, XCircle, User, Box, Edit, Clock, FileText, Wallet } from 'lucide-react';
-import { OrderDetail } from './OrderDetailModal';
+import { Eye, XCircle, User, Box, Edit, Clock, FileText, Wallet, DollarSign } from 'lucide-react';
+import { OrderDetail } from './OrderDetailModal'; // 假设类型从这里导入
 import LocalTime from '@/components/common/LocalTime';
 
+// 扩展类型定义以包含调价字段 (如果 OrderDetail 没有包含,请在这里补全)
+interface ExtendedOrderDetail extends OrderDetail {
+  final_amount?: number;
+  adjustment_delta?: number;
+}
+
 interface OrderTableProps {
-  orders: OrderDetail[]; 
+  orders: ExtendedOrderDetail[]; 
   loading: boolean;
   onCancel: (orderId: string) => void;
   onViewDetail: (order: OrderDetail) => void;
   onEdit: (order: OrderDetail) => void;
-  onCheckPayments: (order: OrderDetail) => void; // 新增:查看/核销支付记录
+  onCheckPayments: (order: OrderDetail) => void; 
+  onAdjustPrice: (order: OrderDetail) => void;   
 }
 
 export default function OrderTable({ 
@@ -19,7 +26,8 @@ export default function OrderTable({
   onCancel, 
   onViewDetail, 
   onEdit, 
-  onCheckPayments 
+  onCheckPayments,
+  onAdjustPrice 
 }: OrderTableProps) {
   
   if (loading) {
@@ -38,7 +46,6 @@ export default function OrderTable({
     );
   }
 
-  // 状态颜色映射
   const getStatusColor = (status: string) => {
     switch (status) {
       case 'paid': return 'bg-green-100 text-green-800 border-green-200';
@@ -50,15 +57,42 @@ export default function OrderTable({
     }
   };
 
-  // 辅助函数:是否可以取消
   const canCancel = (status: string) => status !== 'cancelled' && status !== 'completed' && status !== 'failed';
 
+  // === 辅助函数:渲染价格 ===
+  const renderPrice = (order: ExtendedOrderDetail) => {
+    const currency = order.base_currency;
+    const base = order.base_amount || 0;
+    // 优先使用 final_amount,如果后端没返回,则降级使用 base_amount
+    // 注意:如果 final_amount 为 0 也是合法的,所以判断 undefined
+    const final = order.final_amount !== undefined ? order.final_amount : base;
+    
+    // 如果有调价 (final != base)
+    if (final !== base) {
+      return (
+        <div className="flex flex-col items-end sm:items-start">
+          <span className="text-sm font-bold text-emerald-600">
+            {(final / 100).toFixed(2)} {currency}
+          </span>
+          <span className="text-xs text-slate-400 line-through decoration-slate-400/50">
+            {(base / 100).toFixed(2)}
+          </span>
+        </div>
+      );
+    }
+
+    // 无调价
+    return (
+      <span className="text-sm font-bold text-slate-900">
+        {(base / 100).toFixed(2)} {currency}
+      </span>
+    );
+  };
+
   return (
     <div className="space-y-4">
       
-      {/* =========================== */}
-      {/* 1. Desktop View (Table) */}
-      {/* =========================== */}
+      {/* Desktop View */}
       <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
         <div className="overflow-x-auto">
           <table className="min-w-full divide-y divide-slate-200">
@@ -98,31 +132,32 @@ export default function OrderTable({
                       </div>
                     </div>
                   </td>
-                  <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 font-bold font-mono">
-                    {(order.base_amount / 100).toFixed(2)} {order.base_currency}
+                  
+                  {/* 使用 renderPrice */}
+                  <td className="px-6 py-4 whitespace-nowrap font-mono">
+                    {renderPrice(order)}
                   </td>
+
                   <td className="px-6 py-4 whitespace-nowrap">
                     <span className={`px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full border ${getStatusColor(order.status)}`}>
                       {order.status}
                     </span>
                   </td>
                   <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                    {/* ... 按钮组保持不变 ... */}
                     <div className="flex justify-end gap-2">
-                      
-                      {/* 待支付状态显示人工核销按钮 */}
                       {order.status === 'pending' && (
-                        <button 
-                          onClick={() => onCheckPayments(order)}
-                          className="group flex items-center justify-center p-1.5 rounded-md text-emerald-600 hover:text-emerald-900 bg-emerald-50 hover:bg-emerald-100 transition border border-transparent hover:border-emerald-200"
-                          title="查看/核销支付记录 (处理漏单)"
-                        >
-                          <Wallet size={16} />
-                        </button>
+                        <>
+                          <button onClick={() => onAdjustPrice(order)} className="group flex items-center justify-center p-1.5 rounded-md text-amber-600 hover:text-amber-900 bg-amber-50 hover:bg-amber-100 transition border border-transparent hover:border-amber-200" title="调整价格">
+                            <DollarSign size={16} />
+                          </button>
+                          <button onClick={() => onCheckPayments(order)} className="group flex items-center justify-center p-1.5 rounded-md text-emerald-600 hover:text-emerald-900 bg-emerald-50 hover:bg-emerald-100 transition border border-transparent hover:border-emerald-200" title="核销支付">
+                            <Wallet size={16} />
+                          </button>
+                        </>
                       )}
-
                       <button onClick={() => onEdit(order)} className="p-1.5 rounded-md text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-200" title="修改"><Edit size={16} /></button>
                       <button onClick={() => onViewDetail(order)} className="p-1.5 rounded-md text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-200" title="详情"><Eye size={16} /></button>
-                      
                       {canCancel(order.status) && (
                         <button onClick={() => onCancel(order.id)} className="p-1.5 rounded-md text-red-600 hover:bg-red-50 border border-transparent hover:border-red-200" title="取消"><XCircle size={16} /></button>
                       )}
@@ -135,14 +170,11 @@ export default function OrderTable({
         </div>
       </div>
 
-      {/* =========================== */}
-      {/* 2. Mobile View (Cards) */}
-      {/* =========================== */}
+      {/* Mobile View */}
       <div className="md:hidden space-y-4">
         {orders.map((order) => (
           <div key={order.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
-            
-            {/* Header: ID & Status */}
+            {/* Header ... */}
             <div className="flex justify-between items-start mb-3 border-b border-slate-100 pb-2">
               <div className="flex items-center gap-2 text-slate-500">
                 <FileText size={14} />
@@ -155,7 +187,7 @@ export default function OrderTable({
 
             {/* Info Grid */}
             <div className="space-y-3">
-              {/* Product */}
+              {/* Product ... */}
               <div className="flex items-start gap-3">
                 <div className="p-1.5 bg-blue-50 rounded text-blue-600 shrink-0 mt-0.5">
                   <Box size={16} />
@@ -180,20 +212,25 @@ export default function OrderTable({
                     <span className="truncate font-medium">{order.user_name || order.applicant_name || '-'}</span>
                   </div>
                 </div>
-                <div className="text-right">
+                <div className="text-right flex flex-col items-end justify-center">
                   <span className="text-xs text-slate-400 block mb-0.5">金额</span>
-                  <div className="font-mono font-bold text-slate-900">
-                    {(order.base_amount / 100).toFixed(2)} <span className="text-xs font-normal">{order.base_currency}</span>
+                  <div className="font-mono">
+                    {/* 使用 renderPrice */}
+                    {renderPrice(order)}
                   </div>
                 </div>
               </div>
             </div>
 
-            {/* Actions */}
-            <div className="grid grid-cols-4 gap-2 mt-4 pt-3 border-t border-slate-100">
-              
-              {order.status === 'pending' ? (
+            {/* Actions ... (保持不变) */}
+            <div className="grid grid-cols-5 gap-2 mt-4 pt-3 border-t border-slate-100">
+               {/* ... */}
+               {order.status === 'pending' ? (
                 <>
+                  <button onClick={() => onAdjustPrice(order)} className="col-span-1 flex items-center justify-center py-2 bg-amber-50 text-amber-700 rounded-lg text-sm font-medium active:scale-95 transition border border-amber-200">
+                    <DollarSign size={16} />
+                  </button>
+                  {/* ... other buttons ... */}
                   <button onClick={() => onCheckPayments(order)} className="col-span-1 flex items-center justify-center py-2 bg-emerald-50 text-emerald-700 rounded-lg text-sm font-medium active:scale-95 transition border border-emerald-200">
                     <Wallet size={16} />
                   </button>
@@ -212,7 +249,7 @@ export default function OrderTable({
                   <button onClick={() => onEdit(order)} className="col-span-2 flex items-center justify-center gap-1 py-2 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-medium active:scale-95 transition">
                     <Edit size={16} /> 编辑
                   </button>
-                  <button onClick={() => onViewDetail(order)} className="col-span-2 flex items-center justify-center gap-1 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-medium active:scale-95 transition">
+                  <button onClick={() => onViewDetail(order)} className="col-span-3 flex items-center justify-center gap-1 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-medium active:scale-95 transition">
                     <Eye size={16} /> 详情
                   </button>
                 </>

+ 250 - 0
src/components/admin/payments/ExchangeRateModal.tsx

@@ -0,0 +1,250 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { X, Save, RefreshCw, Loader2, ArrowRight, AlertTriangle, Lock } from 'lucide-react';
+
+interface ExchangeRateModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+}
+
+interface RateConfig {
+  base: string;
+  precision: number;
+  rates: Record<string, number>;
+}
+
+// 常用货币名称映射,增强可读性
+const CURRENCY_NAMES: Record<string, string> = {
+  CNY: '人民币 (Chinese Yuan)',
+  USD: '美元 (US Dollar)',
+  EUR: '欧元 (Euro)',
+  GBP: '英镑 (British Pound)',
+  HKD: '港币 (Hong Kong Dollar)',
+  JPY: '日元 (Japanese Yen)',
+  AUD: '澳元 (Australian Dollar)',
+  CAD: '加元 (Canadian Dollar)',
+  SGD: '新加坡元 (Singapore Dollar)',
+};
+
+export default function ExchangeRateModal({ isOpen, onClose }: ExchangeRateModalProps) {
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  
+  // 默认配置
+  const [config, setConfig] = useState<RateConfig>({
+    base: 'CNY',
+    precision: 4,
+    rates: {}
+  });
+
+  // 获取数据
+  const fetchRates = async () => {
+    setLoading(true);
+    try {
+      const res = await api.get('/api/dynamic-configurations/key/EXCHANGE_RATES');
+      const val = res.data.data?.config_value;
+      
+      // 兼容处理:后端可能返回 JSON 对象,也可能返回 JSON 字符串
+      if (val && typeof val === 'object') {
+        setConfig(val);
+      } else if (typeof val === 'string') {
+        try {
+          setConfig(JSON.parse(val));
+        } catch (e) {
+          console.error("JSON Parse error", e);
+        }
+      }
+    } catch (e) {
+      console.error("Failed to fetch rates", e);
+      // 兜底数据
+      setConfig({
+        base: 'CNY',
+        precision: 4,
+        rates: { "CNY": 1, "USD": 7.25, "EUR": 7.65, "GBP": 9.10, "HKD": 0.92, "JPY": 0.048 }
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (isOpen) fetchRates();
+  }, [isOpen]);
+
+  // 处理输入
+  const handleRateChange = (currency: string, value: string) => {
+    // 允许输入过程中的空字符串,但存储时转为数字
+    const numVal = value === '' ? 0 : parseFloat(value);
+    
+    setConfig(prev => ({
+      ...prev,
+      rates: {
+        ...prev.rates,
+        [currency]: isNaN(numVal) ? 0 : numVal
+      }
+    }));
+  };
+
+  // 提交保存
+  const handleSubmit = async () => {
+    // 安全校验:防止基准货币被篡改
+    if (config.rates[config.base] !== 1) {
+      alert(`错误:基准货币 ${config.base} 的汇率必须保持为 1。请重置后重试。`);
+      return;
+    }
+
+    setSubmitting(true);
+    try {
+      // 必须将对象转为字符串,因为后端数据库字段通常是 TEXT
+      const payload = {
+        config_value: JSON.stringify(config),
+        description: "汇率换算表 (管理员手动更新)"
+      };
+      
+      await api.put('/api/dynamic-configurations/key/EXCHANGE_RATES', payload);
+      alert("汇率配置已保存");
+      onClose();
+    } catch (e: any) {
+      alert("保存失败: " + (e.response?.data?.message || e.message));
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl overflow-hidden flex flex-col max-h-[90vh]">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div>
+            <h3 className="font-bold text-lg text-slate-800 flex items-center gap-2">
+              <RefreshCw size={18} className="text-blue-600"/> 汇率配置
+            </h3>
+            <p className="text-sm text-slate-500 mt-1">
+              系统基准货币:<span className="font-bold text-blue-700">{config.base}</span>
+            </p>
+          </div>
+          <button onClick={onClose} className="p-2 hover:bg-slate-200 rounded-full transition">
+            <X size={20} className="text-slate-500" />
+          </button>
+        </div>
+
+        {/* ⚠️ 警告栏:明确换算逻辑 */}
+        <div className="bg-yellow-50 px-6 py-3 border-b border-yellow-100 flex items-start gap-3">
+          <AlertTriangle className="text-yellow-600 shrink-0 mt-0.5" size={18} />
+          <div className="text-xs text-yellow-800 leading-relaxed">
+            <strong>核心逻辑:</strong> 这里的数值代表 <strong>1 单位该货币</strong> 等值于 <strong>多少单位基准货币 ({config.base})</strong>。<br/>
+            例如:若基准是 CNY,USD 行填入 <code className="bg-yellow-200 px-1 rounded">7.25</code>,代表 <strong>1 USD = 7.25 CNY</strong>。
+          </div>
+        </div>
+
+        {/* Body */}
+        <div className="p-6 overflow-y-auto flex-1 bg-slate-50/50">
+          {loading ? (
+            <div className="flex justify-center py-12">
+              <Loader2 className="animate-spin text-slate-400" size={32} />
+            </div>
+          ) : (
+            <div className="space-y-4">
+              {Object.entries(config.rates).map(([currency, rate]) => {
+                const isBase = currency === config.base;
+                
+                // 辅助计算:反向汇率 (例如 1 CNY = 0.137 USD)
+                // 用于给用户直觉上的校验
+                const inverseRate = rate > 0 ? (1 / rate).toFixed(4) : '0';
+
+                return (
+                  <div 
+                    key={currency} 
+                    className={`
+                      relative p-4 rounded-xl border transition-all
+                      ${isBase ? 'bg-slate-100 border-slate-200 opacity-80' : 'bg-white border-slate-200 hover:border-blue-300 hover:shadow-md'}
+                    `}
+                  >
+                    <div className="flex flex-col md:flex-row md:items-center gap-4">
+                      
+                      {/* 1. 左侧:货币信息 */}
+                      <div className="w-full md:w-32 shrink-0">
+                        <div className="flex items-center gap-2">
+                          <span className="font-bold text-lg text-slate-700">{currency}</span>
+                          {isBase && <Lock size={14} className="text-slate-400" title="基准货币锁定" />}
+                        </div>
+                        <div className="text-xs text-slate-500 truncate" title={CURRENCY_NAMES[currency]}>
+                          {CURRENCY_NAMES[currency] || currency}
+                        </div>
+                      </div>
+
+                      {/* 2. 中间:等式输入区 (防御性设计核心) */}
+                      <div className="flex-1 flex items-center gap-3 bg-slate-50 p-2 rounded-lg border border-slate-200 shadow-inner">
+                        <div className="text-sm font-medium text-slate-500 whitespace-nowrap pl-2">
+                          1 {currency}
+                        </div>
+                        
+                        <ArrowRight size={16} className="text-slate-400" />
+                        
+                        <div className="relative flex-1">
+                          <input
+                            type="number"
+                            step="0.0001"
+                            disabled={isBase}
+                            value={rate}
+                            onChange={(e) => handleRateChange(currency, e.target.value)}
+                            className={`
+                              w-full text-center font-mono font-bold text-lg bg-transparent outline-none
+                              ${isBase ? 'text-slate-500 cursor-not-allowed' : 'text-blue-700'}
+                            `}
+                            placeholder="0.00"
+                          />
+                        </div>
+
+                        <div className="text-sm font-bold text-slate-700 whitespace-nowrap pr-2">
+                          {config.base}
+                        </div>
+                      </div>
+
+                      {/* 3. 右侧:反向参照 (Double Check) */}
+                      {!isBase && (
+                        <div className="w-full md:w-48 shrink-0 text-right md:border-l md:pl-4 border-slate-100">
+                          <div className="text-[10px] text-slate-400 uppercase tracking-wide">
+                            反向参考 (Inverse)
+                          </div>
+                          <div className="text-xs font-mono text-slate-600 mt-1">
+                            1 {config.base} ≈ <span className="font-bold">{inverseRate}</span> {currency}
+                          </div>
+                        </div>
+                      )}
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          )}
+        </div>
+
+        {/* Footer */}
+        <div className="px-6 py-4 border-t bg-slate-50 flex justify-end gap-3">
+          <button
+            onClick={fetchRates}
+            className="flex items-center gap-2 px-4 py-2 text-slate-600 hover:bg-white border border-transparent hover:border-slate-200 rounded-lg transition"
+            disabled={submitting || loading}
+          >
+            <RefreshCw size={16} className={loading ? "animate-spin" : ""} /> 重置
+          </button>
+          <button
+            onClick={handleSubmit}
+            disabled={submitting || loading}
+            className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 shadow-sm transition disabled:opacity-70 disabled:cursor-not-allowed"
+          >
+            {submitting ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
+            保存配置
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 9 - 9
src/components/common/TimeAgo.tsx

@@ -41,22 +41,22 @@ export default function TimeAgo({ date, className = '' }: TimeAgoProps) {
 
       // 处理未来时间(可能是本地时间偏差或服务器时间超前)
       if (diffInSeconds < 0) {
-        setTimeAgo('刚刚');
+        setTimeAgo('just now');
         return;
       }
 
       if (diffInSeconds < 60) {
-        setTimeAgo(`${diffInSeconds}秒前`);
+        setTimeAgo(`${diffInSeconds}s ago`);
       } else if (diffInSeconds < 3600) {
-        setTimeAgo(`${Math.floor(diffInSeconds / 60)}分钟前`);
+        setTimeAgo(`${Math.floor(diffInSeconds / 60)}m ago`);
       } else if (diffInSeconds < 86400) {
-        setTimeAgo(`${Math.floor(diffInSeconds / 3600)}小时前`);
-      } else if (diffInSeconds < 2592000) { // 30
-        setTimeAgo(`${Math.floor(diffInSeconds / 86400)}天前`);
-      } else if (diffInSeconds < 31536000) { // 365
-        setTimeAgo(`${Math.floor(diffInSeconds / 2592000)}个月前`);
+        setTimeAgo(`${Math.floor(diffInSeconds / 3600)}h ago`);
+      } else if (diffInSeconds < 2592000) { // 30 days
+        setTimeAgo(`${Math.floor(diffInSeconds / 86400)}d ago`);
+      } else if (diffInSeconds < 31536000) { // 365 days
+        setTimeAgo(`${Math.floor(diffInSeconds / 2592000)}mo ago`);
       } else {
-        setTimeAgo(`${Math.floor(diffInSeconds / 31536000)}年前`);
+        setTimeAgo(`${Math.floor(diffInSeconds / 31536000)}y ago`);
       }
     };
 

+ 58 - 14
src/components/dashboard/OrderList.tsx

@@ -3,7 +3,7 @@
 import { useEffect, useState } from 'react';
 import { useRouter } from 'next/navigation';
 import api from '@/lib/api';
-import { CreditCard, Loader2, AlertCircle, Package, Clock, Hash, Eye, Search } from 'lucide-react';
+import { CreditCard, Loader2, AlertCircle, Package, Clock, Hash, Eye, Search, Sparkles } from 'lucide-react'; // 引入 Sparkles
 import Pagination from '@/components/common/Pagination';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import LocalTime from '@/components/common/LocalTime';
@@ -14,7 +14,11 @@ export interface Order {
   status: string;
   base_amount: number;
   base_currency: string;
-  amount?: number;
+  // 新增调价相关字段
+  final_amount?: number; 
+  adjustment_delta?: number;
+  
+  amount?: number; // 兼容字段
   currency?: string;
   product_title?: string;
   product_name?: string;
@@ -65,7 +69,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
       setPage(targetPage);
 
     } catch (error) {
-      console.warn("API Error (Using Mock Data):", error);
+      console.warn("API Error", error);
       setOrders([]);
       setTotal(0);
     } finally {
@@ -76,9 +80,51 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
   const handleSearch = () => fetchOrders(1);
   const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
-  const formatMoney = (amount: number, currency: string) => {
-    if (isNaN(amount)) return '0.00';
-    return `${(amount / 100).toFixed(2)} ${currency}`;
+  // 格式化金额辅助函数
+  const formatMoney = (val: number, cur: string) => {
+    if (isNaN(val)) return '0.00';
+    return `${(val / 100).toFixed(2)} ${cur}`;
+  };
+
+  // === 修改:渲染价格逻辑 (支持调价显示) ===
+  const renderPriceSection = (order: Order) => {
+    const currency = order.base_currency || order.currency || 'CNY';
+    const base = order.base_amount || 0;
+    
+    // 如果后端返回了 final_amount,使用它;否则回退到 base_amount
+    // 注意:adjustment_delta 可能为 0,此时 final_amount === base_amount
+    const final = order.final_amount !== undefined ? order.final_amount : base;
+    const delta = order.adjustment_delta || 0;
+    
+    // 是否发生调价
+    const hasAdjustment = delta !== 0;
+
+    if (hasAdjustment) {
+      return (
+        <div className="flex flex-col items-end sm:items-end">
+          <div className="flex items-center gap-1.5">
+            <span className="text-xs text-slate-400 line-through decoration-slate-300">
+              {formatMoney(base, currency)}
+            </span>
+            {/* 调价标签 */}
+            <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium flex items-center ${delta < 0 ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'}`}>
+              <Sparkles size={10} className="mr-0.5" />
+              {delta < 0 ? '省' : '调'} {formatMoney(Math.abs(delta), '')}
+            </span>
+          </div>
+          <span className="text-lg font-bold text-red-600 font-mono">
+             {formatMoney(final, currency)}
+          </span>
+        </div>
+      );
+    }
+
+    // 无调价,显示原价
+    return (
+      <span className="text-lg font-bold text-slate-900 font-mono">
+        {formatMoney(base, currency)}
+      </span>
+    );
   };
 
   const renderStatusBadge = (status: string) => {
@@ -102,7 +148,6 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
 
   return (
     <div className="space-y-4">
-      
       {/* Search Bar */}
       <div className="flex justify-between items-center bg-white p-3 rounded-xl shadow-sm border border-slate-200">
         <div className="relative w-full max-w-sm">
@@ -143,9 +188,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
           <div className="divide-y divide-slate-100">
             {orders.map((order) => {
               const title = order.product_title || order.product_name || t('order.unknown_service');
-              const price = order.base_amount ?? order.amount ?? 0;
-              const currency = order.base_currency || order.currency || 'CNY';
-
+              
               return (
                 <div key={order.id} className="p-5 hover:bg-slate-50 transition flex flex-col sm:flex-row gap-4 sm:items-center">
                   
@@ -175,10 +218,11 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
                   
                   {/* Right Info: Price & Status (Desktop) */}
                   <div className="flex flex-row sm:flex-col items-center sm:items-end justify-between sm:justify-center gap-1 sm:text-right border-t sm:border-0 border-slate-50 pt-3 sm:pt-0">
-                    <div className="text-base font-bold text-slate-900 font-mono">
-                      {formatMoney(price, currency)}
-                    </div>
-                    <div className="hidden sm:block">
+                    
+                    {/* === 使用新的价格渲染逻辑 === */}
+                    {renderPriceSection(order)}
+
+                    <div className="hidden sm:block mt-1">
                       {renderStatusBadge(order.status)}
                     </div>
                   </div>

+ 4 - 3
src/components/dashboard/Sidebar.tsx

@@ -31,8 +31,9 @@ export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
   const { t } = useLanguage();
   
   // 2. 这里配置你的联系方式
-  const TELEGRAM_USERNAME = "visafly_support"; 
-  const WHATSAPP_NUMBER = "1234567890"; 
+  const TELEGRAM_USERNAME = "Visafly Support"; 
+  const TELEGRAM_NUMBER = "+8617386033451"; 
+  const WHATSAPP_NUMBER = "353892125284"; 
 
   const menuItems: MenuItem[] = [
     { id: 'orders', label: t('sidebar.orders'), icon: FileText },
@@ -89,7 +90,7 @@ export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
               <TelegramIcon className="w-3 h-3" />
             </div>
             <a 
-              href={`https://t.me/${TELEGRAM_USERNAME}`}
+              href={`https://t.me/${TELEGRAM_NUMBER}`}
               target="_blank" 
               rel="noopener noreferrer"
               className="hover:text-sky-600 hover:underline truncate"

+ 200 - 0
src/components/slots/CitySlotCard.tsx

@@ -0,0 +1,200 @@
+'use client';
+
+import { Calendar, MapPin, ExternalLink, Clock, AlertCircle, CheckCircle, ListOrdered, Hourglass, Activity, History } from 'lucide-react';
+import LocalTime from '@/components/common/LocalTime';
+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;
+}
+
+export default function CitySlotCard({ data }: { data: SlotSnapshot }) {
+  const { t, lang } = useLanguage();
+  
+  // === 1. 计算过期状态 ===
+  const now = new Date().getTime();
+  let lastCheckTime = 0;
+  
+  if (data.last_check_at) {
+      let timeStr = data.last_check_at;
+      if (!timeStr.endsWith('Z') && !timeStr.includes('+')) timeStr += 'Z';
+      lastCheckTime = new Date(timeStr).getTime();
+  }
+
+  // 阈值:5 分钟
+  const isDataStale = (now - lastCheckTime) > 5 * 60 * 1000;
+
+  const rawStatus = data.availability_status;
+  const isAvailable = rawStatus === 'Available';
+  const isWaitlist = rawStatus === 'Waitlist';
+
+  // 格式化日期
+  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' 
+    });
+  };
+
+  // === 2. 动态样式配置 ===
+  const getStyleConfig = () => {
+    
+    // [特殊处理]:数据过期,但曾经是有号的 -> 显示为“橙色历史记录”风格
+    // 既不显示绿色误导用户,也不显示灰色让用户以为系统挂了
+    if (isDataStale && isAvailable) {
+      return {
+        bg: 'bg-orange-50/40 border-orange-200', // 浅橙色背景
+        badgeBg: 'bg-orange-100 text-orange-700',
+        icon: <History size={14} />, // 历史图标
+        label: lang === 'zh' ? '历史快照' : 'Stale Data', // 提示数据旧
+        dateColor: 'text-slate-700 opacity-80', // 日期颜色加深但不如绿色鲜艳
+        showDate: true // 依然显示日期
+      };
+    }
+
+    // 正常 Available (新鲜数据)
+    if (isAvailable) {
+      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',
+        dateColor: 'text-green-700',
+        showDate: true
+      };
+    }
+
+    // Waitlist
+    if (isWaitlist) {
+      return {
+        bg: 'bg-yellow-50/60 border-yellow-200',
+        badgeBg: 'bg-yellow-100 text-yellow-700',
+        icon: <ListOrdered size={14} />,
+        label: t('slots.status_waitlist') || 'Waitlist Open',
+        dateColor: 'text-yellow-700',
+        showDate: false // Waitlist 通常没日期
+      };
+    }
+
+    // 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',
+      dateColor: 'text-slate-400',
+      showDate: false
+    };
+  };
+
+  const style = getStyleConfig();
+
+  // 决定中间显示什么内容
+  const renderCenterContent = () => {
+    if (style.showDate) {
+      return (
+        <>
+          <Calendar size={18} />
+          {formatDate(data.earliest_date)}
+          {/* 如果过期,加一个小尾巴提示 */}
+          {isDataStale && <span className="text-xs font-normal ml-1 opacity-60">(Maybe gone)</span>}
+        </>
+      );
+    } 
+    
+    if (isWaitlist) {
+      return (
+        <>
+          <Hourglass size={18} />
+          <span>Waitlist</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">
+          {isWaitlist ? 'Current Status' : (t('slots.earliest_date') || 'Earliest Slot')}
+        </p>
+        <div className={`flex items-center gap-2 text-lg font-bold ${style.dateColor}`}>
+          {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={!isDataStale ? "text-green-500" : "text-slate-300"} />
+            {lang === 'zh' ? '监控状态' : 'Monitor'}
+          </span>
+          <div className="flex items-center gap-1">
+             {/* 数据新鲜才闪烁 */}
+             {!isDataStale && (
+               <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 ${!isDataStale ? 'text-green-700' : 'text-slate-400'}`}>
+                {/* 4天前 检测 */}
+                <TimeAgo date={data.last_check_at} />
+             </span>
+          </div>
+        </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>
+    </div>
+  );
+}

+ 6 - 2
src/lib/i18n/locales/en.ts

@@ -34,8 +34,8 @@ export const en = {
     notice: 'Notice',
     confirm_known: 'Got it',
     confirm_title: 'Confirm Action',
-    agree_agreement: "By clicking submit, you agree to the Terms of Service and Privacy Policy"
-
+    agree_agreement: "By clicking submit, you agree to the Terms of Service and Privacy Policy",
+    book_now: 'Book Now'
   },
   auth: {
     welcome_back: 'Welcome Back',
@@ -150,6 +150,8 @@ export const en = {
     feat_fast_desc: 'Connected to official APIs, automated workflows reduce application time to 1/3 of traditional methods.',
   },
   order: {
+    official_fast_secure: 'Officially Secured · Fast Processing · Encrypted Privacy',
+    fill_form_title: 'Application Details',
     search_placeholder: 'Search Order ID or Product Name...',
     history_title: 'Application History',
     empty_title: 'No Orders Found',
@@ -313,6 +315,7 @@ export const en = {
     earliest_date: 'Earliest Date',
     slots_count: 'slots',
     select_hint: 'Please select criteria and click search',
+    slots_may_be_unavailable: 'Slots may be unavailable'
   },
   pagination: {
     prev: 'Previous',
@@ -361,6 +364,7 @@ export const en = {
     cookie: 'Cookie Policy',
   },
   payment: {
+    manual_adjustment: 'Price Adjustment',
     order_created: 'Order Created',
     order_id: 'Order ID',
     select_method: 'Select Payment Method',

+ 6 - 1
src/lib/i18n/locales/zh.ts

@@ -34,7 +34,8 @@ export const zh = {
     notice: '提示',
     confirm_known: '知道了',
     confirm_title: '确认操作',
-    agree_agreement: '点击提交即代表同意服务条款与隐私政策'
+    agree_agreement: '点击提交即代表同意服务条款与隐私政策',
+    book_now: '立即前往预定'
   },
   auth: {
     welcome_back: '欢迎回来',
@@ -149,6 +150,8 @@ export const zh = {
     feat_fast_desc: '对接官方 API,自动化流程将申请时间缩短至传统的 1/3。',
   },
   order: {
+    official_fast_secure: '官方保障 · 极速处理 · 隐私加密',
+    fill_form_title: '填写申请信息',
     search_placeholder: '搜索订单号或商品名称...',
     history_title: '申请记录',
     empty_title: '暂无订单',
@@ -312,6 +315,7 @@ export const zh = {
     earliest_date: '最早可约',
     slots_count: '个时段',
     select_hint: '请选择条件并点击查询',
+    slots_may_be_unavailable: '可能已无'
   },
   pagination: {
     prev: '上一页',
@@ -360,6 +364,7 @@ export const zh = {
     cookie: 'Cookie 政策',
   },
   payment: {
+    manual_adjustment: '人工调价',
     order_created: '订单已创建',
     order_id: '订单号',
     select_method: '选择支付方式',