jerry 4 месяцев назад
Родитель
Сommit
9203709438
42 измененных файлов с 3391 добавлено и 2130 удалено
  1. 38 18
      src/app/admin/cards/page.tsx
  2. 36 34
      src/app/admin/layout.tsx
  3. 60 105
      src/app/admin/orders/page.tsx
  4. 163 0
      src/app/admin/payment-confirmations/page.tsx
  5. 21 12
      src/app/admin/payments/page.tsx
  6. 87 119
      src/app/admin/products/page.tsx
  7. 49 51
      src/app/admin/tasks/page.tsx
  8. 23 15
      src/app/admin/tickets/page.tsx
  9. 42 77
      src/app/admin/users/page.tsx
  10. 13 9
      src/app/create-order/[id]/page.tsx
  11. 55 65
      src/app/dashboard/page.tsx
  12. 28 27
      src/app/knowledge/page.tsx
  13. 51 35
      src/app/page.tsx
  14. 135 0
      src/app/payment/confirm/page.tsx
  15. 2 5
      src/app/services/page.tsx
  16. 38 102
      src/app/slots/page.tsx
  17. 69 36
      src/components/AuthForm.tsx
  18. 78 39
      src/components/CreateOrderForm.tsx
  19. 35 26
      src/components/Footer.tsx
  20. 116 37
      src/components/Navbar.tsx
  21. 96 100
      src/components/PaymentProcessor.tsx
  22. 18 19
      src/components/ServiceList.tsx
  23. 116 98
      src/components/admin/AdminSidebar.tsx
  24. 127 55
      src/components/admin/cards/CardTable.tsx
  25. 157 113
      src/components/admin/orders/OrderTable.tsx
  26. 270 0
      src/components/admin/payments/ConfirmationDetailModal.tsx
  27. 195 0
      src/components/admin/payments/ConfirmationTable.tsx
  28. 143 72
      src/components/admin/payments/ProviderList.tsx
  29. 87 180
      src/components/admin/payments/QrManager.tsx
  30. 137 65
      src/components/admin/products/ProductList.tsx
  31. 59 28
      src/components/admin/products/RoutingManager.tsx
  32. 137 48
      src/components/admin/products/SchemaManager.tsx
  33. 229 226
      src/components/admin/tasks/TaskTable.tsx
  34. 81 49
      src/components/admin/tickets/TicketDetailModal.tsx
  35. 107 49
      src/components/admin/tickets/TicketTable.tsx
  36. 141 81
      src/components/admin/users/UserTable.tsx
  37. 64 64
      src/components/dashboard/OrderList.tsx
  38. 28 26
      src/components/dashboard/TicketList.tsx
  39. 3 2
      src/components/dashboard/TicketModal.tsx
  40. 31 35
      src/components/dashboard/UserTicketDetailModal.tsx
  41. 13 4
      src/lib/i18n/locales/en.ts
  42. 13 4
      src/lib/i18n/locales/zh.ts

+ 38 - 18
src/app/admin/cards/page.tsx

@@ -6,11 +6,10 @@ import { Plus, RefreshCw, Search } from 'lucide-react';
 import CardTable from '@/components/admin/cards/CardTable';
 import CardModal from '@/components/admin/cards/CardModal';
 import Pagination from '@/components/common/Pagination';
-import { CardData } from '@/types/card'; // 确保你有定义这个类型
+import { CardData } from '@/types/card'; 
 
 export default function AdminCardsPage() {
-  // 初始化为空数组,防止 undefined 报错
-  const [cards, setCards] = useState<any[]>([]);
+  const [cards, setCards] = useState<CardData[]>([]);
   const [loading, setLoading] = useState(true);
   const [total, setTotal] = useState(0);
   
@@ -39,8 +38,6 @@ export default function AdminCardsPage() {
         }
       });
 
-      // === 核心修复:适配 API 返回结构 ===
-      // 结构: { code: 0, data: { items: [...], total: 10 } }
       const responseBody = res.data;
       const pageData = responseBody?.data; 
 
@@ -48,7 +45,6 @@ export default function AdminCardsPage() {
         setCards(pageData.items);
         setTotal(pageData.total || 0);
       } else {
-        // 如果数据结构不对,或者 items 为空,重置为空数组
         setCards([]);
         setTotal(0);
       }
@@ -56,7 +52,7 @@ export default function AdminCardsPage() {
       setPage(targetPage);
     } catch (e) {
       console.error("Fetch cards failed", e);
-      setCards([]); // 出错时确保是空数组
+      setCards([]);
     } finally {
       setLoading(false);
     }
@@ -75,39 +71,62 @@ export default function AdminCardsPage() {
   };
 
   return (
-    <div>
-      <div className="flex justify-between items-center mb-6">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部工具栏:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题区域 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">卡片/指南管理</h1>
           <p className="text-sm text-slate-500 mt-1">管理前端展示的签证指南和知识库内容</p>
         </div>
-        <div className="flex gap-3">
-          <div className="relative">
+
+        {/* 操作区域:移动端垂直排列,桌面端水平排列 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
+          
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto flex-1 md:flex-none">
             <input 
               type="text" 
               placeholder="Search title..." 
-              className="pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64"
+              className="w-full md:w-64 pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={e => e.key === 'Enter' && handleSearch()}
             />
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
-          <button onClick={() => fetchCards(page)} className="p-2 bg-white border rounded-lg hover:bg-slate-50 text-slate-600">
-            <RefreshCw size={18} />
-          </button>
-          <button onClick={openCreate} className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 text-sm font-bold">
-            <Plus size={16} /> 发布卡片
-          </button>
+
+          {/* 按钮组:移动端横向排列,自动填充 */}
+          <div className="flex gap-2">
+            <button 
+              onClick={() => fetchCards(page)} 
+              className="p-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition flex-shrink-0"
+              title="刷新列表"
+            >
+              <RefreshCw size={18} />
+            </button>
+            
+            <button 
+              onClick={openCreate} 
+              className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 text-sm font-bold transition shadow-sm whitespace-nowrap"
+            >
+              <Plus size={16} /> 发布卡片
+            </button>
+          </div>
+
         </div>
       </div>
 
+      {/* 表格组件 (内部已适配移动端卡片视图) */}
       <CardTable 
         cards={cards} 
         loading={loading} 
         onEdit={openEdit} 
       />
 
+      {/* 分页组件 */}
       <div className="mt-4">
         <Pagination 
           currentPage={page} 
@@ -117,6 +136,7 @@ export default function AdminCardsPage() {
         />
       </div>
 
+      {/* 弹窗组件 */}
       <CardModal 
         isOpen={isModalOpen} 
         onClose={() => setIsModalOpen(false)} 

+ 36 - 34
src/app/admin/layout.tsx

@@ -4,18 +4,14 @@ import { useEffect, useState } from 'react';
 import { useRouter } from 'next/navigation';
 import AdminSidebar from '@/components/admin/AdminSidebar';
 import { isAdmin, getCurrentUser } from '@/lib/auth';
-import { Menu } from 'lucide-react'; // 引入汉堡图标
 
 export default function AdminLayout({ children }: { children: React.ReactNode }) {
   const router = useRouter();
   const [isAuthorized, setIsAuthorized] = useState(false);
   const [user, setUser] = useState<any>(null);
-  
-  // 控制移动端侧边栏开关
-  const [isSidebarOpen, setSidebarOpen] = useState(false);
 
   useEffect(() => {
-    // 权限检查逻辑 (保持不变)
+    // 权限检查
     const token = localStorage.getItem('rsid');
     if (!token) {
       router.push('/login');
@@ -33,46 +29,52 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
   if (!isAuthorized) return null;
 
   return (
-    <div className="min-h-screen bg-slate-100 flex font-sans text-slate-900">
+    <div className="min-h-screen bg-slate-50 flex flex-col md:flex-row font-sans text-slate-900">
       
-      {/* 侧边栏 (传入状态控制) */}
-      <AdminSidebar 
-        isOpen={isSidebarOpen} 
-        onClose={() => setSidebarOpen(false)} 
-      />
+      {/* 
+         侧边栏组件 
+         - 内部自包含移动端 Header 和开关逻辑
+         - 不需要传递 isOpen / onClose 
+      */}
+      <AdminSidebar />
 
-      {/* 主内容区域 */}
-      <main className="flex-1 overflow-y-auto h-screen flex flex-col w-full">
+      {/* 
+         主内容区域 
+         - flex-1: 占满剩余空间
+         - pt-16: 移动端顶部留出 64px 给 Sidebar 的固定 Header
+         - md:pt-0: 桌面端不需要留白(左右布局)
+         - h-screen: 限制高度以便内部滚动 (可选,视需求而定)
+      */}
+      <main className="flex-1 flex flex-col min-w-0 pt-16 md:pt-0 h-screen overflow-hidden">
         
-        {/* 顶部 Header */}
-        <header className="bg-white shadow-sm h-16 flex items-center justify-between px-4 md:px-8 flex-shrink-0 z-10 sticky top-0">
-          
-          <div className="flex items-center gap-3">
-            {/* === 新增:汉堡菜单按钮 (仅移动端显示) === */}
-            <button 
-              onClick={() => setSidebarOpen(true)}
-              className="p-2 -ml-2 mr-2 text-slate-600 hover:bg-slate-100 rounded-lg md:hidden"
-            >
-              <Menu size={24} />
-            </button>
-            {/* ======================================= */}
-
-            <h2 className="font-bold text-lg text-slate-800 truncate">管理控制台</h2>
-          </div>
+        {/* 
+           Desktop Header (右侧顶部栏)
+           - 显示页面标题和用户信息
+           - 移动端如果觉得太挤可以隐藏 (hidden md:flex),或者保留
+        */}
+        <header className="bg-white shadow-sm h-16 flex items-center justify-between px-6 flex-shrink-0 z-10 border-b border-slate-200">
+          <h2 className="font-bold text-lg text-slate-800">管理控制台</h2>
 
           <div className="flex items-center gap-4">
-            <div className="text-sm text-slate-500 hidden sm:block">
-              {user?.nickname || user?.email} ({user?.role})
+            <div className="text-right hidden sm:block">
+              <div className="text-sm font-medium text-slate-700">
+                {user?.nickname || 'Administrator'}
+              </div>
+              <div className="text-xs text-slate-500">
+                {user?.email}
+              </div>
             </div>
-            <div className="h-8 w-8 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-bold text-xs">
+            <div className="h-9 w-9 bg-blue-100 rounded-full flex items-center justify-center text-blue-600 font-bold text-sm border border-blue-200">
               AD
             </div>
           </div>
         </header>
 
-        {/* 页面内容 */}
-        <div className="p-4 md:p-8 flex-1 w-full overflow-x-hidden">
-          {children}
+        {/* 页面内容滚动区 */}
+        <div className="flex-1 overflow-y-auto p-4 md:p-8 scroll-smooth">
+          <div className="max-w-7xl mx-auto">
+            {children}
+          </div>
         </div>
       </main>
     </div>

+ 60 - 105
src/app/admin/orders/page.tsx

@@ -7,7 +7,7 @@ import { RefreshCw, Search, Plus } 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 Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+import Pagination from '@/components/common/Pagination';
 
 export default function AdminOrdersPage() {
   const router = useRouter();
@@ -18,7 +18,7 @@ export default function AdminOrdersPage() {
   
   // 分页与搜索状态
   const [page, setPage] = useState(1);
-  const [pageSize] = useState(10); // 每页 10 条
+  const [pageSize] = useState(10);
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
   
@@ -28,11 +28,13 @@ export default function AdminOrdersPage() {
   const [isEditOpen, setIsEditOpen] = useState<boolean>(false);
   const [editingOrder, setEditingOrder] = useState<OrderDetail | null>(null);
 
-  // 1. 获取订单列表
+  useEffect(() => {
+    fetchOrders(1);
+  }, []);
+
   const fetchOrders = async (targetPage: number = page) => {
     setLoading(true);
     try {
-      // API: GET /api/vas/order/list?page=1&size=10&keyword=...
       const res = await api.get('/api/vas/order/list_all', {
         params: { 
           keyword,
@@ -41,132 +43,79 @@ export default function AdminOrdersPage() {
         }
       });
       
-      // 适配后端响应结构 (兼容纯数组和分页对象)
-      const data = res.data.data || {};
-      
-      if (Array.isArray(data)) {
-        // 旧接口 (无分页)
-        setOrders(data);
-        setTotal(data.length);
-      } else {
-        // 标准分页接口 { items: [], total: 100 }
-        setOrders(data.items || []);
+      const data = res.data.data;
+      if (data && Array.isArray(data.items)) {
+        setOrders(data.items);
         setTotal(data.total || 0);
+      } else {
+        setOrders([]);
+        setTotal(0);
       }
-      
       setPage(targetPage);
-
     } catch (e) {
-      console.warn("API Error, using mock data for orders");
-      
-      // === Mock Data & 前端模拟分页/搜索 ===
-      const mockData: OrderDetail[] = [
-        { 
-          id: 'ORD-2025-001', user_id: 'u_1001', base_amount: 30000, base_currency: 'CNY', status: 'paid', created_at: '2025-01-01T12:00:00',
-          product_title: '日本单次旅游签证 (VIP)', user_email: 'customer@vip.com', applicant_name: '张三',
-          user_inputs: { "name": "张三", "passport": "E12345678" }
-        },
-        { 
-          id: 'ORD-2025-002', user_id: 'u_1002', base_amount: 8000, base_currency: 'EUR', status: 'pending', created_at: '2025-01-02T09:30:00',
-          product_title: '法国申根签证', user_email: 'li_si@example.com', applicant_name: '李四',
-          user_inputs: { "name": "李四", "passport": "G87654321" }
-        },
-        { 
-          id: 'ORD-2025-003', user_id: 'u_1003', base_amount: 45000, base_currency: 'CNY', status: 'cancelled', created_at: '2025-01-03T14:20:00',
-          product_title: '泰国电子落地签', user_email: 'wang5@test.com', applicant_name: '王五',
-          user_inputs: { "name": "王五" }
-        },
-        // ... 可以复制更多数据测试分页
-      ];
-
-      // 模拟过滤
-      let filtered = mockData;
-      if (keyword) {
-        const lowerKey = keyword.toLowerCase();
-        filtered = mockData.filter(o => 
-          o.id.toLowerCase().includes(lowerKey) || 
-          (o.product_title && o.product_title.toLowerCase().includes(lowerKey)) ||
-          (o.user_email && o.user_email.toLowerCase().includes(lowerKey)) ||
-          (o.applicant_name && o.applicant_name.toLowerCase().includes(lowerKey))
-        );
-      }
-
-      // 模拟切片
-      const start = (targetPage - 1) * pageSize;
-      const end = start + pageSize;
-      setOrders(filtered.slice(start, end));
-      setTotal(filtered.length);
-      setPage(targetPage);
-
+      console.error("Fetch orders failed", e);
+      setOrders([]);
     } finally {
       setLoading(false);
     }
   };
 
-  useEffect(() => {
-    fetchOrders(1);
-  }, []);
+  const handleSearch = () => fetchOrders(1);
+  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
-  // 搜索处理
-  const handleSearch = () => {
-    fetchOrders(1); // 搜索时重置到第一页
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
-
-  // 2. 取消订单
   const handleCancelOrder = async (orderId: string) => {
     if(!confirm(`确定要取消订单 ${orderId} 吗?此操作不可逆。`)) return;
     try {
       await api.post('/api/vas/order/cancel', null, { params: { order_id: orderId } });
       alert("订单已取消");
-      fetchOrders(page); // 刷新当前页
+      fetchOrders(page); 
     } catch (e: any) {
       alert("取消失败: " + (e.response?.data?.message || "未知错误"));
     }
   };
 
-  // 3. 打开详情
   const handleViewDetail = (order: OrderDetail) => {
     setSelectedOrder(order);
     setIsDetailOpen(true);
   };
 
-  // 4. 打开编辑
   const handleEditOrder = (order: OrderDetail) => {
     setEditingOrder(order);
     setIsEditOpen(true);
   };
 
-  // 5. 提交编辑
   const handleSubmitEdit = async (orderId: string, data: any) => {
     try {
       await api.post(`/api/vas/order/patch_user_inputs`, data, { params: { order_id: orderId } });
       alert("订单信息更新成功。");
       setIsEditOpen(false);
-      fetchOrders(page); // 刷新当前页
+      fetchOrders(page);
     } catch (error: any) {
       alert("更新失败: " + (error.response?.data?.message || error.message));
     }
   };
 
   return (
-    <div>
-      <div className="flex justify-between items-center mb-6">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">订单管理</h1>
           <p className="text-sm text-slate-500 mt-1">查看及管理所有用户提交的签证申请</p>
         </div>
-        <div className="flex gap-3">
+        
+        {/* 操作区:移动端垂直,桌面端水平 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
           
-          {/* === 搜索框 === */}
-          <div className="relative hidden md:block">
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto md:w-64">
             <input 
               type="text" 
-              placeholder="搜索订单号/邮箱/商品..." 
-              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              placeholder="搜索订单号/邮箱..." 
+              className="w-full pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
@@ -174,25 +123,29 @@ export default function AdminOrdersPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
 
-          {/* 搜索按钮 (移动端也可以显示) */}
-          <button 
-            onClick={handleSearch} 
-            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition"
-          >
-            <RefreshCw size={16} /> 刷新
-          </button>
-
-          {/* 代客下单 */}
-          <button 
-            onClick={() => router.push('/admin/orders/new')} 
-            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition"
-          >
-            <Plus size={16} /> 代客下单
-          </button>
+          <div className="flex gap-2 w-full sm:w-auto">
+            {/* 刷新按钮:移动端平分宽度 */}
+            <button 
+              onClick={handleSearch} 
+              className="flex-1 sm: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 shadow-sm transition whitespace-nowrap"
+            >
+              <RefreshCw size={16} /> 
+              <span className="md:hidden xl:inline">刷新</span>
+            </button>
+
+            {/* 下单按钮:移动端平分宽度 */}
+            <button 
+              onClick={() => router.push('/admin/orders/new')} 
+              className="flex-[2] sm: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 whitespace-nowrap"
+            >
+              <Plus size={16} /> 
+              <span>代客下单</span>
+            </button>
+          </div>
         </div>
       </div>
       
-      {/* 订单列表表格 */}
+      {/* 订单列表表格 (内部已适配卡片视图) */}
       <OrderTable 
         orders={orders} 
         loading={loading} 
@@ -201,13 +154,15 @@ export default function AdminOrdersPage() {
         onEdit={handleEditOrder} 
       />
 
-      {/* === 分页组件 === */}
-      <Pagination 
-        currentPage={page}
-        total={total}
-        pageSize={pageSize}
-        onPageChange={(p) => fetchOrders(p)}
-      />
+      {/* 分页组件 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchOrders(p)}
+        />
+      </div>
 
       {/* 详情弹窗 */}
       <OrderDetailModal 

+ 163 - 0
src/app/admin/payment-confirmations/page.tsx

@@ -0,0 +1,163 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+import { RefreshCw, Search, CheckCheck } from 'lucide-react';
+import Pagination from '@/components/common/Pagination';
+import ConfirmationTable from '@/components/admin/payments/ConfirmationTable';
+import ConfirmationDetailModal from '@/components/admin/payments/ConfirmationDetailModal';
+import { PaymentConfirmation } from '@/types/payment';
+
+export default function PaymentConfirmationsPage() {
+  const [list, setList] = useState<PaymentConfirmation[]>([]);
+  const [loading, setLoading] = useState(true);
+  
+  // 分页与搜索
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10);
+  const [total, setTotal] = useState(0);
+  const [keyword, setKeyword] = useState('');
+
+  // 弹窗状态
+  const [selectedItem, setSelectedItem] = useState<PaymentConfirmation | null>(null);
+  const [isDetailOpen, setIsDetailOpen] = useState(false);
+
+  useEffect(() => {
+    fetchData(1);
+  }, []);
+
+  const fetchData = async (targetPage: number) => {
+    setLoading(true);
+    try {
+      const res = await api.post('/api/vas/payment_confirmation/list_all', {}, {
+        params: {
+          page: targetPage,
+          size: pageSize,
+          keyword: keyword
+        }
+      });
+      const data = res.data.data;
+      if (data && Array.isArray(data.items)) {
+        setList(data.items);
+        setTotal(data.total || 0);
+      } else {
+        setList([]);
+        setTotal(0);
+      }
+      setPage(targetPage);
+    } catch (error) {
+      console.error("Fetch confirmations failed", error);
+      setList([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = () => fetchData(1);
+
+  // 打开详情
+  const handleViewDetail = (item: PaymentConfirmation) => {
+    setSelectedItem(item);
+    setIsDetailOpen(true);
+  };
+
+  const handleApprove = async (item: PaymentConfirmation) => {
+    const moneyStr = `${(item.amount / 100).toFixed(2)} ${item.currency}`;
+    if (!confirm(`确认已收到支付单 #${item.payment_id} 的款项 (${moneyStr}) 吗?`)) return;
+
+    try {
+      await api.post('/api/vas/payment_confirmation/approve', null, {
+        params: { id: item.id }
+      });
+      alert('已确认收款');
+      fetchData(page);
+    } catch (e: any) {
+      alert('操作失败: ' + (e.response?.data?.message || '未知错误'));
+    }
+  };
+
+  const handleReject = async (item: PaymentConfirmation) => {
+    const reason = prompt("请输入驳回原因:");
+    if (reason === null) return;
+
+    try {
+      await api.post('/api/vas/payment_confirmation/reject', { reason }, {
+        params: { id: item.id }
+      });
+      alert('已驳回');
+      fetchData(page);
+    } catch (e: any) {
+      alert('操作失败: ' + (e.response?.data?.message || '未知错误'));
+    }
+  };
+
+  return (
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        <div>
+          <h1 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
+            <CheckCheck className="text-blue-600" /> 支付确认管理
+          </h1>
+          <p className="text-sm text-slate-500 mt-1">处理用户提交的“我已付款”确认请求</p>
+        </div>
+        
+        {/* 操作区:移动端垂直,桌面端水平 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
+          
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto md:w-64">
+            <input 
+              type="text" 
+              placeholder="搜索 Payment ID / User ID" 
+              className="w-full pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+              value={keyword}
+              onChange={e => setKeyword(e.target.value)}
+              onKeyDown={e => e.key === 'Enter' && handleSearch()}
+            />
+            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          </div>
+          
+          {/* 刷新按钮:移动端拉伸 */}
+          <button 
+            onClick={() => fetchData(page)} 
+            className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition w-full sm:w-auto"
+            title="刷新"
+          >
+            <RefreshCw size={18} />
+            <span className="md:hidden text-sm font-medium">刷新列表</span>
+          </button>
+        </div>
+      </div>
+
+      {/* 表格区域 (内部已适配卡片视图) */}
+      <ConfirmationTable 
+        data={list} 
+        loading={loading}
+        onApprove={handleApprove}
+        onReject={handleReject}
+        onViewDetail={handleViewDetail} 
+      />
+
+      {/* 分页区域 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page} 
+          total={total} 
+          pageSize={pageSize} 
+          onPageChange={fetchData} 
+        />
+      </div>
+
+      {/* 挂载详情弹窗 */}
+      <ConfirmationDetailModal 
+        isOpen={isDetailOpen}
+        onClose={() => setIsDetailOpen(false)}
+        data={selectedItem}
+        onApprove={handleApprove}
+        onReject={handleReject}
+      />
+    </div>
+  );
+}

+ 21 - 12
src/app/admin/payments/page.tsx

@@ -39,10 +39,8 @@ export default function AdminPaymentsPage() {
   const handleSaveProvider = async (data: any) => {
     try {
       if (data.id) {
-        // Update: POST /api/vas/payment_provider/update?id=xxx
         await api.post('/api/vas/payment_provider/update', data, {params : {"id": data.id}});
       } else {
-        // Create: POST /api/vas/payment_provider/create
         await api.post('/api/vas/payment_provider/create', data);
       }
       alert("保存成功");
@@ -53,21 +51,18 @@ export default function AdminPaymentsPage() {
     }
   };
 
-  // 新增:删除服务商逻辑
   const handleDeleteProvider = async (provider: any) => {
     if (!confirm(`确定要删除服务商 "${provider.title || provider.name}" 吗?\n\n警告:删除后该渠道将无法使用,且关联的配置可能会丢失。`)) {
       return;
     }
 
     try {
-      // Delete: delete /api/vas/payment_provider/delete?id=xxx (根据 update 接口风格推测)
-      // 如果后端支持标准的 RESTful DELETE,请改为: await api.delete(`/api/vas/payment_provider/${provider.id}`);
       await api.delete('/api/vas/payment_provider/delete', {
         params: { id: provider.id }
       });
       
       alert("删除成功");
-      fetchProviders(); // 刷新列表
+      fetchProviders(); 
     } catch (e: any) {
       console.error(e);
       alert("删除失败: " + (e.response?.data?.message || "未知错误"));
@@ -90,30 +85,44 @@ export default function AdminPaymentsPage() {
   };
 
   return (
-    <div>
-      <div className="flex justify-between items-center mb-6">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">支付服务商配置</h1>
           <p className="text-sm text-slate-500 mt-1">管理支付渠道 (Stripe, WeChat, Alipay) 及其参数</p>
         </div>
-        <div className="flex gap-3">
-          <button onClick={fetchProviders} className="flex items-center gap-2 px-4 py-2 bg-white border rounded-lg hover:bg-slate-50 text-slate-700 font-medium">
+
+        {/* 操作按钮组:移动端全宽 + 平分 */}
+        <div className="flex gap-3 w-full md:w-auto">
+          <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"
+          >
             <RefreshCw size={16} /> 刷新
           </button>
-          <button onClick={openCreate} className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm">
+          <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"
+          >
             <Plus size={16} /> 添加服务商
           </button>
         </div>
       </div>
 
+      {/* 列表组件 (内部已做 Table/Card 响应式切换) */}
       <ProviderList 
         providers={providers} 
         loading={loading} 
         onEdit={openEdit}
         onManageQr={openQrManager}
-        onDelete={handleDeleteProvider} // 传递删除回调
+        onDelete={handleDeleteProvider}
       />
 
+      {/* Modals */}
       <ProviderModal 
         isOpen={isProviderModalOpen}
         onClose={() => setProviderModalOpen(false)}

+ 87 - 119
src/app/admin/products/page.tsx

@@ -2,37 +2,37 @@
 
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
-import { Plus, RefreshCw, Search } from 'lucide-react'; // 引入 Search
+import { Plus, RefreshCw, Search, FileJson } from 'lucide-react'; 
 import ProductList from '@/components/admin/products/ProductList';
 import ProductModal from '@/components/admin/products/ProductModal';
 import RoutingManager from '@/components/admin/products/RoutingManager';
 import SchemaManager from '@/components/admin/products/SchemaManager';
-import Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+import Pagination from '@/components/common/Pagination';
 
 export default function AdminProductsPage() {
-  // 数据状态
   const [products, setProducts] = useState<any[]>([]);
   const [loading, setLoading] = useState(true);
 
-  // 分页与搜索状态
+  // 分页与搜索
   const [page, setPage] = useState(1);
-  const [pageSize] = useState(10); // 每页 10 条
+  const [pageSize] = useState(10);
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
 
-  // Modals 状态控制
+  // Modals
   const [isProductModalOpen, setProductModalOpen] = useState(false);
   const [isRoutingModalOpen, setRoutingModalOpen] = useState(false);
   const [isSchemaModalOpen, setSchemaModalOpen] = useState(false);
   
-  // 当前操作对象
   const [selectedProduct, setSelectedProduct] = useState<any>(null);
 
-  // 1. 获取商品列表
+  useEffect(() => {
+    fetchProducts(1);
+  }, []);
+
   const fetchProducts = async (targetPage: number = page) => {
     setLoading(true);
     try {
-      // API: GET /api/vas/product/list?page=1&size=10&keyword=...
       const res = await api.get('/api/vas/product/list', {
         params: {
           page: targetPage,
@@ -41,15 +41,23 @@ export default function AdminProductsPage() {
         }
       });
 
-      // 适配后端返回结构 (兼容纯数组和分页对象)
       const data = res.data.data || {};
       
       if (Array.isArray(data)) {
-        // 旧接口 (无分页)
-        setProducts(data);
-        setTotal(data.length);
+        let filtered = data;
+        if (keyword) {
+          const lowerKey = keyword.toLowerCase();
+          filtered = data.filter(p => 
+            p.title.toLowerCase().includes(lowerKey) || 
+            p.country.toLowerCase().includes(lowerKey) ||
+            p.provider.toLowerCase().includes(lowerKey)
+          );
+        }
+        const start = (targetPage - 1) * pageSize;
+        const end = start + pageSize;
+        setProducts(filtered.slice(start, end));
+        setTotal(filtered.length);
       } else {
-        // 标准分页接口 { items: [], total: 100 }
         setProducts(data.items || []);
         setTotal(data.total || 0);
       }
@@ -58,57 +66,16 @@ export default function AdminProductsPage() {
 
     } catch (e) {
       console.warn("Using mock products due to API failure");
-      
-      // === Mock Data & 前端模拟分页/搜索 ===
-      const mockData = [
-        { id: 1, title: 'France Visa (Tourist)', country: 'France', price_amount: 8000, price_currency: 'EUR', provider: 'TROOV', enabled: 1 },
-        { id: 2, title: 'Japan E-Visa', country: 'Japan', price_amount: 3000, price_currency: 'CNY', provider: 'Official', enabled: 1 },
-        { id: 3, title: 'US B1/B2 Interview', country: 'USA', price_amount: 120000, price_currency: 'CNY', provider: 'AIS', enabled: 1 },
-        { id: 4, title: 'Thailand Visa on Arrival', country: 'Thailand', price_amount: 45000, price_currency: 'CNY', provider: 'E-VOA', enabled: 0 },
-        { id: 5, title: 'Schengen Business (Germany)', country: 'Germany', price_amount: 9000, price_currency: 'EUR', provider: 'TLSContact', enabled: 1 },
-        { id: 6, title: 'UK Standard Visitor', country: 'UK', price_amount: 11500, price_currency: 'GBP', provider: 'TLSContact', enabled: 1 },
-        { id: 7, title: 'Singapore E-Visa', country: 'Singapore', price_amount: 30000, price_currency: 'CNY', provider: 'ICA', enabled: 1 },
-        // ... 更多数据用于测试分页 ...
-      ];
-
-      // 模拟过滤
-      let filtered = mockData;
-      if (keyword) {
-        const lowerKey = keyword.toLowerCase();
-        filtered = mockData.filter(p => 
-          p.title.toLowerCase().includes(lowerKey) || 
-          p.country.toLowerCase().includes(lowerKey) ||
-          p.provider.toLowerCase().includes(lowerKey)
-        );
-      }
-
-      // 模拟切片
-      const start = (targetPage - 1) * pageSize;
-      const end = start + pageSize;
-      
-      setProducts(filtered.slice(start, end));
-      setTotal(filtered.length);
-      setPage(targetPage);
-
+      setProducts([]); 
+      setTotal(0);
     } finally {
       setLoading(false);
     }
   };
 
-  useEffect(() => {
-    fetchProducts(1);
-  }, []);
-
-  // 搜索处理
-  const handleSearch = () => {
-    fetchProducts(1); // 搜索重置到第一页
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
+  const handleSearch = () => fetchProducts(1);
+  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
-  // 保存商品 (Create / Update)
   const handleSaveProduct = async (data: any) => {
     try {
       if (data.id) {
@@ -117,50 +84,39 @@ export default function AdminProductsPage() {
         await api.post('/api/vas/product/create', data);
       }
       alert('操作成功');
-      fetchProducts(page); // 刷新当前页
+      fetchProducts(page); 
     } catch (error: any) {
       alert('操作失败: ' + (error.response?.data?.detail || error.message));
       throw error; 
     }
   };
 
-  // --- 事件处理 ---
-
-  const openCreateModal = () => {
-    setSelectedProduct(null);
-    setProductModalOpen(true);
-  };
-
-  const openEditModal = (product: any) => {
-    setSelectedProduct(product);
-    setProductModalOpen(true);
-  };
-
-  const openRoutingModal = (product: any) => {
-    setSelectedProduct(product);
-    setRoutingModalOpen(true);
-  };
-
-  const openSchemaManager = () => {
-    setSchemaModalOpen(true);
-  };
+  const openCreateModal = () => { setSelectedProduct(null); setProductModalOpen(true); };
+  const openEditModal = (product: any) => { setSelectedProduct(product); setProductModalOpen(true); };
+  const openRoutingModal = (product: any) => { setSelectedProduct(product); setRoutingModalOpen(true); };
+  const openSchemaManager = () => { setSchemaModalOpen(true); };
 
   return (
-    <div>
-      {/* Page Header */}
-      <div className="flex justify-between items-center mb-6">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部工具栏:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题区域 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">商品配置中心</h1>
           <p className="text-sm text-slate-500 mt-1">管理上架商品、关联表单及后台路由策略</p>
         </div>
-        <div className="flex gap-3">
+
+        {/* 操作区域:移动端垂直排列 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
           
-          {/* === 搜索框 === */}
-          <div className="relative">
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto flex-1 md:flex-none">
             <input 
               type="text" 
-              placeholder="搜索商品名/国家/Provider..." 
-              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              placeholder="搜索商品名/国家..." 
+              className="w-full md:w-64 pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
@@ -168,30 +124,42 @@ export default function AdminProductsPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
 
-          <button 
-            onClick={handleSearch} 
-            className="flex items-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 shadow-sm"
-          >
-            <RefreshCw size={16} /> 刷新
-          </button>
-          
-          <button 
-            onClick={openSchemaManager} 
-            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-purple-700 font-medium transition shadow-sm"
-          >
-            表单 Schema
-          </button>
-          
-          <button 
-            onClick={openCreateModal} 
-            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition"
-          >
-            <Plus size={16} /> 发布商品
-          </button>
+          {/* 按钮组:移动端 Grid 布局,桌面端 Flex */}
+          <div className="grid grid-cols-4 sm:flex gap-2">
+            
+            {/* 刷新按钮 (只占1格) */}
+            <button 
+              onClick={handleSearch} 
+              className="col-span-1 p-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-600 transition flex justify-center items-center"
+              title="刷新"
+            >
+              <RefreshCw size={18} />
+            </button>
+            
+            {/* Schema 按钮 (占1.5格 -> 实际上 grid 很难 1.5,这里用 col-span-1 + hide text on mobile,或者 col-span-1 icon only) */}
+            {/* 方案:移动端只显示图标,桌面端显示文字 */}
+            <button 
+              onClick={openSchemaManager} 
+              className="col-span-1 sm:w-auto flex items-center justify-center gap-2 px-3 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-purple-700 font-medium transition shadow-sm"
+              title="表单 Schema"
+            >
+              <FileJson size={18} />
+              <span className="hidden sm:inline">Schema</span>
+            </button>
+            
+            {/* 发布按钮 (占2格,最重要) */}
+            <button 
+              onClick={openCreateModal} 
+              className="col-span-2 sm:w-auto 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 whitespace-nowrap"
+            >
+              <Plus size={18} /> 发布商品
+            </button>
+          </div>
+
         </div>
       </div>
 
-      {/* 商品列表组件 */}
+      {/* 商品列表组件 (内部已适配) */}
       <ProductList 
         products={products} 
         loading={loading} 
@@ -199,15 +167,17 @@ export default function AdminProductsPage() {
         onManageRouting={openRoutingModal}
       />
 
-      {/* === 分页组件 === */}
-      <Pagination 
-        currentPage={page}
-        total={total}
-        pageSize={pageSize}
-        onPageChange={(p) => fetchProducts(p)}
-      />
+      {/* 分页组件 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchProducts(p)}
+        />
+      </div>
 
-      {/* 弹窗:创建/编辑商品 */}
+      {/* Modals */}
       <ProductModal 
         isOpen={isProductModalOpen}
         onClose={() => setProductModalOpen(false)}
@@ -216,7 +186,6 @@ export default function AdminProductsPage() {
         onManageSchemas={openSchemaManager} 
       />
 
-      {/* 弹窗:配置路由 */}
       <RoutingManager 
         isOpen={isRoutingModalOpen}
         onClose={() => setRoutingModalOpen(false)}
@@ -224,7 +193,6 @@ export default function AdminProductsPage() {
         productTitle={selectedProduct?.title}
       />
 
-      {/* 弹窗:Schema 管理 */}
       <SchemaManager 
         isOpen={isSchemaModalOpen}
         onClose={() => setSchemaModalOpen(false)}

+ 49 - 51
src/app/admin/tasks/page.tsx

@@ -5,7 +5,7 @@ import api from '@/lib/api';
 import { RefreshCw, Search } from 'lucide-react';
 import TaskTable, { VasTask } from '@/components/admin/tasks/TaskTable';
 import TaskDetailModal from '@/components/admin/tasks/TaskDetailModal';
-import TaskEditModal from '@/components/admin/tasks/TaskEditModal'; // 引入编辑弹窗
+import TaskEditModal from '@/components/admin/tasks/TaskEditModal';
 import Pagination from '@/components/common/Pagination';
 
 export default function AdminTasksPage() {
@@ -17,33 +17,26 @@ export default function AdminTasksPage() {
   const [pageSize] = useState(10);
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
-  
-  // 新增:状态筛选
   const [statusFilter, setStatusFilter] = useState('all');
 
   // 弹窗状态
   const [selectedTask, setSelectedTask] = useState<VasTask | null>(null);
   const [isDetailOpen, setIsDetailOpen] = useState(false);
-  const [isEditOpen, setIsEditOpen] = useState(false); // 编辑弹窗
+  const [isEditOpen, setIsEditOpen] = useState(false);
 
-  // 1. 获取任务列表
   const fetchTasks = async (targetPage: number = page) => {
     setLoading(true);
     try {
-      // 构造 API 参数
       const params: any = {
         keyword,
         page: targetPage,
         size: pageSize,
       };
       
-      // 如果不是 'all',才传 status 参数
       if (statusFilter !== 'all') {
         params.status = statusFilter;
       }
 
-      // API: GET /api/vas/task/pending (注意:很多后端设计 list 接口会支持 filter)
-      // 如果后端还不支持 status 参数,你需要联系后端增加
       const res = await api.get('/api/vas/task/list', { params });
       
       const data = res.data.data || {};
@@ -60,7 +53,6 @@ export default function AdminTasksPage() {
 
     } catch (e) {
       console.warn("API Error, using mock data");
-      // ... Mock Logic (略,保持原有逻辑或简单过滤) ...
       setTasks([]);
       setTotal(0);
     } finally {
@@ -68,10 +60,9 @@ export default function AdminTasksPage() {
     }
   };
 
-  // 监听筛选变化自动刷新
   useEffect(() => {
     fetchTasks(1);
-  }, [statusFilter]); // 当状态改变时,重置到第一页
+  }, [statusFilter]);
 
   const handleSearch = () => {
     fetchTasks(1);
@@ -81,8 +72,6 @@ export default function AdminTasksPage() {
     if (e.key === 'Enter') handleSearch();
   };
 
-  // --- 操作逻辑 ---
-
   const handleRetry = async (taskId: number) => {
     if(!confirm("确定要重置该任务回队列吗?")) return;
     try {
@@ -110,17 +99,13 @@ export default function AdminTasksPage() {
     setIsDetailOpen(true);
   };
 
-  // 打开编辑
   const handleEdit = (task: VasTask) => {
     setSelectedTask(task);
     setIsEditOpen(true);
   };
 
-  // 提交编辑
   const handleSubmitEdit = async (taskId: number, data: any) => {
     try {
-      // TODO: 请确保后端有 Update 接口
-      // 假设接口:POST /api/vas/task/update
       await api.post('/api/vas/task/update', data, {params: {"id": taskId}});
       alert("任务更新成功");
       setIsEditOpen(false);
@@ -131,75 +116,88 @@ export default function AdminTasksPage() {
   };
 
   return (
-    <div>
-      <div className="flex flex-col md:flex-row justify-between items-center mb-6 gap-4">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">系统任务队列</h1>
           <p className="text-sm text-slate-500 mt-1">监控机器人执行状态及调试日志</p>
         </div>
         
-        <div className="flex gap-3 w-full md:w-auto">
+        {/* 操作区:移动端垂直堆叠,桌面端水平排列 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
           
-          {/* === 新增:状态筛选下拉框 === */}
+          {/* 状态筛选:移动端全宽 */}
           <select
-            className="border border-slate-300 rounded-lg text-sm px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500 bg-white"
+            className="w-full sm:w-auto border border-slate-300 rounded-lg text-sm px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500 bg-white appearance-none"
             value={statusFilter}
             onChange={(e) => setStatusFilter(e.target.value)}
           >
-            <option value="all">所有状态 (All Status)</option>
+            <option value="all">所有状态 (All)</option>
             <option value="pending">等待中 (Pending)</option>
             <option value="running">运行中 (Running)</option>
             <option value="grabbed">待确认 (Grabbed)</option>
             <option value="completed">已完成 (Completed)</option>
             <option value="cancelled">已取消 (Cancelled)</option>
+            <option value="failed">失败 (Failed)</option>
           </select>
 
-          {/* 搜索框 */}
-          <div className="relative flex-1 md:w-64">
-            <input 
-              type="text" 
-              placeholder="TaskID / Route / OrderID..." 
-              className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-              value={keyword}
-              onChange={e => setKeyword(e.target.value)}
-              onKeyDown={handleKeyDown}
-            />
-            <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+          {/* 搜索框与刷新按钮组 */}
+          <div className="flex gap-2 w-full sm:w-auto">
+            <div className="relative flex-1 sm:w-64">
+              <input 
+                type="text" 
+                placeholder="TaskID / Route / OrderID" 
+                className="w-full pl-9 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                value={keyword}
+                onChange={e => setKeyword(e.target.value)}
+                onKeyDown={handleKeyDown}
+              />
+              <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
+            </div>
+
+            <button 
+              onClick={handleSearch} 
+              className="flex-shrink-0 p-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-600 transition"
+              title="刷新"
+            >
+              <RefreshCw size={18} />
+            </button>
           </div>
-
-          <button 
-            onClick={handleSearch} 
-            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition"
-          >
-            <RefreshCw size={16} />
-          </button>
+          
         </div>
       </div>
 
+      {/* 表格组件 (内部已适配移动端卡片视图) */}
       <TaskTable 
         tasks={tasks} 
         loading={loading} 
         onRetry={handleRetry} 
         onManualConfirm={handleConfirm} 
         onViewDetail={handleViewDetail} 
-        onEdit={handleEdit} // 传递编辑回调
+        onEdit={handleEdit} 
       />
 
-      <Pagination 
-        currentPage={page}
-        total={total}
-        pageSize={pageSize}
-        onPageChange={(p) => fetchTasks(p)}
-      />
+      {/* 分页组件 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchTasks(p)}
+        />
+      </div>
 
-      {/* 详情弹窗 */}
+      {/* Modals */}
       <TaskDetailModal 
         isOpen={isDetailOpen}
         onClose={() => setIsDetailOpen(false)}
         task={selectedTask}
       />
 
-      {/* 编辑弹窗 */}
       <TaskEditModal 
         isOpen={isEditOpen}
         onClose={() => setIsEditOpen(false)}

+ 23 - 15
src/app/admin/tickets/page.tsx

@@ -25,7 +25,6 @@ export default function AdminTicketsPage() {
   const fetchTickets = async (targetPage: number = page) => {
     setLoading(true);
     try {
-      // API: /api/vas/ticket/list_all
       const res = await api.get('/api/vas/ticket/list_all', {
         params: {
           page: targetPage,
@@ -46,7 +45,7 @@ export default function AdminTicketsPage() {
 
     } catch (error) {
       console.error("Fetch tickets failed", error);
-      setTickets([]); // 实际环境可以留空,或者使用 Mock
+      setTickets([]);
     } finally {
       setLoading(false);
     }
@@ -73,20 +72,26 @@ export default function AdminTicketsPage() {
   };
 
   return (
-    <div className="p-6 max-w-7xl mx-auto">
-      {/* 顶部标题栏 */}
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
       <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题 */}
         <div>
-          <h1 className="text-2xl font-bold text-gray-900">工单管理中心</h1>
-          <p className="text-sm text-gray-500 mt-1">处理用户的退款、投诉及服务请求</p>
+          <h1 className="text-2xl font-bold text-slate-800">工单管理中心</h1>
+          <p className="text-sm text-slate-500 mt-1">处理用户的退款、投诉及服务请求</p>
         </div>
         
-        <div className="flex gap-2">
-          <div className="relative">
+        {/* 操作区:移动端垂直,桌面端水平 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
+          
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto md:w-64">
             <input 
               type="text" 
               placeholder="工单号 / 订单号 / 用户" 
-              className="pl-9 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 outline-none w-64 transition shadow-sm"
+              className="w-full pl-9 pr-4 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition shadow-sm"
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
@@ -94,16 +99,19 @@ export default function AdminTicketsPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
 
+          {/* 刷新按钮:移动端拉伸 */}
           <button 
-            onClick={handleSearch} 
-            className="px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700 text-sm font-medium transition shadow-sm flex items-center gap-2"
+            onClick={() => fetchTickets(page)} 
+            className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 font-medium transition shadow-sm w-full sm:w-auto"
+            title="刷新列表"
           >
-            <RefreshCw size={16} /> 刷新
+            <RefreshCw size={16} /> 
+            <span>刷新</span>
           </button>
         </div>
       </div>
 
-      {/* 列表 */}
+      {/* 列表组件 (内部已适配移动端卡片视图) */}
       <TicketTable 
         tickets={tickets} 
         loading={loading} 
@@ -120,12 +128,12 @@ export default function AdminTicketsPage() {
         />
       </div>
 
-      {/* 详情弹窗 (集成处理功能) */}
+      {/* 详情弹窗 (内部已适配移动端全屏/Tabs) */}
       <TicketDetailModal 
         isOpen={isDetailModalOpen}
         onClose={handleModalClose}
         ticket={detailTicket}
-        onUpdate={() => fetchTickets(page)} // 处理后刷新当前页
+        onUpdate={() => fetchTickets(page)} 
       />
     </div>
   );

+ 42 - 77
src/app/admin/users/page.tsx

@@ -5,28 +5,29 @@ import api from '@/lib/api';
 import { RefreshCw, Search } from 'lucide-react';
 import UserTable, { AdminUser } from '@/components/admin/users/UserTable';
 import UserModal from '@/components/admin/users/UserModal';
-import Pagination from '@/components/common/Pagination'; // 1. 引入分页组件
+import Pagination from '@/components/common/Pagination';
 
 export default function AdminUsersPage() {
-  // 数据状态
   const [users, setUsers] = useState<AdminUser[]>([]);
   const [loading, setLoading] = useState(true);
   
-  // 分页与搜索状态
+  // 分页与搜索
   const [page, setPage] = useState(1);
-  const [pageSize] = useState(10); // 每页 10 条
+  const [pageSize] = useState(10);
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
   
-  // 编辑弹窗
+  // 弹窗
   const [isEditOpen, setIsEditOpen] = useState(false);
   const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null);
 
-  // 1. 获取用户列表
+  useEffect(() => {
+    fetchUsers(1);
+  }, []);
+
   const fetchUsers = async (targetPage: number = page) => {
     setLoading(true);
     try {
-      // API: GET /api/user/list_all?keyword=...&page=1&size=10
       const res = await api.get('/api/user/list_all', { 
         params: { 
           keyword,
@@ -35,15 +36,12 @@ export default function AdminUsersPage() {
         } 
       });
       
-      // 适配后端返回结构 (兼容纯数组和分页对象)
       const data = res.data.data || {};
       
       if (Array.isArray(data)) {
-        // 兼容旧接口 (无分页)
         setUsers(data);
         setTotal(data.length);
       } else {
-        // 标准分页接口 { items: [], total: 100 }
         setUsers(data.items || []);
         setTotal(data.total || 0);
       }
@@ -51,58 +49,16 @@ export default function AdminUsersPage() {
       setPage(targetPage);
 
     } catch (e) {
-      console.warn("API Error, using mock users with pagination logic");
-      
-      // === Mock Data & 前端模拟分页/搜索 ===
-      const mockUsers: AdminUser[] = [
-        { id: '1', email: 'admin@visafly.com', nickname: 'Super Admin', role: 'admin', email_verified: 1, created_at: '2024-01-01' },
-        { id: '2', email: 'user@example.com', nickname: 'John Doe', role: 'user', email_verified: 0, created_at: '2025-01-10' },
-        { id: '3', email: 'alice@test.com', nickname: 'Alice', role: 'user', email_verified: 1, created_at: '2025-02-15' },
-        { id: '4', email: 'bob@test.com', nickname: 'Bob Smith', role: 'user', email_verified: 1, created_at: '2025-02-16' },
-        { id: '5', email: 'charlie@test.com', nickname: 'Charlie', role: 'user', email_verified: 0, created_at: '2025-02-17' },
-        { id: '6', email: 'david@test.com', nickname: 'David', role: 'user', email_verified: 1, created_at: '2025-02-18' },
-        { id: '7', email: 'eve@test.com', nickname: 'Eve', role: 'user', email_verified: 1, created_at: '2025-02-19' },
-        { id: '8', email: 'frank@test.com', nickname: 'Frank', role: 'admin', email_verified: 1, created_at: '2025-02-20' },
-        { id: '9', email: 'grace@test.com', nickname: 'Grace', role: 'user', email_verified: 0, created_at: '2025-02-21' },
-        { id: '10', email: 'heidi@test.com', nickname: 'Heidi', role: 'user', email_verified: 1, created_at: '2025-02-22' },
-        { id: '11', email: 'ivan@test.com', nickname: 'Ivan', role: 'user', email_verified: 1, created_at: '2025-02-23' },
-      ];
-
-      // 模拟过滤
-      let filtered = mockUsers;
-      if (keyword) {
-        const lowerKey = keyword.toLowerCase();
-        filtered = mockUsers.filter(u => 
-          u.email.toLowerCase().includes(lowerKey) || 
-          (u.nickname && u.nickname.toLowerCase().includes(lowerKey)) ||
-          u.id.includes(lowerKey)
-        );
-      }
-
-      // 模拟切片
-      const start = (targetPage - 1) * pageSize;
-      const end = start + pageSize;
-      setUsers(filtered.slice(start, end));
-      setTotal(filtered.length);
-      setPage(targetPage);
-
+      console.warn("API Error, using mock users");
+      setUsers([]);
+      setTotal(0);
     } finally {
       setLoading(false);
     }
   };
 
-  useEffect(() => {
-    fetchUsers(1);
-  }, []);
-
-  // 搜索处理
-  const handleSearch = () => {
-    fetchUsers(1); // 重置到第一页
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
+  const handleSearch = () => fetchUsers(1);
+  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
   const handleEditUser = (user: AdminUser) => {
     setSelectedUser(user);
@@ -111,35 +67,38 @@ export default function AdminUsersPage() {
 
   const handleSubmitUpdate = async (userId: string, data: any) => {
     try {
-      // API: POST /api/user/update?uid=xxx
       await api.post('/api/user/update', data, {
         params: { uid: userId }
       });
-      
       alert("用户信息更新成功");
       setIsEditOpen(false);
-      fetchUsers(page); // 刷新当前页
+      fetchUsers(page);
     } catch (e: any) {
-      console.error(e);
-      alert("更新失败: " + (e.response?.data?.message || e.message || "未知错误"));
+      alert("更新失败: " + (e.response?.data?.message || e.message));
     }
   };
 
   return (
-    <div>
-      <div className="flex justify-between items-center mb-6">
+    <div className="p-4 md:p-6">
+      
+      {/* === 头部区域:响应式布局 === */}
+      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
+        
+        {/* 标题 */}
         <div>
           <h1 className="text-2xl font-bold text-slate-800">用户管理</h1>
           <p className="text-sm text-slate-500 mt-1">查看注册用户、管理权限及状态</p>
         </div>
-        <div className="flex gap-3">
+
+        {/* 操作区:移动端垂直,桌面端水平 */}
+        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
           
-          {/* === 搜索框 === */}
-          <div className="relative">
+          {/* 搜索框:移动端全宽 */}
+          <div className="relative w-full sm:w-auto md:w-64">
             <input 
               type="text" 
               placeholder="搜索邮箱/昵称..." 
-              className="pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 transition"
+              className="w-full pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
@@ -147,28 +106,34 @@ export default function AdminUsersPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
 
+          {/* 刷新按钮:移动端拉伸 */}
           <button 
             onClick={handleSearch} 
-            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm active:scale-95 transition"
+            className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-700 font-medium active:scale-95 transition w-full sm:w-auto"
           >
-            <RefreshCw size={16} /> 刷新
+            <RefreshCw size={16} /> 
+            <span className="md:hidden">刷新列表</span> {/* 移动端显示文字 */}
+            <span className="hidden md:inline">刷新</span>
           </button>
         </div>
       </div>
 
+      {/* 表格组件 (内部已适配卡片视图) */}
       <UserTable 
         users={users} 
         loading={loading} 
         onEdit={handleEditUser} 
       />
 
-      {/* === 分页组件 === */}
-      <Pagination 
-        currentPage={page}
-        total={total}
-        pageSize={pageSize}
-        onPageChange={(p) => fetchUsers(p)}
-      />
+      {/* 分页组件 */}
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page}
+          total={total}
+          pageSize={pageSize}
+          onPageChange={(p) => fetchUsers(p)}
+        />
+      </div>
 
       <UserModal 
         isOpen={isEditOpen} 

+ 13 - 9
src/app/create-order/[id]/page.tsx

@@ -3,10 +3,9 @@
 import CreateOrderForm from '@/components/CreateOrderForm';
 import { useEffect, useState } from 'react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { ArrowLeft } from 'lucide-react'; // 引入图标库,使返回按钮更标准
 
 // 模拟 API 数据源
-// 注意:这里不需要做中英文映射。在真实场景中,后端 API 会根据请求头返回对应语言的标题,
-// 或者前端只负责展示拿到的数据。
 const PRODUCT_MAP: Record<string, string> = {
   '1': '日本单次旅游签证', 
   '2': '泰国电子签',
@@ -14,30 +13,35 @@ const PRODUCT_MAP: Record<string, string> = {
 };
 
 export default function CreateOrderPage({ params }: { params: { id: string } }) {
-  // 1. 获取翻译函数
   const { t } = useLanguage();
   
-  // 2. 初始化状态,默认值可以用翻译后的"加载中"
+  // 初始化状态
   const [productName, setProductName] = useState(t('common.loading'));
 
   useEffect(() => {
     // 模拟 API 请求逻辑
-    // 这里我们只负责展示获取到的数据,不对数据内容本身进行翻译处理
     const name = PRODUCT_MAP[params.id] || `Unknown Product (ID: ${params.id})`;
     setProductName(name);
   }, [params.id]); 
 
   return (
-    <div className="min-h-screen bg-slate-50 py-12 px-4">
+    // 修改 1: 移动端减少垂直内边距 (py-6), 桌面端保持较宽间距 (md:py-12)
+    <div className="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-6">
       <div className="max-w-3xl mx-auto">
+        
+        {/* 修改 2: 优化返回按钮
+            - mb-4 md:mb-6: 调整下方间距
+            - py-2: 增加点击区域高度,方便手指触摸
+            - 引入 ArrowLeft 图标替代文本箭头
+        */}
         <button 
           onClick={() => window.history.back()} 
-          className="mb-6 text-gray-500 hover:text-gray-900 flex items-center text-sm transition-colors"
+          className="mb-4 md:mb-6 text-gray-500 hover:text-gray-900 flex items-center text-sm transition-colors py-2 active:scale-95 origin-left"
         >
-          {/* 3. 只翻译静态的按钮文案 */}
-          ← {t('common.back')}
+          <ArrowLeft size={16} className="mr-1" /> {t('common.back')}
         </button>
         
+        {/* 表单组件 (内部已经做好了响应式处理) */}
         <CreateOrderForm 
           productId={params.id} 
           productName={productName} 

+ 55 - 65
src/app/dashboard/page.tsx

@@ -2,9 +2,8 @@
 
 import { useState, useEffect } from 'react';
 import { useRouter } from 'next/navigation';
-import { LogOut, Plus } from 'lucide-react';
+import { LogOut, Plus, FileText, LifeBuoy, Settings } from 'lucide-react'; // 引入图标
 
-// 引入组件
 import Sidebar from '@/components/dashboard/Sidebar';
 import OrderList from '@/components/dashboard/OrderList';
 import TicketList from '@/components/dashboard/TicketList';
@@ -12,9 +11,7 @@ import ProfileSettings from '@/components/dashboard/ProfileSettings';
 import TicketModal from '@/components/dashboard/TicketModal';
 import UserOrderDetailModal, { UserOrder } from '@/components/dashboard/UserOrderDetailModal';
 import UserTicketDetailModal, { UserTicket } from '@/components/dashboard/UserTicketDetailModal';
-// 1. 引入 BindEmailModal
 import BindEmailModal from '@/components/BindEmailModal';
-
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function DashboardPage() {
@@ -31,50 +28,38 @@ export default function DashboardPage() {
   const [isTicketDetailOpen, setIsTicketDetailOpen] = useState<boolean>(false);
   const [selectedTicket, setSelectedTicket] = useState<UserTicket | null>(null);
   const [refreshTickets, setRefreshTickets] = useState<number>(0);
-
-  // 2. 新增:强制绑定邮箱的控制状态
+  
+  // 强制绑定
   const [isForceBindEmailOpen, setIsForceBindEmailOpen] = useState(false);
-  const [isCheckingAuth, setIsCheckingAuth] = useState(true); // 用于避免页面闪烁
+  const [isCheckingAuth, setIsCheckingAuth] = useState(true);
 
-  // 3. 初始化检查:是否已登录 & 是否已绑定邮箱
   useEffect(() => {
     const checkUserStatus = () => {
       const token = localStorage.getItem('rsid');
       const userStr = localStorage.getItem('user_info');
 
-      // A. 未登录 -> 跳转登录
       if (!token) {
         router.replace('/login');
         return;
       }
 
-      // B. 检查邮箱
       let hasEmail = false;
       if (userStr) {
         try {
           const user = JSON.parse(userStr);
-          // 判断逻辑:必须有 email 且包含 @ (根据你的业务逻辑,如果是临时生成的假邮箱也算未绑定)
           if (user.email && user.email.includes('@')) {
             hasEmail = true;
           }
-        } catch (e) {
-          console.error("User info parse error", e);
-        }
+        } catch (e) { console.error(e); }
       }
 
-      if (!hasEmail) {
-        // 未绑定 -> 打开强制弹窗
-        setIsForceBindEmailOpen(true);
-      }
-      
+      if (!hasEmail) setIsForceBindEmailOpen(true);
       setIsCheckingAuth(false);
     };
 
     checkUserStatus();
   }, [router]);
 
-  // --- 事件处理 ---
-
   const handleLogout = () => {
     if (confirm(t('dashboard.logout_confirm'))) {
       localStorage.removeItem('rsid');
@@ -84,22 +69,16 @@ export default function DashboardPage() {
     }
   };
 
-  // 4. 处理强制绑定的逻辑
   const handleForceBindClose = () => {
-    // 如果用户关闭弹窗但没绑定成功 -> 踢回首页 (强制逻辑)
-    // 只有 onSuccess 会将 isForceBindEmailOpen 设为 false,所以这里直接跳转
     alert("为了保障账户安全,访问控制台前必须绑定邮箱。");
     router.push('/'); 
   };
 
   const handleForceBindSuccess = () => {
-    // 绑定成功 -> 关闭弹窗,允许留在控制台
     setIsForceBindEmailOpen(false);
-    // 强制刷新一下页面或重新获取数据,确保状态最新
     window.location.reload(); 
   };
 
-  // ... (其他 openCreateTicketModal 等函数保持不变)
   const openCreateTicketModal = (orderId: string = '') => {
     setTicketDefaultOrderId(orderId);
     setIsTicketModalOpen(true);
@@ -115,45 +94,50 @@ export default function DashboardPage() {
     setIsTicketDetailOpen(true);
   };
 
-  const handleTicketUpdate = () => {
-    setRefreshTickets(prev => prev + 1);
-  };
+  const handleTicketUpdate = () => setRefreshTickets(prev => prev + 1);
 
-  // 5. 如果还在检查状态,显示 Loading (防止内容闪烁)
   if (isCheckingAuth) {
     return <div className="min-h-screen bg-slate-50 flex items-center justify-center text-gray-400">Loading...</div>;
   }
 
+  // 移动端 Tab 定义
+  const mobileTabs = [
+    { id: 'orders', label: t('sidebar.orders'), icon: FileText },
+    { id: 'tickets', label: t('sidebar.tickets'), icon: LifeBuoy },
+    { id: 'settings', label: t('sidebar.settings'), icon: Settings },
+  ];
+
   return (
-    <div className="min-h-screen bg-slate-50">
-      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
+    <div className="min-h-screen bg-slate-50 pb-20 lg:pb-10"> {/* 移动端底部留空给 TabBar */}
+      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-10">
         
-        {/* ... Header 部分保持不变 ... */}
-        <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
+        {/* === Header: 响应式布局 === */}
+        <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 lg:mb-8">
           <div>
             <h1 className="text-2xl font-bold text-gray-900">{t('dashboard.title')}</h1>
             <p className="text-sm text-gray-500 mt-1">{t('dashboard.subtitle')}</p>
           </div>
-          <div className="flex gap-3">
+          <div className="flex gap-3 w-full sm:w-auto">
              <button 
                onClick={() => router.push('/services')} 
-               className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition shadow-sm"
+               className="flex-1 sm:flex-none flex justify-center items-center gap-2 bg-blue-600 text-white px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-blue-700 transition shadow-sm active:scale-95"
              >
                <Plus size={16} /> {t('common.new_application')}
              </button>
              <button 
                onClick={handleLogout} 
-               className="flex items-center gap-2 bg-white border border-gray-300 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition"
+               className="flex-1 sm:flex-none flex justify-center items-center gap-2 bg-white border border-gray-300 px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition active:scale-95"
              >
                <LogOut size={16} /> {t('nav.logout')}
              </button>
           </div>
         </div>
 
+        {/* === Main Content === */}
         <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
           
-          {/* 左侧侧边栏 */}
-          <div className="lg:col-span-1">
+          {/* 左侧侧边栏 (仅在桌面端显示) */}
+          <div className="hidden lg:block lg:col-span-1">
             <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
           </div>
 
@@ -172,7 +156,7 @@ export default function DashboardPage() {
                 <div className="flex justify-end">
                   <button 
                     onClick={() => openCreateTicketModal()}
-                    className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm"
+                    className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm w-full sm:w-auto justify-center"
                   >
                     <Plus size={16} /> {t('ticket.create_new')}
                   </button>
@@ -189,30 +173,36 @@ export default function DashboardPage() {
         </div>
       </div>
 
-      {/* === 6. 挂载强制绑定邮箱弹窗 === */}
-      <BindEmailModal 
-        isOpen={isForceBindEmailOpen} 
-        onClose={handleForceBindClose} // 用户点击 X 或遮罩 -> 踢出页面
-        onSuccess={handleForceBindSuccess} // 绑定成功 -> 留在页面
-      />
-
-      {/* ... 其他弹窗保持不变 ... */}
-      <TicketModal 
-        isOpen={isTicketModalOpen} 
-        onClose={() => setIsTicketModalOpen(false)}
-        defaultOrderId={ticketDefaultOrderId}
-      />
-      <UserOrderDetailModal 
-        isOpen={isOrderDetailOpen} 
-        onClose={() => setIsOrderDetailOpen(false)}
-        order={selectedOrder}
-      />
-      <UserTicketDetailModal 
-        isOpen={isTicketDetailOpen} 
-        onClose={() => setIsTicketDetailOpen(false)}
-        ticket={selectedTicket}
-        onUpdate={handleTicketUpdate}
-      />
+      {/* === 移动端底部导航栏 (TabBar) === */}
+      <div className="fixed bottom-0 left-0 w-full bg-white border-t border-gray-200 lg:hidden z-40 pb-safe">
+        <div className="grid grid-cols-3 h-16">
+          {mobileTabs.map((tab) => {
+            const Icon = tab.icon;
+            const isActive = activeTab === tab.id;
+            return (
+              <button
+                key={tab.id}
+                onClick={() => {
+                  setActiveTab(tab.id);
+                  window.scrollTo({ top: 0, behavior: 'smooth' }); // 切换时回到顶部
+                }}
+                className={`flex flex-col items-center justify-center gap-1 transition ${
+                  isActive ? 'text-blue-600' : 'text-gray-400 hover:text-gray-600'
+                }`}
+              >
+                <Icon size={24} className={isActive ? 'fill-blue-100' : ''} />
+                <span className="text-[10px] font-medium">{tab.label}</span>
+              </button>
+            );
+          })}
+        </div>
+      </div>
+
+      {/* === 全局弹窗 === */}
+      <BindEmailModal isOpen={isForceBindEmailOpen} onClose={handleForceBindClose} onSuccess={handleForceBindSuccess} />
+      <TicketModal isOpen={isTicketModalOpen} onClose={() => setIsTicketModalOpen(false)} defaultOrderId={ticketDefaultOrderId} />
+      <UserOrderDetailModal isOpen={isOrderDetailOpen} onClose={() => setIsOrderDetailOpen(false)} order={selectedOrder} />
+      <UserTicketDetailModal isOpen={isTicketDetailOpen} onClose={() => setIsTicketDetailOpen(false)} ticket={selectedTicket} onUpdate={handleTicketUpdate} />
     </div>
   );
 }

+ 28 - 27
src/app/knowledge/page.tsx

@@ -5,40 +5,33 @@ import api from '@/lib/api';
 import { Search, BookOpen, Loader2 } from 'lucide-react';
 import KnowledgeCard from '@/components/knowledge/KnowledgeCard';
 import Pagination from '@/components/common/Pagination';
-// 1. 引入语言 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function KnowledgePage() {
   const [loading, setLoading] = useState(true);
   const [cards, setCards] = useState<any[]>([]);
   
-  // 2. 获取语言状态
   const { t, lang } = useLanguage();
 
-  // 搜索与分页
   const [keyword, setKeyword] = useState('');
   const [page, setPage] = useState(1);
   const [pageSize] = useState(9);
   const [total, setTotal] = useState(0);
 
-  // 初始化 & 监听语言变化
   useEffect(() => {
     fetchCards(1);
-  }, [lang]); // 3. 当 lang 变化时重新加载
+  }, [lang]);
 
   const fetchCards = async (targetPage: number) => {
     setLoading(true);
     try {
-      // 4. 映射语言参数 (zh -> chinese, en -> english)
       const cultureParam = lang === 'zh' ? 'chinese' : 'english';
-
-      // API: GET /api/cards/view2
       const res = await api.get('/api/cards/view2', {
         params: {
-          keyword: keyword, // 注意:通常 API 参数名是单数 keyword
+          keyword: keyword,
           page: targetPage,
           size: pageSize,
-          culture: cultureParam // 动态传递语言
+          culture: cultureParam
         }
       });
 
@@ -54,8 +47,7 @@ export default function KnowledgePage() {
       setPage(targetPage);
 
     } catch (error) {
-      console.warn("API Error, using mock data");
-      // Mock 数据也可以根据语言稍微区分一下,或者保持不变
+      console.warn("API Error", error);
       setCards([]); 
       setTotal(0);
     } finally {
@@ -72,34 +64,42 @@ export default function KnowledgePage() {
   };
 
   return (
-    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+    // 调整 1: 移动端减少垂直 Padding
+    <div className="min-h-screen bg-slate-50 py-6 px-4 md:py-12 md:px-6">
       <div className="max-w-7xl mx-auto">
         
         {/* Header Section */}
-        <div className="text-center mb-12">
-          <h1 className="text-3xl font-bold text-slate-900 flex items-center justify-center gap-3">
-            <BookOpen className="text-blue-600" /> {t('knowledge.title')}
+        <div className="text-center mb-8 md:mb-12">
+          {/* 调整 2: 响应式字体大小 */}
+          <h1 className="text-2xl md:text-3xl font-bold text-slate-900 flex items-center justify-center gap-2 md:gap-3">
+            <BookOpen className="text-blue-600 w-6 h-6 md:w-8 md:h-8" /> 
+            {t('knowledge.title')}
           </h1>
-          <p className="text-slate-500 mt-3 max-w-2xl mx-auto">
+          <p className="text-sm md:text-base text-slate-500 mt-2 md:mt-3 max-w-2xl mx-auto px-2">
             {t('knowledge.subtitle')}
           </p>
         </div>
 
         {/* Search Bar */}
-        <div className="max-w-2xl mx-auto mb-12 relative">
-          <div className="relative">
+        <div className="max-w-2xl mx-auto mb-8 md:mb-12 relative">
+          <div className="relative group">
             <input 
               type="text" 
               placeholder={t('knowledge.search_placeholder')} 
-              className="w-full pl-12 pr-28 py-4 rounded-xl border border-slate-200 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-lg transition"
+              // 调整 3: 移动端输入框稍微紧凑一点,调整 padding 防止文字被按钮遮挡
+              className="w-full pl-10 md:pl-12 pr-24 md:pr-28 py-3 md:py-4 rounded-xl border border-slate-200 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-base md:text-lg transition"
               value={keyword}
               onChange={(e) => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
             />
-            <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={24} />
+            
+            {/* 图标尺寸适配 */}
+            <Search className="absolute left-3 md:left-4 top-1/2 -translate-y-1/2 text-slate-400 w-5 h-5 md:w-6 md:h-6" />
+            
+            {/* 按钮尺寸适配 */}
             <button 
               onClick={handleSearch}
-              className="absolute right-2 top-1/2 -translate-y-1/2 bg-slate-900 text-white px-6 py-2.5 rounded-lg font-bold hover:bg-slate-800 transition"
+              className="absolute right-1.5 md:right-2 top-1/2 -translate-y-1/2 bg-slate-900 text-white px-4 md:px-6 py-1.5 md:py-2.5 rounded-lg font-bold hover:bg-slate-800 transition text-sm md:text-base shadow-sm active:scale-95"
             >
               {t('common.search')}
             </button>
@@ -109,15 +109,16 @@ export default function KnowledgePage() {
         {/* Content Grid */}
         {loading ? (
           <div className="flex justify-center py-20">
-            <Loader2 className="animate-spin text-blue-600 w-10 h-10" />
+            <Loader2 className="animate-spin text-blue-600 w-8 h-8 md:w-10 md:h-10" />
           </div>
         ) : cards.length === 0 ? (
-          <div className="text-center py-20 text-slate-400">
-            <BookOpen size={48} className="mx-auto mb-4 opacity-20" />
+          <div className="text-center py-16 md:py-20 text-slate-400">
+            <BookOpen size={48} className="mx-auto mb-4 opacity-20 w-12 h-12 md:w-16 md:h-16" />
             <p>{t('knowledge.empty_state')}</p>
           </div>
         ) : (
-          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start">
+          // Grid 布局在移动端默认 cols-1,这里保持不变即可
+          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 items-start">
             {cards.map((card) => (
               <KnowledgeCard key={card.id} data={card} />
             ))}
@@ -125,7 +126,7 @@ export default function KnowledgePage() {
         )}
 
         {/* Pagination */}
-        <div className="mt-8">
+        <div className="mt-6 md:mt-8">
           <Pagination 
             currentPage={page}
             total={total}

+ 51 - 35
src/app/page.tsx

@@ -2,14 +2,11 @@
 
 import Link from "next/link";
 import { ArrowRight, Globe, CheckCircle, Zap, FileText, Bot } from "lucide-react";
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function Home() {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
 
-  // 3. 将 steps 定义移入组件内部,以便使用 t()
   const steps = [
     {
       icon: Globe,
@@ -35,44 +32,60 @@ export default function Home() {
 
   return (
     <div className="flex flex-col items-center">
-      {/* Hero Section */}
-      <section className="w-full bg-white py-24 px-4 text-center border-b border-slate-100">
-        <h1 className="text-5xl font-bold tracking-tight text-slate-900 mb-6">
-          {t('home.hero_title_prefix')} <span className="text-blue-600">{t('home.hero_title_highlight')}</span>
-        </h1>
-        <p className="text-xl text-slate-500 max-w-2xl mx-auto mb-10">
-          {t('home.hero_subtitle')}
-        </p>
-        <div className="flex gap-4 justify-center">
-          <Link href="/services" className="px-8 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 flex items-center gap-2 transition shadow-lg shadow-blue-200">
-            {t('home.cta_start')} <ArrowRight size={20} />
-          </Link>
-          <Link href="/slots" className="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 transition">
-            {t('home.cta_check_slots')}
-          </Link>
+      
+      {/* === Hero Section === */}
+      {/* 调整:移动端 py-16, 桌面端 py-24 */}
+      <section className="w-full bg-white py-16 md:py-24 px-4 text-center border-b border-slate-100">
+        <div className="max-w-4xl mx-auto">
+          {/* 调整:移动端 text-3xl, 桌面端 text-5xl */}
+          <h1 className="text-3xl md:text-5xl font-bold tracking-tight text-slate-900 mb-6 leading-tight">
+            {t('home.hero_title_prefix')} <span className="text-blue-600">{t('home.hero_title_highlight')}</span>
+          </h1>
+          
+          {/* 调整:移动端 text-base, 桌面端 text-xl */}
+          <p className="text-base md:text-xl text-slate-500 max-w-2xl mx-auto mb-10 px-2">
+            {t('home.hero_subtitle')}
+          </p>
+          
+          {/* 调整:移动端垂直排列(flex-col),桌面端水平(sm:flex-row) */}
+          <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
+            <Link 
+              href="/services" 
+              className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 flex items-center justify-center gap-2 transition shadow-lg shadow-blue-200 active:scale-95"
+            >
+              {t('home.cta_start')} <ArrowRight size={20} />
+            </Link>
+            <Link 
+              href="/slots" 
+              className="w-full sm:w-auto px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 transition flex items-center justify-center active:scale-95"
+            >
+              {t('home.cta_check_slots')}
+            </Link>
+          </div>
         </div>
       </section>
 
-      {/* Work Process */}
-      <section className="w-full bg-slate-50 py-20 px-4">
+      {/* === Work Process === */}
+      {/* 调整:移动端 py-12, 桌面端 py-20 */}
+      <section className="w-full bg-slate-50 py-12 md:py-20 px-4">
         <div className="max-w-7xl mx-auto">
-          <div className="text-center mb-16">
-            <h2 className="text-3xl font-bold text-slate-900">{t('home.process_title')}</h2>
-            <p className="text-slate-500 mt-2">{t('home.process_subtitle')}</p>
+          <div className="text-center mb-12 md:mb-16">
+            <h2 className="text-2xl md:text-3xl font-bold text-slate-900">{t('home.process_title')}</h2>
+            <p className="text-slate-500 mt-2 text-sm md:text-base">{t('home.process_subtitle')}</p>
           </div>
 
           <div className="grid grid-cols-1 md:grid-cols-4 gap-8 relative">
-            {/* 连接线 */}
+            {/* 连接线 (仅在大屏幕显示) */}
             <div className="hidden md:block absolute top-12 left-0 w-full h-0.5 bg-slate-200 -z-10 transform scale-x-75" />
 
             {steps.map((step, index) => {
               const Icon = step.icon;
               return (
                 <div key={index} className="flex flex-col items-center text-center bg-white p-6 rounded-xl shadow-sm border border-slate-100 relative z-10 h-full hover:-translate-y-1 transition duration-300">
-                  <div className="w-24 h-24 bg-blue-50 rounded-full flex items-center justify-center mb-6 border-4 border-white shadow-sm text-blue-600">
-                    <Icon size={40} />
+                  <div className="w-20 h-20 md:w-24 md:h-24 bg-blue-50 rounded-full flex items-center justify-center mb-6 border-4 border-white shadow-sm text-blue-600 flex-shrink-0">
+                    <Icon size={32} className="md:w-10 md:h-10" />
                   </div>
-                  <h3 className="text-xl font-bold text-slate-800 mb-3">{step.title}</h3>
+                  <h3 className="text-lg md:text-xl font-bold text-slate-800 mb-3">{step.title}</h3>
                   <p className="text-sm text-slate-500 leading-relaxed">
                     {step.desc}
                   </p>
@@ -83,28 +96,31 @@ export default function Home() {
         </div>
       </section>
       
-      {/* Features */}
-      <section className="max-w-7xl mx-auto py-20 px-4 grid md:grid-cols-3 gap-10">
+      {/* === Features === */}
+      {/* 调整:移动端 py-12, gap-6 */}
+      <section className="max-w-7xl mx-auto py-12 md:py-20 px-4 grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-10">
         <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
           <div className="bg-blue-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
             <Globe className="text-blue-600" />
           </div>
-          <h3 className="text-xl font-bold mb-2">{t('home.feat_global_title')}</h3>
-          <p className="text-slate-600">{t('home.feat_global_desc')}</p>
+          <h3 className="text-lg md:text-xl font-bold mb-2">{t('home.feat_global_title')}</h3>
+          <p className="text-sm md:text-base text-slate-600 leading-relaxed">{t('home.feat_global_desc')}</p>
         </div>
+        
         <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
           <div className="bg-green-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
             <CheckCircle className="text-green-600" />
           </div>
-          <h3 className="text-xl font-bold mb-2">{t('home.feat_success_title')}</h3>
-          <p className="text-slate-600">{t('home.feat_success_desc')}</p>
+          <h3 className="text-lg md:text-xl font-bold mb-2">{t('home.feat_success_title')}</h3>
+          <p className="text-sm md:text-base text-slate-600 leading-relaxed">{t('home.feat_success_desc')}</p>
         </div>
+        
         <div className="p-8 bg-white rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
           <div className="bg-purple-100 w-12 h-12 rounded-lg flex items-center justify-center mb-4">
             <Zap className="text-purple-600" />
           </div>
-          <h3 className="text-xl font-bold mb-2">{t('home.feat_fast_title')}</h3>
-          <p className="text-slate-600">{t('home.feat_fast_desc')}</p>
+          <h3 className="text-lg md:text-xl font-bold mb-2">{t('home.feat_fast_title')}</h3>
+          <p className="text-sm md:text-base text-slate-600 leading-relaxed">{t('home.feat_fast_desc')}</p>
         </div>
       </section>
     </div>

+ 135 - 0
src/app/payment/confirm/page.tsx

@@ -0,0 +1,135 @@
+'use client';
+
+import { useEffect, useState, useRef } from 'react';
+import { useSearchParams, useRouter } from 'next/navigation';
+import api from '@/lib/api';
+import { Loader2, CheckCircle, XCircle, ArrowLeft } from 'lucide-react';
+
+export default function PaymentConfirmPage() {
+  const searchParams = useSearchParams();
+  const router = useRouter();
+  
+  // 防止 React StrictMode 下执行两次
+  const hasFetched = useRef(false);
+
+  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
+  const [message, setMessage] = useState('正在验证支付信息...');
+  const [paymentId, setPaymentId] = useState<string>('');
+
+  useEffect(() => {
+    const pid = searchParams.get('payment_id');
+    const token = searchParams.get('token');
+
+    if (!pid || !token) {
+      setStatus('error');
+      setMessage('参数缺失:URL 中必须包含 payment_id 和 token');
+      return;
+    }
+
+    setPaymentId(pid);
+
+    // 核心逻辑:调用确认接口
+    const confirmPayment = async () => {
+      if (hasFetched.current) return;
+      hasFetched.current = true;
+
+      try {
+        // API: GET /api/vas/payment/confirm?payment_id=1&token=1
+        await api.get('/api/vas/payment/confirm', {
+          params: {
+            payment_id: pid,
+            token: token
+          }
+        });
+
+        setStatus('success');
+        setMessage('支付已成功确认!系统正在处理后续流程。');
+      } catch (error: any) {
+        console.error(error);
+        setStatus('error');
+        // 获取后端返回的错误信息
+        const errorMsg = error.response?.data?.message || error.message || '未知错误';
+        setMessage(`确认失败: ${errorMsg}`);
+      }
+    };
+
+    confirmPayment();
+  }, [searchParams]);
+
+  // 渲染不同的状态 UI
+  return (
+    <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
+      <div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden border border-slate-100">
+        
+        {/* 顶部装饰条 */}
+        <div className={`h-2 w-full ${
+          status === 'loading' ? 'bg-blue-500 animate-pulse' :
+          status === 'success' ? 'bg-green-500' : 'bg-red-500'
+        }`} />
+
+        <div className="p-8 text-center">
+          {/* 图标状态 */}
+          <div className="flex justify-center mb-6">
+            {status === 'loading' && (
+              <div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center">
+                <Loader2 className="w-10 h-10 text-blue-600 animate-spin" />
+              </div>
+            )}
+            {status === 'success' && (
+              <div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
+                <CheckCircle className="w-10 h-10 text-green-600" />
+              </div>
+            )}
+            {status === 'error' && (
+              <div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
+                <XCircle className="w-10 h-10 text-red-600" />
+              </div>
+            )}
+          </div>
+
+          {/* 标题与描述 */}
+          <h1 className="text-xl font-bold text-slate-900 mb-2">
+            {status === 'loading' ? '处理中...' : 
+             status === 'success' ? '操作成功' : '操作失败'}
+          </h1>
+          
+          <p className={`text-sm mb-6 leading-relaxed ${
+            status === 'error' ? 'text-red-500' : 'text-slate-500'
+          }`}>
+            {message}
+          </p>
+
+          {/* 详情信息 (仅在非加载状态显示) */}
+          {status !== 'loading' && paymentId && (
+            <div className="bg-slate-50 rounded-lg p-3 mb-6 text-xs text-slate-400 font-mono">
+              Payment ID: {paymentId}
+            </div>
+          )}
+
+          {/* 底部按钮 */}
+          <div className="space-y-3">
+            <button 
+              onClick={() => router.push('/admin/orders')}
+              className="w-full flex items-center justify-center gap-2 bg-slate-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm"
+            >
+              <ArrowLeft size={16} /> 返回订单管理
+            </button>
+            
+            {status === 'error' && (
+              <button 
+                onClick={() => window.location.reload()}
+                className="w-full py-2.5 text-slate-600 text-sm hover:bg-slate-50 rounded-lg transition"
+              >
+                重试
+              </button>
+            )}
+          </div>
+        </div>
+      </div>
+
+      <p className="mt-8 text-xs text-slate-400">
+        Visafly Admin System &copy; {new Date().getFullYear()}
+      </p>
+    </div>
+  );
+}

+ 2 - 5
src/app/services/page.js → src/app/services/page.tsx

@@ -1,17 +1,14 @@
 'use client';
 
 import ServiceList from '@/components/ServiceList';
-// 1. 引入语言 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function ServicesPage() {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
 
   return (
-    <div className="max-w-7xl mx-auto py-12 px-4">
-      {/* 3. 替换硬编码标题 */}
-      <h1 className="text-3xl font-bold mb-8 text-gray-900">
+    <div className="max-w-7xl mx-auto px-4 sm:px-6 py-6 md:py-12">
+      <h1 className="text-2xl md:text-3xl font-bold mb-6 md:mb-8 text-gray-900">
         {t('services.title')}
       </h1>
       <ServiceList />

+ 38 - 102
src/app/slots/page.tsx

@@ -1,22 +1,14 @@
 'use client';
 
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
 import api from '@/lib/api';
-import { Search, MapPin, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react';
+import { Search, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+import LocalTime from '@/components/common/LocalTime';
 
-// === 类型定义 ===
-
-interface TimeSlot {
-  time: string;
-  label?: string; // e.g. "Prime Time", "VIP"
-}
-
-interface DayAvailability {
-  date: string;
-  times: TimeSlot[];
-}
-
+// === 类型定义 (保持不变) ===
+interface TimeSlot { time: string; label?: string; }
+interface DayAvailability { date: string; times: TimeSlot[]; }
 interface SlotSnapshot {
   id: number;
   country: string;
@@ -25,106 +17,42 @@ interface SlotSnapshot {
   availability_status: 'None' | 'Available' | 'Waitlist';
   earliest_date: string | null;
   snapshot_at: string;
-  slots_data: DayAvailability[]; // 对应数据库的 availability JSON
+  availability: DayAvailability[]; 
 }
 
 export default function SlotQueryPage() {
   const [loading, setLoading] = useState(false);
   const [snapshot, setSnapshot] = useState<SlotSnapshot | null>(null);
-
   const { t, lang } = useLanguage();
   
-  // 筛选状态
   const [country, setCountry] = useState('France');
   const [city, setCity] = useState('Dublin');
-  const [visaType, setVisaType] = useState('Short Stay');
+  const [visaType, setVisaType] = useState('Tourist');
 
-  // 模拟配置 (实际项目中这些选项也应该从 API 获取)
+  // ... (options 和 fetchSlots 保持不变) ...
   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'
-    ]
+    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 fetchSlots = async () => {
     setLoading(true);
     try {
-      // API: GET /api/public/slots/latest
       const res = await api.get('/api/slots/latest', {
         params: { country, city, visa_type: visaType }
       });
-      
       const data = res.data.data;
-      if (data) {
-        setSnapshot(data);
-      } else {
-        setSnapshot(null); // 无数据
-      }
+      if (data) setSnapshot(data);
+      else setSnapshot(null);
     } catch (e) {
-      console.warn("API Error, using mock data");
-      // --- Mock Data 演示 ---
-      setSnapshot({
-        id: 1,
-        country, city, visa_type: visaType,
-        availability_status: 'Available',
-        earliest_date: '2025-02-14',
-        snapshot_at: new Date().toISOString(),
-        slots_data: [
-          {
-            date: '2025-02-14',
-            times: [
-              { time: '09:00', label: 'Premium' },
-              { time: '09:15', label: 'Premium' },
-              { time: '10:30', label: 'Normal' }
-            ]
-          },
-          {
-            date: '2025-02-18',
-            times: [
-              { time: '14:00', label: 'Normal' },
-              { time: '15:00', label: 'Normal' }
-            ]
-          }
-        ]
-      });
+      console.warn("API Error");
+      setSnapshot(null);
     } finally {
       setLoading(false);
     }
   };
 
-  // 辅助函数:格式化日期
   const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
     const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
@@ -132,17 +60,19 @@ export default function SlotQueryPage() {
   };
 
   return (
-    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+    // 调整 1: 移动端 Padding
+    <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="text-center mb-10">
-          <h1 className="text-3xl font-bold text-slate-900">{t('slots.title')}</h1>
-          <p className="text-slate-500 mt-2">{t('slots.subtitle')}</p>
+        <div className="text-center mb-8 md:mb-10">
+          <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>
         </div>
 
         {/* 筛选区 */}
-        <div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
+        <div className="bg-white p-5 md:p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
+          {/* 调整 2: 移动端单列布局 */}
           <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
             <div>
               <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.country')}</label>
@@ -172,11 +102,13 @@ export default function SlotQueryPage() {
               </select>
             </div>
           </div>
+          
           <div className="mt-6 flex justify-end">
+            {/* 调整 3: 移动端按钮全宽 */}
             <button 
               onClick={fetchSlots}
               disabled={loading}
-              className="flex items-center gap-2 bg-slate-900 text-white px-8 py-2.5 rounded-lg font-bold hover:bg-slate-800 transition disabled:opacity-70 shadow-lg shadow-slate-200"
+              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"
             >
               {loading ? <RefreshCw className="animate-spin" size={18} /> : <Search size={18} />}
               {t('common.search')}
@@ -189,25 +121,27 @@ export default function SlotQueryPage() {
           <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
             
             {/* 概览 Banner */}
-            <div className={`p-6 rounded-xl border flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4
+            <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-center gap-4">
-                <div className={`p-3 rounded-full ${snapshot.availability_status === 'Available' ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-500'}`}>
+              <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 items-center gap-1 mt-1">
-                    <Clock size={12} /> {t('slots.updated_at')}: {new Date(snapshot.snapshot_at).toLocaleString()}
+                  <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="bg-white/60 px-5 py-3 rounded-lg border border-black/5 text-center sm:text-right">
+                <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>
@@ -215,9 +149,10 @@ export default function SlotQueryPage() {
             </div>
 
             {/* 具体的 Slot 列表 */}
-            {snapshot.slots_data && snapshot.slots_data.length > 0 && (
+            {snapshot.availability && snapshot.availability.length > 0 && (
+              // 调整 4: 移动端单列卡片
               <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
-                {snapshot.slots_data.map((day, idx) => (
+                {snapshot.availability.map((day, idx) => (
                   <div key={idx} className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition">
                     <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex justify-between items-center">
                       <div className="flex items-center gap-2 font-bold text-slate-700">
@@ -228,6 +163,7 @@ export default function SlotQueryPage() {
                         {day.times.length} {t('slots.slots_count')}
                       </span>
                     </div>
+                    
                     <div className="p-4 grid grid-cols-2 gap-2">
                       {day.times.map((slot, tIdx) => (
                         <div key={tIdx} className="text-sm border border-slate-100 rounded p-2 text-center hover:border-blue-300 hover:bg-blue-50 transition cursor-default">

+ 69 - 36
src/components/AuthForm.tsx

@@ -4,12 +4,11 @@ import { useState } from 'react';
 import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
 import ForgotPasswordModal from '@/components/ForgotPasswordModal';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { Zap } from 'lucide-react'; // 引入一个图标增加视觉效果
 
 export default function AuthForm() {
   const router = useRouter();
-  // 2. 获取翻译函数
   const { t } = useLanguage();
 
   const [isLoginMode, setIsLoginMode] = useState<boolean>(true);
@@ -22,8 +21,16 @@ export default function AuthForm() {
     setLoading(true);
 
     try {
-      const endpoint = isLoginMode ? '/api/auth/login' : '/api/auth/auto-register';
-      const res = await api.post(endpoint, formData);
+      let res;
+      
+      if (isLoginMode) {
+        // === 登录逻辑 ===
+        res = await api.post('/api/auth/login', formData);
+      } else {
+        // === 自动注册逻辑 ===
+        // 不需要传 email/password,通常传一个标识来源的字段即可
+        res = await api.post('/api/auth/auto-register', {});
+      }
       
       const data = res.data.data || res.data;
       const token = data.token || data.access_token;
@@ -34,6 +41,12 @@ export default function AuthForm() {
           localStorage.setItem('user_info', JSON.stringify(data.user));
         }
         window.dispatchEvent(new Event('storage'));
+        
+        // 如果是自动注册,给个提示
+        if (!isLoginMode) {
+          alert(t('auth.auto_register_success'));
+        }
+
         router.push('/dashboard');
       } else {
         alert(t('auth.login_success_no_token'));
@@ -52,46 +65,66 @@ export default function AuthForm() {
       <h2 className="text-2xl font-bold text-center mb-6">
         {isLoginMode ? t('auth.welcome_back') : t('auth.auto_register')}
       </h2>
+      
       <form onSubmit={handleSubmit} className="space-y-5">
-        <div>
-          <label className="block text-sm font-medium mb-1 text-gray-700">{t('auth.email_label')}</label>
-          <input
-            type="email" required
-            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
-            value={formData.email}
-            onChange={(e) => setFormData({...formData, email: e.target.value})}
-            placeholder={t('auth.email_placeholder')}
-          />
-        </div>
-        <div>
-          <div className="flex justify-between items-center mb-1">
-            <label className="block text-sm font-medium text-gray-700">{t('auth.password_label')}</label>
-            {isLoginMode && (
-              <button 
-                type="button"
-                onClick={() => setIsForgotOpen(true)}
-                className="text-xs text-blue-600 hover:text-blue-800 hover:underline"
-              >
-                {t('auth.forgot_password')}
-              </button>
-            )}
+        
+        {/* 只有在登录模式下才显示输入框 */}
+        {isLoginMode ? (
+          <>
+            <div>
+              <label className="block text-sm font-medium mb-1 text-gray-700">{t('auth.email_label')}</label>
+              <input
+                type="email" required
+                className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
+                value={formData.email}
+                onChange={(e) => setFormData({...formData, email: e.target.value})}
+                placeholder={t('auth.email_placeholder')}
+              />
+            </div>
+            <div>
+              <div className="flex justify-between items-center mb-1">
+                <label className="block text-sm font-medium text-gray-700">{t('auth.password_label')}</label>
+                <button 
+                  type="button"
+                  onClick={() => setIsForgotOpen(true)}
+                  className="text-xs text-blue-600 hover:text-blue-800 hover:underline"
+                >
+                  {t('auth.forgot_password')}
+                </button>
+              </div>
+              <input
+                type="password" required
+                className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
+                value={formData.password}
+                onChange={(e) => setFormData({...formData, password: e.target.value})}
+                placeholder="••••••••"
+              />
+            </div>
+          </>
+        ) : (
+          // 自动注册模式下的提示信息
+          <div className="bg-blue-50 p-6 rounded-lg text-center border border-blue-100">
+            <div className="flex justify-center mb-3">
+              <div className="bg-blue-100 p-3 rounded-full text-blue-600">
+                <Zap size={24} />
+              </div>
+            </div>
+            <h3 className="font-bold text-blue-900 mb-2">{t('auth.auto_reg_title')}</h3>
+            <p className="text-sm text-blue-700 leading-relaxed">
+              {t('auth.auto_reg_desc')}
+            </p>
           </div>
-          <input
-            type="password" required
-            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition"
-            value={formData.password}
-            onChange={(e) => setFormData({...formData, password: e.target.value})}
-            placeholder="••••••••"
-          />
-        </div>
+        )}
+
         <button 
           type="submit" 
           disabled={loading} 
-          className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50"
+          className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50 shadow-md"
         >
-          {loading ? t('common.processing') : (isLoginMode ? t('auth.login_btn') : t('auth.register_btn'))}
+          {loading ? t('common.processing') : (isLoginMode ? t('auth.login_btn') : t('auth.auto_reg_btn_action'))}
         </button>
       </form>
+
       <div className="mt-6 text-center border-t border-gray-100 pt-4">
         <button 
           type="button" 

+ 78 - 39
src/components/CreateOrderForm.tsx

@@ -22,7 +22,7 @@ interface ProductDetail {
   description: string;
   price_amount: number;
   price_currency: string;
-  schema_id?: number; // 关联的 Schema ID
+  schema_id?: number; 
   extra_fields?: any; 
 }
 
@@ -132,7 +132,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     if (token) return true;
 
     try {
-      console.log("检测到未登录,正在进行自动注册...");
+      console.log("Detecting no session, auto-registering...");
       // 2. 调用自动注册
       const res = await api.post('/api/auth/auto-register', {
         register_ip: 'client-lazy-init'
@@ -154,7 +154,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       return false;
     } catch (e) {
       console.error("Auto register failed", e);
-      alert(t('auth.login_success_no_token')); // 或使用更具体的自动注册失败提示
+      alert(t('auth.login_success_no_token')); 
       router.push('/login');
       return false;
     }
@@ -168,7 +168,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     
     try {
       const user = JSON.parse(userStr);
-      // 判断逻辑:必须有 email 且包含 @ 符号 (排除空或非法值)
+      // 判断逻辑:必须有 email 且包含 @ 符号
       if (user.email && user.email.includes('@')) {
         return true; 
       }
@@ -189,14 +189,14 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     setSubmitting(true);
 
     try {
-      // 1. 确保已登录 (懒注册)
+      // 1. 确保已登录
       const isLoggedIn = await ensureUserLoggedIn();
       if (!isLoggedIn) {
         setSubmitting(false);
         return;
       }
 
-      // 2. 拦截逻辑:检查邮箱
+      // 2. 检查邮箱
       if (!checkUserEmail()) {
         setIsBindEmailOpen(true); // 打开绑定弹窗
         setSubmitting(false); // 暂停提交
@@ -227,16 +227,17 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
 
   // 绑定成功回调
   const handleBindSuccess = () => {
-    // 绑定成功后,不自动提交,而是让用户确认信息后再次点击提交按钮
-    // 这样体验更可控
+    // 绑定成功后,保持在当前页面,用户可再次点击提交
   };
 
   // 渲染单个字段
   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-sm";
     const label = fieldSchema.title || key;
+    // 使用翻译后的 Placeholder
+    const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
 
-    // 枚举类型 (Select)
+    // 1. 枚举类型 (Select)
     if (fieldSchema.enum && fieldSchema.enum.length > 0) {
       return (
         <select
@@ -254,11 +255,32 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       );
     }
 
-    // Input 类型推断
+    // 2. 日期/时间类型 (使用原生 input + 焦点切换技巧以显示 placeholder)
+    if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
+      const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
+      
+      return (
+        <input
+          key={key}
+          type="text" // 默认为 text 以显示 placeholder
+          required={required}
+          className={commonClasses}
+          placeholder={placeholderText}
+          value={formValues[key] || ''}
+          onChange={(e) => handleInputChange(key, e.target.value)}
+          // 聚焦时切换为日期选择器
+          onFocus={(e) => (e.target.type = realType)} 
+          // 失焦且无值时切回文本
+          onBlur={(e) => {
+            if (!e.target.value) e.target.type = 'text'; 
+          }}
+        />
+      );
+    }
+
+    // 3. 其他 Input 类型推断
     let inputType = 'text';
     if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
-    if (fieldSchema.format === 'date') inputType = 'date';
-    if (fieldSchema.format === 'date-time') inputType = 'datetime-local';
     if (fieldSchema.format === 'email') inputType = 'email';
 
     return (
@@ -267,7 +289,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
         type={inputType}
         required={required}
         className={commonClasses}
-        placeholder={fieldSchema.description || `${t('common.enter')} ${label}`}
+        placeholder={placeholderText}
         value={formValues[key] || ''}
         onChange={(e) => handleInputChange(key, e.target.value)}
       />
@@ -289,7 +311,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       const propA = properties[a];
       const propB = properties[b];
       
-      // 获取权重,默认为 999 (排在最后)
       const orderA = propA.order ?? propA['x-order'] ?? 999;
       const orderB = propB.order ?? propB['x-order'] ?? 999;
       
@@ -307,56 +328,74 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
   const hasFields = sortedKeys.length > 0;
 
   return (
-    <div className="bg-white p-8 rounded-xl shadow-sm border">
+    // 响应式内边距
+    <div className="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
+      
       {/* 头部:商品信息 */}
-      <div className="mb-8 pb-6 border-b border-gray-100">
-        <div className="flex justify-between items-start">
-          <h1 className="text-2xl font-bold text-gray-900">{product.title || productName}</h1>
-          <span className="text-xl font-bold text-blue-600">
-             {/* 假设金额单位是分,显示为元 */}
+      <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">{product.description}</p>
+        <p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
         
-        <div className="mt-4 flex items-center gap-2 text-xs text-amber-600 bg-amber-50 px-3 py-2 rounded-lg w-fit">
-          <Info size={14} />
-          <span>{t('order.fill_form_hint')}</span>
+        {/* 提示信息:移动端全宽 */}
+        <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 onSubmit={handleSubmit} className="space-y-6">
+      <form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
         {!hasFields && (
-          <div className="text-center py-8 text-gray-400 text-sm">
+          <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')}
           </div>
         )}
 
         {sortedKeys.map((key) => (
           <div key={key}>
-            <label className="block text-sm font-medium mb-1.5 text-gray-700">
+            <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">{properties[key].description}</p>
+              <p className="text-xs text-gray-400 mt-1.5 leading-snug">{properties[key].description}</p>
             )}
           </div>
         ))}
 
-        <button
-          type="submit"
-          disabled={submitting}
-          className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50 flex justify-center items-center mt-8 shadow-lg shadow-blue-200"
-        >
-          {submitting ? (
-            <>
-              <Loader2 className="animate-spin mr-2 w-4 h-4"/> {t('common.processing')}
-            </>
-          ) : `${t('order.submit_and_pay')} ${(product.price_amount / 100).toFixed(2)} ${product.price_currency}`}
-        </button>
+        <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">
+            点击提交即代表同意服务条款与隐私政策
+          </p>
+        </div>
       </form>
 
       {/* 绑定邮箱弹窗 */}

+ 35 - 26
src/components/Footer.tsx

@@ -16,47 +16,56 @@ export default function Footer() {
   }
 
   return (
-    <footer className="bg-white border-t border-slate-200 py-12 mt-auto">
+    <footer className="bg-white border-t border-slate-200 py-8 md:py-12 mt-auto">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
-        <div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
-          {/* Brand */}
-          <div className="col-span-1 md:col-span-2">
-            <div className="flex items-center gap-2 mb-4">
+        
+        {/* 
+           Grid 布局调整:
+           - 移动端:grid-cols-2 (2列)
+           - 桌面端:md:grid-cols-4 (4列)
+        */}
+        <div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-8 md:mb-12">
+          
+          {/* Brand - 移动端跨2列居中,桌面端跨2列左对齐 */}
+          <div className="col-span-2 text-center md:text-left">
+            <div className="flex items-center justify-center md:justify-start gap-2 mb-4">
               <Plane className="text-blue-600" size={24} />
               <span className="text-xl font-bold text-slate-900">Visafly</span>
             </div>
-            <p className="text-sm text-slate-500 max-w-xs leading-relaxed">
+            <p className="text-sm text-slate-500 max-w-xs mx-auto md:mx-0 leading-relaxed">
               {t('footer.description')}
             </p>
           </div>
 
-          {/* Quick Links */}
-          <div>
-            <h4 className="font-bold text-slate-900 mb-4">{t('footer.quick_links')}</h4>
-            <ul className="space-y-2 text-sm text-slate-600">
-              <li><Link href="/services" className="hover:text-blue-600">{t('nav.services')}</Link></li>
-              <li><Link href="/slots" className="hover:text-blue-600">{t('nav.slots')}</Link></li>
-              <li><Link href="/dashboard" className="hover:text-blue-600">{t('nav.dashboard')}</Link></li>
+          {/* Quick Links - 移动端占1列 */}
+          <div className="col-span-1 pl-4 md:pl-0">
+            <h4 className="font-bold text-slate-900 mb-4 text-sm md:text-base">{t('footer.quick_links')}</h4>
+            <ul className="space-y-3 text-sm text-slate-600">
+              <li><Link href="/services" className="hover:text-blue-600 transition block py-1">{t('nav.services')}</Link></li>
+              <li><Link href="/slots" className="hover:text-blue-600 transition block py-1">{t('nav.slots')}</Link></li>
+              <li><Link href="/dashboard" className="hover:text-blue-600 transition block py-1">{t('nav.dashboard')}</Link></li>
             </ul>
           </div>
 
-          {/* Support */}
-          <div>
-            <h4 className="font-bold text-slate-900 mb-4">{t('footer.support')}</h4>
-            <ul className="space-y-2 text-sm text-slate-600">
-              <li><Link href="/refund-policy" className="hover:text-blue-600">{t('footer.refund_policy')}</Link></li>
-              <li><Link href="#" className="hover:text-blue-600">{t('footer.terms')}</Link></li>
-              <li><Link href="#" className="hover:text-blue-600">{t('footer.contact')}</Link></li>
+          {/* Support - 移动端占1列 */}
+          <div className="col-span-1 pl-4 md:pl-0">
+            <h4 className="font-bold text-slate-900 mb-4 text-sm md:text-base">{t('footer.support')}</h4>
+            <ul className="space-y-3 text-sm text-slate-600">
+              <li><Link href="/refund-policy" className="hover:text-blue-600 transition block py-1">{t('footer.refund_policy')}</Link></li>
+              <li><Link href="#" className="hover:text-blue-600 transition block py-1">{t('footer.terms')}</Link></li>
+              <li><Link href="#" className="hover:text-blue-600 transition block py-1">{t('footer.contact')}</Link></li>
             </ul>
           </div>
         </div>
 
-        <div className="border-t border-slate-100 pt-8 flex flex-col md:flex-row justify-between items-center text-xs text-slate-400">
-          <p>&copy; {currentYear} Visafly Inc. {t('footer.rights_reserved')}</p>
-          <div className="flex gap-4 mt-2 md:mt-0">
-            {/* === 修改了这里 === */}
-            <Link href="/privacy" className="hover:text-slate-600">{t('footer.privacy')}</Link>
-            <Link href="/cookie-policy" className="hover:text-slate-600">{t('footer.cookie')}</Link>
+        {/* Bottom Bar - 移动端垂直排列,桌面端水平两端对齐 */}
+        <div className="border-t border-slate-100 pt-8 flex flex-col md:flex-row justify-between items-center text-xs text-slate-400 gap-4 md:gap-0">
+          <p className="text-center md:text-left">
+            &copy; {currentYear} Visafly Inc. {t('footer.rights_reserved')}
+          </p>
+          <div className="flex gap-6 md:gap-4">
+            <Link href="/privacy" className="hover:text-slate-600 transition p-2 md:p-0">{t('footer.privacy')}</Link>
+            <Link href="/cookie-policy" className="hover:text-slate-600 transition p-2 md:p-0">{t('footer.cookie')}</Link>
           </div>
         </div>
       </div>

+ 116 - 37
src/components/Navbar.tsx

@@ -1,20 +1,23 @@
 'use client';
 
 import Link from 'next/link';
-import { useRouter } from 'next/navigation';
+import { useRouter, usePathname } from 'next/navigation';
 import { useEffect, useState } from 'react';
-// 1. 引入 LayoutDashboard 图标
-import { Plane, ShieldCheck, CalendarSearch, BookOpen, Briefcase, LayoutDashboard } from 'lucide-react';
+import { 
+  Plane, ShieldCheck, CalendarSearch, BookOpen, Briefcase, LayoutDashboard, Menu, X, LogOut, UserCircle 
+} from 'lucide-react';
 import { isAdmin, logout } from '@/lib/auth';
 import LanguageSwitcher from '@/components/common/LanguageSwitcher';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function Navbar() {
   const router = useRouter();
+  const pathname = usePathname();
   const { t } = useLanguage();
   
   const [isLogged, setIsLogged] = useState(false);
   const [showAdmin, setShowAdmin] = useState(false);
+  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
 
   useEffect(() => {
     const checkStatus = () => {
@@ -28,52 +31,51 @@ export default function Navbar() {
     return () => window.removeEventListener('storage', checkStatus);
   }, []);
 
+  // 后台管理页面隐藏全局导航
+  if (pathname?.startsWith('/admin')) {
+    return null;
+  }
+
   const handleLogout = () => {
     logout();
+    setIsMobileMenuOpen(false); 
+  };
+
+  const handleLinkClick = () => {
+    setIsMobileMenuOpen(false);
   };
 
   return (
+    // 使用 sticky 定位,并设置高 z-index
     <nav className="bg-white border-b shadow-sm sticky top-0 z-50">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
-        <div className="flex justify-between h-16">
+        <div className="flex justify-between h-16 relative z-50 bg-white">
+          
           {/* Logo */}
           <div className="flex items-center cursor-pointer" onClick={() => router.push('/')}>
             <Plane className="text-blue-600 mr-2" />
             <span className="text-2xl font-bold text-blue-600">Visafly</span>
           </div>
 
-          {/* Navigation Links & Actions */}
-          <div className="flex items-center space-x-4 sm:space-x-6">
-            
-            <Link 
-              href="/services" 
-              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
-            >
+          {/* Desktop Nav */}
+          <div className="hidden md:flex items-center space-x-6">
+            <Link href="/services" className="nav-link flex items-center gap-1">
               <Briefcase size={18} /> {t('nav.services') || '服务列表'}
             </Link>
-
-            <Link 
-              href="/slots" 
-              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
-            >
+            <Link href="/slots" className="nav-link flex items-center gap-1">
               <CalendarSearch size={18} /> {t('nav.slots') || '名额查询'}
             </Link>
-
-            <Link 
-              href="/knowledge" 
-              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
-            >
+            <Link href="/knowledge" className="nav-link flex items-center gap-1">
               <BookOpen size={18} /> {t('nav.guide') || '办理指南'}
             </Link>
+          </div>
 
-            {/* 分割线 & 语言切换器 */}
-            <div className="h-6 w-px bg-slate-200 mx-2"></div>
+          {/* Desktop Actions */}
+          <div className="hidden md:flex items-center gap-4">
+            <div className="h-6 w-px bg-slate-200"></div>
             <LanguageSwitcher />
-            
             {isLogged ? (
-              <div className="flex items-center space-x-4 ml-2 pl-2 sm:pl-4 sm:border-l border-gray-200">
-                
-                {/* 管理员入口 */}
+              <div className="flex items-center gap-4 ml-2">
                 {showAdmin && (
                   <Link 
                     href="/admin" 
@@ -82,27 +84,104 @@ export default function Navbar() {
                     <ShieldCheck size={16} className="mr-1 text-blue-600" /> {t('nav.admin') || '管理后台'}
                   </Link>
                 )}
-
-                {/* 2. 修改控制台链接:添加图标 */}
-                <Link 
-                  href="/dashboard" 
-                  className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
-                >
+                <Link href="/dashboard" className="nav-link flex items-center gap-1">
                   <LayoutDashboard size={18} /> {t('nav.dashboard')}
                 </Link>
-
-                <button onClick={handleLogout} className="text-red-500 hover:text-red-700 font-medium text-sm">
-                  {t('nav.logout')}
+                <button onClick={handleLogout} className="text-red-500 hover:text-red-700 font-medium text-sm flex items-center gap-1">
+                  <LogOut size={16} /> {t('nav.logout')}
                 </button>
               </div>
             ) : (
-              <Link href="/login" className="bg-blue-600 text-white px-5 py-2 rounded-lg hover:bg-blue-700 transition font-medium shadow-sm ml-2 text-sm sm:text-base">
+              <Link href="/login" className="bg-blue-600 text-white px-5 py-2 rounded-lg hover:bg-blue-700 transition font-medium shadow-sm ml-2">
                 {t('nav.login') || '登录 / 注册'}
               </Link>
             )}
           </div>
+
+          {/* Mobile Menu Button */}
+          <div className="flex md:hidden items-center gap-3">
+             <LanguageSwitcher />
+             <button
+              onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
+              className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition"
+            >
+              {isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
+            </button>
+          </div>
         </div>
       </div>
+
+      {/* === Mobile Menu & Overlay === */}
+      {isMobileMenuOpen && (
+        <>
+          {/* 1. 遮罩层 (全屏半透明背景,点击关闭) */}
+          <div 
+            className="fixed inset-0 top-16 bg-black/50 z-40 md:hidden backdrop-blur-sm animate-in fade-in duration-200"
+            onClick={() => setIsMobileMenuOpen(false)}
+          ></div>
+
+          {/* 2. 下拉菜单 (绝对定位,位于 Navbar 下方) */}
+          <div className="md:hidden bg-white border-t border-slate-100 absolute w-full left-0 z-50 shadow-xl animate-in slide-in-from-top-2 duration-200 rounded-b-xl overflow-hidden">
+            <div className="px-4 pt-2 pb-6 space-y-2">
+              <div className="space-y-1 py-2 border-b border-slate-100">
+                <MobileLink href="/services" icon={Briefcase} onClick={handleLinkClick}>
+                  {t('nav.services') || '服务列表'}
+                </MobileLink>
+                <MobileLink href="/slots" icon={CalendarSearch} onClick={handleLinkClick}>
+                  {t('nav.slots') || '名额查询'}
+                </MobileLink>
+                <MobileLink href="/knowledge" icon={BookOpen} onClick={handleLinkClick}>
+                  {t('nav.guide') || '办理指南'}
+                </MobileLink>
+              </div>
+              <div className="pt-2 space-y-2">
+                {isLogged ? (
+                  <>
+                    {showAdmin && (
+                      <MobileLink href="/admin" icon={ShieldCheck} onClick={handleLinkClick} className="text-purple-600 bg-purple-50 border border-purple-100">
+                        {t('nav.admin') || '管理后台'}
+                      </MobileLink>
+                    )}
+                    <MobileLink href="/dashboard" icon={LayoutDashboard} onClick={handleLinkClick}>
+                      {t('nav.dashboard')}
+                    </MobileLink>
+                    <button 
+                      onClick={handleLogout}
+                      className="w-full flex items-center gap-3 px-3 py-3 rounded-lg text-base font-medium text-red-600 hover:bg-red-50 transition"
+                    >
+                      <LogOut size={20} /> {t('nav.logout')}
+                    </button>
+                  </>
+                ) : (
+                  <div className="pt-2">
+                    <Link 
+                      href="/login" 
+                      onClick={handleLinkClick}
+                      className="flex w-full justify-center items-center gap-2 bg-blue-600 text-white px-5 py-3.5 rounded-xl font-bold shadow-md active:scale-95 transition"
+                    >
+                      <UserCircle size={20} /> {t('nav.login') || '登录 / 注册'}
+                    </Link>
+                  </div>
+                )}
+              </div>
+            </div>
+          </div>
+        </>
+      )}
     </nav>
   );
+}
+
+// 辅助组件:移动端链接项
+function MobileLink({ href, icon: Icon, children, onClick, className = '' }: any) {
+  return (
+    <Link 
+      href={href} 
+      onClick={onClick}
+      className={`flex items-center gap-3 px-3 py-3.5 rounded-lg text-base font-medium text-slate-700 hover:text-slate-900 hover:bg-slate-50 transition active:bg-slate-100 ${className}`}
+    >
+      <Icon size={20} className="text-slate-500" />
+      {children}
+    </Link>
+  );
 }

+ 96 - 100
src/components/PaymentProcessor.tsx

@@ -3,64 +3,35 @@
 import { useEffect, useState } from 'react';
 import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
-import { Loader2, ArrowLeft, Sparkles, ExternalLink, Clock, ArrowRightLeft } from 'lucide-react';
+import { Loader2, ArrowLeft, Sparkles, ExternalLink, Clock, ArrowRightLeft, CheckCircle2, AlertCircle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-// 1. 引入 LocalTime
 import LocalTime from '@/components/common/LocalTime';
 
-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;
-  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; 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; }
 
 export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
   const router = useRouter();
-  const { t } = useLanguage(); // 不需要 lang 了,LocalTime 内部处理
+  const { t, lang } = useLanguage();
 
   const [step, setStep] = useState<1 | 2>(1);
-  const [loading, setLoading] = useState<boolean>(false);
+  const [loading, setLoading] = useState<boolean>(false); 
+  const [confirming, setConfirming] = useState<boolean>(false); 
   
   const [providers, setProviders] = useState<PaymentProvider[]>([]);
   const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
   const [qrCode, setQrCode] = useState<string>('');
-  
-  // 2. 移除了 expireTimeDisplay 状态和相关的 useEffect
-  // 因为 LocalTime 组件会处理这部分逻辑
 
-  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); }
   };
 
   const handlePay = async (providerName: string) => {
@@ -71,51 +42,49 @@ 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
-      });
-      
+      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); 
 
       if (data.channel === 'online_link') {
-        if (data.payment_url) {
-          setStep(2);
-        } else {
-          alert(t('payment.link_gen_failed'));
-        }
-      } 
-      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 alert(t('payment.link_gen_failed'));
+      } 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 {
-          alert(t('payment.qr_gen_failed'));
-        }
+        if (qrUrl) { setQrCode(qrUrl); setStep(2); } 
+        else alert(t('payment.qr_gen_failed'));
       } else {
         alert(`${t('payment.unsupported_channel')}: ${data.channel}`);
       }
-
     } catch (error: any) {
-      console.error(error);
       const errorMsg = error.response?.data?.message || error.response?.data?.detail || "";
-      if (errorMsg.includes("active payment")) {
-        alert(t('payment.active_payment_exists'));
-      } else {
-        alert(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')));
-      }
-    } finally {
-      setLoading(false);
-    }
+      if (errorMsg.includes("active payment")) alert(t('payment.active_payment_exists'));
+      else alert(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')));
+    } finally { setLoading(false); }
+  };
+
+  const handleUserConfirm = async () => {
+    if (!paymentData) return;
+    setConfirming(true);
+    try {
+      const payload = {
+        payment_id: paymentData.id,
+        amount: paymentData.amount,
+        currency: paymentData.currency,
+        random_offset: paymentData.random_offset,
+        confirmed_at: new Date().toISOString()
+      };
+      await api.post('/api/vas/payment/confirm_by_user', payload);
+      alert(t('payment.confirm_success_alert')); 
+      router.push('/dashboard');
+    } catch (error: any) {
+      const msg = error.response?.data?.message || t('common.unknown_error');
+      alert(`${t('payment.confirm_failed')}: ${msg}`);
+    } finally { setConfirming(false); }
   };
 
   const handleBack = () => {
@@ -124,40 +93,47 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
     setQrCode('');
   };
 
-  const formatMoney = (amount: number, currency: string) => {
-    return `${(amount / 100).toFixed(2)} ${currency}`;
+  const formatMoney = (amount: number, currency: string) => `${(amount / 100).toFixed(2)} ${currency}`;
+
+  // 动态时间格式化
+  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' });
   };
 
   return (
-    <div className="bg-white p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
+    // 调整 1: 响应式 Padding
+    <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: Select Method */}
+      {/* === Step 1: Select Method === */}
       {step === 1 && (
         <>
-          <h2 className="text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
-          <p className="text-gray-500 mb-8 text-sm">{t('payment.order_id')}: <span className="font-mono font-bold text-gray-700">{orderId}</span></p>
+          <h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
+          <p className="text-gray-500 mb-6 md:mb-8 text-sm break-all">{t('payment.order_id')}: <span className="font-mono font-bold text-gray-700">{orderId}</span></p>
 
           <h3 className="text-left font-semibold mb-4 text-gray-800">{t('payment.select_method')}</h3>
           
           {providers.length === 0 ? (
              <div className="text-gray-400 py-4 text-sm">{t('common.loading')}</div>
           ) : (
-            <div className="grid grid-cols-2 gap-4">
+            // 调整 2: 移动端单列,桌面端双列
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
               {providers.map((p, idx) => (
                 <button
                   key={idx}
                   onClick={() => handlePay(p.name)}
                   disabled={loading}
-                  className="group flex flex-col items-center justify-center p-4 py-6 border rounded-xl hover:bg-blue-50 hover:border-blue-200 transition bg-white h-auto min-h-[140px]"
+                  className="group flex flex-col items-center justify-center p-4 py-5 md:py-6 border rounded-xl hover:bg-blue-50 hover:border-blue-200 transition bg-white h-auto active:scale-95 shadow-sm"
                 >
                   {p.icon ? (
                     <img 
                       src={p.icon} 
                       alt={p.name} 
-                      className="h-16 w-full object-contain mb-3 group-hover:scale-105 transition-transform" 
+                      className="h-12 w-full object-contain mb-2 md:h-16 md:mb-3 group-hover:scale-105 transition-transform" 
                     />
                   ) : (
-                    <div className="h-16 w-16 bg-gray-100 rounded-full mb-3 flex items-center justify-center text-gray-400 text-xs font-bold">
+                    <div className="h-12 w-12 md:h-16 md:w-16 bg-gray-100 rounded-full mb-2 md:mb-3 flex items-center justify-center text-gray-400 text-xs font-bold">
                       {p.name.substring(0, 3)}
                     </div>
                   )}
@@ -171,19 +147,20 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
         </>
       )}
 
-      {/* Step 2: Payment Detail */}
+      {/* === Step 2: Payment Detail & Confirm === */}
       {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}
-              className="text-gray-400 hover:text-gray-600 flex items-center text-sm transition"
+              className="text-gray-400 hover:text-gray-600 flex items-center text-sm transition active:scale-95 py-2 pr-2"
             >
               <ArrowLeft className="w-4 h-4 mr-1" /> {t('payment.reselect')}
             </button>
-            <div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
+            <div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded whitespace-nowrap">
               <Clock className="w-3 h-3 mr-1" />
-              {/* 3. 使用 LocalTime 组件,并通过 options 仅显示时间 (HH:mm) */}
               <LocalTime 
                 date={paymentData.expire_at} 
                 options={{ hour: '2-digit', minute: '2-digit' }} 
@@ -192,11 +169,10 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
             </div>
           </div>
 
-          <h2 className="text-xl font-bold mb-4 text-gray-900 text-center">{t('payment.confirm_info')}</h2>
+          <h2 className="text-lg md:text-xl font-bold mb-4 text-gray-900 text-center">{t('payment.confirm_info')}</h2>
 
           {/* Amount Card */}
-          <div className="bg-slate-50 rounded-xl p-5 mb-6 border border-slate-100 space-y-3">
-            
+          <div className="bg-slate-50 rounded-xl p-4 md:p-5 mb-6 border border-slate-100 space-y-3">
             <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>
@@ -215,7 +191,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
                   <Sparkles size={14} className="fill-red-500" /> {t('payment.random_discount')}
                 </span>
                 <span>
-                  {paymentData.random_offset > 0 ? '-' : ''} 
+                  {paymentData.random_offset > 0 ? '+' : ''} 
                   {formatMoney(paymentData.random_offset, paymentData.currency)}
                 </span>
               </div>
@@ -225,7 +201,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
             <div className="flex justify-between items-end">
               <span className="text-gray-600 font-medium pb-1">{t('payment.actual_pay')}</span>
-              <span className="text-3xl font-bold text-blue-600">
+              <span className="text-2xl md:text-3xl font-bold text-blue-600">
                 {formatMoney(paymentData.amount, paymentData.currency)}
               </span>
             </div>
@@ -233,27 +209,32 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
           {/* Action Area */}
           <div className="text-center">
-            {paymentData.channel === 'online_link' ? (
-              <div className="space-y-4">
+            
+            {/* Link Payment */}
+            {paymentData.channel === 'online_link' && (
+              <div className="space-y-4 mb-8">
                 <p className="text-sm text-gray-500">{t('payment.link_pay_hint')}</p>
                 <a 
                   href={paymentData.payment_url} 
                   target="_blank"
                   rel="noopener noreferrer"
-                  className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 font-bold transition shadow-lg shadow-blue-200"
+                  className="w-full flex items-center justify-center gap-2 px-6 py-3.5 bg-blue-600 text-white rounded-xl hover:bg-blue-700 font-bold transition shadow-lg shadow-blue-200 active:scale-95"
                 >
                   {t('payment.go_to_pay')} <ExternalLink size={18} />
                 </a>
               </div>
-            ) : (
-              <div className="space-y-4">
+            )}
+
+            {/* QR Payment */}
+            {paymentData.channel === 'qr_static' && (
+              <div className="space-y-4 mb-8">
                 <p className="text-sm text-gray-500">{t('payment.qr_pay_hint')}</p>
-                <div className="bg-white p-4 inline-block rounded-xl border shadow-sm ring-4 ring-slate-50">
+                <div className="bg-white p-4 inline-block rounded-xl border shadow-sm ring-4 ring-slate-50 max-w-full">
                   {qrCode ? (
                     <img 
                       src={qrCode.startsWith('http') || qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`} 
                       alt="Payment QR" 
-                      className="w-48 h-48 object-contain bg-white rounded-lg" 
+                      className="w-48 h-48 md:w-52 md:h-52 object-contain bg-white rounded-lg" 
                     />
                   ) : (
                     <div className="w-48 h-48 flex items-center justify-center text-gray-400">{t('common.loading')}</div>
@@ -262,14 +243,29 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
               </div>
             )}
 
-            <div className="mt-8">
+            {/* Confirm Area */}
+            <div className="mt-8 pt-6 border-t border-slate-100">
+              <div className="flex items-start gap-2 bg-amber-50 p-3 rounded-lg text-left mb-4 border border-amber-100">
+                <AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
+                <p className="text-xs text-amber-800 leading-relaxed">
+                  {t('payment.confirm_hint_text')}
+                </p>
+              </div>
+
               <button 
-                onClick={() => router.push('/dashboard')} 
-                className="text-blue-600 hover:text-blue-800 hover:underline font-medium text-sm transition"
+                onClick={handleUserConfirm} 
+                disabled={confirming} 
+                className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3.5 rounded-xl transition shadow-lg shadow-green-200 flex items-center justify-center gap-2 active:scale-[0.98]"
               >
+                {confirming ? <Loader2 size={20} className="animate-spin" /> : <CheckCircle2 size={20} />}
                 {t('payment.completed_btn')}
               </button>
+              
+              <p className="text-xs text-gray-400 mt-3">
+                {t('payment.confirm_subtext')}
+              </p>
             </div>
+
           </div>
         </div>
       )}

+ 18 - 19
src/components/ServiceList.tsx

@@ -5,7 +5,6 @@ import { useRouter } from 'next/navigation';
 import api from '@/lib/api';
 import { Loader2, Search, MapPin, Filter, X, Globe } from 'lucide-react';
 import Pagination from '@/components/common/Pagination';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface Product {
@@ -22,7 +21,6 @@ interface Product {
 
 export default function ServiceList() {
   const router = useRouter();
-  // 2. 获取翻译函数
   const { t } = useLanguage();
   
   const [products, setProducts] = useState<Product[]>([]);
@@ -37,7 +35,6 @@ export default function ServiceList() {
   const [pageSize] = useState(9);
   const [total, setTotal] = useState(0);
 
-  // 选项配置
   const countries = ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'];
   const visaTypes = ['Tourist','Business','Family','Student','Work','Transit','e-Visa'];
 
@@ -86,7 +83,6 @@ export default function ServiceList() {
 
     } catch (err) {
       console.warn("API Error, using mock data");
-      // Mock Data Fallback
       setProducts([]);
       setTotal(0);
     } finally {
@@ -94,9 +90,7 @@ export default function ServiceList() {
     }
   };
 
-  const handleSearch = () => {
-    fetchProducts(1);
-  };
+  const handleSearch = () => fetchProducts(1);
 
   const handleReset = () => {
     setKeyword('');
@@ -112,56 +106,61 @@ export default function ServiceList() {
   return (
     <div className="space-y-8">
       
-      {/* 筛选工具栏 */}
+      {/* === 筛选工具栏:响应式调整 === */}
       <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
         <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
+          
+          {/* 搜索框 */}
           <div className="relative md:col-span-1">
             <input 
               type="text" 
               placeholder={t('services.search_placeholder')} 
-              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+              className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               onChange={(e) => setKeyword(e.target.value)}
               onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
             />
-            <Search size={18} className="absolute left-3 top-3 text-slate-400" />
+            <Search size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
           </div>
 
+          {/* 国家筛选 */}
           <div className="relative md:col-span-1">
             <select 
-              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
+              className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
               value={selectedCountry}
               onChange={(e) => setSelectedCountry(e.target.value)}
             >
               <option value="">{t('services.all_countries')}</option>
               {countries.map(c => <option key={c} value={c}>{c}</option>)}
             </select>
-            <Globe size={18} className="absolute left-3 top-3 text-slate-400" />
+            <Globe size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
           </div>
 
+          {/* 类型筛选 */}
           <div className="relative md:col-span-1">
             <select 
-              className="w-full pl-10 pr-4 py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
+              className="w-full pl-10 pr-4 py-3 md:py-2.5 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none appearance-none bg-white text-slate-700"
               value={selectedType}
               onChange={(e) => setSelectedType(e.target.value)}
             >
               <option value="">{t('services.all_types')}</option>
               {visaTypes.map(t => <option key={t} value={t}>{t}</option>)}
             </select>
-            <Filter size={18} className="absolute left-3 top-3 text-slate-400" />
+            <Filter size={18} className="absolute left-3 top-3.5 md:top-3 text-slate-400" />
           </div>
 
+          {/* 按钮组 */}
           <div className="flex gap-2 md:col-span-1">
             <button 
               onClick={handleSearch}
-              className="flex-1 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition shadow-sm flex items-center justify-center gap-2"
+              className="flex-1 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition shadow-sm flex items-center justify-center gap-2 py-3 md:py-2.5 active:scale-95"
             >
               <Search size={16} /> {t('common.search')}
             </button>
             {(keyword || selectedCountry || selectedType) && (
               <button 
                 onClick={handleReset}
-                className="px-3 border border-slate-300 text-slate-500 rounded-lg hover:bg-slate-50 hover:text-red-500 transition"
+                className="px-4 border border-slate-300 text-slate-500 rounded-lg hover:bg-slate-50 hover:text-red-500 transition py-3 md:py-2.5 active:scale-95"
                 title={t('services.reset_filter')}
               >
                 <X size={18} />
@@ -171,7 +170,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" />
@@ -187,11 +186,11 @@ export default function ServiceList() {
           <p className="text-slate-500 text-sm">{t('services.no_result_desc')}</p>
         </div>
       ) : (
-        <div className="grid md:grid-cols-3 gap-6">
+        // 修改:移动端 grid-cols-1,平板 grid-cols-2,桌面 grid-cols-3
+        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
           {products.map((item) => (
             <div key={item.id} className="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:border-blue-300 hover:shadow-md transition flex flex-col group h-full">
               
-              {/* 标签行 */}
               <div className="flex justify-between items-start mb-4">
                 <div className="flex flex-wrap gap-2">
                   <div className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded text-xs font-bold flex items-center gap-1.5 border border-blue-100">

+ 116 - 98
src/components/admin/AdminSidebar.tsx

@@ -4,20 +4,10 @@ import { useState, useEffect } from 'react';
 import { usePathname, useRouter } from 'next/navigation';
 import Link from 'next/link';
 import { 
-  LayoutDashboard, 
-  ShoppingBag, 
-  LifeBuoy, 
-  Settings, 
-  LogOut, 
-  Activity,
-  CreditCard,
-  Users,
-  ChevronLeft,
-  ChevronRight,
-  Plane,
-  LucideIcon,
-  CalendarClock,
-  LayoutGrid
+  LayoutDashboard, ShoppingBag, LifeBuoy, Settings, LogOut, 
+  Activity, CreditCard, Users, ChevronLeft, ChevronRight, 
+  Plane, LucideIcon, CalendarClock, LayoutGrid, CheckCheck,
+  Menu, X     
 } from 'lucide-react';
 
 interface MenuItem {
@@ -30,32 +20,33 @@ export default function AdminSidebar() {
   const pathname = usePathname();
   const router = useRouter();
   
-  // 控制折叠状态
   const [isCollapsed, setIsCollapsed] = useState(false);
-  const [mounted, setMounted] = useState(false); // 用于解决 Hydration 不匹配问题
+  const [isMobileOpen, setIsMobileOpen] = useState(false);
+  const [mounted, setMounted] = useState(false);
 
   const menu: MenuItem[] = [
     { name: '概览', href: '/admin', icon: LayoutDashboard },
     { name: '用户管理', href: '/admin/users', icon: Users },
     { name: '工单处理', href: '/admin/tickets', icon: LifeBuoy },
     { name: '订单管理', href: '/admin/orders', icon: ShoppingBag },
+    { name: '支付确认', href: '/admin/payment-confirmations', icon: CheckCheck },
     { name: '支付配置', href: '/admin/payments', icon: CreditCard },
     { name: '商品配置', href: '/admin/products', icon: Settings },
     { name: '系统任务', href: '/admin/tasks', icon: Activity },
-    { name: 'TROOV Slot监控', href: '/admin/slots', icon: CalendarClock }, // 新增
+    { name: 'TROOV Slot监控', href: '/admin/slots', icon: CalendarClock },
     { name: '卡片管理', href: '/admin/cards', icon: LayoutGrid }, 
   ];
 
-  // 初始化:从 localStorage 读取状态
   useEffect(() => {
     setMounted(true);
     const savedState = localStorage.getItem('sidebar_collapsed');
-    if (savedState) {
-      setIsCollapsed(JSON.parse(savedState));
-    }
+    if (savedState) setIsCollapsed(JSON.parse(savedState));
   }, []);
 
-  // 切换并保存状态
+  useEffect(() => {
+    if (isMobileOpen) setIsMobileOpen(false);
+  }, [pathname]);
+
   const toggleSidebar = () => {
     const newState = !isCollapsed;
     setIsCollapsed(newState);
@@ -70,91 +61,118 @@ export default function AdminSidebar() {
     }
   };
 
-  // 防止服务端渲染和客户端渲染不一致导致的闪烁
   if (!mounted) return null;
 
   return (
-    <aside 
-      className={`bg-slate-900 text-white flex-shrink-0 hidden md:flex flex-col h-screen sticky top-0 transition-all duration-300 ease-in-out
-        ${isCollapsed ? 'w-20' : 'w-64'}
-      `}
-    >
-      {/* Header / Logo Area */}
-      <div className={`h-16 flex items-center border-b border-slate-800 transition-all duration-300 ${isCollapsed ? 'justify-center px-0' : 'px-6 gap-3'}`}>
-        <Plane className="text-blue-500 flex-shrink-0" size={24} />
-        
-        {/* 文字部分:折叠时隐藏 */}
-        <div className={`font-bold text-xl whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
-          <span className="text-blue-400">Visafly</span> Admin
+    <>
+      {/* === 1. 移动端顶部 Header (Fixed) === */}
+      <div className="md:hidden fixed top-0 left-0 right-0 h-16 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-4 z-40 shadow-md">
+        <div className="flex items-center gap-2 font-bold text-xl text-white">
+          <Plane className="text-blue-500" size={24} />
+          <span>Visafly Admin</span>
         </div>
+        <button 
+          onClick={() => setIsMobileOpen(!isMobileOpen)}
+          className="text-slate-300 hover:text-white p-2 rounded-md active:bg-slate-800 transition"
+        >
+          {isMobileOpen ? <X size={24} /> : <Menu size={24} />}
+        </button>
       </div>
-      
-      {/* Navigation */}
-      <nav className="flex-1 p-3 space-y-1 overflow-y-auto overflow-x-hidden">
-        {menu.map((item) => {
-          const Icon = item.icon;
-          const isActive = pathname === item.href; // 精确匹配
-          // 如果有子路由(比如 /admin/orders/new),可以使用 pathname.startsWith(item.href)
 
-          return (
-            <Link 
-              key={item.href}
-              href={item.href}
-              title={isCollapsed ? item.name : ''} // 折叠时显示 tooltip
-              className={`flex items-center rounded-lg transition-all duration-200 group relative
-                ${isCollapsed ? 'justify-center px-2 py-3' : 'px-4 py-3 gap-3'}
-                ${isActive 
-                  ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' 
-                  : 'text-slate-400 hover:bg-slate-800 hover:text-white'
-                }
-              `}
-            >
-              <Icon size={20} className={`flex-shrink-0 transition-transform duration-200 ${!isCollapsed && isActive ? 'scale-110' : ''}`} />
-              
-              {/* 菜单文字 */}
-              <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0 hidden' : 'w-auto opacity-100 font-medium'}`}>
-                {item.name}
-              </span>
+      {/* === 2. 遮罩层 === */}
+      {isMobileOpen && (
+        <div 
+          className="fixed inset-0 bg-black/50 z-40 md:hidden backdrop-blur-sm"
+          onClick={() => setIsMobileOpen(false)}
+        />
+      )}
 
-              {/* 折叠时的悬浮提示 (可选) */}
-              {isCollapsed && (
-                <div className="absolute left-full ml-2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50 whitespace-nowrap">
-                  {item.name}
-                </div>
-              )}
-            </Link>
-          );
-        })}
-      </nav>
+      {/* === 3. 侧边栏主体 === */}
+      <aside 
+        className={`
+          bg-slate-900 text-white flex flex-col h-screen transition-all duration-300 ease-in-out
+          
+          /* 移动端: 固定定位,侧滑效果,Top=0 (覆盖 Header) */
+          fixed top-0 left-0 z-50 
+          w-64
+          ${isMobileOpen ? 'translate-x-0 shadow-2xl' : '-translate-x-full'}
+
+          /* 桌面端: 恢复 Sticky */
+          md:translate-x-0 md:sticky md:top-0 md:shadow-none
+          ${isCollapsed ? 'md:w-20' : 'md:w-64'}
+        `}
+      >
+        {/* Logo (仅桌面端显示,移动端用 Header) */}
+        <div className={`h-16 items-center border-b border-slate-800 transition-all duration-300 hidden md:flex flex-shrink-0 ${isCollapsed ? 'justify-center px-0' : 'px-6 gap-3'}`}>
+          <Plane className="text-blue-500 flex-shrink-0" size={24} />
+          <div className={`font-bold text-xl whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
+            <span className="text-blue-400">Visafly</span> Admin
+          </div>
+        </div>
 
-      {/* Footer Actions */}
-      <div className="p-3 border-t border-slate-800 space-y-1">
+        {/* 移动端菜单内标题 (可选,增加层次感) */}
+        <div className="flex md:hidden h-16 items-center justify-between px-6 border-b border-slate-800 flex-shrink-0 bg-slate-900">
+           <span className="text-lg font-bold text-slate-100">管理菜单</span>
+           <button onClick={() => setIsMobileOpen(false)} className="text-slate-400"><X size={20}/></button>
+        </div>
         
-        {/* 折叠切换按钮 */}
-        <button
-          onClick={toggleSidebar}
-          className="flex items-center w-full rounded-lg text-slate-500 hover:bg-slate-800 hover:text-white transition-colors
-            justify-center py-2
-          "
-          title={isCollapsed ? "展开侧边栏" : "折叠侧边栏"}
-        >
-          {isCollapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
-        </button>
+        {/* 菜单列表 */}
+        <nav className="flex-1 p-3 space-y-1 overflow-y-auto overflow-x-hidden">
+          {menu.map((item) => {
+            const Icon = item.icon;
+            const isActive = pathname === item.href;
+            return (
+              <Link 
+                key={item.href}
+                href={item.href}
+                title={isCollapsed ? item.name : ''}
+                className={`
+                  flex items-center rounded-lg transition-all duration-200 group relative
+                  px-4 py-3 gap-3
+                  ${isCollapsed ? 'md:justify-center md:px-2 md:py-3' : 'md:px-4 md:py-3 md:gap-3'}
+                  ${isActive 
+                    ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' 
+                    : 'text-slate-400 hover:bg-slate-800 hover:text-white'
+                  }
+                `}
+              >
+                <Icon size={20} className={`flex-shrink-0 transition-transform duration-200 ${!isCollapsed && isActive ? 'scale-110' : ''}`} />
+                <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 block ${isCollapsed ? 'md:w-0 md:opacity-0 md:hidden' : 'w-auto opacity-100'}`}>
+                  {item.name}
+                </span>
+                {isCollapsed && (
+                  <div className="hidden md:block absolute left-full ml-2 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50 whitespace-nowrap shadow-lg">
+                    {item.name}
+                  </div>
+                )}
+              </Link>
+            );
+          })}
+        </nav>
 
-        {/* 退出按钮 */}
-        <button 
-          onClick={handleLogout}
-          title={isCollapsed ? "退出登录" : ""}
-          className={`flex items-center w-full rounded-lg text-slate-400 hover:text-red-400 hover:bg-slate-800 transition-colors
-            ${isCollapsed ? 'justify-center py-3' : 'px-4 py-3 gap-3'}
-          `}
-        >
-          <LogOut size={20} className="flex-shrink-0" />
-          <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0 hidden' : 'w-auto opacity-100 font-medium'}`}>
-            退出登录
-          </span>
-        </button>
-      </div>
-    </aside>
+        {/* Footer Actions */}
+        <div className="p-3 border-t border-slate-800 space-y-1 flex-shrink-0">
+          <button
+            onClick={toggleSidebar}
+            className="hidden md:flex items-center w-full rounded-lg text-slate-500 hover:bg-slate-800 hover:text-white transition-colors justify-center py-2"
+          >
+            {isCollapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
+          </button>
+          <button 
+            onClick={handleLogout}
+            className={`
+              flex items-center w-full rounded-lg text-slate-400 hover:text-red-400 hover:bg-slate-800 transition-colors
+              px-4 py-3 gap-3
+              ${isCollapsed ? 'md:justify-center md:py-3' : 'md:px-4 md:py-3 md:gap-3'}
+            `}
+          >
+            <LogOut size={20} className="flex-shrink-0" />
+            <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 block ${isCollapsed ? 'md:w-0 md:opacity-0 md:hidden' : 'w-auto opacity-100'}`}>
+              退出登录
+            </span>
+          </button>
+        </div>
+      </aside>
+    </>
   );
 }

+ 127 - 55
src/components/admin/cards/CardTable.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { Edit, Image as ImageIcon } from 'lucide-react';
+import { Edit, Image as ImageIcon, MapPin, Tag, Globe } from 'lucide-react';
 
 export interface KnowledgeCard {
   id: number;
@@ -33,63 +33,135 @@ export default function CardTable({ cards, loading, onEdit }: CardTableProps) {
   if (cards.length === 0) return <div className="p-12 text-center text-gray-500">暂无卡片数据</div>;
 
   return (
-    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
-      <table className="min-w-full divide-y divide-slate-200">
-        <thead className="bg-slate-50">
-          <tr>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Image</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Title / Content</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Info</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Language</th>
-            <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Action</th>
-          </tr>
-        </thead>
-        <tbody className="divide-y divide-slate-100">
-          {cards.map((card, idx) => {
-            const imgUrl = getImageUrl(card.image);
-            return (
-              <tr key={card.id || idx} className="hover:bg-slate-50">
-                <td className="px-6 py-4">
-                  <div className="w-16 h-12 bg-slate-100 rounded overflow-hidden flex items-center justify-center border border-slate-200">
-                    {imgUrl ? (
-                      <img src={imgUrl} alt="cover" className="w-full h-full object-cover" />
-                    ) : (
-                      <ImageIcon className="text-slate-300" size={20} />
-                    )}
-                  </div>
-                </td>
-                <td className="px-6 py-4">
-                  <div className="text-sm font-bold text-gray-900 line-clamp-1">{card.title}</div>
-                  <div className="text-xs text-gray-500 line-clamp-1 mt-1 max-w-[200px]">{card.content.replace(/<[^>]+>/g, '')}</div>
-                </td>
-                <td className="px-6 py-4">
-                  <div className="flex flex-col gap-1">
-                    <span className="text-xs font-medium bg-blue-50 text-blue-700 px-2 py-0.5 rounded w-fit">
-                      {card.country || 'No Country'}
-                    </span>
-                    <span className="text-xs text-gray-500">
-                      {card.label || '-'}
+    <div className="space-y-4">
+      
+      {/* =========================== */}
+      {/* 1. Desktop View (Table) - 中大屏幕显示 */}
+      {/* =========================== */}
+      <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Image</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Title / Content</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Info</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Language</th>
+              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Action</th>
+            </tr>
+          </thead>
+          <tbody className="divide-y divide-slate-100">
+            {cards.map((card, idx) => {
+              const imgUrl = getImageUrl(card.image);
+              return (
+                <tr key={card.id || idx} className="hover:bg-slate-50 transition-colors">
+                  <td className="px-6 py-4">
+                    <div className="w-16 h-12 bg-slate-100 rounded overflow-hidden flex items-center justify-center border border-slate-200">
+                      {imgUrl ? (
+                        <img src={imgUrl} alt="cover" className="w-full h-full object-cover" />
+                      ) : (
+                        <ImageIcon className="text-slate-300" size={20} />
+                      )}
+                    </div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <div className="text-sm font-bold text-gray-900 line-clamp-1 max-w-[200px]" title={card.title}>{card.title}</div>
+                    <div className="text-xs text-gray-500 line-clamp-1 mt-1 max-w-[200px]">{(card.content || '').replace(/<[^>]+>/g, '')}</div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <div className="flex flex-col gap-1 items-start">
+                      <span className="text-xs font-medium bg-blue-50 text-blue-700 px-2 py-0.5 rounded w-fit whitespace-nowrap">
+                        {card.country || 'No Country'}
+                      </span>
+                      <span className="text-xs text-gray-500">
+                        {card.label || '-'}
+                      </span>
+                    </div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <span className={`text-xs px-2 py-1 rounded font-medium ${card.culture === 'english' ? 'bg-purple-50 text-purple-700' : 'bg-red-50 text-red-700'}`}>
+                      {card.culture}
                     </span>
+                  </td>
+                  <td className="px-6 py-4 text-right">
+                    <button 
+                      onClick={() => onEdit(card)}
+                      className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
+                    >
+                      <Edit size={14} className="mr-1" /> Edit
+                    </button>
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+
+      {/* =========================== */}
+      {/* 2. Mobile View (Cards) - 小屏幕显示 */}
+      {/* =========================== */}
+      <div className="md:hidden space-y-4">
+        {cards.map((card, idx) => {
+          const imgUrl = getImageUrl(card.image);
+          const cleanContent = (card.content || '').replace(/<[^>]+>/g, '');
+
+          return (
+            <div key={card.id || idx} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+              
+              {/* Header: Image & Title */}
+              <div className="flex gap-4 mb-3">
+                <div className="w-20 h-20 bg-slate-100 rounded-lg overflow-hidden flex-shrink-0 border border-slate-200 flex items-center justify-center">
+                  {imgUrl ? (
+                    <img src={imgUrl} alt="cover" className="w-full h-full object-cover" />
+                  ) : (
+                    <ImageIcon className="text-slate-300" size={24} />
+                  )}
+                </div>
+                <div className="flex-1 min-w-0">
+                  <div className="flex justify-between items-start">
+                    <h3 className="text-sm font-bold text-slate-900 line-clamp-2 leading-snug mb-1">
+                      {card.title}
+                    </h3>
                   </div>
-                </td>
-                <td className="px-6 py-4">
-                  <span className={`text-xs px-2 py-1 rounded font-medium ${card.culture === 'english' ? 'bg-purple-50 text-purple-700' : 'bg-red-50 text-red-700'}`}>
-                    {card.culture}
+                  <p className="text-xs text-slate-500 line-clamp-2">
+                    {cleanContent || '暂无内容摘要'}
+                  </p>
+                </div>
+              </div>
+
+              {/* Meta Info Grid */}
+              <div className="flex flex-wrap gap-2 mb-4">
+                <span className="inline-flex items-center gap-1 text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded border border-blue-100">
+                  <MapPin size={10} /> {card.country || 'Global'}
+                </span>
+                
+                {card.label && (
+                  <span className="inline-flex items-center gap-1 text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded border border-slate-200">
+                    <Tag size={10} /> {card.label}
                   </span>
-                </td>
-                <td className="px-6 py-4 text-right">
-                  <button 
-                    onClick={() => onEdit(card)}
-                    className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm"
-                  >
-                    <Edit size={14} className="mr-1" /> Edit
-                  </button>
-                </td>
-              </tr>
-            );
-          })}
-        </tbody>
-      </table>
+                )}
+
+                <span className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded border ${
+                  card.culture === 'english' 
+                    ? 'bg-purple-50 text-purple-700 border-purple-100' 
+                    : 'bg-red-50 text-red-700 border-red-100'
+                }`}>
+                  <Globe size={10} /> {card.culture}
+                </span>
+              </div>
+
+              {/* Action Buttons */}
+              <button 
+                onClick={() => onEdit(card)}
+                className="w-full flex items-center justify-center gap-2 py-2.5 bg-slate-50 text-slate-700 rounded-lg text-sm font-medium border border-slate-200 active:scale-95 transition-transform hover:bg-slate-100"
+              >
+                <Edit size={16} /> 编辑卡片
+              </button>
+            </div>
+          );
+        })}
+      </div>
+
     </div>
   );
 }

+ 157 - 113
src/components/admin/orders/OrderTable.tsx

@@ -1,11 +1,11 @@
 'use client';
 
-import { Eye, XCircle, User, Box, Edit } from 'lucide-react';
+import { Eye, XCircle, User, Box, Edit, Clock, FileText } from 'lucide-react';
 import { OrderDetail } from './OrderDetailModal';
 // 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
-// 复用 OrderDetailModal 中的类型定义,确保一致性
+// 复用 OrderDetailModal 中的类型定义
 interface OrderTableProps {
   orders: OrderDetail[]; 
   loading: boolean;
@@ -35,129 +35,173 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
   // 状态颜色映射
   const getStatusColor = (status: string) => {
     switch (status) {
-      case 'paid': return 'bg-green-100 text-green-800';
-      case 'succeeded': return 'bg-green-100 text-green-800';
-      case 'pending': return 'bg-yellow-100 text-yellow-800';
-      case 'cancelled': return 'bg-red-100 text-red-800';
-      case 'failed': return 'bg-red-100 text-red-800';
-      default: return 'bg-gray-100 text-gray-800';
+      case 'paid': return 'bg-green-100 text-green-800 border-green-200';
+      case 'succeeded': return 'bg-green-100 text-green-800 border-green-200';
+      case 'pending': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
+      case 'cancelled': return 'bg-red-50 text-red-600 border-red-100'; // 红色更明显
+      case 'failed': return 'bg-red-100 text-red-800 border-red-200';
+      default: return 'bg-gray-100 text-gray-800 border-gray-200';
     }
   };
 
+  // 辅助函数:是否可以取消
+  const canCancel = (status: string) => status !== 'cancelled' && status !== 'completed' && status !== 'failed';
+
   return (
-    <div className="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">
-          <thead className="bg-slate-50">
-            <tr>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
-                订单号 / 创建时间
-              </th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
-                商品信息
-              </th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
-                用户信息
-              </th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
-                金额
-              </th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
-                状态
-              </th>
-              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
-                操作
-              </th>
-            </tr>
-          </thead>
-          <tbody className="bg-white divide-y divide-slate-200">
-            {orders.map((order) => (
-              <tr key={order.id} className="hover:bg-slate-50 transition-colors">
-                
-                {/* 1. 订单号 & 时间 */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="text-sm font-medium text-slate-900 font-mono">{order.id}</div>
-                  <div className="text-xs text-slate-400 mt-1">
-                    {/* 2. 使用 LocalTime 组件替代 new Date().toLocaleString() */}
+    <div className="space-y-4">
+      
+      {/* =========================== */}
+      {/* 1. Desktop View (Table) - 仅在中大屏幕显示 */}
+      {/* =========================== */}
+      <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">
+            <thead className="bg-slate-50">
+              <tr>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">订单号 / 创建时间</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">商品信息</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">用户信息</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">金额</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
+                <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
+              </tr>
+            </thead>
+            <tbody className="bg-white divide-y divide-slate-200">
+              {orders.map((order) => (
+                <tr key={order.id} className="hover:bg-slate-50 transition-colors">
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="text-sm font-medium text-slate-900 font-mono">{order.id}</div>
+                    <div className="text-xs text-slate-400 mt-1">
+                      <LocalTime date={order.created_at} />
+                    </div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <div className="flex items-center">
+                      <Box size={16} className="text-slate-400 mr-2 flex-shrink-0" />
+                      <span className="text-sm text-slate-700 truncate max-w-[150px]" title={order.product_title}>
+                        {order.product_name || order.product_title || '未知商品'}
+                      </span>
+                    </div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <div className="flex items-center">
+                      <User size={16} className="text-slate-400 mr-2 flex-shrink-0" />
+                      <div className="flex flex-col">
+                        <span className="text-sm text-slate-700 font-medium">{order.user_name || order.applicant_name || '未填写'}</span>
+                        <span className="text-xs text-slate-400">{order.user_email || order.user_id}</span>
+                      </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}
+                  </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">
+                      <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>
+                      )}
+                    </div>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      {/* =========================== */}
+      {/* 2. Mobile View (Cards) - 仅在小屏幕显示 */}
+      {/* =========================== */}
+      <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 */}
+            <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} />
+                <span className="text-xs font-mono font-bold">{order.id}</span>
+              </div>
+              <span className={`px-2 py-0.5 text-xs font-bold rounded border ${getStatusColor(order.status)}`}>
+                {order.status.toUpperCase()}
+              </span>
+            </div>
+
+            {/* Info Grid */}
+            <div className="space-y-3">
+              {/* 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} />
+                </div>
+                <div>
+                  <div className="text-sm font-bold text-slate-800 line-clamp-2">
+                    {order.product_name || order.product_title || '未知商品'}
+                  </div>
+                  <div className="text-xs text-slate-500 mt-1 flex items-center gap-1">
+                    <Clock size={12} />
                     <LocalTime date={order.created_at} />
                   </div>
-                </td>
+                </div>
+              </div>
 
-                {/* 2. 商品信息 */}
-                <td className="px-6 py-4">
-                  <div className="flex items-center">
-                    <Box size={16} className="text-slate-400 mr-2 flex-shrink-0" />
-                    <span className="text-sm text-slate-700 truncate max-w-[150px]" title={order.product_title}>
-                      {order.product_name || order.product_title || '未知商品'}
-                    </span>
+              {/* User & Price */}
+              <div className="bg-slate-50 rounded p-3 grid grid-cols-2 gap-2 text-sm border border-slate-100">
+                <div>
+                  <span className="text-xs text-slate-400 block mb-0.5">申请人</span>
+                  <div className="flex items-center gap-1.5 text-slate-700">
+                    <User size={12} />
+                    <span className="truncate font-medium">{order.user_name || order.applicant_name || '-'}</span>
                   </div>
-                </td>
-
-                {/* 3. 用户信息 */}
-                <td className="px-6 py-4">
-                  <div className="flex items-center">
-                    <User size={16} className="text-slate-400 mr-2 flex-shrink-0" />
-                    <div className="flex flex-col">
-                      <span className="text-sm text-slate-700 font-medium">
-                        {order.user_name || order.applicant_name || '未填写'}
-                      </span>
-                      <span className="text-xs text-slate-400">
-                        {order.user_email || order.user_id}
-                      </span>
-                    </div>
+                </div>
+                <div className="text-right">
+                  <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>
-                </td>
-
-                {/* 4. 金额 */}
-                <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}
-                </td>
+                </div>
+              </div>
+            </div>
 
-                {/* 5. 状态 */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(order.status)}`}>
-                    {order.status}
-                  </span>
-                </td>
+            {/* Actions */}
+            <div className="grid grid-cols-3 gap-2 mt-4 pt-3 border-t border-slate-100">
+              <button 
+                onClick={() => onEdit(order)}
+                className="flex items-center justify-center gap-1py-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="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>
 
-                {/* 6. 操作按钮 */}
-                <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
-                  <div className="flex justify-end gap-2">
-                    
-                    {/* 编辑按钮 */}
-                    <button 
-                      onClick={() => onEdit(order)}
-                      className="group flex items-center justify-center p-1.5 rounded-md text-indigo-600 hover:text-indigo-900 bg-indigo-50 hover:bg-indigo-100 transition border border-transparent hover:border-indigo-200"
-                      title="修改订单信息"
-                    >
-                      <Edit size={16} />
-                    </button>
+              {canCancel(order.status) ? (
+                <button 
+                  onClick={() => onCancel(order.id)}
+                  className="flex items-center justify-center gap-1 py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium active:scale-95 transition"
+                >
+                  <XCircle size={16} /> 取消
+                </button>
+              ) : (
+                <button disabled className="flex items-center justify-center gap-1 py-2 bg-gray-50 text-gray-400 rounded-lg text-sm font-medium cursor-not-allowed">
+                  <XCircle size={16} /> 取消
+                </button>
+              )}
+            </div>
 
-                    {/* 详情按钮 */}
-                    <button 
-                      onClick={() => onViewDetail(order)}
-                      className="group flex items-center justify-center p-1.5 rounded-md text-blue-600 hover:text-blue-900 bg-blue-50 hover:bg-blue-100 transition border border-transparent hover:border-blue-200"
-                      title="查看详情"
-                    >
-                      <Eye size={16} />
-                    </button>
-                    
-                    {/* 取消按钮 */}
-                    {order.status !== 'cancelled' && order.status !== 'completed' && (
-                      <button 
-                        onClick={() => onCancel(order.id)}
-                        className="group flex items-center justify-center p-1.5 rounded-md text-red-600 hover:text-red-900 bg-red-50 hover:bg-red-100 transition border border-transparent hover:border-red-200"
-                        title="取消订单"
-                      >
-                        <XCircle size={16} />
-                      </button>
-                    )}
-                  </div>
-                </td>
-              </tr>
-            ))}
-          </tbody>
-        </table>
+          </div>
+        ))}
       </div>
     </div>
   );

+ 270 - 0
src/components/admin/payments/ConfirmationDetailModal.tsx

@@ -0,0 +1,270 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { X, User, CreditCard, ShoppingBag, Loader2, CheckCircle, XCircle, ArrowUpRight } from 'lucide-react';
+import api from '@/lib/api';
+import LocalTime from '@/components/common/LocalTime';
+
+export interface PaymentConfirmation {
+    id: number;              // 确认单 ID
+    payment_id: number;      // 关联的支付单 ID
+    user_id: string;         // 用户 ID
+    status: string;          // pending, approved, rejected
+    confirmed_at: string;    // 用户点击确认的时间 (ISO String)
+    created_at: string;      // 记录创建时间 (ISO String)
+    admin_id?: string | null;
+    admin_confirmed_at?: string | null;
+    
+    // === 新增字段 (用于对账) ===
+    amount: number;          // 用户确认的金额 (分)
+    currency: string;        // 货币 (如 CNY)
+    random_offset: number;   // 随机立减金额 (分)
+}
+
+interface ConfirmationDetailModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  data: PaymentConfirmation | null;
+  onApprove: (item: PaymentConfirmation) => void;
+  onReject: (item: PaymentConfirmation) => void;
+}
+
+export default function ConfirmationDetailModal({ isOpen, onClose, data, onApprove, onReject }: ConfirmationDetailModalProps) {
+  const [loading, setLoading] = useState(false);
+  
+  // 详情数据状态
+  const [paymentInfo, setPaymentInfo] = useState<any>(null);
+  const [orderInfo, setOrderInfo] = useState<any>(null);
+  const [userInfo, setUserInfo] = useState<any>(null);
+
+  useEffect(() => {
+    if (isOpen && data) {
+      fetchDetails();
+    } else {
+      // 重置数据
+      setPaymentInfo(null);
+      setOrderInfo(null);
+      setUserInfo(null);
+    }
+  }, [isOpen, data]);
+
+  const fetchDetails = async () => {
+    if (!data) return;
+    setLoading(true);
+    try {
+      // 1. 获取用户信息 (已有 user_id)
+      const userRes = await api.get('/api/user/detail', { params: { user_id: data.user_id } });
+      setUserInfo(userRes.data.data);
+
+      // 2. 获取支付详情 (已有 payment_id)
+      // 假设 API: GET /api/vas/payment/detail?payment_id=...
+      const payRes = await api.get('/api/vas/payment/detail', { params: { payment_id: data.payment_id } });
+      const paymentData = payRes.data.data;
+      setPaymentInfo(paymentData);
+
+      // 3. 获取订单详情 (通过支付详情里的 order_id)
+      if (paymentData && paymentData.order_id) {
+        // 假设 API: GET /api/vas/order/detail?order_id=...
+        // 如果只有 list_all,可能需要 filter,这里假设有 detail 接口
+        const orderRes = await api.get('/api/vas/order/detail', { params: { order_id: paymentData.order_id } });
+        setOrderInfo(orderRes.data.data);
+      }
+
+    } catch (error) {
+      console.error("Failed to load details", error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen || !data) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden">
+        
+        {/* Header */}
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div>
+             <h3 className="font-bold text-gray-900 text-lg">支付确认详情</h3>
+             <p className="text-xs text-gray-500 mt-1">Confirmation ID: #{data.id}</p>
+          </div>
+          <button onClick={onClose} className="p-1 hover:bg-gray-200 rounded-full transition">
+            <X size={24} className="text-gray-500" />
+          </button>
+        </div>
+
+        {/* Content */}
+        <div className="flex-1 overflow-y-auto p-6 bg-slate-50/50">
+          {loading ? (
+            <div className="flex flex-col items-center justify-center py-20 text-slate-400">
+              <Loader2 className="w-10 h-10 animate-spin mb-2" />
+              <p>正在加载关联数据...</p>
+            </div>
+          ) : (
+            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+              
+              {/* 1. 用户提交的确认信息 */}
+              <div className="md:col-span-2 bg-white p-5 rounded-xl shadow-sm border border-blue-100">
+                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+                  <CheckCircle size={16} className="text-blue-600" /> 用户提交的确认信息
+                </h4>
+                <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
+                  <div>
+                    <span className="block text-xs text-slate-400">提交金额</span>
+                    <span className="font-bold text-slate-900 text-lg">
+                      {(data.amount / 100).toFixed(2)} {data.currency}
+                    </span>
+                  </div>
+                  <div>
+                    <span className="block text-xs text-slate-400">随机立减</span>
+                    <span className="font-medium text-red-500">
+                       -{(data.random_offset / 100).toFixed(2)}
+                    </span>
+                  </div>
+                  <div>
+                    <span className="block text-xs text-slate-400">用户点击时间</span>
+                    <LocalTime date={data.confirmed_at} className="font-medium text-slate-700"/>
+                  </div>
+                  <div>
+                    <span className="block text-xs text-slate-400">当前状态</span>
+                    <span className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${
+                      data.status === 'pending' ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-700'
+                    }`}>
+                      {data.status.toUpperCase()}
+                    </span>
+                  </div>
+                </div>
+              </div>
+
+              {/* 2. 支付单详情 (系统记录) */}
+              <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
+                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+                  <CreditCard size={16} className="text-slate-500" /> 关联支付单 (Payment)
+                </h4>
+                {paymentInfo ? (
+                  <div className="space-y-3 text-sm">
+                    <div className="flex justify-between border-b border-slate-50 pb-2">
+                      <span className="text-slate-500">Payment ID</span>
+                      <span className="font-mono">{paymentInfo.id}</span>
+                    </div>
+                    <div className="flex justify-between border-b border-slate-50 pb-2">
+                      <span className="text-slate-500">渠道 Provider</span>
+                      <span className="font-medium uppercase">{paymentInfo.provider}</span>
+                    </div>
+                    <div className="flex justify-between border-b border-slate-50 pb-2">
+                      <span className="text-slate-500">应付金额</span>
+                      <span className="font-bold">
+                        {(paymentInfo.amount / 100).toFixed(2)} {paymentInfo.currency}
+                      </span>
+                    </div>
+                    <div className="flex justify-between">
+                      <span className="text-slate-500">外部流水号</span>
+                      <span className="font-mono text-xs max-w-[150px] truncate" title={paymentInfo.external_trade_no}>
+                        {paymentInfo.external_trade_no || '-'}
+                      </span>
+                    </div>
+                  </div>
+                ) : (
+                  <p className="text-slate-400 text-xs">未找到支付记录</p>
+                )}
+              </div>
+
+              {/* 3. 订单详情 */}
+              <div className="bg-white p-5 rounded-xl shadow-sm border border-slate-200">
+                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+                  <ShoppingBag size={16} className="text-slate-500" /> 关联订单 (Order)
+                </h4>
+                {orderInfo ? (
+                  <div className="space-y-3 text-sm">
+                    <div className="flex justify-between border-b border-slate-50 pb-2">
+                      <span className="text-slate-500">Order ID</span>
+                      <span className="font-mono">{orderInfo.id}</span>
+                    </div>
+                    <div className="flex justify-between border-b border-slate-50 pb-2">
+                      <span className="text-slate-500">商品名称</span>
+                      <span className="font-medium text-right max-w-[180px] truncate" title={orderInfo.product_title}>
+                        {orderInfo.product_title}
+                      </span>
+                    </div>
+                    <div className="flex justify-between">
+                      <span className="text-slate-500">申请人</span>
+                      <span className="font-medium">
+                        {orderInfo.user_inputs?.first_name} {orderInfo.user_inputs?.last_name}
+                      </span>
+                    </div>
+                  </div>
+                ) : (
+                  <p className="text-slate-400 text-xs">未找到订单记录</p>
+                )}
+              </div>
+
+              {/* 4. 用户信息 */}
+              <div className="md:col-span-2 bg-white p-5 rounded-xl shadow-sm border border-slate-200">
+                <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
+                  <User size={16} className="text-slate-500" /> 下单用户 (User)
+                </h4>
+                {userInfo ? (
+                  <div className="flex items-center gap-6 text-sm">
+                    <div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden">
+                      {userInfo.avatar_url ? (
+                        <img src={userInfo.avatar_url} className="w-full h-full object-cover" />
+                      ) : (
+                        <User className="text-slate-400" />
+                      )}
+                    </div>
+                    <div className="grid grid-cols-1 sm:grid-cols-3 gap-x-12 gap-y-2 flex-1">
+                      <div>
+                        <span className="block text-xs text-slate-400">昵称</span>
+                        <span className="font-medium">{userInfo.nickname || '-'}</span>
+                      </div>
+                      <div>
+                        <span className="block text-xs text-slate-400">邮箱</span>
+                        <span className="font-medium">{userInfo.email}</span>
+                      </div>
+                      <div>
+                        <span className="block text-xs text-slate-400">手机号</span>
+                        <span className="font-medium">{userInfo.phone || '-'}</span>
+                      </div>
+                    </div>
+                  </div>
+                ) : (
+                  <p className="text-slate-400 text-xs">未找到用户信息</p>
+                )}
+              </div>
+
+            </div>
+          )}
+        </div>
+
+        {/* Footer Actions */}
+        <div className="p-4 bg-white border-t flex justify-end gap-3">
+          <button 
+            onClick={onClose}
+            className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium"
+          >
+            关闭
+          </button>
+          
+          {data.status === 'pending' && (
+            <>
+              <button 
+                onClick={() => { onReject(data); onClose(); }}
+                className="px-4 py-2 bg-red-50 text-red-600 border border-red-200 rounded-lg hover:bg-red-100 text-sm font-medium flex items-center gap-2"
+              >
+                <XCircle size={16} /> 驳回
+              </button>
+              <button 
+                onClick={() => { onApprove(data); onClose(); }}
+                className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-bold flex items-center gap-2 shadow-sm"
+              >
+                <CheckCircle size={16} /> 确认已收款
+              </button>
+            </>
+          )}
+        </div>
+
+      </div>
+    </div>
+  );
+}

+ 195 - 0
src/components/admin/payments/ConfirmationTable.tsx

@@ -0,0 +1,195 @@
+'use client';
+
+import { CheckCircle, XCircle, Clock, User, CreditCard, Sparkles, Eye } from 'lucide-react';
+import LocalTime from '@/components/common/LocalTime';
+import { PaymentConfirmation } from '@/types/payment';
+
+interface ConfirmationTableProps {
+  data: PaymentConfirmation[];
+  loading: boolean;
+  onApprove: (item: PaymentConfirmation) => void;
+  onReject: (item: PaymentConfirmation) => void;
+  onViewDetail: (item: PaymentConfirmation) => void;
+}
+
+export default function ConfirmationTable({ data, loading, onApprove, onReject, onViewDetail }: ConfirmationTableProps) {
+  
+  if (loading) {
+    return <div className="p-12 text-center text-gray-500">加载数据中...</div>;
+  }
+
+  if (!data || data.length === 0) {
+    return <div className="p-12 text-center text-gray-500">暂无待确认记录</div>;
+  }
+
+  const getStatusBadge = (status: string) => {
+    switch (status) {
+      case 'pending':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><Clock size={12}/> 待审核</span>;
+      case 'approved':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle size={12}/> 已通过</span>;
+      case 'rejected':
+        return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle size={12}/> 已驳回</span>;
+      default:
+        return <span className="bg-gray-100 text-gray-800 text-xs px-2 py-0.5 rounded">{status}</span>;
+    }
+  };
+
+  const formatMoney = (amount: number, currency: string) => {
+    return `${(amount / 100).toFixed(2)} ${currency}`;
+  };
+
+  return (
+    <div className="space-y-4">
+      
+      {/* ======================= */}
+      {/* 1. Desktop View (Table) */}
+      {/* ======================= */}
+      <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">
+            <thead className="bg-slate-50">
+              <tr>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">ID / 提交时间</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">关联支付单</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">确认金额</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">用户</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">用户点击时间</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase whitespace-nowrap">状态</th>
+                <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase whitespace-nowrap sticky right-0 bg-slate-50 shadow-sm sm:static sm:shadow-none">操作</th>
+              </tr>
+            </thead>
+            <tbody className="divide-y divide-slate-100">
+              {data.map((item) => (
+                <tr key={item.id} className="hover:bg-slate-50">
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="text-sm font-mono text-slate-900 font-bold">#{item.id}</div>
+                    <div className="text-xs text-slate-400 mt-1"><LocalTime date={item.created_at} /></div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="flex items-center text-blue-600 font-mono text-sm">
+                      <CreditCard size={14} className="mr-1" /> PID: {item.payment_id}
+                    </div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="flex flex-col">
+                      <span className="text-sm font-bold text-slate-900">{formatMoney(item.amount, item.currency)}</span>
+                      {item.random_offset > 0 && (
+                        <span className="text-xs text-red-500 flex items-center gap-0.5"><Sparkles size={10} /> -{formatMoney(item.random_offset, item.currency)}</span>
+                      )}
+                    </div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="flex items-center text-slate-600 text-xs font-mono">
+                      <User size={14} className="mr-1 text-slate-400" />
+                      <span title={item.user_id} className="truncate max-w-[120px]">{item.user_id}</span>
+                    </div>
+                  </td>
+                  <td className="px-6 py-4 text-sm text-slate-700 whitespace-nowrap">
+                    <LocalTime date={item.confirmed_at} className="font-medium" />
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    {getStatusBadge(item.status)}
+                  </td>
+                  <td className="px-6 py-4 text-right whitespace-nowrap sticky right-0 bg-white sm:static border-l border-slate-100 sm:border-none shadow-sm sm:shadow-none">
+                    <div className="flex justify-end gap-2 items-center">
+                      <button onClick={() => onViewDetail(item)} className="p-1.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition" title="查看详情">
+                        <Eye size={18} />
+                      </button>
+                      {item.status === 'pending' && (
+                        <>
+                          <div className="w-px h-4 bg-slate-200 mx-1 hidden sm:block"></div>
+                          <button onClick={() => onApprove(item)} className="flex items-center gap-1 px-3 py-1.5 bg-green-50 text-green-700 rounded hover:bg-green-100 text-xs font-bold transition border border-green-200">
+                            <CheckCircle size={14} /> 确认
+                          </button>
+                          <button onClick={() => onReject(item)} className="flex items-center gap-1 px-3 py-1.5 bg-white text-red-600 rounded hover:bg-red-50 text-xs font-medium transition border border-red-200">
+                            <XCircle size={14} /> 驳回
+                          </button>
+                        </>
+                      )}
+                    </div>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      {/* ======================= */}
+      {/* 2. Mobile View (Cards)  */}
+      {/* ======================= */}
+      <div className="md:hidden space-y-4">
+        {data.map((item) => (
+          <div key={item.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+            
+            {/* Header: ID + Status */}
+            <div className="flex justify-between items-start mb-3">
+              <div className="flex flex-col">
+                <span className="text-sm font-mono text-slate-900 font-bold">Confirmation #{item.id}</span>
+                <span className="text-xs text-slate-400 flex items-center gap-1 mt-0.5">
+                  <Clock size={10} /> <LocalTime date={item.created_at} />
+                </span>
+              </div>
+              <div>{getStatusBadge(item.status)}</div>
+            </div>
+
+            {/* Info Grid */}
+            <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-4">
+              <div className="flex justify-between items-center">
+                <span className="text-slate-400 text-xs">关联支付</span>
+                <span className="font-mono text-blue-600 font-medium">PID: {item.payment_id}</span>
+              </div>
+              
+              <div className="flex justify-between items-center">
+                <span className="text-slate-400 text-xs">确认金额</span>
+                <div className="text-right">
+                  <span className="font-bold text-slate-900 block">{formatMoney(item.amount, item.currency)}</span>
+                  {item.random_offset > 0 && (
+                    <span className="text-[10px] text-red-500 flex items-center justify-end gap-0.5">
+                      <Sparkles size={8} /> -{formatMoney(item.random_offset, item.currency)}
+                    </span>
+                  )}
+                </div>
+              </div>
+
+              <div className="flex justify-between items-center pt-2 border-t border-slate-200">
+                 <span className="text-slate-400 text-xs">用户</span>
+                 <span className="text-xs font-mono text-slate-600 truncate max-w-[150px]">{item.user_id}</span>
+              </div>
+            </div>
+
+            {/* Actions */}
+            <div className="grid grid-cols-4 gap-2">
+              <button 
+                onClick={() => onViewDetail(item)}
+                className={`flex items-center justify-center py-2.5 bg-white border border-slate-200 text-slate-600 rounded-lg text-sm font-medium active:scale-95 transition ${item.status === 'pending' ? 'col-span-1' : 'col-span-4'}`}
+              >
+                <Eye size={18} />
+              </button>
+              
+              {item.status === 'pending' && (
+                <>
+                  <button 
+                    onClick={() => onApprove(item)}
+                    className="col-span-2 flex items-center justify-center gap-1 py-2.5 bg-green-600 text-white rounded-lg text-sm font-bold active:scale-95 transition shadow-sm"
+                  >
+                    <CheckCircle size={16} /> 确认收款
+                  </button>
+                  <button 
+                    onClick={() => onReject(item)}
+                    className="col-span-1 flex items-center justify-center gap-1 py-2.5 bg-red-50 text-red-600 border border-red-100 rounded-lg text-sm font-medium active:scale-95 transition"
+                  >
+                    <XCircle size={16} />
+                  </button>
+                </>
+              )}
+            </div>
+
+          </div>
+        ))}
+      </div>
+
+    </div>
+  );
+}

+ 143 - 72
src/components/admin/payments/ProviderList.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { Edit, QrCode, CheckCircle, XCircle, Link as LinkIcon, Trash2 } from 'lucide-react';
+import { Edit, QrCode, CheckCircle, XCircle, Link as LinkIcon, Trash2, Coins } from 'lucide-react';
 
 interface Provider {
   id: number;
@@ -17,91 +17,162 @@ interface ProviderListProps {
   loading: boolean;
   onEdit: (provider: Provider) => void;
   onManageQr: (provider: Provider) => void;
-  onDelete: (provider: Provider) => void; // 新增删除回调
+  onDelete: (provider: Provider) => void;
 }
 
 export default function ProviderList({ providers, loading, onEdit, onManageQr, onDelete }: ProviderListProps) {
   if (loading) return <div className="p-12 text-center text-gray-500">加载中...</div>;
 
+  // 提取状态徽章渲染逻辑,供移动端和桌面端复用
+  const renderStatus = (enabled: number) => (
+    enabled ? (
+      <span className="text-green-600 flex items-center text-xs font-medium bg-green-50 px-2 py-0.5 rounded border border-green-100">
+        <CheckCircle size={12} className="mr-1"/> Active
+      </span>
+    ) : (
+      <span className="text-gray-400 flex items-center text-xs font-medium bg-gray-50 px-2 py-0.5 rounded border border-gray-100">
+        <XCircle size={12} className="mr-1"/> Disabled
+      </span>
+    )
+  );
+
+  // 提取渠道徽章渲染逻辑
+  const renderChannel = (channel: string) => (
+    <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border
+      ${channel === 'qr_static' 
+        ? 'bg-purple-50 text-purple-700 border-purple-100' 
+        : 'bg-blue-50 text-blue-700 border-blue-100'}`}>
+      {channel === 'qr_static' ? <QrCode size={12}/> : <LinkIcon size={12}/>}
+      {channel === 'qr_static' ? 'QR Static' : 'Online Link'}
+    </span>
+  );
+
   return (
-    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
-      <table className="min-w-full divide-y divide-slate-200">
-        <thead className="bg-slate-50">
-          <tr>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Icon / Name</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Channel Type</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Currency</th>
-            <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
-            <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Actions</th>
-          </tr>
-        </thead>
-        <tbody className="divide-y divide-slate-100">
-          {providers.map((p) => (
-            <tr key={p.id} className="hover:bg-slate-50">
-              <td className="px-6 py-4">
-                <div className="flex items-center gap-3">
-                  {p.icon ? (
-                    <img src={p.icon} alt={p.name} className="w-8 h-8 object-contain rounded border bg-white" />
-                  ) : (
-                    <div className="w-8 h-8 bg-gray-100 rounded flex items-center justify-center text-xs font-bold text-gray-400">
-                      {p.name.substring(0, 2).toUpperCase()}
+    <div className="bg-white rounded-lg shadow border border-slate-200 overflow-hidden">
+      
+      {/* ======================= */}
+      {/* 1. Desktop View (Table) */}
+      {/* ======================= */}
+      <div className="hidden md:block overflow-x-auto">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Icon / Name</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Channel Type</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Currency</th>
+              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
+              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Actions</th>
+            </tr>
+          </thead>
+          <tbody className="divide-y divide-slate-100">
+            {providers.map((p) => (
+              <tr key={p.id} className="hover:bg-slate-50">
+                <td className="px-6 py-4">
+                  <div className="flex items-center gap-3">
+                    {p.icon ? (
+                      <img src={p.icon} alt={p.name} className="w-8 h-8 object-contain rounded border bg-white" />
+                    ) : (
+                      <div className="w-8 h-8 bg-gray-100 rounded flex items-center justify-center text-xs font-bold text-gray-400">
+                        {p.name.substring(0, 2).toUpperCase()}
+                      </div>
+                    )}
+                    <div>
+                      <div className="text-sm font-medium text-gray-900">{p.title || p.name}</div>
+                      <div className="text-xs text-gray-500 font-mono">{p.name}</div>
                     </div>
-                  )}
-                  <div>
-                    <div className="text-sm font-medium text-gray-900">{p.title || p.name}</div>
-                    <div className="text-xs text-gray-500 font-mono">{p.name}</div>
                   </div>
-                </div>
-              </td>
-              <td className="px-6 py-4">
-                <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium
-                  ${p.channel === 'qr_static' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800'}`}>
-                  {p.channel === 'qr_static' ? <QrCode size={12}/> : <LinkIcon size={12}/>}
-                  {p.channel === 'qr_static' ? 'QR Static' : 'Online Link'}
-                </span>
-              </td>
-              <td className="px-6 py-4 text-sm text-gray-600 font-bold">{p.currency}</td>
-              <td className="px-6 py-4">
-                {p.enabled ? (
-                  <span className="text-green-600 flex items-center text-xs font-medium"><CheckCircle size={14} className="mr-1"/> Active</span>
-                ) : (
-                  <span className="text-gray-400 flex items-center text-xs font-medium"><XCircle size={14} className="mr-1"/> Disabled</span>
-                )}
-              </td>
-              <td className="px-6 py-4 text-right text-sm font-medium space-x-2">
-                {/* 仅当类型为 qr_static 时显示管理二维码按钮 */}
-                {p.channel === 'qr_static' && (
-                  <button 
-                    onClick={() => onManageQr(p)}
-                    className="text-purple-600 hover:text-purple-900 inline-flex items-center bg-purple-50 px-2 py-1 rounded hover:bg-purple-100 transition"
-                    title="Manage QR Codes"
-                  >
-                    <QrCode size={14} className="mr-1" /> QR
+                </td>
+                <td className="px-6 py-4">{renderChannel(p.channel)}</td>
+                <td className="px-6 py-4 text-sm text-gray-600 font-bold">{p.currency}</td>
+                <td className="px-6 py-4">{renderStatus(p.enabled)}</td>
+                <td className="px-6 py-4 text-right text-sm font-medium space-x-2">
+                  {p.channel === 'qr_static' && (
+                    <button 
+                      onClick={() => onManageQr(p)}
+                      className="text-purple-600 hover:text-purple-900 inline-flex items-center bg-purple-50 px-2 py-1 rounded hover:bg-purple-100 transition"
+                      title="Manage QR Codes"
+                    >
+                      <QrCode size={14} className="mr-1" /> QR
+                    </button>
+                  )}
+                  <button onClick={() => onEdit(p)} className="text-blue-600 hover:text-blue-900 inline-flex items-center px-2 py-1 rounded hover:bg-blue-50 transition">
+                    <Edit size={14} className="mr-1" /> Edit
+                  </button>
+                  <button onClick={() => onDelete(p)} className="text-red-600 hover:text-red-900 inline-flex items-center px-2 py-1 rounded hover:bg-red-50 transition">
+                    <Trash2 size={14} className="mr-1" /> Del
                   </button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {/* ======================= */}
+      {/* 2. Mobile View (Cards)  */}
+      {/* ======================= */}
+      <div className="md:hidden divide-y divide-slate-100">
+        {providers.map((p) => (
+          <div key={p.id} className="p-4 space-y-3">
+            {/* Header: Icon + Info + Status */}
+            <div className="flex justify-between items-start">
+              <div className="flex items-center gap-3">
+                {p.icon ? (
+                  <img src={p.icon} alt={p.name} className="w-10 h-10 object-contain rounded border bg-white" />
+                ) : (
+                  <div className="w-10 h-10 bg-gray-100 rounded flex items-center justify-center text-xs font-bold text-gray-400">
+                    {p.name.substring(0, 2).toUpperCase()}
+                  </div>
                 )}
-                
-                {/* 编辑按钮 */}
-                <button 
-                  onClick={() => onEdit(p)}
-                  className="text-blue-600 hover:text-blue-900 inline-flex items-center px-2 py-1 rounded hover:bg-blue-50 transition"
-                  title="Edit Provider"
-                >
-                  <Edit size={14} className="mr-1" /> Edit
-                </button>
+                <div>
+                  <div className="text-sm font-bold text-gray-900">{p.title || p.name}</div>
+                  <div className="text-xs text-gray-500 font-mono">{p.name}</div>
+                </div>
+              </div>
+              <div>{renderStatus(p.enabled)}</div>
+            </div>
+
+            {/* Details: Type & Currency */}
+            <div className="flex items-center gap-3 text-sm">
+              {renderChannel(p.channel)}
+              <span className="flex items-center text-gray-600 font-medium text-xs bg-slate-50 px-2 py-0.5 rounded border border-slate-100">
+                <Coins size={12} className="mr-1 text-slate-400"/> {p.currency}
+              </span>
+            </div>
 
-                {/* 删除按钮 (新增) */}
+            {/* Actions Bar */}
+            <div className="grid grid-cols-3 gap-2 pt-1">
+              {p.channel === 'qr_static' ? (
                 <button 
-                  onClick={() => onDelete(p)}
-                  className="text-red-600 hover:text-red-900 inline-flex items-center px-2 py-1 rounded hover:bg-red-50 transition"
-                  title="Delete Provider"
+                  onClick={() => onManageQr(p)}
+                  className="flex items-center justify-center gap-1 py-2 bg-purple-50 text-purple-700 rounded-lg text-sm font-medium border border-purple-100 active:scale-95 transition"
                 >
-                  <Trash2 size={14} className="mr-1" /> Del
+                  <QrCode size={16} /> QR Codes
                 </button>
-              </td>
-            </tr>
-          ))}
-        </tbody>
-      </table>
+              ) : (
+                <div className="hidden"></div> // 占位保持布局,或者可以用 col-span-2 调整
+              )}
+              
+              <button 
+                onClick={() => onEdit(p)}
+                className={`flex items-center justify-center gap-1 py-2 bg-slate-50 text-slate-700 rounded-lg text-sm font-medium border border-slate-200 active:scale-95 transition ${p.channel !== 'qr_static' ? 'col-span-2' : ''}`}
+              >
+                <Edit size={16} /> Edit
+              </button>
+
+              <button 
+                onClick={() => onDelete(p)}
+                className={`flex items-center justify-center gap-1 py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium border border-red-100 active:scale-95 transition ${p.channel !== 'qr_static' ? 'col-span-1' : ''}`}
+              >
+                <Trash2 size={16} /> Del
+              </button>
+            </div>
+          </div>
+        ))}
+        {providers.length === 0 && (
+           <div className="p-8 text-center text-gray-400 text-sm">暂无数据</div>
+        )}
+      </div>
     </div>
   );
 }

+ 87 - 180
src/components/admin/payments/QrManager.tsx

@@ -3,19 +3,9 @@
 import { useState, useEffect, useRef } from 'react';
 import api from '@/lib/api';
 import { 
-  Loader2, 
-  Trash2, 
-  Plus, 
-  Save, 
-  X, 
-  QrCode, 
-  FileImage, 
-  Smartphone,
-  ToggleLeft,
-  ToggleRight,
-  Upload
+  Loader2, Trash2, Plus, Save, X, QrCode, FileImage, 
+  Smartphone, ToggleLeft, ToggleRight, Upload 
 } from 'lucide-react';
-// 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
 interface QrManagerProps {
@@ -28,7 +18,6 @@ interface QrManagerProps {
 interface PaymentQr {
   id: number;
   qr_code: string; 
-  image_url?: string;
   priority?: number;
   is_active: boolean | number; 
   description?: string;
@@ -43,7 +32,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const [togglingId, setTogglingId] = useState<number | null>(null);
   
   const fileInputRef = useRef<HTMLInputElement>(null);
-
   const [form, setForm] = useState({
     qr_code: '',
     description: '',
@@ -52,9 +40,13 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     is_active: 1
   });
 
+  // 移动端 Tab: 'list' (查看列表) | 'add' (添加)
+  const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
+
   useEffect(() => {
     if (isOpen && providerId) {
       fetchQrs();
+      setActiveTab('list'); // 默认显示列表
     }
   }, [isOpen, providerId]);
 
@@ -67,7 +59,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
       const list = Array.isArray(res.data.data) ? res.data.data : [];
       setQrs(list);
     } catch (e) {
-      console.warn("Fetch QR failed");
       setQrs([]);
     } finally {
       setLoading(false);
@@ -77,51 +68,33 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
-
-    if (file.size > 2 * 1024 * 1024) {
-      alert("图片太大,请选择小于 2MB 的图片");
-      return;
-    }
-
+    if (file.size > 2 * 1024 * 1024) return alert("图片太大,请选择小于 2MB 的图片");
     const reader = new FileReader();
-    reader.onloadend = () => {
-      const base64String = reader.result as string;
-      setForm(prev => ({ ...prev, qr_code: base64String }));
-    };
+    reader.onloadend = () => setForm(prev => ({ ...prev, qr_code: reader.result as string }));
     reader.readAsDataURL(file);
   };
 
   const handleClearImage = () => {
     setForm(prev => ({ ...prev, qr_code: '' }));
-    if (fileInputRef.current) {
-      fileInputRef.current.value = '';
-    }
+    if (fileInputRef.current) fileInputRef.current.value = '';
   };
 
   const handleAdd = async () => {
     if (!form.qr_code) return alert("请上传二维码图片");
-    
     setSubmitting(true);
     try {
-      const payload = {
+      await api.post('/api/vas/payment_qr/create', {
         provider: providerName,
-        qr_code: form.qr_code,
-        device: form.device,
+        ...form,
         is_active: Number(form.is_active),
         priority: Number(form.priority),
-        description: form.description
-      };
-
-      await api.post('/api/vas/payment_qr/create', payload);
-      
+      });
       alert("添加成功");
       fetchQrs();
-      
       setForm({ qr_code: '', description: '', device: '', priority: 10, is_active: 1 });
       if (fileInputRef.current) fileInputRef.current.value = '';
-      
+      setActiveTab('list'); // 添加成功后切回列表
     } catch (e: any) {
-      console.error(e);
       alert("添加失败: " + (e.response?.data?.message || "未知错误"));
     } finally {
       setSubmitting(false);
@@ -131,7 +104,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleDelete = async (id: number) => {
     if (!confirm("确定删除此二维码吗?")) return;
     try {
-      await api.delete(`/api/vas/payment_qr/${id}`); // 如果API有变动,请调整此处
+      await api.delete(`/api/vas/payment_qr/${id}`);
       fetchQrs();
     } catch (e) {
       alert("删除失败");
@@ -141,9 +114,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleToggleActive = async (qr: PaymentQr) => {
     setTogglingId(qr.id);
     try {
-      await api.put(`/api/vas/payment_qr/${qr.id}`, { // 假设更新接口
-        is_active: !qr.is_active 
-      });
+      await api.put(`/api/vas/payment_qr/${qr.id}`, { is_active: !qr.is_active });
       fetchQrs();
     } catch (e: any) {
       alert("状态更新失败");
@@ -152,116 +123,109 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     }
   };
 
-  const isRenderableImage = (content: string) => {
-    return content && (content.startsWith('data:image') || content.startsWith('http'));
-  };
+  const isRenderableImage = (content: string) => content && (content.startsWith('data:image') || content.startsWith('http'));
 
   if (!isOpen) 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-5xl overflow-hidden flex flex-col h-[85vh] animate-in zoom-in duration-200">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
+      <div className="bg-white w-full h-full md:w-full md:max-w-5xl md:h-[85vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
         
         {/* Header */}
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+        <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-slate-50 flex-shrink-0">
           <div className="flex items-center gap-3">
-            <div className="bg-green-100 p-2 rounded-lg text-green-600">
+            <div className="bg-green-100 p-2 rounded-lg text-green-600 hidden md:block">
               <QrCode size={24} />
             </div>
             <div>
-              <h3 className="font-bold text-gray-900 text-lg">静态收款码管理</h3>
-              <p className="text-sm text-gray-500">
-                当前服务商代码: <span className="font-mono font-medium text-blue-600 bg-blue-50 px-1 rounded">{providerName}</span>
+              <h3 className="font-bold text-gray-900 text-base md:text-lg">静态收款码管理</h3>
+              <p className="text-xs md:text-sm text-gray-500">
+                服务商: <span className="font-mono font-medium text-blue-600 bg-blue-50 px-1 rounded">{providerName}</span>
               </p>
             </div>
           </div>
-          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1">
             <X size={24} />
           </button>
         </div>
 
+        {/* Mobile Tabs */}
+        <div className="flex md:hidden border-b border-gray-200">
+          <button 
+            onClick={() => setActiveTab('list')}
+            className={`flex-1 py-3 text-sm font-medium ${activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
+          >
+            二维码列表 ({qrs.length})
+          </button>
+          <button 
+            onClick={() => setActiveTab('add')}
+            className={`flex-1 py-3 text-sm font-medium ${activeTab === 'add' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
+          >
+            添加新码
+          </button>
+        </div>
+
         {/* Content */}
-        <div className="flex flex-1 overflow-hidden">
+        <div className="flex flex-1 overflow-hidden relative">
           
-          {/* 左侧:添加/编辑表单 */}
-          <div className="w-1/3 bg-slate-50 border-r p-5 overflow-y-auto">
+          {/* 左侧:添加表单 (Desktop: 30%, Mobile: 根据 Tab 显示) */}
+          <div className={`
+            flex-col bg-slate-50 overflow-y-auto border-r border-slate-200 p-5
+            ${activeTab === 'add' ? 'flex w-full' : 'hidden'} 
+            md:flex md:w-1/3
+          `}>
             <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
               <Plus size={16} /> 添加收款码
             </h4>
             <div className="space-y-4">
               
-              {/* 图片上传区域 */}
+              {/* 图片上传 */}
               <div>
                 <label className="text-xs font-bold text-slate-500 mb-2 block">二维码图片 *</label>
-                
-                <input 
-                  type="file" 
-                  accept="image/*" 
-                  ref={fileInputRef}
-                  className="hidden"
-                  onChange={handleFileChange}
-                />
-
+                <input type="file" accept="image/*" ref={fileInputRef} className="hidden" onChange={handleFileChange} />
                 {!form.qr_code ? (
-                  <div 
-                    onClick={() => fileInputRef.current?.click()}
-                    className="border-2 border-dashed border-slate-300 rounded-lg h-32 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition bg-white"
-                  >
+                  <div onClick={() => fileInputRef.current?.click()} className="border-2 border-dashed border-slate-300 rounded-lg h-32 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition bg-white">
                     <Upload size={24} className="text-slate-400 mb-2" />
                     <span className="text-xs text-slate-600">点击选择图片</span>
-                    <span className="text-[10px] text-slate-400 mt-1">支持 PNG, JPG (Max 2MB)</span>
                   </div>
                 ) : (
                   <div className="relative border rounded-lg bg-white p-2">
-                    <img 
-                      src={form.qr_code} 
-                      alt="Preview" 
-                      className="w-full h-40 object-contain rounded"
-                    />
-                    <button 
-                      onClick={handleClearImage}
-                      className="absolute top-2 right-2 bg-red-100 text-red-600 p-1.5 rounded-full hover:bg-red-200 transition shadow-sm"
-                      title="移除图片"
-                    >
+                    <img src={form.qr_code} alt="Preview" className="w-full h-40 object-contain rounded" />
+                    <button onClick={handleClearImage} className="absolute top-2 right-2 bg-red-100 text-red-600 p-1.5 rounded-full hover:bg-red-200">
                       <Trash2 size={14} />
                     </button>
-                    <div className="text-[10px] text-slate-400 mt-2 text-center truncate px-2">
-                      Base64 ({form.qr_code.length} chars)
-                    </div>
                   </div>
                 )}
               </div>
               
               <div>
-                <label className="text-xs font-bold text-slate-500 mb-1 block">备注 (Description)</label>
+                <label className="text-xs font-bold text-slate-500 mb-1 block">备注</label>
                 <input 
-                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm"
                   placeholder="例如:支付宝个人收款码-01"
-                  value={form.description}
-                  onChange={e => setForm({...form, description: e.target.value})}
+                  value={form.description} onChange={e => setForm({...form, description: e.target.value})}
                 />
               </div>
 
               <div>
-                <label className="text-xs font-bold text-slate-500 mb-1 block">设备名称 (Device)</label>
+                <label className="text-xs font-bold text-slate-500 mb-1 block">设备名称</label>
                 <input 
-                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  type="text" className="w-full border border-slate-300 rounded p-2 text-sm"
                   placeholder="例如:iPhone 13"
-                  value={form.device}
-                  onChange={e => setForm({...form, device: e.target.value})}
+                  value={form.device} onChange={e => setForm({...form, device: e.target.value})}
                 />
               </div>
 
               <div className="grid grid-cols-2 gap-3">
                 <div>
-                  <label className="text-xs font-bold text-slate-500 mb-1 block">权重 (Priority)</label>
-                  <input type="number" className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  <label className="text-xs font-bold text-slate-500 mb-1 block">权重</label>
+                  <input type="number" className="w-full border rounded p-2 text-sm"
                     value={form.priority} onChange={e => setForm({...form, priority: parseInt(e.target.value) || 0})}
                   />
                 </div>
                 <div>
                   <label className="text-xs font-bold text-slate-500 mb-1 block">状态</label>
-                  <select className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
+                  <select className="w-full border rounded p-2 text-sm"
                     value={form.is_active} onChange={e => setForm({...form, is_active: parseInt(e.target.value)})}
                   >
                     <option value={1}>启用</option>
@@ -270,20 +234,20 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
                 </div>
               </div>
 
-              <button 
-                onClick={handleAdd}
-                disabled={submitting}
-                className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 flex justify-center items-center gap-2 mt-4 shadow-sm transition disabled:opacity-50"
-              >
+              <button onClick={handleAdd} disabled={submitting} className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 flex justify-center items-center gap-2 mt-4 shadow-sm transition disabled:opacity-50">
                 {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
                 提交新增
               </button>
             </div>
           </div>
 
-          {/* 右侧:预览列表 */}
-          <div className="w-2/3 p-6 overflow-y-auto bg-white">
-            <div className="flex justify-between items-center mb-4">
+          {/* 右侧:预览列表 (Desktop: 70%, Mobile: 根据 Tab 显示) */}
+          <div className={`
+            flex-col bg-white p-4 md:p-6 overflow-y-auto
+            ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
+            md:flex md:w-2/3
+          `}>
+            <div className="hidden md:flex justify-between items-center mb-4">
               <h4 className="text-sm font-bold text-slate-800">现有收款码 ({qrs.length})</h4>
               <span className="text-xs text-slate-400">系统将根据权重轮询使用</span>
             </div>
@@ -299,95 +263,38 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
               <div className="grid grid-cols-1 gap-4">
                 {qrs.map((item) => {
                   const isActive = Boolean(item.is_active);
-
                   return (
-                    <div 
-                      key={item.id} 
-                      className={`border rounded-lg p-4 flex gap-5 items-start transition group
-                        ${isActive ? 'border-slate-200 hover:border-blue-300 hover:shadow-sm' : 'border-slate-100 bg-slate-50 opacity-75'}
-                      `}
-                    >
+                    <div key={item.id} className={`border rounded-lg p-4 flex gap-4 items-start transition ${isActive ? 'border-slate-200' : 'border-slate-100 bg-slate-50 opacity-75'}`}>
                       
-                      {/* 图片预览区 */}
-                      <div className="w-24 h-24 bg-white rounded-lg flex items-center justify-center flex-shrink-0 border border-slate-100 overflow-hidden relative">
+                      {/* 缩略图 */}
+                      <div className="w-20 h-20 md:w-24 md:h-24 bg-white rounded-lg flex items-center justify-center flex-shrink-0 border border-slate-100 overflow-hidden relative">
                         {isRenderableImage(item.qr_code) ? (
-                          <img 
-                            src={item.qr_code} 
-                            alt="QR Preview" 
-                            className={`w-full h-full object-contain ${!isActive ? 'grayscale' : ''}`}
-                          />
+                          <img src={item.qr_code} alt="QR" className={`w-full h-full object-contain ${!isActive ? 'grayscale' : ''}`} />
                         ) : (
-                          <div className="text-center">
-                            <QrCode size={24} className="text-gray-400 mx-auto mb-1" />
-                            <span className="text-[10px] text-gray-400">无效图片</span>
-                          </div>
-                        )}
-                        
-                        {!isActive && (
-                          <div className="absolute inset-0 bg-gray-900/60 flex items-center justify-center backdrop-blur-[1px]">
-                            <span className="text-white text-[10px] font-bold px-2 py-1 bg-black/60 rounded shadow-sm border border-white/20">
-                              已禁用
-                            </span>
-                          </div>
+                          <QrCode size={24} className="text-gray-400" />
                         )}
+                        {!isActive && <div className="absolute inset-0 bg-black/50 flex items-center justify-center"><span className="text-white text-[10px] font-bold px-2 py-1 bg-black/60 rounded">已禁用</span></div>}
                       </div>
                       
-                      {/* 信息 */}
-                      <div className="flex-1 min-w-0 py-1 space-y-2">
+                      {/* 信息 */}
+                      <div className="flex-1 min-w-0 space-y-1.5">
                         <div className="flex items-center gap-2 flex-wrap">
-                          <span className="text-xs font-mono bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-bold">ID: {item.id}</span>
-                          
-                          {isActive ? 
-                            <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded font-medium">Running</span> : 
-                            <span className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded font-medium">Stopped</span>
-                          }
-                          
-                          <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded font-medium">权重: {item.priority}</span>
-                          
-                          {item.device && (
-                            <span className="text-xs bg-orange-50 text-orange-700 px-2 py-0.5 rounded font-medium flex items-center gap-1 border border-orange-100">
-                              <Smartphone size={10} /> {item.device}
-                            </span>
-                          )}
+                          <span className="text-xs font-mono bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-bold">#{item.id}</span>
+                          <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded font-medium">权重 {item.priority}</span>
+                          {item.device && <span className="text-xs bg-orange-50 text-orange-700 px-2 py-0.5 rounded font-medium flex items-center gap-1 border border-orange-100"><Smartphone size={10} /> {item.device}</span>}
                         </div>
-                        
-                        <p className="text-sm font-bold text-gray-800">
-                          {item.description || "未命名收款码"}
-                        </p>
-
-                        <div className="bg-slate-50 p-2 rounded text-[10px] font-mono text-slate-500 break-all border h-10 overflow-hidden relative">
-                          {item.qr_code ? item.qr_code.substring(0, 100) + '...' : '无数据'}
-                          <div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-slate-50 to-transparent"></div>
-                        </div>
-                        
+                        <p className="text-sm font-bold text-gray-800 line-clamp-1">{item.description || "未命名"}</p>
                         <div className="text-[10px] text-gray-400">
-                          {/* 2. 使用 LocalTime 组件替代 toLocaleString */}
-                          创建时间: {item.created_at ? <LocalTime date={item.created_at} /> : '-'}
+                           创建于 <LocalTime date={item.created_at} />
                         </div>
                       </div>
 
-                      {/* 操作区 */}
-                      <div className="flex flex-col gap-2 border-l pl-4 border-slate-100">
-                        <button 
-                          onClick={() => handleToggleActive(item)}
-                          disabled={togglingId === item.id}
-                          className={`transition-colors ${isActive ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-gray-600'}`}
-                          title={isActive ? "点击禁用" : "点击启用"}
-                        >
-                          {togglingId === item.id ? (
-                            <Loader2 size={24} className="animate-spin text-blue-600" />
-                          ) : isActive ? (
-                            <ToggleRight size={28} />
-                          ) : (
-                            <ToggleLeft size={28} />
-                          )}
+                      {/* 操作 */}
+                      <div className="flex flex-col gap-2 border-l pl-3 border-slate-100">
+                        <button onClick={() => handleToggleActive(item)} disabled={togglingId === item.id} className={isActive ? 'text-green-600' : 'text-gray-400'}>
+                          {togglingId === item.id ? <Loader2 size={24} className="animate-spin" /> : isActive ? <ToggleRight size={28} /> : <ToggleLeft size={28} />}
                         </button>
-
-                        <button 
-                          onClick={() => handleDelete(item.id)}
-                          className="text-slate-300 hover:text-red-500 p-1 rounded hover:bg-red-50 transition self-center mt-auto"
-                          title="删除"
-                        >
+                        <button onClick={() => handleDelete(item.id)} className="text-slate-300 hover:text-red-500 p-1 rounded transition mt-auto">
                           <Trash2 size={18} />
                         </button>
                       </div>

+ 137 - 65
src/components/admin/products/ProductList.tsx

@@ -6,8 +6,8 @@ interface Product {
   id: number;
   title: string;
   country: string;
-  city: string;      // 新增:城市
-  visa_type: string; // 新增:签证类型
+  city: string;
+  visa_type: string;
   price_amount: number;
   price_currency: string;
   provider: string;
@@ -24,71 +24,143 @@ interface ProductListProps {
 export default function ProductList({ products, loading, onEdit, onManageRouting }: ProductListProps) {
   if (loading) return <div className="p-8 text-center text-gray-500">加载商品中...</div>;
 
+  // 提取渲染状态的逻辑,供复用
+  const renderStatus = (enabled: number) => (
+    enabled ? (
+      <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
+        <CheckCircle size={12} className="mr-1"/> 上架中
+      </span>
+    ) : (
+      <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
+        <XCircle size={12} className="mr-1"/> 已下架
+      </span>
+    )
+  );
+
+  // 提取渲染签证类型的逻辑
+  const renderVisaType = (type: string) => (
+    type ? (
+      <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100">
+        {type}
+      </span>
+    ) : <span className="text-gray-400">-</span>
+  );
+
   return (
-    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
-      <table className="min-w-full divide-y divide-slate-200">
-        <thead className="bg-slate-50">
-          <tr>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">标题</th>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">国家/地区</th>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">城市</th> {/* 新增列头 */}
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">签证类型</th> {/* 新增列头 */}
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">价格</th>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Provider</th>
-            <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
-            <th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
-          </tr>
-        </thead>
-        <tbody className="divide-y divide-slate-100">
-          {products.map((product) => (
-            <tr key={product.id} className="hover:bg-slate-50">
-              <td className="px-4 py-3 text-sm text-gray-500 font-mono">{product.id}</td>
-              <td className="px-4 py-3 text-sm font-medium text-gray-900">{product.title}</td>
-              <td className="px-4 py-3 text-sm text-gray-600">{product.country}</td>
-              <td className="px-4 py-3 text-sm text-gray-600">{product.city || '-'}</td> {/* 新增内容 */}
-              <td className="px-4 py-3 text-sm text-gray-600">
-                {product.visa_type ? (
-                   <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 border border-blue-100">
-                     {product.visa_type}
-                   </span>
-                ) : '-'}
-              </td> {/* 新增内容,加了一点简单的样式使其更像标签 */}
-              <td className="px-4 py-3 text-sm font-bold text-gray-800">
-                {(product.price_amount / 100).toFixed(2)} {product.price_currency}
-              </td>
-              <td className="px-4 py-3 text-sm text-gray-600">{product.provider}</td>
-              <td className="px-4 py-3 text-sm">
-                {product.enabled ? (
-                  <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
-                    <CheckCircle size={12} className="mr-1"/> 上架中
-                  </span>
-                ) : (
-                  <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
-                    <XCircle size={12} className="mr-1"/> 已下架
-                  </span>
-                )}
-              </td>
-              <td className="px-4 py-3 text-right text-sm font-medium space-x-3">
-                <button 
-                  onClick={() => onManageRouting(product)}
-                  className="text-purple-600 hover:text-purple-900 inline-flex items-center"
-                  title="配置路由"
-                >
-                  <Network size={16} className="mr-1" /> 路由
-                </button>
-                <button 
-                  onClick={() => onEdit(product)}
-                  className="text-blue-600 hover:text-blue-900 inline-flex items-center"
-                  title="编辑详情"
-                >
-                  <Edit size={16} className="mr-1" /> 编辑
-                </button>
-              </td>
+    <div className="space-y-4">
+      {/* =========================== */}
+      {/* 1. 桌面端视图 (表格) - hidden on mobile, block on md */}
+      {/* =========================== */}
+      <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
+        <table className="min-w-full divide-y divide-slate-200">
+          <thead className="bg-slate-50">
+            <tr>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">标题</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">国家/地区</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">城市</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">签证类型</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">价格</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Provider</th>
+              <th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
+              <th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
             </tr>
-          ))}
-        </tbody>
-      </table>
+          </thead>
+          <tbody className="divide-y divide-slate-100">
+            {products.map((product) => (
+              <tr key={product.id} className="hover:bg-slate-50">
+                <td className="px-4 py-3 text-sm text-gray-500 font-mono">{product.id}</td>
+                <td className="px-4 py-3 text-sm font-medium text-gray-900">{product.title}</td>
+                <td className="px-4 py-3 text-sm text-gray-600">{product.country}</td>
+                <td className="px-4 py-3 text-sm text-gray-600">{product.city || '-'}</td>
+                <td className="px-4 py-3 text-sm text-gray-600">{renderVisaType(product.visa_type)}</td>
+                <td className="px-4 py-3 text-sm font-bold text-gray-800">
+                  {(product.price_amount / 100).toFixed(2)} {product.price_currency}
+                </td>
+                <td className="px-4 py-3 text-sm text-gray-600">{product.provider}</td>
+                <td className="px-4 py-3 text-sm">{renderStatus(product.enabled)}</td>
+                <td className="px-4 py-3 text-right text-sm font-medium space-x-3">
+                  <button 
+                    onClick={() => onManageRouting(product)}
+                    className="text-purple-600 hover:text-purple-900 inline-flex items-center"
+                    title="配置路由"
+                  >
+                    <Network size={16} className="mr-1" /> 路由
+                  </button>
+                  <button 
+                    onClick={() => onEdit(product)}
+                    className="text-blue-600 hover:text-blue-900 inline-flex items-center"
+                    title="编辑详情"
+                  >
+                    <Edit size={16} className="mr-1" /> 编辑
+                  </button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {/* =========================== */}
+      {/* 2. 移动端视图 (卡片) - block on mobile, hidden on md */}
+      {/* =========================== */}
+      <div className="md:hidden space-y-4">
+        {products.map((product) => (
+          <div key={product.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+            {/* Header: Title & Status */}
+            <div className="flex justify-between items-start mb-3">
+              <div>
+                <div className="text-sm font-bold text-gray-900 line-clamp-1">{product.title}</div>
+                <div className="text-xs text-gray-500 font-mono mt-0.5">ID: {product.id}</div>
+              </div>
+              <div>{renderStatus(product.enabled)}</div>
+            </div>
+
+            {/* Content Grid */}
+            <div className="grid grid-cols-2 gap-y-2 gap-x-4 text-sm text-gray-600 mb-4 bg-slate-50 p-3 rounded">
+              <div>
+                <span className="text-xs text-gray-400 block">国家/城市</span>
+                <span className="font-medium">{product.country} {product.city ? `/ ${product.city}` : ''}</span>
+              </div>
+              <div>
+                <span className="text-xs text-gray-400 block">签证类型</span>
+                {renderVisaType(product.visa_type)}
+              </div>
+              <div>
+                <span className="text-xs text-gray-400 block">价格</span>
+                <span className="font-bold text-gray-800">
+                  {(product.price_amount / 100).toFixed(2)} {product.price_currency}
+                </span>
+              </div>
+              <div>
+                <span className="text-xs text-gray-400 block">Provider</span>
+                <span>{product.provider}</span>
+              </div>
+            </div>
+
+            {/* Action Buttons */}
+            <div className="flex gap-3">
+              <button 
+                onClick={() => onManageRouting(product)}
+                className="flex-1 flex items-center justify-center py-2 bg-purple-50 text-purple-700 rounded-lg text-sm font-medium border border-purple-100 hover:bg-purple-100 active:scale-95 transition-transform"
+              >
+                <Network size={16} className="mr-1.5" /> 路由配置
+              </button>
+              <button 
+                onClick={() => onEdit(product)}
+                className="flex-1 flex items-center justify-center py-2 bg-white text-blue-600 rounded-lg text-sm font-medium border border-slate-300 hover:bg-slate-50 active:scale-95 transition-transform"
+              >
+                <Edit size={16} className="mr-1.5" /> 编辑详情
+              </button>
+            </div>
+          </div>
+        ))}
+        {products.length === 0 && (
+          <div className="text-center text-gray-500 py-8 bg-white rounded-lg border border-dashed">
+            暂无数据
+          </div>
+        )}
+      </div>
     </div>
   );
 }

+ 59 - 28
src/components/admin/products/RoutingManager.tsx

@@ -26,30 +26,32 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
   const [loading, setLoading] = useState(false);
   const [submitting, setSubmitting] = useState(false);
   
+  // 移动端 Tab 状态: 'list' | 'add'
+  const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
+
   // 新增 Routing 的表单状态
   const [newRouting, setNewRouting] = useState({
     routing_key: '',
     script_version: 'latest',
     priority: 10,
-    config: '{}', // JSON 字符串用于编辑器
+    config: '{}', 
   });
 
   // 加载路由列表
   useEffect(() => {
     if (isOpen && productId) {
       fetchRoutings();
+      setActiveTab('list'); // 默认显示列表
     }
   }, [isOpen, productId]);
 
   const fetchRoutings = async () => {
     setLoading(true);
     try {
-      // API: GET /api/vas/product_routing/list?product_id={id}
-      // 注意:如果你的后端还没有这个接口,请参考后端文档补充
       const res = await api.get('/api/vas/product_routing/list', { params: { product_id: productId } });
       setRoutings(Array.isArray(res.data.data) ? res.data.data : []);
     } catch (e) {
-      console.warn("Fetch routings failed (API missing?), using mock data");
+      console.warn("Fetch routings failed");
       setRoutings([]); 
     } finally {
       setLoading(false);
@@ -61,7 +63,6 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
     
     setSubmitting(true);
     try {
-      // 验证并解析 JSON
       let configObj = {};
       try {
         configObj = JSON.parse(newRouting.config);
@@ -73,14 +74,14 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
 
       await api.post('/api/vas/product_routing/create', {
         ...newRouting,
-        config: configObj, // 发送对象给后端
+        config: configObj,
         product_id: productId
       });
       
       alert("路由添加成功");
-      fetchRoutings(); // 刷新列表
-      // 重置表单
+      fetchRoutings(); 
       setNewRouting({ routing_key: '', script_version: 'latest', priority: 10, config: '{}' });
+      setActiveTab('list'); // 提交成功后切回列表
     } catch (e: any) {
       alert("添加失败: " + (e.response?.data?.message || e.message));
     } finally {
@@ -101,29 +102,55 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
   if (!isOpen) 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-5xl overflow-hidden flex flex-col h-[90vh]">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
+      {/* 
+         容器:
+         - 移动端:w-full h-full (全屏), rounded-none
+         - 桌面端:max-w-5xl h-[90vh], rounded-xl
+      */}
+      <div className="bg-white w-full h-full md:w-full md:max-w-5xl md:h-[90vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
+        
         {/* Header */}
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+        <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-slate-50 flex-shrink-0">
           <div className="flex items-center gap-3">
-            <div className="bg-purple-100 p-2 rounded-lg text-purple-600">
+            <div className="bg-purple-100 p-2 rounded-lg text-purple-600 hidden md:block">
               <Network size={24} />
             </div>
             <div>
-              <h3 className="font-bold text-gray-900 text-lg">路由策略配置</h3>
-              <p className="text-sm text-gray-500">商品: <span className="font-medium text-gray-700">{productTitle}</span> (ID: {productId})</p>
+              <h3 className="font-bold text-gray-900 text-base md:text-lg">路由策略配置</h3>
+              <p className="text-xs md:text-sm text-gray-500">商品: <span className="font-medium text-gray-700">{productTitle}</span></p>
             </div>
           </div>
-          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1">
             <X size={24} />
           </button>
         </div>
 
+        {/* Mobile Tabs */}
+        <div className="flex md:hidden border-b border-gray-200">
+          <button 
+            onClick={() => setActiveTab('list')}
+            className={`flex-1 py-3 text-sm font-medium ${activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
+          >
+            规则列表 ({routings.length})
+          </button>
+          <button 
+            onClick={() => setActiveTab('add')}
+            className={`flex-1 py-3 text-sm font-medium ${activeTab === 'add' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
+          >
+            添加新规则
+          </button>
+        </div>
+
         {/* Content */}
-        <div className="flex flex-1 overflow-hidden">
+        <div className="flex flex-1 overflow-hidden relative">
           
-          {/* 左侧:添加表单 */}
-          <div className="w-1/3 bg-slate-50 border-r border-slate-200 p-5 overflow-y-auto">
+          {/* 左侧:添加表单 (Desktop: 30%, Mobile: 根据 Tab 显示) */}
+          <div className={`
+            flex-col bg-slate-50 overflow-y-auto border-r border-slate-200 p-5
+            ${activeTab === 'add' ? 'flex w-full' : 'hidden'} 
+            md:flex md:w-1/3
+          `}>
             <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
               <Plus size={16} /> 添加新规则
             </h4>
@@ -172,7 +199,7 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
               <button 
                 onClick={handleAddRouting}
                 disabled={submitting}
-                className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 transition flex items-center justify-center gap-2 shadow-sm disabled:opacity-50"
+                className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 transition flex items-center justify-center gap-2 shadow-sm disabled:opacity-50 mt-4"
               >
                 {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
                 保存规则
@@ -180,15 +207,19 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
             </div>
           </div>
 
-          {/* 右侧:列表 */}
-          <div className="w-2/3 p-6 overflow-y-auto bg-white">
-            <h4 className="text-sm font-bold text-slate-800 mb-4">现有路由列表</h4>
+          {/* 右侧:列表 (Desktop: 70%, Mobile: 根据 Tab 显示) */}
+          <div className={`
+            flex-col bg-white p-4 md:p-6 overflow-y-auto
+            ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
+            md:flex md:w-2/3
+          `}>
+            <h4 className="text-sm font-bold text-slate-800 mb-4 hidden md:block">现有路由列表</h4>
             
             {loading ? (
               <div className="text-center py-20 text-slate-400"><Loader2 className="animate-spin mx-auto mb-2" /> 加载中...</div>
             ) : routings.length === 0 ? (
               <div className="text-center py-20 border-2 border-dashed border-slate-100 rounded-lg text-slate-400">
-                暂无路由配置,请在左侧添加。
+                暂无路由配置,请添加。
               </div>
             ) : (
               <div className="space-y-4">
@@ -196,7 +227,7 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
                   <div key={r.id} className="border border-slate-200 rounded-lg p-4 hover:border-blue-300 transition group">
                     <div className="flex justify-between items-start mb-3">
                       <div>
-                        <div className="flex items-center gap-2">
+                        <div className="flex items-center gap-2 flex-wrap">
                           <span className="font-mono text-sm font-bold text-blue-700 bg-blue-50 px-2 py-0.5 rounded">
                             {r.routing_key}
                           </span>
@@ -211,16 +242,16 @@ export default function RoutingManager({ productId, productTitle, isOpen, onClos
                       </div>
                       <button 
                         onClick={() => handleDeleteRouting(r.id)}
-                        className="text-slate-400 hover:text-red-600 p-1.5 rounded-md hover:bg-red-50 transition"
+                        className="text-slate-400 hover:text-red-600 p-2 rounded-md hover:bg-red-50 transition"
                         title="删除此规则"
                       >
-                        <Trash2 size={16} />
+                        <Trash2 size={18} />
                       </button>
                     </div>
                     
                     {/* Config Preview */}
-                    <div className="bg-slate-50 rounded p-3 text-xs font-mono text-slate-600 break-all border border-slate-100">
-                      {typeof r.config === 'string' ? r.config : JSON.stringify(r.config)}
+                    <div className="bg-slate-50 rounded p-3 text-xs font-mono text-slate-600 break-all border border-slate-100 max-h-32 overflow-y-auto">
+                      {typeof r.config === 'string' ? r.config : JSON.stringify(r.config, null, 2)}
                     </div>
                   </div>
                 ))}

+ 137 - 48
src/components/admin/products/SchemaManager.tsx

@@ -2,7 +2,7 @@
 
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
-import { Loader2, Trash2, Plus, Save, X, FileJson } from 'lucide-react';
+import { Loader2, Plus, Save, X, FileJson, List, Edit3 } from 'lucide-react';
 import JsonEditor from '@/components/common/JsonEditor';
 
 interface SchemaManagerProps {
@@ -13,7 +13,7 @@ interface SchemaManagerProps {
 interface SchemaItem {
   id: number;
   name: string;
-  schema_json: string; // 核心字段:JSON Schema 字符串
+  schema_json: string; 
   description?: string;
 }
 
@@ -21,8 +21,11 @@ export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
   const [schemas, setSchemas] = useState<SchemaItem[]>([]);
   const [loading, setLoading] = useState(false);
   
+  // 移动端 Tab 状态: 'list' | 'editor'
+  const [activeTab, setActiveTab] = useState<'list' | 'editor'>('list');
+
   // 编辑模式状态
-  const [editingId, setEditingId] = useState<number | null>(null); // null 表示新增模式
+  const [editingId, setEditingId] = useState<number | null>(null);
   const [formData, setFormData] = useState({
     name: '',
     description: '',
@@ -30,20 +33,20 @@ export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
   });
 
   useEffect(() => {
-    if (isOpen) fetchSchemas();
+    if (isOpen) {
+      fetchSchemas();
+      setActiveTab('list'); // 打开时默认显示列表
+    }
   }, [isOpen]);
 
   const fetchSchemas = async () => {
     setLoading(true);
     try {
-      // TODO: 后端 API: GET /api/vas/schema/list
       const res = await api.get('/api/vas/schema/list');
       setSchemas(Array.isArray(res.data.data) ? res.data.data : []);
     } catch (e) {
       console.warn("API Error, using mock schema data");
-      setSchemas([
-        { id: 1, name: 'General Visa Form', description: '通用签证表单', schema_json: '{"type":"object"}' }
-      ]);
+      setSchemas([]);
     } finally {
       setLoading(false);
     }
@@ -52,74 +55,130 @@ export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
   const handleSave = async () => {
     if (!formData.name) return alert("名称必填");
     try {
-      // 验证 JSON
-      JSON.parse(formData.schema_json);
+      JSON.parse(formData.schema_json); // 校验 JSON
 
       if (editingId) {
-        // Update
-        // TODO: 后端 API: POST /api/vas/schema/update
         await api.post(`/api/vas/schema/update`, formData, {params: {"id": editingId}});
       } else {
-        // Create
-        // TODO: 后端 API: POST /api/vas/schema/create
         await api.post('/api/vas/schema/create', formData);
       }
       
       alert("保存成功");
-      setEditingId(null); // 回到列表模式
-      setFormData({ name: '', description: '', schema_json: '{}' });
       fetchSchemas();
+      
+      // 保存后逻辑:
+      // 如果是新建,保存后清空表单并留再编辑页,或者切回列表
+      // 这里选择保留在当前页,但如果是移动端可以考虑切回列表
+      if (!editingId) {
+         setFormData({ name: '', description: '', schema_json: '{}' });
+      }
     } catch (e: any) {
       alert("保存失败: " + (e.message || "JSON 格式错误"));
     }
   };
 
+  const handleCreateNew = () => {
+    setEditingId(null);
+    setFormData({ name: '', description: '', schema_json: '{}' });
+    setActiveTab('editor'); // 移动端自动切换到编辑器 tab
+  };
+
   const handleEdit = (item: SchemaItem) => {
     setEditingId(item.id);
     setFormData({
       name: item.name,
       description: item.description || '',
-      // 如果后端存的是对象,这里转字符串;如果是字符串直接用
       schema_json: typeof item.schema_json === 'string' ? item.schema_json : JSON.stringify(item.schema_json, null, 2)
     });
+    setActiveTab('editor'); // 移动端自动切换到编辑器 tab
   };
 
   const handleDelete = async (id: number) => {
     if(!confirm("删除 Schema 可能导致关联商品失效,确定删除吗?")) return;
     try {
-      // TODO: 后端 API
       await api.delete('/api/vas/schema/delete', {params: {"id": id}});
       fetchSchemas();
+      if (editingId === id) {
+        setEditingId(null);
+        setFormData({ name: '', description: '', schema_json: '{}' });
+        setActiveTab('list'); // 删除后切回列表
+      }
     } catch (e) { alert("删除失败"); }
   };
 
   if (!isOpen) 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-4xl overflow-hidden flex flex-col h-[85vh]">
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
+      {/* 
+         容器:
+         - 移动端:全屏 (w-full h-full),无圆角
+         - 桌面端:固定大小 (max-w-4xl h-[85vh]),有圆角
+      */}
+      <div className="bg-white w-full h-full md:w-full md:max-w-4xl md:h-[85vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
+        
+        {/* Header */}
+        <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-slate-50 flex-shrink-0">
           <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
             <FileJson className="text-purple-600" /> 表单 Schema 管理
           </h3>
-          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Mobile Tabs (仅移动端显示) */}
+        <div className="flex md:hidden border-b border-gray-200 flex-shrink-0">
+          <button 
+            onClick={() => setActiveTab('list')}
+            className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
+              activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
+            }`}
+          >
+            <List size={16} /> 列表
+          </button>
+          <button 
+            onClick={() => setActiveTab('editor')}
+            className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
+              activeTab === 'editor' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
+            }`}
+          >
+            <Edit3 size={16} /> {editingId ? '编辑' : '新建'}
+          </button>
         </div>
 
-        <div className="flex flex-1 overflow-hidden">
-          {/* 左侧:列表 */}
-          <div className="w-1/3 border-r overflow-y-auto bg-slate-50 p-4">
+        {/* Content Body */}
+        <div className="flex flex-1 overflow-hidden relative">
+          
+          {/* 
+             左侧:列表 
+             - 移动端:根据 activeTab === 'list' 显示/隐藏
+             - 桌面端:md:flex md:w-1/3
+          */}
+          <div className={`
+            flex-col bg-slate-50 border-r border-slate-200 overflow-y-auto p-4
+            ${activeTab === 'list' ? 'flex w-full' : 'hidden'} 
+            md:flex md:w-1/3
+          `}>
             <button 
-              onClick={() => { setEditingId(null); setFormData({name:'', description:'', schema_json:'{}'}); }}
-              className="w-full mb-4 py-2 border-2 border-dashed border-blue-300 text-blue-600 rounded-lg hover:bg-blue-50 flex items-center justify-center gap-2 font-bold text-sm"
+              onClick={handleCreateNew}
+              className="w-full mb-4 py-2.5 border-2 border-dashed border-blue-300 text-blue-600 rounded-lg hover:bg-blue-50 flex items-center justify-center gap-2 font-bold text-sm transition"
             >
               <Plus size={16} /> 新建 Schema
             </button>
             <div className="space-y-2">
+              {loading && <div className="text-center py-4 text-gray-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</div>}
+              {!loading && schemas.length === 0 && <div className="text-center py-10 text-gray-400 text-xs">暂无数据</div>}
+              
               {schemas.map(s => (
                 <div 
                   key={s.id} 
                   onClick={() => handleEdit(s)}
-                  className={`p-3 rounded-lg cursor-pointer border transition ${editingId === s.id ? 'bg-white border-blue-500 shadow-md ring-1 ring-blue-500' : 'bg-white border-slate-200 hover:border-blue-300'}`}
+                  className={`p-3 rounded-lg cursor-pointer border transition ${
+                    editingId === s.id 
+                      ? 'bg-white border-blue-500 shadow-md ring-1 ring-blue-500' 
+                      : 'bg-white border-slate-200 hover:border-blue-300'
+                  }`}
                 >
                   <div className="font-bold text-slate-800 text-sm">{s.name}</div>
                   <div className="text-xs text-slate-500 truncate mt-1">ID: {s.id}</div>
@@ -128,41 +187,71 @@ export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
             </div>
           </div>
 
-          {/* 右侧:编辑区 */}
-          <div className="w-2/3 p-6 overflow-y-auto">
-            <h4 className="text-lg font-bold mb-4 border-b pb-2">
-              {editingId ? `编辑 Schema #${editingId}` : '创建新 Schema'}
+          {/* 
+             右侧:编辑区 
+             - 移动端:根据 activeTab === 'editor' 显示/隐藏
+             - 桌面端:md:flex md:w-2/3
+          */}
+          <div className={`
+            flex-col p-4 md:p-6 overflow-y-auto w-full
+            ${activeTab === 'editor' ? 'flex' : 'hidden'}
+            md:flex md:w-2/3
+          `}>
+            <h4 className="text-lg font-bold mb-4 border-b pb-2 flex justify-between items-center">
+              <span>{editingId ? `编辑 Schema #${editingId}` : '创建新 Schema'}</span>
+              {editingId && (
+                <button onClick={() => handleDelete(editingId)} className="text-red-500 hover:text-red-700 text-xs flex items-center gap-1 md:hidden">
+                  <Trash2 size={14} /> 删除
+                </button>
+              )}
             </h4>
+            
             <div className="space-y-4">
-              <div className="grid grid-cols-2 gap-4">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                 <div>
-                  <label className="block text-sm font-medium mb-1">名称 (Name)</label>
-                  <input type="text" className="w-full border rounded p-2 text-sm" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} />
+                  <label className="block text-sm font-medium mb-1 text-slate-700">名称 (Name) <span className="text-red-500">*</span></label>
+                  <input 
+                    type="text" 
+                    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.name} 
+                    onChange={e => setFormData({...formData, name: e.target.value})} 
+                    placeholder="e.g. Japan Visa Form"
+                  />
                 </div>
                 <div>
-                  <label className="block text-sm font-medium mb-1">描述 (Desc)</label>
-                  <input type="text" className="w-full border rounded p-2 text-sm" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} />
+                  <label className="block text-sm font-medium mb-1 text-slate-700">描述 (Desc)</label>
+                  <input 
+                    type="text" 
+                    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.description} 
+                    onChange={e => setFormData({...formData, description: e.target.value})} 
+                    placeholder="简短描述用途"
+                  />
                 </div>
               </div>
 
-              {/* 使用通用 JSON 编辑器 */}
-              <JsonEditor 
-                label="Schema JSON (定义动态表单结构)" 
-                value={formData.schema_json} 
-                onChange={(val) => setFormData({...formData, schema_json: val})}
-                height="h-80"
-              />
+              {/* JSON Editor */}
+              <div className="flex-1 min-h-[300px]">
+                 <JsonEditor 
+                  label="Schema JSON (定义动态表单结构)" 
+                  value={formData.schema_json} 
+                  onChange={(val) => setFormData({...formData, schema_json: val})}
+                  height="h-[400px] md:h-80" // 移动端稍微高一点
+                  placeholder='{"type": "object", ...}'
+                />
+              </div>
 
-              <div className="flex justify-end gap-3 pt-4 border-t">
+              <div className="flex justify-end gap-3 pt-4 border-t mt-auto">
                 {editingId && (
-                  <button onClick={() => handleDelete(editingId)} className="text-red-600 px-4 text-sm hover:underline">删除</button>
+                  <button onClick={() => handleDelete(editingId)} className="text-red-600 px-4 text-sm hover:underline hidden md:block">删除</button>
                 )}
-                <button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2 rounded shadow hover:bg-blue-700 flex items-center gap-2">
-                  <Save size={16} /> 保存配置
+                <button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-bold shadow hover:bg-blue-700 flex items-center gap-2 transition w-full md:w-auto justify-center">
+                  <Save size={18} /> 保存配置
                 </button>
               </div>
             </div>
           </div>
+
         </div>
       </div>
     </div>

+ 229 - 226
src/components/admin/tasks/TaskTable.tsx

@@ -2,37 +2,23 @@
 
 import { useState } from 'react';
 import { 
-  RotateCcw, 
-  CheckCircle, 
-  ChevronDown, 
-  ChevronUp, 
-  Terminal, 
-  FileJson, 
-  User, 
-  History, 
-  Edit,
-  AlertCircle 
+  RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson, 
+  User, History, Edit, AlertCircle, FileText
 } from 'lucide-react';
 
-// 定义任务数据结构 (导出供其他组件使用)
 export interface VasTask {
   id: number;
   order_id: string;
   routing_key: string;
-  status: string;       // pending, grabbed, running, cancelled, completed, failed
+  status: string;
   priority: number;
   script_version?: string;
   attempt_count: number;
   notify_count: number;
-  
-  // JSON 字段
   config?: any;
   user_inputs?: any;
-  
-  // 数组字段 (日志)
   grabbed_history?: string[];
   meta?: string[];
-  
   created_at: string;
   updated_at: string;
   expire_at: string;
@@ -43,253 +29,270 @@ interface TaskTableProps {
   loading: boolean;
   onRetry: (taskId: number) => void;
   onManualConfirm: (taskId: number) => void;
-  onEdit: (task: VasTask) => void;       // 编辑回调
-  onViewDetail: (task: VasTask) => void; // 详情回调 (可选,保留以备不时之需)
+  onEdit: (task: VasTask) => void;
+  onViewDetail: (task: VasTask) => void;
 }
 
 export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
-  // 控制展开行的 ID 集合
   const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
 
   const toggleRow = (id: number) => {
     const newSet = new Set(expandedRows);
-    if (newSet.has(id)) {
-      newSet.delete(id);
-    } else {
-      newSet.add(id);
-    }
+    if (newSet.has(id)) newSet.delete(id);
+    else newSet.add(id);
     setExpandedRows(newSet);
   };
 
-  // 辅助函数:获取数组最后一条
   const getLastItem = (arr: any) => {
     if (!Array.isArray(arr) || arr.length === 0) return null;
     const last = arr[arr.length - 1];
     return typeof last === 'string' ? last : JSON.stringify(last);
   };
 
-  // 辅助函数:从 UserInputs 提取摘要
   const getUserSummary = (inputs: any) => {
     if (!inputs) return '-';
     const name = inputs.name || inputs.applicant_name || inputs.first_name || inputs.full_name;
     const passport = inputs.passport || inputs.passport_no || inputs.passport_number;
-    
     if (name && passport) return `${name}`;
     if (name) return name;
     return 'User Data';
   };
 
-  if (loading) {
-    return (
-      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
-        <div className="text-gray-500 text-sm">加载系统任务中...</div>
-      </div>
-    );
-  }
+  // 提取状态徽章渲染逻辑
+  const renderStatus = (status: string) => (
+    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
+      ${status === 'failed' ? 'bg-red-100 text-red-700' : 
+        status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' : 
+        status === 'completed' ? 'bg-green-100 text-green-700' :
+        'bg-gray-100 text-gray-600'}`}>
+      {status}
+    </span>
+  );
 
-  if (tasks.length === 0) {
-    return (
-      <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
-        <div className="text-gray-500 text-sm">暂无待处理任务</div>
-      </div>
-    );
-  }
+  if (loading) return <div className="p-12 text-center text-gray-500 bg-white rounded-lg border border-slate-200">加载系统任务中...</div>;
+  if (tasks.length === 0) return <div className="p-12 text-center text-gray-500 bg-white rounded-lg border border-slate-200">暂无待处理任务</div>;
 
   return (
-    <div className="bg-white rounded-lg shadow overflow-hidden border border-slate-200">
-      <div className="overflow-x-auto">
-        {/* 使用 table-fixed 强制列宽,防止内容挤压 */}
-        <table className="min-w-full text-sm text-left table-fixed">
-          <thead className="bg-slate-50 border-b border-slate-200">
-            <tr>
-              <th className="w-12 px-4 py-3"></th>
-              <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
-              <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
-              <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
-              <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
-              <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
-              <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
-            </tr>
-          </thead>
-
-          {/* 
-            使用多个 tbody 是合法的 HTML 结构,
-            可以完美解决“展开行”导致的样式错位问题。
-          */}
-          {tasks.map((task) => {
-            const isExpanded = expandedRows.has(task.id);
-            const lastHistory = getLastItem(task.grabbed_history);
-            const lastMeta = getLastItem(task.meta);
-            
-            return (
-              <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
-                
-                {/* === 主行 === */}
-                <tr className="cursor-pointer" onClick={() => toggleRow(task.id)}>
-                  <td className="px-4 py-4 text-slate-400 align-top">
-                    {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
-                  </td>
+    <div className="space-y-4">
+      
+      {/* ======================= */}
+      {/* 1. Desktop View (Table) */}
+      {/* ======================= */}
+      <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 text-sm text-left table-fixed">
+            <thead className="bg-slate-50 border-b border-slate-200">
+              <tr>
+                <th className="w-12 px-4 py-3"></th>
+                <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
+                <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
+                <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
+                <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
+                <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
+                <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
+              </tr>
+            </thead>
+            {tasks.map((task) => {
+              const isExpanded = expandedRows.has(task.id);
+              const lastHistory = getLastItem(task.grabbed_history);
+              const lastMeta = getLastItem(task.meta);
+              
+              return (
+                <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
+                  <tr className="cursor-pointer" onClick={() => toggleRow(task.id)}>
+                    <td className="px-4 py-4 text-slate-400 align-top">
+                      {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
+                    </td>
+                    <td className="px-4 py-4 align-top">
+                      <div className="font-mono text-slate-500 text-xs mb-1">#{task.id}</div>
+                      <span className="font-mono text-[10px] text-blue-700 bg-blue-50 px-2 py-1 rounded border border-blue-100 inline-block break-all">
+                        {task.routing_key}
+                      </span>
+                    </td>
+                    <td className="px-4 py-4 align-top">
+                      <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
+                      <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
+                        <User size={12} className="text-slate-400 flex-shrink-0" /> {getUserSummary(task.user_inputs)}
+                      </div>
+                    </td>
+                    <td className="px-4 py-4 align-top">
+                      {lastHistory ? (
+                        <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
+                          <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
+                          {lastHistory}
+                        </div>
+                      ) : <span className="text-xs text-gray-300 italic">No history</span>}
+                    </td>
+                    <td className="px-4 py-4 align-top">
+                      {lastMeta ? (
+                        <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
+                          {lastMeta}
+                        </div>
+                      ) : <span className="text-xs text-gray-300 italic">No logs</span>}
+                      <div className="text-[10px] text-slate-300 mt-1 text-right">
+                        {new Date(task.updated_at).toLocaleTimeString()}
+                      </div>
+                    </td>
+                    <td className="px-4 py-4 align-top">
+                      {renderStatus(task.status)}
+                      {task.attempt_count > 0 && <div className="text-[10px] text-slate-400 mt-1 pl-1">Retry: {task.attempt_count}</div>}
+                    </td>
+                    <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
+                      <div className="flex justify-end gap-1 flex-wrap">
+                        <button onClick={() => onEdit(task)} className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-100"><Edit size={16} /></button>
+                        <button onClick={() => onRetry(task.id)} className="p-1.5 rounded text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-100"><RotateCcw size={16} /></button>
+                        <button onClick={() => onManualConfirm(task.id)} className="p-1.5 rounded text-green-600 hover:bg-green-50 border border-transparent hover:border-green-100"><CheckCircle size={16} /></button>
+                      </div>
+                    </td>
+                  </tr>
                   
-                  {/* 1. ID & Route */}
-                  <td className="px-4 py-4 align-top">
-                    <div className="font-mono text-slate-500 text-xs mb-1">#{task.id}</div>
-                    <span className="font-mono text-[10px] text-blue-700 bg-blue-50 px-2 py-1 rounded border border-blue-100 inline-block break-all">
-                      {task.routing_key}
-                    </span>
-                  </td>
-
-                  {/* 2. Order & User */}
-                  <td className="px-4 py-4 align-top">
-                    <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
-                    <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
-                      <User size={12} className="text-slate-400 flex-shrink-0" /> 
-                      {getUserSummary(task.user_inputs)}
-                    </div>
-                  </td>
+                  {/* Expanded Detail (Desktop Only) */}
+                  {isExpanded && (
+                    <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
+                      <td colSpan={7} className="px-4 py-4">
+                         <TaskDetailContent task={task} />
+                      </td>
+                    </tr>
+                  )}
+                </tbody>
+              );
+            })}
+          </table>
+        </div>
+      </div>
 
-                  {/* 3. 业务进度 (只显示最后一条) */}
-                  <td className="px-4 py-4 align-top">
-                    {lastHistory ? (
-                      <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
-                        <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
-                        {lastHistory}
-                      </div>
-                    ) : (
-                      <span className="text-xs text-gray-300 italic">No history</span>
-                    )}
-                  </td>
+      {/* ======================= */}
+      {/* 2. Mobile View (Cards)  */}
+      {/* ======================= */}
+      <div className="md:hidden space-y-4">
+        {tasks.map((task) => {
+          const isExpanded = expandedRows.has(task.id);
+          const lastHistory = getLastItem(task.grabbed_history);
 
-                  {/* 4. 系统调试 (只显示最后一条) */}
-                  <td className="px-4 py-4 align-top">
-                    {lastMeta ? (
-                      <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
-                        {lastMeta}
-                      </div>
-                    ) : (
-                      <span className="text-xs text-gray-300 italic">No logs</span>
-                    )}
-                    <div className="text-[10px] text-slate-300 mt-1 text-right">
-                      {new Date(task.updated_at).toLocaleTimeString()}
-                    </div>
-                  </td>
+          return (
+            <div key={task.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+              {/* Header: ID, Status, Route */}
+              <div className="flex justify-between items-start mb-3">
+                <div className="flex flex-col gap-1">
+                  <div className="flex items-center gap-2">
+                    <span className="text-sm font-bold text-slate-900">Task #{task.id}</span>
+                    {renderStatus(task.status)}
+                  </div>
+                  <span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit">{task.routing_key}</span>
+                </div>
+                {/* Mobile Actions */}
+                <div className="flex gap-2">
+                   <button onClick={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw size={18}/></button>
+                   <button onClick={() => onManualConfirm(task.id)} className="p-2 bg-green-50 text-green-600 rounded-lg active:scale-95"><CheckCircle size={18}/></button>
+                </div>
+              </div>
 
-                  {/* 5. 状态 */}
-                  <td className="px-4 py-4 align-top">
-                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
-                      ${task.status === 'failed' ? 'bg-red-100 text-red-700' : 
-                        task.status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' : 
-                        task.status === 'completed' ? 'bg-green-100 text-green-700' :
-                        'bg-gray-100 text-gray-600'}`}>
-                      {task.status}
-                    </span>
-                    {task.attempt_count > 0 && (
-                      <div className="text-[10px] text-slate-400 mt-1 pl-1">
-                        Retry: {task.attempt_count}
-                      </div>
-                    )}
-                  </td>
+              {/* Info Grid */}
+              <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
+                 <div className="flex justify-between">
+                   <span className="text-xs text-slate-400">Order ID</span>
+                   <span className="font-mono font-medium">{task.order_id}</span>
+                 </div>
+                 <div className="flex justify-between">
+                   <span className="text-xs text-slate-400">Applicant</span>
+                   <span className="font-medium truncate max-w-[150px]">{getUserSummary(task.user_inputs)}</span>
+                 </div>
+                 <div className="pt-2 border-t border-slate-200 mt-1">
+                   <p className="text-xs text-slate-400 mb-1">Latest History:</p>
+                   <p className="text-xs text-slate-700 line-clamp-2">{lastHistory || 'No history'}</p>
+                 </div>
+              </div>
 
-                  {/* 6. 操作按钮 */}
-                  <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
-                    <div className="flex justify-end gap-1 flex-wrap">
-                      <button 
-                        onClick={() => onEdit(task)} 
-                        className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 transition border border-transparent hover:border-indigo-100" 
-                        title="编辑任务配置"
-                      >
-                        <Edit size={16} />
-                      </button>
-                      <button 
-                        onClick={() => onRetry(task.id)} 
-                        className="p-1.5 rounded text-blue-600 hover:bg-blue-50 transition border border-transparent hover:border-blue-100" 
-                        title="重置回队列"
-                      >
-                        <RotateCcw size={16} />
-                      </button>
-                      <button 
-                        onClick={() => onManualConfirm(task.id)} 
-                        className="p-1.5 rounded text-green-600 hover:bg-green-50 transition border border-transparent hover:border-green-100" 
-                        title="强制完成"
-                      >
-                        <CheckCircle size={16} />
-                      </button>
-                    </div>
-                  </td>
-                </tr>
+              {/* Expand Button */}
+              <button 
+                onClick={() => toggleRow(task.id)}
+                className="w-full flex items-center justify-center py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-xs font-medium active:bg-slate-50 transition"
+              >
+                {isExpanded ? <>收起详情 <ChevronUp size={14} className="ml-1"/></> : <>查看日志 & 配置 <ChevronDown size={14} className="ml-1"/></>}
+              </button>
 
-                {/* === 展开详情行 (跨越所有列) === */}
-                {isExpanded && (
-                  <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
-                    <td colSpan={7} className="px-4 py-4">
-                      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
-                        
-                        {/* 左列:配置 & 表单 */}
-                        <div className="space-y-4">
-                          <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
-                            <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
-                              <FileJson size={14} /> Config (配置)
-                            </div>
-                            <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
-                              {JSON.stringify(task.config, null, 2)}
-                            </pre>
-                          </div>
-                          <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
-                            <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-blue-600 flex items-center gap-2">
-                              <User size={14} /> User Inputs (用户输入)
-                            </div>
-                            <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px]">
-                              {JSON.stringify(task.user_inputs, null, 2)}
-                            </pre>
-                          </div>
-                        </div>
+              {/* Expanded Detail (Mobile) */}
+              {isExpanded && (
+                <div className="mt-3 pt-3 border-t border-slate-100">
+                  <TaskDetailContent task={task} />
+                  <div className="mt-4 pt-4 border-t border-slate-100">
+                     <button onClick={() => onEdit(task)} className="w-full flex items-center justify-center py-2.5 bg-indigo-50 text-indigo-600 rounded-lg text-sm font-bold border border-indigo-100">
+                       <Edit size={16} className="mr-2"/> 编辑任务配置
+                     </button>
+                  </div>
+                </div>
+              )}
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
 
-                        {/* 中列:业务历史 (History) */}
-                        <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
-                          <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
-                            <History size={14} /> Grabbed History
-                          </div>
-                          <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
-                            {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
-                              task.grabbed_history.map((line, i) => (
-                                <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
-                                  <span className="text-slate-300 select-none w-6 text-right">{(i+1).toString()}</span>
-                                  <span className="break-all">{line}</span>
-                                </div>
-                              ))
-                            ) : (
-                              <span className="text-slate-400 italic">暂无业务日志</span>
-                            )}
-                          </div>
-                        </div>
+// === 提取详情内容组件,供 Desktop 和 Mobile 复用 ===
+function TaskDetailContent({ task }: { task: VasTask }) {
+  return (
+    <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
+      
+      {/* Config & Inputs */}
+      <div className="space-y-4">
+        <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
+          <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
+            <FileJson size={14} /> Config
+          </div>
+          <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px] whitespace-pre-wrap break-all">
+            {JSON.stringify(task.config, null, 2)}
+          </pre>
+        </div>
+        <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
+          <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-blue-600 flex items-center gap-2">
+            <User size={14} /> User Inputs
+          </div>
+          <pre className="p-3 overflow-x-auto text-slate-600 font-mono max-h-[200px] whitespace-pre-wrap break-all">
+            {JSON.stringify(task.user_inputs, null, 2)}
+          </pre>
+        </div>
+      </div>
 
-                        {/* 右列:调试日志 (Meta) */}
-                        <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
-                          <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
-                            <Terminal size={14} /> Meta / Debug Logs
-                          </div>
-                          <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
-                            {Array.isArray(task.meta) && task.meta.length > 0 ? (
-                              task.meta.map((line, i) => (
-                                <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
-                                  <span className="text-slate-600 mr-1">$</span>
-                                  <span>{line}</span>
-                                </div>
-                              ))
-                            ) : (
-                              <span className="text-slate-600 italic">暂无调试信息</span>
-                            )}
-                          </div>
-                        </div>
+      {/* History */}
+      <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
+        <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
+          <History size={14} /> Grabbed History
+        </div>
+        <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
+          {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
+            task.grabbed_history.map((line, i) => (
+              <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
+                <span className="text-slate-300 select-none w-5 text-right">{i+1}</span>
+                <span className="break-all">{line}</span>
+              </div>
+            ))
+          ) : (
+            <span className="text-slate-400 italic">暂无业务日志</span>
+          )}
+        </div>
+      </div>
 
-                      </div>
-                    </td>
-                  </tr>
-                )}
-              </tbody>
-            );
-          })}
-        </table>
+      {/* Meta/Debug */}
+      <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
+        <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
+          <Terminal size={14} /> Debug Logs
+        </div>
+        <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
+          {Array.isArray(task.meta) && task.meta.length > 0 ? (
+            task.meta.map((line, i) => (
+              <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
+                <span className="text-slate-600 mr-1">$</span>
+                <span>{line}</span>
+              </div>
+            ))
+          ) : (
+            <span className="text-slate-600 italic">暂无调试信息</span>
+          )}
+        </div>
       </div>
+
     </div>
   );
 }

+ 81 - 49
src/components/admin/tickets/TicketDetailModal.tsx

@@ -2,16 +2,15 @@
 
 import { useState, useEffect, useRef } from 'react';
 import api from '@/lib/api';
-import { X, Send, User, Shield, Check, Ban, RefreshCw, Loader2, MessageSquare } from 'lucide-react';
+import { X, Send, User, Shield, Check, Ban, RefreshCw, Loader2, MessageSquare, Info, MessageCircle } from 'lucide-react';
 import { AdminTicket } from './TicketTable';
-// 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
 interface TicketDetailModalProps {
   isOpen: boolean;
   onClose: () => void;
   ticket: AdminTicket | null;
-  onUpdate?: () => void; // 状态更新后刷新列表
+  onUpdate?: () => void;
 }
 
 interface TicketMessage {
@@ -30,7 +29,9 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
   const [statusUpdating, setStatusUpdating] = useState(false);
   const messagesEndRef = useRef<HTMLDivElement>(null);
 
-  // 加载消息
+  // 移动端 Tab 状态: 'detail' | 'chat'
+  const [activeTab, setActiveTab] = useState<'detail' | 'chat'>('chat');
+
   const fetchMessages = async () => {
     if (!ticket) return;
     setLoadingMsg(true);
@@ -39,7 +40,6 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
         params: { ticket_id: ticket.id, page: 1, size: 50 }
       });
       if (res.data.data?.items) {
-        // 按时间正序
         const sorted = res.data.data.items.sort((a: any, b: any) => 
           new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
         );
@@ -56,10 +56,10 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
   useEffect(() => {
     if (isOpen && ticket) {
       fetchMessages();
+      setActiveTab('chat'); // 默认进入聊天
     }
   }, [isOpen, ticket]);
 
-  // 发送回复
   const handleSend = async (e: React.FormEvent) => {
     e.preventDefault();
     if (!reply.trim() || !ticket) return;
@@ -81,11 +81,9 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
     }
   };
 
-  // 修改状态
   const handleStatusChange = async (status: 'resolved' | 'info_required' | 'rejected') => {
-    // 根据 API 要求,status 变更需要 comment
     const comment = prompt("请输入处理备注/回复给用户的内容 (必填):", "");
-    if (comment === null) return; // 取消
+    if (comment === null) return;
     if (!comment.trim()) {
       alert("备注不能为空");
       return;
@@ -93,7 +91,6 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
 
     setStatusUpdating(true);
     try {
-      // API: POST /api/vas/tickets/status?ticket_id=...
       await api.post(`/api/vas/tickets/status`, {
         status,
         comment
@@ -105,7 +102,6 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
       if (onUpdate) onUpdate();
       onClose();
     } catch (e: any) {
-      console.error(e);
       alert("操作失败: " + (e.response?.data?.message || '未知错误'));
     } finally {
       setStatusUpdating(false);
@@ -115,34 +111,71 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
   if (!isOpen || !ticket) 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-5xl h-[85vh] flex flex-col overflow-hidden animate-in zoom-in duration-200">
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
+      {/* 
+         容器:
+         - 移动端:w-full h-full (全屏), rounded-none
+         - 桌面端:max-w-5xl h-[85vh], rounded-xl
+      */}
+      <div className="bg-white w-full h-full md:w-full md:max-w-5xl md:h-[85vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
         
         {/* Header */}
-        <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
-          <div>
-            <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
-              工单 #{ticket.id} 
-              <span className="text-sm font-normal text-gray-500 bg-white border px-2 py-0.5 rounded-md">{ticket.type}</span>
-            </h3>
-            <div className="text-xs text-gray-500 mt-1 flex items-center gap-1">
-              用户: {ticket.user_id} | 订单: {ticket.order_id} | 创建: 
-              {/* 2. 使用 LocalTime 显示创建时间 */}
-              <LocalTime date={ticket.created_at} />
+        <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-gray-50 flex-shrink-0">
+          <div className="flex flex-col">
+            <div className="flex items-center gap-2">
+              <h3 className="font-bold text-gray-900 text-base md:text-lg">工单 #{ticket.id}</h3>
+              <span className="text-xs font-normal text-gray-500 bg-white border px-2 py-0.5 rounded-md">{ticket.type}</span>
+            </div>
+            <div className="text-xs text-gray-500 mt-1 hidden md:flex items-center gap-1">
+              用户: {ticket.user_id} | 订单: {ticket.order_id} | 创建: <LocalTime date={ticket.created_at} />
             </div>
           </div>
-          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 hover:bg-gray-200 rounded-full transition">
-            <X size={24} />
+          <button onClick={onClose} className="p-2 hover:bg-gray-200 rounded-full transition">
+            <X className="text-gray-500" size={24} />
+          </button>
+        </div>
+
+        {/* Mobile Tabs (仅移动端显示) */}
+        <div className="flex md:hidden border-b border-gray-200">
+          <button 
+            onClick={() => setActiveTab('chat')}
+            className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
+              activeTab === 'chat' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
+            }`}
+          >
+            <MessageCircle size={16} /> 对话
+          </button>
+          <button 
+            onClick={() => setActiveTab('detail')}
+            className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
+              activeTab === 'detail' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
+            }`}
+          >
+            <Info size={16} /> 详情与操作
           </button>
         </div>
 
         {/* Body Container */}
-        <div className="flex-1 flex overflow-hidden">
+        <div className="flex-1 flex overflow-hidden relative">
           
-          {/* 左侧:详情与操作 (30%) */}
-          <div className="w-80 border-r border-slate-200 bg-slate-50 flex flex-col overflow-y-auto">
+          {/* 
+             左侧:详情与操作 (Desktop: 30%, Mobile: 根据 Tab 显示)
+             使用 absolute + transform 在移动端做切换,或者简单的 hidden/block
+          */}
+          <div className={`
+            flex-col bg-slate-50 overflow-y-auto border-r border-slate-200
+            ${activeTab === 'detail' ? 'flex w-full' : 'hidden'} 
+            md:flex md:w-80
+          `}>
             <div className="p-5 space-y-6">
               
+              {/* 移动端显示的额外信息 */}
+              <div className="md:hidden bg-white p-3 rounded-lg border border-slate-200 text-xs text-gray-500 space-y-1">
+                <p>用户: {ticket.user_id}</p>
+                <p>订单: {ticket.order_id}</p>
+                <p>创建: <LocalTime date={ticket.created_at} /></p>
+              </div>
+
               {/* 状态卡片 */}
               <div className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
                 <label className="text-xs font-bold text-slate-400 uppercase block mb-2">当前状态</label>
@@ -166,21 +199,21 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
                   <button 
                     onClick={() => handleStatusChange('resolved')}
                     disabled={statusUpdating || ticket.status === 'resolved'}
-                    className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                    className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-300 text-white rounded-lg text-sm font-medium transition shadow-sm active:scale-95"
                   >
                     <Check size={16} /> 批准 / 已解决
                   </button>
                   <button 
                      onClick={() => handleStatusChange('info_required')}
                      disabled={statusUpdating || ticket.status === 'info_required'}
-                     className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                     className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300 text-white rounded-lg text-sm font-medium transition shadow-sm active:scale-95"
                   >
                     <RefreshCw size={16} /> 要求补充资料
                   </button>
                   <button 
                      onClick={() => handleStatusChange('rejected')}
                      disabled={statusUpdating || ticket.status === 'rejected'}
-                     className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-red-300 text-white rounded-lg text-sm font-medium transition shadow-sm"
+                     className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-red-600 hover:bg-red-700 disabled:bg-red-300 text-white rounded-lg text-sm font-medium transition shadow-sm active:scale-95"
                   >
                     <Ban size={16} /> 拒绝 / 驳回
                   </button>
@@ -190,9 +223,15 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
             </div>
           </div>
 
-          {/* 右侧:聊天记录 (70%) */}
-          <div className="flex-1 flex flex-col bg-white">
-            <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-slate-50/30">
+          {/* 
+             右侧:聊天记录 (Desktop: 70%, Mobile: 根据 Tab 显示) 
+          */}
+          <div className={`
+            flex-col bg-white
+            ${activeTab === 'chat' ? 'flex w-full' : 'hidden'}
+            md:flex md:flex-1
+          `}>
+            <div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-4 bg-slate-50/30">
               {loadingMsg ? (
                 <div className="h-full flex flex-col items-center justify-center text-gray-400">
                   <Loader2 className="animate-spin w-8 h-8 mb-2" />
@@ -208,13 +247,10 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
                   const isAdmin = msg.sender_type === 'admin' || msg.sender_type === 'system';
                   return (
                     <div key={msg.id} className={`flex ${isAdmin ? 'justify-end' : 'justify-start'}`}>
-                      <div className={`flex max-w-[80%] ${isAdmin ? 'flex-row-reverse' : 'flex-row'} items-end gap-2`}>
-                        {/* Avatar */}
+                      <div className={`flex max-w-[85%] md:max-w-[80%] ${isAdmin ? 'flex-row-reverse' : 'flex-row'} items-end gap-2`}>
                         <div className={`flex-shrink-0 h-8 w-8 rounded-full flex items-center justify-center ${isAdmin ? 'bg-purple-100 text-purple-600' : 'bg-gray-200 text-gray-600'}`}>
                           {isAdmin ? <Shield size={14} /> : <User size={14} />}
                         </div>
-                        
-                        {/* Bubble */}
                         <div>
                           <div className={`px-4 py-2 rounded-2xl text-sm shadow-sm whitespace-pre-wrap ${
                             isAdmin 
@@ -223,13 +259,9 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
                           }`}>
                             {msg.content}
                           </div>
-                          <div className={`text-[10px] text-gray-400 mt-1 px-1 flex items-center gap-1 ${isAdmin ? 'justify-end' : 'justify-start'}`}>
+                          <div className={`text-[10px] text-gray-400 mt-1 px-1 flex gap-1 items-center ${isAdmin ? 'justify-end' : 'justify-start'}`}>
                              {msg.sender_type === 'system' ? '系统' : (isAdmin ? '客服' : '用户')} • 
-                             {/* 3. 使用 LocalTime 显示消息时间 (简洁格式) */}
-                             <LocalTime 
-                               date={msg.created_at} 
-                               options={{ month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }}
-                             />
+                             <LocalTime date={msg.created_at} options={{ month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }} />
                           </div>
                         </div>
                       </div>
@@ -240,22 +272,22 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
               <div ref={messagesEndRef} />
             </div>
 
-            {/* Input */}
-            <div className="p-4 border-t bg-white">
+            {/* Input Area - 只有在聊天 Tab 且状态非关闭时才需要输入框,这里简化为一直显示 */}
+            <div className="p-3 md:p-4 border-t bg-white flex-shrink-0">
               <form onSubmit={handleSend} className="flex gap-2">
                 <input
                   type="text"
                   value={reply}
                   onChange={(e) => setReply(e.target.value)}
                   placeholder="输入回复内容..."
-                  className="flex-1 border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-purple-500 outline-none transition"
+                  className="flex-1 border border-slate-300 rounded-full px-4 py-2.5 text-sm focus:ring-2 focus:ring-purple-500 outline-none transition"
                 />
                 <button 
                   type="submit" 
                   disabled={sending || !reply.trim()}
-                  className="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium shadow-sm transition flex items-center gap-2"
+                  className="bg-purple-600 text-white p-2.5 rounded-full hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition flex-shrink-0"
                 >
-                  {sending ? <Loader2 className="animate-spin w-4 h-4" /> : <Send size={16} />} 发送
+                  {sending ? <Loader2 className="animate-spin w-5 h-5" /> : <Send size={20} />}
                 </button>
               </form>
             </div>

+ 107 - 49
src/components/admin/tickets/TicketTable.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { Eye, Clock, CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
+import { Eye, Clock, CheckCircle, XCircle, AlertCircle, HelpCircle, User, FileText } from 'lucide-react';
 // 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
@@ -25,7 +25,6 @@ interface TicketTableProps {
 
 export default function TicketTable({ tickets, loading, onViewDetail }: TicketTableProps) {
   
-  // Loading
   if (loading) {
     return (
       <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
@@ -34,7 +33,6 @@ export default function TicketTable({ tickets, loading, onViewDetail }: TicketTa
     );
   }
 
-  // Empty
   if (tickets.length === 0) {
     return (
       <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
@@ -69,54 +67,114 @@ export default function TicketTable({ tickets, loading, onViewDetail }: TicketTa
   };
 
   return (
-    <div className="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">
-          <thead className="bg-slate-50">
-            <tr>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">ID</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">类型</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">关联订单 / 用户</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">描述摘要</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">创建时间</th>
-              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
-            </tr>
-          </thead>
-          <tbody className="bg-white divide-y divide-slate-200">
-            {tickets.map((ticket) => (
-              <tr key={ticket.id} className="hover:bg-slate-50 transition-colors">
-                <td className="px-6 py-4 text-sm font-mono text-gray-500">#{ticket.id}</td>
-                <td className="px-6 py-4 text-sm font-bold text-gray-800">{getTypeText(ticket.type)}</td>
-                <td className="px-6 py-4">
-                  <div className="text-sm font-medium text-blue-600 font-mono">{ticket.order_id}</div>
-                  <div className="text-xs text-gray-500 mt-0.5">{ticket.user_id}</div>
-                </td>
-                <td className="px-6 py-4">
-                  <div className="text-sm text-gray-600 max-w-[200px] truncate" title={ticket.reason}>
-                    {ticket.reason}
-                  </div>
-                </td>
-                <td className="px-6 py-4 whitespace-nowrap">
-                  {getStatusBadge(ticket.status)}
-                </td>
-                <td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
-                  {/* 2. 使用 LocalTime 组件显示创建时间 */}
-                  <LocalTime date={ticket.created_at} />
-                </td>
-                <td className="px-6 py-4 text-right">
-                  <button 
-                    onClick={() => onViewDetail(ticket)}
-                    className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
-                  >
-                    <Eye size={16} className="mr-1" /> 处理/详情
-                  </button>
-                </td>
+    <div className="space-y-4">
+      
+      {/* ======================= */}
+      {/* 1. Desktop View (Table) - 仅在中大屏幕显示 */}
+      {/* ======================= */}
+      <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">
+            <thead className="bg-slate-50">
+              <tr>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">ID</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">类型</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">关联订单 / 用户</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">描述摘要</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">创建时间</th>
+                <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
               </tr>
-            ))}
-          </tbody>
-        </table>
+            </thead>
+            <tbody className="bg-white divide-y divide-slate-200">
+              {tickets.map((ticket) => (
+                <tr key={ticket.id} className="hover:bg-slate-50 transition-colors">
+                  <td className="px-6 py-4 text-sm font-mono text-gray-500">#{ticket.id}</td>
+                  <td className="px-6 py-4 text-sm font-bold text-gray-800">{getTypeText(ticket.type)}</td>
+                  <td className="px-6 py-4">
+                    <div className="text-sm font-medium text-blue-600 font-mono">{ticket.order_id}</div>
+                    <div className="text-xs text-gray-500 mt-0.5">{ticket.user_id}</div>
+                  </td>
+                  <td className="px-6 py-4">
+                    <div className="text-sm text-gray-600 max-w-[200px] truncate" title={ticket.reason}>
+                      {ticket.reason}
+                    </div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    {getStatusBadge(ticket.status)}
+                  </td>
+                  <td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
+                    <LocalTime date={ticket.created_at} />
+                  </td>
+                  <td className="px-6 py-4 text-right">
+                    <button 
+                      onClick={() => onViewDetail(ticket)}
+                      className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
+                    >
+                      <Eye size={16} className="mr-1" /> 处理/详情
+                    </button>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
       </div>
+
+      {/* ======================= */}
+      {/* 2. Mobile View (Cards) - 仅在小屏幕显示 */}
+      {/* ======================= */}
+      <div className="md:hidden space-y-4">
+        {tickets.map((ticket) => (
+          <div key={ticket.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+            {/* Header: ID + Status */}
+            <div className="flex justify-between items-start mb-3">
+              <div className="flex items-center gap-2">
+                <span className="text-sm font-mono text-slate-500 font-bold">#{ticket.id}</span>
+                <span className="text-sm font-bold text-slate-900">{getTypeText(ticket.type)}</span>
+              </div>
+              <div>{getStatusBadge(ticket.status)}</div>
+            </div>
+
+            {/* Info Grid */}
+            <div className="space-y-2 text-sm text-slate-600 mb-4 bg-slate-50 p-3 rounded-lg border border-slate-100">
+              <div className="flex justify-between">
+                <span className="flex items-center text-xs text-slate-400">
+                  <FileText size={12} className="mr-1"/> 订单号
+                </span>
+                <span className="font-mono text-blue-600">{ticket.order_id}</span>
+              </div>
+              <div className="flex justify-between">
+                <span className="flex items-center text-xs text-slate-400">
+                  <User size={12} className="mr-1"/> 用户
+                </span>
+                <span className="truncate max-w-[150px]">{ticket.user_id}</span>
+              </div>
+              <div className="flex justify-between">
+                <span className="flex items-center text-xs text-slate-400">
+                  <Clock size={12} className="mr-1"/> 时间
+                </span>
+                <span><LocalTime date={ticket.created_at} /></span>
+              </div>
+              
+              {/* 描述摘要 */}
+              <div className="pt-2 border-t border-slate-200 mt-2">
+                <span className="text-xs text-slate-400 block mb-1">描述:</span>
+                <p className="text-slate-800 line-clamp-2">{ticket.reason}</p>
+              </div>
+            </div>
+
+            {/* Action Button */}
+            <button 
+              onClick={() => onViewDetail(ticket)}
+              className="w-full flex items-center justify-center py-2.5 bg-blue-600 text-white rounded-lg text-sm font-bold active:scale-95 transition-transform shadow-sm shadow-blue-200"
+            >
+              <Eye size={16} className="mr-2" /> 处理工单
+            </button>
+          </div>
+        ))}
+      </div>
+
     </div>
   );
 }

+ 141 - 81
src/components/admin/users/UserTable.tsx

@@ -1,6 +1,6 @@
 'use client';
 
-import { Edit, Shield, CheckCircle, XCircle, User } from 'lucide-react';
+import { Edit, Shield, CheckCircle, XCircle, User, Mail, Phone, Clock } from 'lucide-react';
 
 // 定义用户类型 (参考 VasUserOut)
 export interface AdminUser {
@@ -9,7 +9,7 @@ export interface AdminUser {
   phone?: string;
   nickname?: string;
   role?: string; // 'admin' | 'user'
-  email_verified?: number | boolean; // 1 or 0, true or false
+  email_verified?: number | boolean; 
   created_at: string;
   avatar_url?: string;
 }
@@ -25,92 +25,152 @@ export default function UserTable({ users, loading, onEdit }: UserTableProps) {
     return <div className="p-12 text-center text-gray-500">加载用户数据中...</div>;
   }
 
-  return (
-    <div className="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">
-          <thead className="bg-slate-50">
-            <tr>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">用户</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">角色 (Role)</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">联系方式</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
-              <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">注册时间</th>
-              <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
-            </tr>
-          </thead>
-          <tbody className="bg-white divide-y divide-slate-200">
-            {users.map((user) => (
-              <tr key={user.id} className="hover:bg-slate-50 transition">
-                
-                {/* 用户基本信息 */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="flex items-center">
-                    <div className="h-10 w-10 flex-shrink-0 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 overflow-hidden">
-                      {user.avatar_url ? (
-                        <img src={user.avatar_url} alt="" className="h-full w-full object-cover" />
-                      ) : (
-                        <User size={20} />
-                      )}
-                    </div>
-                    <div className="ml-4">
-                      <div className="text-sm font-medium text-slate-900">{user.nickname || '未设置昵称'}</div>
-                      <div className="text-xs text-slate-500 font-mono">ID: {user.id}</div>
-                    </div>
-                  </div>
-                </td>
+  if (users.length === 0) {
+    return <div className="p-12 text-center text-gray-500">暂无用户数据</div>;
+  }
 
-                {/* 角色 */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  {user.role === 'admin' ? (
-                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
-                      <Shield size={12} className="mr-1" /> 管理员
-                    </span>
-                  ) : (
-                    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600">
-                      普通用户
-                    </span>
-                  )}
-                </td>
+  // --- 辅助渲染函数 (复用于 Desktop 和 Mobile) ---
 
-                {/* 联系方式 */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  <div className="text-sm text-slate-900">{user.email}</div>
-                  <div className="text-xs text-slate-500">{user.phone || '-'}</div>
-                </td>
+  const renderRole = (role?: string) => (
+    role === 'admin' ? (
+      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 border border-purple-200">
+        <Shield size={12} className="mr-1" /> 管理员
+      </span>
+    ) : (
+      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600 border border-slate-200">
+        普通用户
+      </span>
+    )
+  );
 
-                {/* 状态 (Email Verified) */}
-                <td className="px-6 py-4 whitespace-nowrap">
-                  {user.email_verified ? (
-                    <span className="text-green-600 flex items-center text-xs font-medium">
-                      <CheckCircle size={14} className="mr-1" /> 已验证
-                    </span>
-                  ) : (
-                    <span className="text-orange-500 flex items-center text-xs font-medium">
-                      <XCircle size={14} className="mr-1" /> 未验证
-                    </span>
-                  )}
-                </td>
+  const renderStatus = (verified?: number | boolean) => (
+    verified ? (
+      <span className="text-green-600 flex items-center text-xs font-medium">
+        <CheckCircle size={14} className="mr-1" /> 已验证
+      </span>
+    ) : (
+      <span className="text-orange-500 flex items-center text-xs font-medium">
+        <XCircle size={14} className="mr-1" /> 未验证
+      </span>
+    )
+  );
 
-                {/* 时间 */}
-                <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
-                  {new Date(user.created_at).toLocaleDateString()}
-                </td>
+  const renderAvatar = (user: AdminUser) => (
+    <div className="h-10 w-10 flex-shrink-0 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 overflow-hidden border border-slate-200">
+      {user.avatar_url ? (
+        <img src={user.avatar_url} alt="" className="h-full w-full object-cover" />
+      ) : (
+        <User size={20} />
+      )}
+    </div>
+  );
 
-                {/* 操作 */}
-                <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
-                  <button 
-                    onClick={() => onEdit(user)}
-                    className="text-blue-600 hover:text-blue-900 inline-flex items-center bg-blue-50 px-3 py-1.5 rounded hover:bg-blue-100 transition"
-                  >
-                    <Edit size={14} className="mr-1" /> 编辑
-                  </button>
-                </td>
+  return (
+    <div className="space-y-4">
+      
+      {/* ======================= */}
+      {/* 1. Desktop View (Table) */}
+      {/* ======================= */}
+      <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">
+            <thead className="bg-slate-50">
+              <tr>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">用户</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">角色</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">联系方式</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">状态</th>
+                <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">注册时间</th>
+                <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">操作</th>
               </tr>
-            ))}
-          </tbody>
-        </table>
+            </thead>
+            <tbody className="bg-white divide-y divide-slate-200">
+              {users.map((user) => (
+                <tr key={user.id} className="hover:bg-slate-50 transition">
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="flex items-center">
+                      {renderAvatar(user)}
+                      <div className="ml-4">
+                        <div className="text-sm font-medium text-slate-900">{user.nickname || '未设置昵称'}</div>
+                        <div className="text-xs text-slate-500 font-mono max-w-[100px] truncate" title={user.id}>{user.id}</div>
+                      </div>
+                    </div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">{renderRole(user.role)}</td>
+                  <td className="px-6 py-4 whitespace-nowrap">
+                    <div className="text-sm text-slate-900">{user.email}</div>
+                    <div className="text-xs text-slate-500">{user.phone || '-'}</div>
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap">{renderStatus(user.email_verified)}</td>
+                  <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
+                    {new Date(user.created_at).toLocaleDateString()}
+                  </td>
+                  <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
+                    <button 
+                      onClick={() => onEdit(user)}
+                      className="text-blue-600 hover:text-blue-900 inline-flex items-center bg-blue-50 px-3 py-1.5 rounded hover:bg-blue-100 transition"
+                    >
+                      <Edit size={14} className="mr-1" /> 编辑
+                    </button>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      {/* ======================= */}
+      {/* 2. Mobile View (Cards)  */}
+      {/* ======================= */}
+      <div className="md:hidden space-y-4">
+        {users.map((user) => (
+          <div key={user.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
+            
+            {/* Header: Avatar, Name, Role */}
+            <div className="flex justify-between items-start mb-4">
+              <div className="flex items-center gap-3">
+                {renderAvatar(user)}
+                <div>
+                  <div className="text-sm font-bold text-slate-900">{user.nickname || '未设置昵称'}</div>
+                  <div className="text-xs text-slate-400 font-mono">{user.id}</div>
+                </div>
+              </div>
+              <div>{renderRole(user.role)}</div>
+            </div>
+
+            {/* Info Grid */}
+            <div className="space-y-2 text-sm bg-slate-50 p-3 rounded-lg border border-slate-100 mb-4">
+              <div className="flex justify-between items-center">
+                <span className="text-slate-500 flex items-center gap-1 text-xs"><Mail size={12}/> 邮箱</span>
+                <span className="font-medium text-slate-700">{user.email}</span>
+              </div>
+              <div className="flex justify-between items-center">
+                <span className="text-slate-500 flex items-center gap-1 text-xs"><Phone size={12}/> 手机</span>
+                <span className="font-medium text-slate-700">{user.phone || '-'}</span>
+              </div>
+              <div className="flex justify-between items-center">
+                <span className="text-slate-500 flex items-center gap-1 text-xs"><Clock size={12}/> 注册</span>
+                <span className="text-slate-700">{new Date(user.created_at).toLocaleDateString()}</span>
+              </div>
+              <div className="flex justify-between items-center pt-1 border-t border-slate-200 mt-1">
+                <span className="text-slate-500 text-xs">账号状态</span>
+                {renderStatus(user.email_verified)}
+              </div>
+            </div>
+
+            {/* Actions */}
+            <button 
+              onClick={() => onEdit(user)}
+              className="w-full flex items-center justify-center py-2.5 bg-blue-50 text-blue-600 rounded-lg text-sm font-bold active:scale-95 transition-transform border border-blue-100"
+            >
+              <Edit size={16} className="mr-2" /> 编辑用户信息
+            </button>
+
+          </div>
+        ))}
       </div>
+
     </div>
   );
 }

+ 64 - 64
src/components/dashboard/OrderList.tsx

@@ -6,12 +6,11 @@ import api from '@/lib/api';
 import { CreditCard, Loader2, AlertCircle, Package, Clock, Hash, Eye, Search } from 'lucide-react';
 import Pagination from '@/components/common/Pagination';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-// 1. 引入新创建的时间组件
 import LocalTime from '@/components/common/LocalTime';
 
 export interface Order {
   id: string;
-  created_at: string; // API 返回的 UTC 时间字符串
+  created_at: string; 
   status: string;
   base_amount: number;
   base_currency: string;
@@ -75,10 +74,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
   };
 
   const handleSearch = () => fetchOrders(1);
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
+  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
   const formatMoney = (amount: number, currency: string) => {
     if (isNaN(amount)) return '0.00';
@@ -106,13 +102,14 @@ 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">
           <input 
             type="text" 
             placeholder={t('order.search_placeholder')} 
-            className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+            className="w-full pl-9 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
             value={keyword}
             onChange={(e) => setKeyword(e.target.value)}
             onKeyDown={handleKeyDown}
@@ -121,7 +118,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
         </div>
         <button 
           onClick={handleSearch}
-          className="ml-3 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 transition"
+          className="ml-3 px-4 py-2 bg-slate-100 text-slate-600 rounded-lg text-sm font-medium hover:bg-slate-200 transition whitespace-nowrap"
         >
           {t('common.search')}
         </button>
@@ -135,85 +132,88 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
         </div>
         
         {loading ? (
-          <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-gray-400" /></div>
+          <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600 w-8 h-8" /></div>
         ) : orders.length === 0 ? (
-          <div className="p-12 text-center">
-            <div className="mx-auto h-12 w-12 text-gray-300 mb-4 bg-gray-50 rounded-full flex items-center justify-center">
-              <AlertCircle className="w-6 h-6" />
-            </div>
-            <h3 className="mt-2 text-sm font-semibold text-gray-900">{t('order.empty_title')}</h3>
-            <p className="mt-1 text-sm text-gray-500">{t('order.empty_desc')}</p>
+          <div className="p-12 text-center text-gray-500">
+            <AlertCircle className="w-8 h-8 mx-auto mb-2 opacity-50" />
+            <h3 className="text-sm font-medium">{t('order.empty_title')}</h3>
+            <p className="text-xs mt-1 opacity-70">{t('order.empty_desc')}</p>
           </div>
         ) : (
-          <ul className="divide-y divide-gray-100">
+          <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 (
-                <li key={order.id} className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-6 hover:bg-slate-50 transition gap-4">
+                <div key={order.id} className="p-5 hover:bg-slate-50 transition flex flex-col sm:flex-row gap-4 sm:items-center">
                   
-                  {/* Left Info */}
-                  <div className="min-w-0 flex-1 space-y-2">
-                    <div className="flex items-start justify-between sm:justify-start sm:gap-4">
-                      <div className="flex items-center gap-2">
-                        <Package size={18} className="text-blue-600 flex-shrink-0" />
-                        <p className="text-base font-bold text-gray-900 leading-snug">{title}</p>
-                      </div>
-                      <div className="sm:hidden">{renderStatusBadge(order.status)}</div>
-                    </div>
-                    <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 pl-6 sm:pl-7">
-                      <div className="flex items-center gap-1 font-mono bg-slate-100 px-1.5 py-0.5 rounded">
-                        <Hash size={10} /> {order.id}
+                  {/* Left Info: Title & ID */}
+                  <div className="flex-1 min-w-0 space-y-2">
+                    <div className="flex justify-between items-start">
+                      <div className="flex items-start gap-3">
+                        <div className="p-2 bg-blue-50 text-blue-600 rounded-lg shrink-0 mt-0.5">
+                          <Package size={20} />
+                        </div>
+                        <div>
+                          <h3 className="text-sm font-bold text-slate-900 leading-snug line-clamp-2">{title}</h3>
+                          <div className="text-xs text-slate-400 mt-1 flex items-center gap-2 font-mono">
+                            <span className="flex items-center gap-1"><Hash size={10} /> {order.id}</span>
+                            <span className="w-px h-3 bg-slate-200"></span>
+                            <span className="flex items-center gap-1"><Clock size={10} /> <LocalTime date={order.created_at} /></span>
+                          </div>
+                        </div>
                       </div>
-                      <div className="flex items-center gap-1">
-                        <Clock size={12} /> 
-                        {/* 2. 使用 LocalTime 组件替换原始的 time 标签 */}
-                        <LocalTime date={order.created_at} />
+                      
+                      {/* Mobile Status Badge (Top Right) */}
+                      <div className="sm:hidden">
+                        {renderStatusBadge(order.status)}
                       </div>
                     </div>
                   </div>
                   
-                  {/* Right Actions */}
-                  <div className="flex w-full sm:w-auto items-center justify-between sm:justify-end gap-6 sm:pl-4 sm:border-l sm:border-slate-100">
-                    <div className="text-right">
-                      <p className="text-lg font-bold text-slate-900">{formatMoney(price, currency)}</p>
-                      <div className="hidden sm:block mt-1">{renderStatusBadge(order.status)}</div>
+                  {/* 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">
+                      {renderStatusBadge(order.status)}
                     </div>
+                  </div>
+                  
+                  {/* Actions: Mobile (Full Width) / Desktop (Row) */}
+                  <div className="grid grid-cols-2 sm:flex sm:items-center gap-2 pt-2 sm:pt-0 sm:pl-4 sm:border-l sm:border-slate-100">
                     
-                    <div className="flex items-center gap-2">
+                    <button
+                      onClick={() => onViewDetail(order)}
+                      className="col-span-1 sm:w-auto py-2 px-3 flex items-center justify-center rounded-lg border border-slate-200 text-slate-600 text-xs font-medium hover:bg-white hover:border-blue-300 hover:text-blue-600 active:scale-95 transition"
+                    >
+                      <Eye size={14} className="mr-1.5" /> {t('order.view_details')}
+                    </button>
+
+                    {order.status === 'pending' ? (
                       <button
-                        onClick={() => onViewDetail(order)}
-                        className="inline-flex items-center justify-center p-2 rounded-lg text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition border border-transparent hover:border-blue-100"
-                        title={t('order.view_details')}
+                        onClick={() => router.push(`/payment/${order.id}`)}
+                        className="col-span-1 sm:w-auto py-2 px-4 flex items-center justify-center rounded-lg bg-blue-600 text-white text-xs font-bold hover:bg-blue-700 active:scale-95 transition shadow-sm"
                       >
-                        <Eye size={20} />
+                        <CreditCard size={14} className="mr-1.5" /> {t('order.pay_now')}
                       </button>
-
-                      {order.status === 'pending' && (
-                        <button
-                          onClick={() => router.push(`/payment/${order.id}`)}
-                          className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 transition"
-                        >
-                          {t('order.pay_now')}
-                        </button>
-                      )}
-
-                      {(order.status === 'paid' || order.status === 'succeeded' || order.status === 'completed') && (
-                        <button
-                          onClick={() => onRequestTicket(order.id)}
-                          className="inline-flex items-center justify-center rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition"
-                        >
-                          {t('order.support')}
-                        </button>
-                      )}
-                    </div>
+                    ) : (
+                      <button
+                        onClick={() => onRequestTicket(order.id)}
+                        className="col-span-1 sm:w-auto py-2 px-4 flex items-center justify-center rounded-lg bg-white border border-slate-300 text-slate-700 text-xs font-medium hover:bg-slate-50 active:scale-95 transition"
+                      >
+                        {t('order.support')}
+                      </button>
+                    )}
                   </div>
-                </li>
+
+                </div>
               );
             })}
-          </ul>
+          </div>
         )}
         
         <Pagination 

+ 28 - 26
src/components/dashboard/TicketList.tsx

@@ -6,7 +6,6 @@ import { Loader2, MessageSquare, AlertCircle, Clock, CheckCircle, XCircle, Arrow
 import Pagination from '@/components/common/Pagination'; 
 import { UserTicket } from './UserTicketDetailModal';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-// 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
 interface TicketListProps {
@@ -67,13 +66,8 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
     }
   };
 
-  const handleSearch = () => {
-    fetchTickets(1);
-  };
-
-  const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') handleSearch();
-  };
+  const handleSearch = () => fetchTickets(1);
+  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
   const getStatusConfig = (status: string) => {
     const label = t(`ticket.status.${status}`) !== `ticket.status.${status}` 
@@ -112,7 +106,7 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
         </div>
         <button 
           onClick={handleSearch}
-          className="px-5 py-2 bg-slate-800 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition shadow-sm"
+          className="px-5 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium hover:bg-slate-700 transition shadow-sm whitespace-nowrap"
         >
           {t('common.search')}
         </button>
@@ -143,47 +137,48 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
               const isActionRequired = ticket.status === 'info_required';
               
               return (
-                <div key={ticket.id} className="p-5 hover:bg-slate-50 transition group">
+                <div key={ticket.id} className="p-5 hover:bg-slate-50 transition group flex flex-col gap-4">
+                  
+                  {/* Top Row: Info & Actions */}
                   <div className="flex flex-col sm:flex-row gap-4">
+                    
                     {/* Left Info */}
-                    <div className="flex-1">
-                      <div className="flex items-center flex-wrap gap-2 mb-2">
+                    <div className="flex-1 min-w-0">
+                      <div className="flex flex-wrap items-center gap-2 mb-2">
                         <span className="font-bold text-gray-900 text-base">{getTypeText(ticket.type)}</span>
                         <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border ${status.color}`}>
                           <StatusIcon size={12} /> {status.text}
                         </span>
-                        <span className="text-xs text-gray-400 font-mono">#{ticket.id}</span>
+                        <span className="text-xs text-gray-400 font-mono hidden sm:inline">#{ticket.id}</span>
                       </div>
                       
                       <div className="text-sm text-gray-600 mb-3 line-clamp-2">
                         {ticket.reason}
                       </div>
 
-                      <div className="flex items-center gap-4 text-xs text-gray-400">
+                      <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-400">
                         <span className="flex items-center gap-1">
-                          <FileText size={12} /> {t('ticket.order_id')}: {ticket.order_id}
+                          <FileText size={12} /> {t('ticket.order_id')}: <span className="font-mono text-slate-500">{ticket.order_id}</span>
                         </span>
                         <span className="flex items-center gap-1">
-                          <Clock size={12} /> 
-                          {/* 2. 使用 LocalTime 组件 */}
-                          <LocalTime date={ticket.created_at} />
+                          <Clock size={12} /> <LocalTime date={ticket.created_at} />
                         </span>
                       </div>
                     </div>
 
                     {/* Right Actions */}
-                    <div className="flex flex-col justify-center items-end gap-2 min-w-[120px]">
+                    <div className="flex flex-col sm:justify-center sm:items-end gap-2 w-full sm:w-auto mt-2 sm:mt-0 pt-3 sm:pt-0 border-t sm:border-0 border-slate-50">
                       {isActionRequired ? (
                         <button 
                           onClick={() => onViewDetail(ticket)}
-                          className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold shadow-sm shadow-blue-200 transition"
+                          className="w-full sm:w-auto flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold shadow-sm shadow-blue-200 transition active:scale-95"
                         >
                           {t('ticket.reply')} <ArrowRight size={16} />
                         </button>
                       ) : (
                         <button 
                           onClick={() => onViewDetail(ticket)}
-                          className="w-full flex items-center justify-center gap-2 px-4 py-2 border border-slate-200 rounded-lg text-slate-600 hover:bg-white hover:border-blue-400 hover:text-blue-600 text-sm font-medium transition bg-slate-50"
+                          className="w-full sm:w-auto flex items-center justify-center gap-2 px-4 py-2.5 border border-slate-200 rounded-lg text-slate-600 hover:bg-white hover:border-blue-400 hover:text-blue-600 text-sm font-medium transition bg-white sm:bg-transparent active:scale-95"
                         >
                           <Eye size={16} /> {t('ticket.view_details')}
                         </button>
@@ -191,16 +186,23 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
                     </div>
                   </div>
 
-                  {/* Admin Feedback */}
+                  {/* === Admin Feedback (显示在底部,全宽) === */}
                   {ticket.admin_comment && (
-                    <div className="mt-4 bg-slate-100/80 border-l-4 border-blue-400 p-3 rounded-r text-sm text-slate-700 flex gap-2">
-                       <MessageSquare className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" />
+                    <div className="bg-blue-50/50 border border-blue-100 rounded-lg p-3 flex gap-3 text-sm animate-in fade-in slide-in-from-top-1">
+                       <div className="mt-0.5 p-1 bg-blue-100 rounded text-blue-600 h-fit">
+                          <MessageSquare size={14} />
+                       </div>
                        <div>
-                         <span className="font-bold text-slate-900 mr-1">{t('ticket.latest_feedback')}:</span>
-                         {ticket.admin_comment}
+                         <span className="font-bold text-slate-900 block mb-1 text-xs uppercase tracking-wide opacity-80">
+                           {t('ticket.latest_feedback')}
+                         </span>
+                         <p className="text-slate-700 leading-relaxed">
+                           {ticket.admin_comment}
+                         </p>
                        </div>
                     </div>
                   )}
+
                 </div>
               );
             })}

+ 3 - 2
src/components/dashboard/TicketModal.tsx

@@ -3,7 +3,6 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { Loader2, X, AlertTriangle } from 'lucide-react';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface TicketModalProps {
@@ -14,7 +13,6 @@ interface TicketModalProps {
 }
 
 export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId = '' }: TicketModalProps) {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
 
   const [loading, setLoading] = useState<boolean>(false);
@@ -45,6 +43,9 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
     try {
       await api.post('/api/vas/ticket/create', form);
       
+      // === 新增:成功提示 ===
+      alert(t('ticket.create_success'));
+
       if (onSuccess) onSuccess(); 
       onClose();
       

+ 31 - 35
src/components/dashboard/UserTicketDetailModal.tsx

@@ -4,7 +4,6 @@ import { useState, useEffect, useRef } from 'react';
 import api from '@/lib/api';
 import { X, Send, User, Headset, Paperclip, Loader2, Clock, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-// 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 
 export interface UserTicket {
@@ -123,52 +122,53 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
     );
   };
 
-  // 2. 移除了 formatTime 辅助函数,改用 LocalTime
-
   if (!isOpen || !ticket) return null;
 
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
-      <div className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" onClick={onClose} />
+    // 修改 1: 移动端全屏容器 (p-0, h-full, w-full)
+    <div className="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-6 bg-black/50 backdrop-blur-sm">
+      
+      {/* 遮罩层 (点击关闭) */}
+      <div className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity hidden sm:block" onClick={onClose} />
 
-      <div className="relative w-full max-w-2xl h-[80vh] bg-white rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in duration-200">
+      {/* 弹窗主体 */}
+      <div className="relative w-full h-full sm:max-w-2xl sm:h-[80vh] bg-white sm:rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in duration-200">
         
         {/* Header */}
-        <div className="px-6 py-4 border-b bg-gray-50 flex justify-between items-center flex-shrink-0">
-          <div>
-            <div className="flex items-center gap-3">
-              <h3 className="font-bold text-gray-900 text-lg">
+        <div className="px-4 py-3 sm:px-6 sm:py-4 border-b bg-gray-50 flex justify-between items-center flex-shrink-0 safe-area-top">
+          <div className="min-w-0 flex-1 mr-4">
+            <div className="flex items-center flex-wrap gap-2">
+              <h3 className="font-bold text-gray-900 text-lg truncate">
                 {t('ticket.title_prefix')} #{ticket.id}
               </h3>
               {renderStatus(ticket.status)}
             </div>
-            <p className="text-xs text-gray-500 mt-1">
+            <p className="text-xs text-gray-500 mt-1 truncate">
               {t('ticket.related_order')}: <span className="font-mono">{ticket.order_id}</span>
             </p>
           </div>
-          <button onClick={onClose} className="p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition">
-            <X size={20} />
+          <button onClick={onClose} className="p-2 text-gray-400 hover:text-gray-600 rounded-full hover:bg-gray-100 transition flex-shrink-0">
+            <X size={24} />
           </button>
         </div>
 
         {/* Messages List */}
-        <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6">
+        <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6 overscroll-contain">
           
           {/* 原始工单描述 */}
           <div className="flex justify-center">
             <div className="bg-white border border-gray-200 text-gray-600 text-xs px-4 py-2 rounded-full shadow-sm flex items-center gap-1">
               {t('ticket.created_at')} 
-              {/* 3. 替换为 LocalTime */}
               <LocalTime date={ticket.created_at} />
             </div>
           </div>
           <div className="flex justify-end">
-            <div className="flex flex-row-reverse items-end gap-2 max-w-[85%]">
+            <div className="flex flex-row-reverse items-end gap-2 max-w-[90%] sm:max-w-[85%]">
                <div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
                  <User size={14} className="text-blue-600" />
                </div>
-               <div>
-                  <div className="bg-blue-600 text-white px-4 py-3 rounded-2xl rounded-tr-none shadow-sm text-sm">
+               <div className="min-w-0">
+                  <div className="bg-blue-600 text-white px-4 py-3 rounded-2xl rounded-tr-none shadow-sm text-sm break-words">
                     <p className="font-bold text-xs text-blue-100 mb-1 border-b border-blue-500 pb-1">{t('ticket.description_title')}</p>
                     {ticket.reason}
                   </div>
@@ -186,14 +186,10 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
 
               if (isSystem) {
                 return (
-                  <div key={msg.id} className="flex justify-center my-4">
-                    <span className="text-[10px] text-gray-400 bg-gray-100 px-2 py-1 rounded flex items-center gap-1">
-                      {t('ticket.system_message')}: {msg.content} - 
-                      {/* 3. 系统消息时间:只显示时分 */}
-                      <LocalTime 
-                        date={msg.created_at} 
-                        options={{ hour: '2-digit', minute: '2-digit' }}
-                      />
+                  <div key={msg.id} className="flex justify-center my-4 px-4">
+                    <span className="text-[10px] text-gray-400 bg-gray-100 px-3 py-1.5 rounded-full flex flex-wrap justify-center items-center gap-1 text-center">
+                      <span className="font-bold">{t('ticket.system_message')}:</span> {msg.content} 
+                      <span className="opacity-60">- <LocalTime date={msg.created_at} options={{ hour: '2-digit', minute: '2-digit' }}/></span>
                     </span>
                   </div>
                 );
@@ -201,7 +197,7 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
 
               return (
                 <div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
-                  <div className={`flex items-end gap-2 max-w-[80%] ${isMe ? 'flex-row-reverse' : 'flex-row'}`}>
+                  <div className={`flex items-end gap-2 max-w-[90%] sm:max-w-[80%] ${isMe ? 'flex-row-reverse' : 'flex-row'}`}>
                     
                     {/* 头像 */}
                     <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
@@ -211,8 +207,8 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                     </div>
 
                     {/* 气泡 */}
-                    <div className={`flex flex-col ${isMe ? 'items-end' : 'items-start'}`}>
-                      <div className={`px-4 py-2 text-sm shadow-sm ${
+                    <div className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} min-w-0`}>
+                      <div className={`px-4 py-2.5 text-sm shadow-sm break-words ${
                         isMe 
                           ? 'bg-blue-600 text-white rounded-2xl rounded-tr-none' 
                           : 'bg-white text-gray-800 border border-gray-100 rounded-2xl rounded-tl-none'
@@ -221,7 +217,6 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                       </div>
                       <span className="text-[10px] text-gray-400 mt-1 px-1 flex items-center gap-1">
                         {isMe ? t('ticket.sender_me') : t('ticket.sender_support')} • 
-                        {/* 3. 聊天消息时间:简洁格式 (月/日 时:分) */}
                         <LocalTime 
                           date={msg.created_at} 
                           options={{ month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }}
@@ -238,10 +233,10 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
           <div ref={messagesEndRef} />
         </div>
 
-        {/* Input */}
-        <div className="p-4 bg-white border-t border-gray-100 flex-shrink-0">
+        {/* Input - 修改 2: 移动端底部适配 (pb-safe) */}
+        <div className="p-3 sm:p-4 bg-white border-t border-gray-100 flex-shrink-0 safe-area-bottom">
           <form onSubmit={handleSend} className="flex items-end gap-2">
-            <button type="button" className="p-3 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-50 transition" title={t('ticket.upload_tooltip')}>
+            <button type="button" className="p-2 sm:p-3 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-50 transition" title={t('ticket.upload_tooltip')}>
               <Paperclip size={20} />
             </button>
             <div className="flex-1 bg-gray-50 rounded-xl border border-gray-200 focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 focus-within:bg-white transition-all">
@@ -263,12 +258,13 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
             <button 
               type="submit" 
               disabled={sending || !replyContent.trim()}
-              className="p-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition-all flex items-center justify-center"
+              className="p-2.5 sm:p-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition-all flex items-center justify-center flex-shrink-0"
             >
               {sending ? <Loader2 className="animate-spin w-5 h-5" /> : <Send size={20} />}
             </button>
           </form>
-          <div className="text-center mt-2">
+          {/* 移动端提示简化 */}
+          <div className="text-center mt-2 hidden sm:block">
              <p className="text-[10px] text-gray-400">{t('ticket.urgent_tip')}</p>
           </div>
         </div>

+ 13 - 4
src/lib/i18n/locales/en.ts

@@ -32,7 +32,6 @@ export const en = {
   },
   auth: {
     welcome_back: 'Welcome Back',
-    auto_register: 'Auto Register',
     email_label: 'Email',
     email_placeholder: 'name@example.com',
     password_label: 'Password',
@@ -42,6 +41,11 @@ export const en = {
     no_account: "Don't have an account? Auto Register",
     has_account: 'Already have an account? Login',
     login_success_no_token: 'Login successful but token missing',
+    auto_register: 'Quick Start',
+    auto_register_success: 'Registration successful! Redirecting...',
+    auto_reg_title: 'Instant Account',
+    auto_reg_desc: 'No password required. The system will create a secure account for you. Please bind email later to prevent data loss.',
+    auto_reg_btn_action: 'Get Started Now',
   },
   forgot_password: {
     title: 'Forgot Password',
@@ -212,6 +216,7 @@ export const en = {
     send_failed: 'Send failed, please try again later',
     upload_tooltip: 'Upload attachment (Coming soon)',
     urgent_tip: 'For urgent matters, please email support@visafly.top',
+    create_success: 'Ticket submitted successfully! We will process it shortly',
     types: {
       refund: 'Refund Request',
       dispute: 'Dispute',
@@ -359,15 +364,19 @@ export const en = {
     exchange_rate: 'Exchange Rate',
     random_discount: 'Random Discount',
     actual_pay: 'Total to Pay',
-    link_pay_hint: 'Click the button below to proceed to payment',
     go_to_pay: 'Pay Now',
-    qr_pay_hint: 'Please scan the QR code to pay',
-    completed_btn: 'I have paid, check order status',
     link_gen_failed: 'Failed to generate payment link',
     qr_gen_failed: 'Failed to get payment QR code',
     unsupported_channel: 'Unsupported payment channel',
     active_payment_exists: 'There is already an active payment for this order.',
     init_failed: 'Payment initialization failed',
+    link_pay_hint: 'Click the button below to pay, then return here:',
+    qr_pay_hint: 'Scan to pay, then simply click the confirm button below:',
+    completed_btn: 'I Have Paid, Confirm Now',
+    confirm_hint_text: 'Important: After completing the payment, you MUST click the button below. This notifies our system to verify your transaction immediately.',
+    confirm_subtext: 'System will verify automatically upon clicking',
+    confirm_success_alert: 'Confirmation received! We are verifying your payment now. Please check order status shortly.',
+    confirm_failed: 'Submission failed',
   },
   privacy_content: {
     last_updated: 'Last Updated: January 1, 2025',

+ 13 - 4
src/lib/i18n/locales/zh.ts

@@ -32,7 +32,6 @@ export const zh = {
   },
   auth: {
     welcome_back: '欢迎回来',
-    auto_register: '自动注册',
     email_label: '邮箱',
     email_placeholder: 'name@example.com',
     password_label: '密码',
@@ -42,6 +41,11 @@ export const zh = {
     no_account: '没有账号?点击自动注册',
     has_account: '已有账号?点击登录',
     login_success_no_token: '登录成功,但未获取到 Token',
+    auto_register: '快速开始',
+    auto_register_success: '注册成功!正在进入控制台...',
+    auto_reg_title: '一键生成账户',
+    auto_reg_desc: '无需设置密码,系统将为您创建一个安全账户。进入控制台后建议绑定邮箱以防丢失。',
+    auto_reg_btn_action: '立即进入',
   },
   forgot_password: {
     title: '找回密码',
@@ -212,6 +216,7 @@ export const zh = {
     send_failed: '发送失败,请稍后重试',
     upload_tooltip: '上传附件(暂不可用)',
     urgent_tip: '如需紧急处理,请发送邮件至 support@visafly.top',
+    create_success: '工单提交成功!我们会尽快为您处理',
     types: {
       refund: '退款申请',
       dispute: '交易纠纷',
@@ -359,15 +364,19 @@ export const zh = {
     exchange_rate: '参考汇率',
     random_discount: '随机立减',
     actual_pay: '实际需付',
-    link_pay_hint: '点击下方按钮前往安全支付页面',
     go_to_pay: '前往支付',
-    qr_pay_hint: '请使用 App 扫码支付',
-    completed_btn: '我已完成支付,查看订单状态',
     link_gen_failed: '支付链接生成失败',
     qr_gen_failed: '未获取到支付二维码',
     unsupported_channel: '不支持的支付渠道',
     active_payment_exists: '当前订单已有一个未完成的支付,请稍后再试或联系客服。',
     init_failed: '支付初始化失败',
+    link_pay_hint: '请点击下方蓝色按钮进行支付,支付完成后返回此页:',
+    qr_pay_hint: '请使用 App 扫码支付,支付完成后请务必点击下方确认:',
+    completed_btn: '我已付款,点击确认', // 更直接
+    confirm_hint_text: '重要提示:支付成功后,请务必点击下方的“我已付款”按钮。系统将根据您的确认通知财务查账,否则订单可能无法及时处理。',
+    confirm_subtext: '点击后系统将自动进行核验',
+    confirm_success_alert: '已收到您的付款确认!系统正在加急核对账单,请留意订单状态变化。',
+    confirm_failed: '提交确认失败',
   },
   privacy_content: {
     last_updated: '最后更新日期:2025年1月1日',