jerry il y a 4 mois
Parent
commit
4477f9b5d6
46 fichiers modifiés avec 2772 ajouts et 1071 suppressions
  1. 144 204
      package-lock.json
  2. 1 0
      package.json
  3. 128 0
      src/app/admin/cards/page.tsx
  4. 45 0
      src/app/cookie-policy/page.tsx
  5. 18 11
      src/app/create-order/[id]/page.tsx
  6. 86 51
      src/app/dashboard/page.tsx
  7. 14 10
      src/app/dashboard/settings/page.tsx
  8. 20 35
      src/app/knowledge/page.tsx
  9. 14 10
      src/app/layout.tsx
  10. 29 23
      src/app/page.tsx
  11. 40 0
      src/app/privacy/page.tsx
  12. 96 32
      src/app/refund-policy/page.tsx
  13. 9 1
      src/app/services/page.js
  14. 16 15
      src/app/slots/page.tsx
  15. 15 15
      src/components/AuthForm.tsx
  16. 53 64
      src/components/BindEmailModal.tsx
  17. 25 18
      src/components/CreateOrderForm.tsx
  18. 26 12
      src/components/Footer.tsx
  19. 45 44
      src/components/ForgotPasswordModal.tsx
  20. 33 18
      src/components/Navbar.tsx
  21. 49 65
      src/components/PaymentProcessor.tsx
  22. 26 64
      src/components/ServiceList.tsx
  23. 3 1
      src/components/admin/AdminSidebar.tsx
  24. 301 0
      src/components/admin/cards/CardModal.tsx
  25. 95 0
      src/components/admin/cards/CardTable.tsx
  26. 8 2
      src/components/admin/orders/OrderDetailModal.tsx
  27. 7 4
      src/components/admin/orders/OrderTable.tsx
  28. 14 19
      src/components/admin/payments/QrManager.tsx
  29. 14 5
      src/components/admin/tickets/TicketDetailModal.tsx
  30. 4 1
      src/components/admin/tickets/TicketTable.tsx
  31. 115 41
      src/components/common/JsonEditor.tsx
  32. 19 0
      src/components/common/LanguageSwitcher.tsx
  33. 68 0
      src/components/common/LocalTime.tsx
  34. 13 8
      src/components/common/Pagination.tsx
  35. 23 34
      src/components/dashboard/ChangePasswordModal.tsx
  36. 31 58
      src/components/dashboard/OrderList.tsx
  37. 50 65
      src/components/dashboard/ProfileSettings.tsx
  38. 67 14
      src/components/dashboard/Sidebar.tsx
  39. 34 41
      src/components/dashboard/TicketList.tsx
  40. 26 19
      src/components/dashboard/TicketModal.tsx
  41. 29 19
      src/components/dashboard/UserOrderDetailModal.tsx
  42. 49 40
      src/components/dashboard/UserTicketDetailModal.tsx
  43. 14 8
      src/components/knowledge/KnowledgeCard.tsx
  44. 70 0
      src/lib/i18n/LanguageContext.tsx
  45. 393 0
      src/lib/i18n/locales/en.ts
  46. 393 0
      src/lib/i18n/locales/zh.ts

Fichier diff supprimé car celui-ci est trop grand
+ 144 - 204
package-lock.json


+ 1 - 0
package.json

@@ -14,6 +14,7 @@
         "next": "14.1.0",
         "react": "^18",
         "react-dom": "^18",
+        "react-is": "^19.2.3",
         "recharts": "^3.6.0"
     },
     "devDependencies": {

+ 128 - 0
src/app/admin/cards/page.tsx

@@ -0,0 +1,128 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import api from '@/lib/api';
+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'; // 确保你有定义这个类型
+
+export default function AdminCardsPage() {
+  // 初始化为空数组,防止 undefined 报错
+  const [cards, setCards] = useState<any[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [total, setTotal] = useState(0);
+  
+  // 筛选与分页
+  const [page, setPage] = useState(1);
+  const [pageSize] = useState(10);
+  const [keyword, setKeyword] = useState('');
+  
+  // 弹窗状态
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [selectedCard, setSelectedCard] = useState<CardData | null>(null);
+
+  useEffect(() => {
+    fetchCards(1);
+  }, []);
+
+  const fetchCards = async (targetPage: number) => {
+    setLoading(true);
+    try {
+      const res = await api.get('/api/cards/view2', {
+        params: {
+          keyword: keyword,
+          page: targetPage,
+          size: pageSize,
+          culture: 'english'
+        }
+      });
+
+      // === 核心修复:适配 API 返回结构 ===
+      // 结构: { code: 0, data: { items: [...], total: 10 } }
+      const responseBody = res.data;
+      const pageData = responseBody?.data; 
+
+      if (pageData && Array.isArray(pageData.items)) {
+        setCards(pageData.items);
+        setTotal(pageData.total || 0);
+      } else {
+        // 如果数据结构不对,或者 items 为空,重置为空数组
+        setCards([]);
+        setTotal(0);
+      }
+      
+      setPage(targetPage);
+    } catch (e) {
+      console.error("Fetch cards failed", e);
+      setCards([]); // 出错时确保是空数组
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = () => fetchCards(1);
+
+  const openCreate = () => {
+    setSelectedCard(null);
+    setIsModalOpen(true);
+  };
+
+  const openEdit = (card: CardData) => {
+    setSelectedCard(card);
+    setIsModalOpen(true);
+  };
+
+  return (
+    <div>
+      <div className="flex justify-between items-center 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">
+            <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"
+              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>
+      </div>
+
+      <CardTable 
+        cards={cards} 
+        loading={loading} 
+        onEdit={openEdit} 
+      />
+
+      <div className="mt-4">
+        <Pagination 
+          currentPage={page} 
+          total={total} 
+          pageSize={pageSize} 
+          onPageChange={fetchCards} 
+        />
+      </div>
+
+      <CardModal 
+        isOpen={isModalOpen} 
+        onClose={() => setIsModalOpen(false)} 
+        onSuccess={() => fetchCards(page)}
+        card={selectedCard}
+      />
+    </div>
+  );
+}

+ 45 - 0
src/app/cookie-policy/page.tsx

@@ -0,0 +1,45 @@
+'use client';
+
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { Cookie } from 'lucide-react';
+
+export default function CookiePolicyPage() {
+  const { t } = useLanguage();
+
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+      <div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-sm border border-slate-200 p-8 sm:p-12">
+        
+        {/* Header */}
+        <div className="flex items-center gap-3 mb-8 border-b border-slate-100 pb-6">
+          <div className="p-3 bg-orange-100 text-orange-600 rounded-lg">
+            <Cookie size={32} />
+          </div>
+          {/* 使用 footer.cookie 作为标题 (Cookie 政策) */}
+          <h1 className="text-3xl font-bold text-slate-900">{t('footer.cookie')}</h1>
+        </div>
+
+        {/* Content */}
+        <div className="prose prose-slate max-w-none text-slate-600 leading-relaxed">
+          <p className="text-sm text-slate-400 mb-8">{t('cookie_content.last_updated')}</p>
+          
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('cookie_content.section1_title')}</h3>
+          <p>{t('cookie_content.section1_desc')}</p>
+
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('cookie_content.section2_title')}</h3>
+          <ul className="list-disc pl-5 space-y-2">
+            <li>
+              <strong>{t('cookie_content.section2_item1_label')}</strong> {t('cookie_content.section2_item1_desc')}
+            </li>
+            <li>
+              <strong>{t('cookie_content.section2_item2_label')}</strong> {t('cookie_content.section2_item2_desc')}
+            </li>
+          </ul>
+
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('cookie_content.section3_title')}</h3>
+          <p>{t('cookie_content.section3_desc')}</p>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 18 - 11
src/app/create-order/[id]/page.tsx

@@ -1,34 +1,41 @@
 'use client';
 
-import CreateOrderForm from '@/components/CreateOrderForm'; // 如果报错,尝试 ../../../components/CreateOrderForm
+import CreateOrderForm from '@/components/CreateOrderForm';
 import { useEffect, useState } from 'react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
-// 这里暂时硬编码商品名称映射,或者你可以稍后改为从 API 获取详情
+// 模拟 API 数据源
+// 注意:这里不需要做中英文映射。在真实场景中,后端 API 会根据请求头返回对应语言的标题,
+// 或者前端只负责展示拿到的数据。
 const PRODUCT_MAP: Record<string, string> = {
-  '1': '日本单次旅游签证',
+  '1': '日本单次旅游签证', 
   '2': '泰国电子签',
   '3': '美国B1/B2咨询',
-  // 如果你的真实ID是其他数字,请在这里添加
 };
 
 export default function CreateOrderPage({ params }: { params: { id: string } }) {
-  const [productName, setProductName] = useState('加载中...');
+  // 1. 获取翻译函数
+  const { t } = useLanguage();
+  
+  // 2. 初始化状态,默认值可以用翻译后的"加载中"
+  const [productName, setProductName] = useState(t('common.loading'));
 
   useEffect(() => {
-    // 如果有获取商品详情的 API,应该在这里调用
-    // 目前先用 ID 查找简单的名字,或者直接显示 ID
-    const name = PRODUCT_MAP[params.id] || `未知商品 (ID: ${params.id})`;
+    // 模拟 API 请求逻辑
+    // 这里我们只负责展示获取到的数据,不对数据内容本身进行翻译处理
+    const name = PRODUCT_MAP[params.id] || `Unknown Product (ID: ${params.id})`;
     setProductName(name);
-  }, [params.id]);
+  }, [params.id]); 
 
   return (
     <div className="min-h-screen bg-slate-50 py-12 px-4">
       <div className="max-w-3xl mx-auto">
         <button 
           onClick={() => window.history.back()} 
-          className="mb-6 text-gray-500 hover:text-gray-900 flex items-center text-sm"
+          className="mb-6 text-gray-500 hover:text-gray-900 flex items-center text-sm transition-colors"
         >
-          ← 返回服务列表
+          {/* 3. 只翻译静态的按钮文案 */}
+          ← {t('common.back')}
         </button>
         
         <CreateOrderForm 

+ 86 - 51
src/app/dashboard/page.tsx

@@ -1,107 +1,155 @@
 'use client';
 
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 import { useRouter } from 'next/navigation';
 import { LogOut, Plus } from 'lucide-react';
 
-// 引入各个业务组件
+// 引入组件
 import Sidebar from '@/components/dashboard/Sidebar';
 import OrderList from '@/components/dashboard/OrderList';
 import TicketList from '@/components/dashboard/TicketList';
 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() {
   const router = useRouter();
+  const { t } = useLanguage();
   
-  // 当前激活的标签页: 'orders' | 'tickets' | 'settings'
   const [activeTab, setActiveTab] = useState<string>('orders');
   
-  // --- 状态管理 ---
-
-  // 1. 新建工单弹窗
+  // 状态管理
   const [isTicketModalOpen, setIsTicketModalOpen] = useState<boolean>(false);
   const [ticketDefaultOrderId, setTicketDefaultOrderId] = useState<string>('');
-
-  // 2. 订单详情弹窗
   const [isOrderDetailOpen, setIsOrderDetailOpen] = useState<boolean>(false);
   const [selectedOrder, setSelectedOrder] = useState<UserOrder | null>(null);
-
-  // 3. 工单详情/回复弹窗
   const [isTicketDetailOpen, setIsTicketDetailOpen] = useState<boolean>(false);
   const [selectedTicket, setSelectedTicket] = useState<UserTicket | null>(null);
-  
-  // 4. 刷新触发器 (用于子组件重新加载数据)
   const [refreshTickets, setRefreshTickets] = useState<number>(0);
 
+  // 2. 新增:强制绑定邮箱的控制状态
+  const [isForceBindEmailOpen, setIsForceBindEmailOpen] = useState(false);
+  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);
+        }
+      }
+
+      if (!hasEmail) {
+        // 未绑定 -> 打开强制弹窗
+        setIsForceBindEmailOpen(true);
+      }
+      
+      setIsCheckingAuth(false);
+    };
+
+    checkUserStatus();
+  }, [router]);
+
   // --- 事件处理 ---
 
   const handleLogout = () => {
-    if (confirm('确定要退出登录吗?')) {
+    if (confirm(t('dashboard.logout_confirm'))) {
       localStorage.removeItem('rsid');
       localStorage.removeItem('user_info');
-      // 触发 storage 事件,通知 Navbar 更新状态
       window.dispatchEvent(new Event('storage'));
       router.push('/login');
     }
   };
 
-  // 打开新建工单弹窗 (可选带入 OrderID)
+  // 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);
-    // 如果是在订单页点击售后,为了体验顺畅,可以考虑自动切到工单列表,或者保持不动
-    // setActiveTab('tickets'); 
   };
 
-  // 打开订单详情
   const openOrderDetail = (order: UserOrder) => {
     setSelectedOrder(order);
     setIsOrderDetailOpen(true);
   };
 
-  // 打开工单详情
   const openTicketDetail = (ticket: UserTicket) => {
     setSelectedTicket(ticket);
     setIsTicketDetailOpen(true);
   };
 
-  // 工单更新回调 (比如用户回复了信息,刷新列表)
   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>;
+  }
+
   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">
         
-        {/* === 顶部 Header === */}
+        {/* ... Header 部分保持不变 ... */}
         <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
           <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-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">
              <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"
              >
-               <Plus size={16} /> 新建申请
+               <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"
              >
-               <LogOut size={16} /> 退出
+               <LogOut size={16} /> {t('nav.logout')}
              </button>
           </div>
         </div>
 
-        {/* === 主布局 === */}
         <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
           
           {/* 左侧侧边栏 */}
@@ -111,8 +159,6 @@ export default function DashboardPage() {
 
           {/* 右侧内容区 */}
           <div className="lg:col-span-3">
-            
-            {/* Tab 1: 订单列表 */}
             {activeTab === 'orders' && (
               <div className="animate-in fade-in slide-in-from-right-4 duration-300">
                 <OrderList 
@@ -121,59 +167,48 @@ export default function DashboardPage() {
                 />
               </div>
             )}
-
-            {/* Tab 2: 售后工单 */}
             {activeTab === 'tickets' && (
               <div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
-                {/* 顶部操作栏 */}
                 <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"
                   >
-                    <Plus size={16} /> 提交新工单
+                    <Plus size={16} /> {t('ticket.create_new')}
                   </button>
                 </div>
-                
-                {/* 工单列表组件 */}
-                <TicketList 
-                  onViewDetail={openTicketDetail} 
-                  refreshTrigger={refreshTickets}
-                />
+                <TicketList onViewDetail={openTicketDetail} refreshTrigger={refreshTickets}/>
               </div>
             )}
-
-            {/* Tab 3: 账户设置 (新增) */}
             {activeTab === 'settings' && (
               <div className="animate-in fade-in slide-in-from-right-4 duration-300">
-                {/* ProfileSettings 组件内部处理了 查看/编辑 模式切换 */}
                 <ProfileSettings />
               </div>
             )}
-
           </div>
         </div>
       </div>
 
-      {/* === 全局弹窗组件 === */}
+      {/* === 6. 挂载强制绑定邮箱弹窗 === */}
+      <BindEmailModal 
+        isOpen={isForceBindEmailOpen} 
+        onClose={handleForceBindClose} // 用户点击 X 或遮罩 -> 踢出页面
+        onSuccess={handleForceBindSuccess} // 绑定成功 -> 留在页面
+      />
 
-      {/* 1. 新建工单弹窗 */}
+      {/* ... 其他弹窗保持不变 ... */}
       <TicketModal 
         isOpen={isTicketModalOpen} 
         onClose={() => setIsTicketModalOpen(false)}
         defaultOrderId={ticketDefaultOrderId}
       />
-
-      {/* 2. 订单详情弹窗 */}
       <UserOrderDetailModal 
         isOpen={isOrderDetailOpen} 
         onClose={() => setIsOrderDetailOpen(false)}
         order={selectedOrder}
       />
-
-      {/* 3. 工单详情/回复弹窗 */}
       <UserTicketDetailModal 
-        isOpen={isTicketDetailOpen}
+        isOpen={isTicketDetailOpen} 
         onClose={() => setIsTicketDetailOpen(false)}
         ticket={selectedTicket}
         onUpdate={handleTicketUpdate}

+ 14 - 10
src/app/dashboard/settings/page.tsx

@@ -4,9 +4,13 @@ import ProfileSettings from '@/components/dashboard/ProfileSettings';
 import Sidebar from '@/components/dashboard/Sidebar';
 import { useRouter } from 'next/navigation';
 import { LogOut, Plus } from 'lucide-react';
+// 1. 引入 useLanguage Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function SettingsPage() {
   const router = useRouter();
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
 
   const handleLogout = () => {
     localStorage.removeItem('rsid');
@@ -18,31 +22,31 @@ export default function SettingsPage() {
     <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">
         
-        {/* Header (与 Dashboard 保持一致) */}
+        {/* Header */}
         <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
           <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-gray-900">{t('settings.title')}</h1>
+            <p className="text-sm text-gray-500 mt-1">{t('settings.subtitle')}</p>
           </div>
           <div className="flex gap-3">
              <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"
              >
-               <Plus size={16} /> 新建申请
+               <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"
              >
-               <LogOut size={16} /> 退出
+               <LogOut size={16} /> {t('nav.logout')}
              </button>
           </div>
         </div>
 
         <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
           
-          {/* 左侧侧边栏 (复用 Sidebar,它会自动检测路径变为设置模式) */}
+          {/* 左侧侧边栏 */}
           <div className="lg:col-span-1">
             <Sidebar />
           </div>
@@ -56,16 +60,16 @@ export default function SettingsPage() {
                 <ProfileSettings />
               </div>
               
-              {/* 右侧提示信息 (在大屏幕显示) */}
+              {/* 右侧提示信息 */}
               <div className="hidden xl:block space-y-6">
                 <div className="bg-blue-50 p-6 rounded-xl border border-blue-100">
-                  <h3 className="font-bold text-blue-800 mb-2 text-sm">账户安全</h3>
+                  <h3 className="font-bold text-blue-800 mb-2 text-sm">{t('settings.security_tip_title')}</h3>
                   <p className="text-xs text-blue-600 leading-relaxed mb-4">
-                    为了保障您的账户安全,建议您定期更新个人信息。如果您发现账户有异常登录情况,请立即联系客服。
+                    {t('settings.security_tip_desc')}
                   </p>
                   <div className="h-px bg-blue-200 my-4"></div>
                   <p className="text-xs text-blue-500">
-                    当前账号状态:<span className="font-bold text-green-600">正常</span>
+                    {t('settings.account_status')} <span className="font-bold text-green-600">{t('settings.status_normal')}</span>
                   </p>
                 </div>
               </div>

+ 20 - 35
src/app/knowledge/page.tsx

@@ -5,32 +5,40 @@ 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 变化时重新加载
 
   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: {
-          keywords: keyword,
+          keyword: keyword, // 注意:通常 API 参数名是单数 keyword
           page: targetPage,
           size: pageSize,
-          culture: 'english'
+          culture: cultureParam // 动态传递语言
         }
       });
 
@@ -47,27 +55,9 @@ export default function KnowledgePage() {
 
     } catch (error) {
       console.warn("API Error, using mock data");
-      setCards([
-        {
-          id: 53,
-          image: null,
-          title: "French Visa Stamps and Envelope Purchase",
-          content: "<p>You can purchase stamps and envelopes at An Post. Just tell the staff that they are for France Visa use, and they will provide them to you. The stamp costs €9.5, and you can buy a large yellow envelope.</p>",
-          label: "France Visa",
-          country: "France",
-          created_at: "2024-11-26T11:22:55"
-        },
-        {
-          id: 54,
-          image: "3,3f53a1b2ec", 
-          title: "Photo Requirements for Schengen Visa",
-          content: "<p>Photos must be 35x45mm, white background, no glasses. <a href='#'>Check details here</a>.</p><ul><li>Size: 35x45mm</li><li>Background: White</li></ul>",
-          label: "General",
-          country: "Schengen",
-          created_at: "2024-12-01T09:00:00"
-        }
-      ]);
-      setTotal(2);
+      // Mock 数据也可以根据语言稍微区分一下,或者保持不变
+      setCards([]); 
+      setTotal(0);
     } finally {
       setLoading(false);
     }
@@ -88,10 +78,10 @@ export default function KnowledgePage() {
         {/* 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" /> 办理指南 & 常见问题
+            <BookOpen className="text-blue-600" /> {t('knowledge.title')}
           </h1>
           <p className="text-slate-500 mt-3 max-w-2xl mx-auto">
-            这里汇集了签证办理过程中的常见问题解答、材料准备指南以及最新政策解读。
+            {t('knowledge.subtitle')}
           </p>
         </div>
 
@@ -100,19 +90,18 @@ export default function KnowledgePage() {
           <div className="relative">
             <input 
               type="text" 
-              placeholder="搜索关键词,例如:照片、邮票、指纹..." 
+              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"
               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} />
             <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"
             >
-              搜索
+              {t('common.search')}
             </button>
           </div>
         </div>
@@ -125,13 +114,9 @@ export default function KnowledgePage() {
         ) : cards.length === 0 ? (
           <div className="text-center py-20 text-slate-400">
             <BookOpen size={48} className="mx-auto mb-4 opacity-20" />
-            <p>未找到相关指南,请尝试更换关键词</p>
+            <p>{t('knowledge.empty_state')}</p>
           </div>
         ) : (
-          /* 
-             核心修复:添加 items-start 
-             这会让每一行的卡片顶部对齐,但高度各算各的,不会互相拉伸。
-          */
           <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start">
             {cards.map((card) => (
               <KnowledgeCard key={card.id} data={card} />

+ 14 - 10
src/app/layout.tsx

@@ -1,19 +1,23 @@
 import './globals.css';
 import Navbar from '@/components/Navbar';
-import Footer from '@/components/Footer'; // 引入 Footer
+import Footer from '@/components/Footer';
+// 1. 引入语言上下文 Provider
+import { LanguageProvider } from '@/lib/i18n/LanguageContext';
 
 export default function RootLayout({ children }: { children: React.ReactNode }) {
   return (
     <html lang="zh-CN">
-      <body className="bg-slate-50 min-h-screen flex flex-col"> {/* flex flex-col 确保 footer 沉底 */}
-        <Navbar />
-        
-        {/* main 自动撑开高度 */}
-        <main className="flex-grow">
-          {children}
-        </main>
-        
-        <Footer /> {/* 放在这里 */}
+      <body className="bg-slate-50 min-h-screen flex flex-col">
+        {/* 2. 使用 LanguageProvider 包裹整个应用的内容 */}
+        <LanguageProvider>
+          <Navbar />
+          
+          <main className="flex-grow">
+            {children}
+          </main>
+          
+          <Footer />
+        </LanguageProvider>
       </body>
     </html>
   );

+ 29 - 23
src/app/page.tsx

@@ -1,29 +1,35 @@
 'use client';
 
 import Link from "next/link";
-import { ArrowRight, Globe, CheckCircle, Zap, FileText, UserCheck, Bot, CreditCard } from "lucide-react";
+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,
-      title: "1. 选择服务",
-      desc: "在服务列表中找到您需要办理的国家和签证类型(如日本旅游签、法国申根签)。"
+      title: t('home.step_1_title'),
+      desc: t('home.step_1_desc')
     },
     {
       icon: FileText,
-      title: "2. 提交资料",
-      desc: "填写简单的申请表单(支持 JSON 动态表单),无需繁琐的手写文件。"
+      title: t('home.step_2_title'),
+      desc: t('home.step_2_desc')
     },
     {
       icon: Bot,
-      title: "3. 智能托管",
-      desc: "我们的 24/7 机器人系统会自动为您监控名额并锁定预约,无需人工守候。"
+      title: t('home.step_3_title'),
+      desc: t('home.step_3_desc')
     },
     {
       icon: CheckCircle,
-      title: "4. 成功出签",
-      desc: "预约成功后支付费用(支持随机立减),获取确认函,准备递签。"
+      title: t('home.step_4_title'),
+      desc: t('home.step_4_desc')
     }
   ];
 
@@ -32,31 +38,31 @@ export default function Home() {
       {/* 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">
-          签证申请,<span className="text-blue-600">从未如此简单</span>
+          {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">
-          Visafly 为您提供全球签证自动化处理服务。实时追踪状态,专家级审核,让您的出行无后顾之忧。
+          {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">
-            开始申请 <ArrowRight size={20} />
+            {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>
         </div>
       </section>
 
-      {/* Work Process (新增模块) */}
+      {/* Work Process */}
       <section className="w-full bg-slate-50 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">工作流程</h2>
-            <p className="text-slate-500 mt-2">只需 4 步,全自动化处理您的签证需求</p>
+            <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>
 
           <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) => {
@@ -83,22 +89,22 @@ export default function Home() {
           <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">覆盖全球</h3>
-          <p className="text-slate-600">支持美国、日本、申根区等超过 50 个国家和地区的签证办理。</p>
+          <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>
         </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">高成功率</h3>
-          <p className="text-slate-600">智能系统预审加上人工专家复核,确保资料准确无误。</p>
+          <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>
         </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">极速处理</h3>
-          <p className="text-slate-600">对接官方 API,自动化流程将申请时间缩短至传统的 1/3。</p>
+          <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>
         </div>
       </section>
     </div>

+ 40 - 0
src/app/privacy/page.tsx

@@ -0,0 +1,40 @@
+'use client';
+
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { Shield } from 'lucide-react';
+
+export default function PrivacyPage() {
+  const { t } = useLanguage();
+
+  return (
+    <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
+      <div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-sm border border-slate-200 p-8 sm:p-12">
+        
+        {/* Header */}
+        <div className="flex items-center gap-3 mb-8 border-b border-slate-100 pb-6">
+          <div className="p-3 bg-blue-100 text-blue-600 rounded-lg">
+            <Shield size={32} />
+          </div>
+          {/* 使用 footer.privacy 作为标题 (隐私政策) */}
+          <h1 className="text-3xl font-bold text-slate-900">{t('footer.privacy')}</h1>
+        </div>
+
+        {/* Content */}
+        <div className="prose prose-slate max-w-none text-slate-600 leading-relaxed">
+          <p className="text-sm text-slate-400 mb-8">{t('privacy_content.last_updated')}</p>
+          
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('privacy_content.section1_title')}</h3>
+          <p>{t('privacy_content.section1_desc')}</p>
+
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('privacy_content.section2_title')}</h3>
+          <p>{t('privacy_content.section2_desc')}</p>
+
+          <h3 className="text-xl font-bold text-slate-900 mt-8 mb-4">{t('privacy_content.section3_title')}</h3>
+          <p>{t('privacy_content.section3_desc')}</p>
+          
+          {/* 预留更多内容 */}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 96 - 32
src/app/refund-policy/page.tsx

@@ -1,8 +1,28 @@
 'use client';
 
-import { ShieldCheck, HelpCircle, AlertTriangle } from 'lucide-react';
+import { ShieldCheck, HelpCircle, AlertTriangle, Mail } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+
+// 定义简单的品牌图标组件,避免引入额外的图标库
+const TelegramIcon = ({ className }) => (
+  <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
+    <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 11.944 0zm4.925 8.531l-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.446 1.394c-.16.16-.295.295-.605.295l.213-3.054 5.56-5.022c.242-.213-.054-.333-.373-.121l-6.869 4.326-2.96-.924c-.643-.204-.657-.643.136-.953l11.57-4.458c.535-.196 1.006.128.832.941z"/>
+  </svg>
+);
+
+const WhatsAppIcon = ({ className }) => (
+  <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
+    <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.008-.57-.008-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
+  </svg>
+);
 
 export default function RefundPolicyPage() {
+  const { t } = useLanguage();
+
+  // 请在这里填入你的真实账号
+  const TELEGRAM_USERNAME = "your_telegram_id"; // 例如: visafly_support
+  const WHATSAPP_NUMBER = "your_phone_number";  // 例如: 1234567890 (不需要加 + 号)
+
   return (
     <div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6">
       <div className="max-w-4xl mx-auto bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
@@ -12,78 +32,122 @@ export default function RefundPolicyPage() {
           <div className="flex justify-center mb-4">
             <ShieldCheck size={48} className="text-blue-400" />
           </div>
-          <h1 className="text-3xl font-bold mb-2">退款政策 & 服务条款</h1>
-          <p className="text-slate-400">为了保障您的权益,请仔细阅读以下条款</p>
+          <h1 className="text-3xl font-bold mb-2">{t('refund.title')}</h1>
+          <p className="text-slate-400">{t('refund.subtitle')}</p>
         </div>
 
         {/* Content */}
         <div className="p-8 sm:p-12 space-y-10 text-slate-700 leading-relaxed">
           
-          {/* Section 1 */}
+          {/* Section 1: 全额退款 */}
           <section>
             <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
               <span className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm font-bold">1</span>
-              全额退款情形
+              {t('refund.full_refund_title')}
             </h2>
-            <p className="mb-4">在以下情况下,您可以申请全额退款:</p>
+            <p className="mb-4">{t('refund.full_refund_desc')}</p>
             <ul className="list-disc pl-6 space-y-2 text-sm marker:text-blue-500">
-              <li>您已支付订单,但我们的系统尚未开始执行任何预约操作(Status 为 Pending)。</li>
-              <li>由于 Visafly 系统故障导致重复扣款。</li>
-              <li>我们在承诺的时限内(通常为 30 天,具体视服务而定)未能为您成功预约到名额。</li>
+              <li>{t('refund.full_refund_item_1')}</li>
+              <li>{t('refund.full_refund_item_2')}</li>
+              <li>{t('refund.full_refund_item_3')}</li>
             </ul>
           </section>
 
-          {/* Section 2 */}
+          {/* Section 2: 无法退款 */}
           <section>
             <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
               <span className="w-8 h-8 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center text-sm font-bold">2</span>
-              无法退款情形
+              {t('refund.no_refund_title')}
             </h2>
             <div className="bg-orange-50 border-l-4 border-orange-400 p-4 mb-4 text-sm text-orange-800">
               <p className="font-bold flex items-center gap-2 mb-1">
-                <AlertTriangle size={16} /> 重要提示
+                <AlertTriangle size={16} /> {t('refund.important_note')}
               </p>
-              <p>一旦服务进入执行阶段或产生第三方费用,退款将受到限制。</p>
+              <p>{t('refund.important_note_desc')}</p>
             </div>
             <ul className="list-disc pl-6 space-y-2 text-sm marker:text-orange-500">
-              <li>我们的系统已经为您成功预约到了名额(以截图或确认函为准)。</li>
-              <li>您的签证申请已经递交至大使馆或签证中心。</li>
-              <li>因您提供的个人信息(如护照号)错误导致预约失败或无效。</li>
-              <li>您因个人原因(如改变行程、生病)决定放弃申请,但此时我们已完成了预约工作。</li>
+              <li>{t('refund.no_refund_item_1')}</li>
+              <li>{t('refund.no_refund_item_2')}</li>
+              <li>{t('refund.no_refund_item_3')}</li>
+              <li>{t('refund.no_refund_item_4')}</li>
             </ul>
           </section>
 
-          {/* Section 3 */}
+          {/* Section 3: 流程 */}
           <section>
             <h2 className="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
               <span className="w-8 h-8 rounded-full bg-slate-100 text-slate-600 flex items-center justify-center text-sm font-bold">3</span>
-              退款流程
+              {t('refund.process_title')}
             </h2>
             <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-center text-sm">
               <div className="p-4 border rounded-lg bg-slate-50">
-                <div className="font-bold text-slate-900 mb-1">1. 提交工单</div>
-                <p className="text-slate-500">在控制台选择订单,点击“售后/帮助”提交申请。</p>
+                <div className="font-bold text-slate-900 mb-1">1. {t('refund.step_1_title')}</div>
+                <p className="text-slate-500">{t('refund.step_1_desc')}</p>
               </div>
               <div className="p-4 border rounded-lg bg-slate-50">
-                <div className="font-bold text-slate-900 mb-1">2. 客服审核</div>
-                <p className="text-slate-500">我们将在 1-3 个工作日内核实订单状态。</p>
+                <div className="font-bold text-slate-900 mb-1">2. {t('refund.step_2_title')}</div>
+                <p className="text-slate-500">{t('refund.step_2_desc')}</p>
               </div>
               <div className="p-4 border rounded-lg bg-slate-50">
-                <div className="font-bold text-slate-900 mb-1">3. 原路退回</div>
-                <p className="text-slate-500">批准后,资金将在 5-10 个工作日原路退回。</p>
+                <div className="font-bold text-slate-900 mb-1">3. {t('refund.step_3_title')}</div>
+                <p className="text-slate-500">{t('refund.step_3_desc')}</p>
               </div>
             </div>
           </section>
 
-          {/* Section 4 */}
+          {/* Section 4: 联系方式 (已更新) */}
           <section className="border-t pt-8">
-            <h2 className="text-lg font-bold text-slate-900 mb-2 flex items-center gap-2">
-              <HelpCircle size={20} className="text-slate-400" /> 还有疑问?
+            <h2 className="text-lg font-bold text-slate-900 mb-6 flex items-center gap-2">
+              <HelpCircle size={20} className="text-slate-400" /> {t('refund.contact_title')}
             </h2>
-            <p className="text-sm">
-              如果您对退款政策有任何疑问,请联系我们的客服团队:
-              <a href="mailto:support@visafly.com" className="text-blue-600 hover:underline ml-1">support@visafly.com</a>
-            </p>
+            
+            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+              {/* Email Button */}
+              <a 
+                href="mailto:support@visafly.top" 
+                className="flex items-center gap-3 p-4 rounded-xl border border-slate-200 bg-white hover:border-blue-500 hover:bg-blue-50 transition-all group"
+              >
+                <div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
+                  <Mail size={20} />
+                </div>
+                <div className="flex-1 min-w-0">
+                  <div className="text-sm font-semibold text-slate-900">Email Support</div>
+                  <div className="text-xs text-slate-500 truncate">support@visafly.top</div>
+                </div>
+              </a>
+
+              {/* Telegram Button */}
+              <a 
+                href={`https://t.me/${TELEGRAM_USERNAME}`}
+                target="_blank"
+                rel="noopener noreferrer"
+                className="flex items-center gap-3 p-4 rounded-xl border border-slate-200 bg-white hover:border-sky-500 hover:bg-sky-50 transition-all group"
+              >
+                <div className="w-10 h-10 rounded-full bg-sky-100 flex items-center justify-center text-sky-600 group-hover:bg-sky-500 group-hover:text-white transition-colors">
+                  <TelegramIcon className="w-5 h-5" />
+                </div>
+                <div className="flex-1 min-w-0">
+                  <div className="text-sm font-semibold text-slate-900">Telegram</div>
+                  <div className="text-xs text-slate-500 truncate">@{TELEGRAM_USERNAME}</div>
+                </div>
+              </a>
+
+              {/* WhatsApp Button */}
+              <a 
+                href={`https://wa.me/${WHATSAPP_NUMBER}`}
+                target="_blank"
+                rel="noopener noreferrer"
+                className="flex items-center gap-3 p-4 rounded-xl border border-slate-200 bg-white hover:border-green-500 hover:bg-green-50 transition-all group"
+              >
+                <div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 group-hover:bg-green-500 group-hover:text-white transition-colors">
+                  <WhatsAppIcon className="w-5 h-5" />
+                </div>
+                <div className="flex-1 min-w-0">
+                  <div className="text-sm font-semibold text-slate-900">WhatsApp</div>
+                  <div className="text-xs text-slate-500 truncate">Chat with us</div>
+                </div>
+              </a>
+            </div>
           </section>
 
         </div>

+ 9 - 1
src/app/services/page.js

@@ -1,11 +1,19 @@
 '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">
-      <h1 className="text-3xl font-bold mb-8 text-gray-900">热门签证服务</h1>
+      {/* 3. 替换硬编码标题 */}
+      <h1 className="text-3xl font-bold mb-8 text-gray-900">
+        {t('services.title')}
+      </h1>
       <ServiceList />
     </div>
   );

+ 16 - 15
src/app/slots/page.tsx

@@ -3,6 +3,7 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { Search, MapPin, Calendar, Clock, RefreshCw, AlertCircle, CheckCircle } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 // === 类型定义 ===
 
@@ -30,6 +31,8 @@ interface SlotSnapshot {
 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');
@@ -124,7 +127,8 @@ export default function SlotQueryPage() {
   // 辅助函数:格式化日期
   const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
-    return date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' });
+    const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
+    return date.toLocaleDateString(locale, { month: 'long', day: 'numeric', weekday: 'short' });
   };
 
   return (
@@ -133,15 +137,15 @@ export default function SlotQueryPage() {
         
         {/* 标题区 */}
         <div className="text-center mb-10">
-          <h1 className="text-3xl font-bold text-slate-900">签证名额查询</h1>
-          <p className="text-slate-500 mt-2">实时查询各使馆最新可预约名额</p>
+          <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>
 
         {/* 筛选区 */}
         <div className="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 mb-8">
           <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
             <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">申请国家</label>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.country')}</label>
               <select 
                 className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
                 value={country} onChange={e => setCountry(e.target.value)}
@@ -150,7 +154,7 @@ export default function SlotQueryPage() {
               </select>
             </div>
             <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">递签城市</label>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.city')}</label>
               <select 
                 className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
                 value={city} onChange={e => setCity(e.target.value)}
@@ -159,7 +163,7 @@ export default function SlotQueryPage() {
               </select>
             </div>
             <div>
-              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">签证类型</label>
+              <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('product.visa_type')}</label>
               <select 
                 className="w-full border rounded-lg p-2.5 bg-slate-50 outline-none focus:ring-2 focus:ring-blue-500 transition"
                 value={visaType} onChange={e => setVisaType(e.target.value)}
@@ -175,7 +179,7 @@ export default function SlotQueryPage() {
               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"
             >
               {loading ? <RefreshCw className="animate-spin" size={18} /> : <Search size={18} />}
-              查询名额
+              {t('common.search')}
             </button>
           </div>
         </div>
@@ -194,17 +198,17 @@ export default function SlotQueryPage() {
                 </div>
                 <div>
                   <h3 className={`text-lg font-bold ${snapshot.availability_status === 'Available' ? 'text-green-800' : 'text-gray-700'}`}>
-                    {snapshot.availability_status === 'Available' ? '当前有名额可约' : '暂无名额'}
+                    {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} /> 数据更新于: {new Date(snapshot.snapshot_at).toLocaleString()}
+                    <Clock size={12} /> {t('slots.updated_at')}: {new Date(snapshot.snapshot_at).toLocaleString()}
                   </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">
-                  <p className="text-xs font-bold uppercase tracking-wider opacity-60">最早可约</p>
+                  <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,18 +219,15 @@ export default function SlotQueryPage() {
               <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
                 {snapshot.slots_data.map((day, idx) => (
                   <div key={idx} className="bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition">
-                    {/* 日期头 */}
                     <div className="bg-slate-50 px-4 py-3 border-b border-slate-100 flex justify-between items-center">
                       <div className="flex items-center gap-2 font-bold text-slate-700">
                         <Calendar size={16} className="text-blue-500" />
                         {formatDate(day.date)}
                       </div>
                       <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">
-                        {day.times.length} 个时段
+                        {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">
@@ -246,7 +247,7 @@ export default function SlotQueryPage() {
           !loading && (
             <div className="text-center py-20 text-slate-400">
               <Search size={48} className="mx-auto mb-4 opacity-20" />
-              <p>请选择条件并点击查询</p>
+              <p>{t('slots.select_hint')}</p>
             </div>
           )
         )}

+ 15 - 15
src/components/AuthForm.tsx

@@ -3,16 +3,18 @@
 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';
 
 export default function AuthForm() {
   const router = useRouter();
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
   const [isLoginMode, setIsLoginMode] = useState<boolean>(true);
   const [loading, setLoading] = useState<boolean>(false);
   const [formData, setFormData] = useState({ email: '', password: '' });
-
-  // 控制忘记密码弹窗
   const [isForgotOpen, setIsForgotOpen] = useState(false);
 
   const handleSubmit = async (e: React.FormEvent) => {
@@ -34,12 +36,12 @@ export default function AuthForm() {
         window.dispatchEvent(new Event('storage'));
         router.push('/dashboard');
       } else {
-        alert("登录成功,但未获取到 Token");
+        alert(t('auth.login_success_no_token'));
       }
     } catch (error: any) {
       console.error(error);
-      const msg = error.response?.data?.message || "请求失败";
-      alert(`错误: ${msg}`);
+      const msg = error.response?.data?.message || t('common.unknown_error');
+      alert(`${t('common.error')}: ${msg}`);
     } finally {
       setLoading(false);
     }
@@ -48,30 +50,29 @@ export default function AuthForm() {
   return (
     <div className="w-full max-w-md p-8 bg-white rounded-xl shadow-xl border border-gray-100">
       <h2 className="text-2xl font-bold text-center mb-6">
-        {isLoginMode ? '欢迎回来' : '自动注册'}
+        {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">邮箱</label>
+          <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="name@example.com"
+            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">密码</label>
-            {/* === 新增:忘记密码链接 === */}
+            <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>
             )}
           </div>
@@ -88,7 +89,7 @@ export default function AuthForm() {
           disabled={loading} 
           className="w-full py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition disabled:opacity-50"
         >
-          {loading ? '处理中...' : (isLoginMode ? '登录' : '注册')}
+          {loading ? t('common.processing') : (isLoginMode ? t('auth.login_btn') : t('auth.register_btn'))}
         </button>
       </form>
       <div className="mt-6 text-center border-t border-gray-100 pt-4">
@@ -97,11 +98,10 @@ export default function AuthForm() {
           onClick={() => setIsLoginMode(!isLoginMode)} 
           className="text-blue-600 text-sm hover:underline"
         >
-          {isLoginMode ? '没有账号?点击自动注册' : '已有账号?点击登录'}
+          {isLoginMode ? t('auth.no_account') : t('auth.has_account')}
         </button>
       </div>
 
-      {/* === 挂载忘记密码弹窗 === */}
       <ForgotPasswordModal 
         isOpen={isForgotOpen} 
         onClose={() => setIsForgotOpen(false)} 

+ 53 - 64
src/components/BindEmailModal.tsx

@@ -3,6 +3,8 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { X, Mail, Loader2, Save, Lock, ArrowRight, ArrowLeft } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface BindEmailModalProps {
   isOpen: boolean;
@@ -11,18 +13,17 @@ interface BindEmailModalProps {
 }
 
 export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmailModalProps) {
-  // 状态管理
-  const [step, setStep] = useState<1 | 2>(1); // 1: 输入邮箱, 2: 输入验证码
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
+  const [step, setStep] = useState<1 | 2>(1); 
   const [loading, setLoading] = useState(false);
   
-  // 表单数据
   const [email, setEmail] = useState('');
   const [code, setCode] = useState('');
   
-  // 倒计时
   const [countdown, setCountdown] = useState(0);
 
-  // 处理倒计时
   useEffect(() => {
     let timer: NodeJS.Timeout;
     if (countdown > 0) {
@@ -31,88 +32,74 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
     return () => clearTimeout(timer);
   }, [countdown]);
 
-  // 重置状态当弹窗打开时
   useEffect(() => {
     if (isOpen) {
       setStep(1);
       setCode('');
-      // email 不清空,保留用户上次输入的
     }
   }, [isOpen]);
 
-  // --- 第一步:发送验证码 ---
-  const handleSendCode = async (e: React.FormEvent) => {
-    e.preventDefault();
-    if (!email) return alert("请输入邮箱");
+  const handleSendCode = async (e?: React.FormEvent) => {
+    if (e) e.preventDefault();
+    if (!email) return alert(t('bind_email.alert_input_email'));
+    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return alert(t('bind_email.alert_invalid_email'));
     
     setLoading(true);
     try {
-      // -----------------------------------------------------------
-      // TODO: 请替换为你真实的【发送验证码】接口
-      // 假设接口: POST /api/auth/send-bind-code
-      // Body: { email: "..." }
-      // -----------------------------------------------------------
       await api.post('/api/auth/send-bind-code', { email });
       
-      alert(`验证码已发送至 ${email}`);
+      alert(`${t('bind_email.alert_code_sent')} ${email}`);
       setStep(2);
-      setCountdown(60); // 60秒倒计时
+      setCountdown(60); 
     } catch (error: any) {
       console.error(error);
-      alert("发送失败: " + (error.response?.data?.message || "未知错误"));
+      const msg = error.response?.data?.message || t('common.unknown_error');
+      alert(`${t('bind_email.alert_send_failed')}: ${msg}`);
     } finally {
       setLoading(false);
     }
   };
 
-  // --- 第二步:提交验证并绑定 ---
   const handleVerifyAndBind = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!code) return alert("请输入验证码");
+    if (!code) return alert(t('bind_email.alert_input_code'));
+    if (code.length !== 6) return alert(t('bind_email.alert_code_length'));
 
     setLoading(true);
     try {
-      // -----------------------------------------------------------
-      // TODO: 请替换为你真实的【验证并绑定】接口
-      // 假设接口: POST /api/auth/bind-email (带验证码)
-      // Body: { email: "...", code: "..." }
-      // -----------------------------------------------------------
       const res = await api.post('/api/auth/bind-email', { 
         email, 
         code 
       });
 
-      // === 核心逻辑:更新本地 Token 和 UserInfo ===
-      // 因为绑定成功后,后端返回了正式的 access_token (可能权限更高)
       const data = res.data.data || res.data;
       const newToken = data.token || data.access_token;
       const newUser = data.user;
 
       if (newToken) {
-        console.log("邮箱绑定成功,更新本地凭证...");
+        console.log("Bind success, updating token...");
         localStorage.setItem('rsid', newToken);
         
         if (newUser) {
           localStorage.setItem('user_info', JSON.stringify(newUser));
         } else {
-          // 如果后端没返回 user,手动更新一下本地 email
           const oldUser = JSON.parse(localStorage.getItem('user_info') || '{}');
           localStorage.setItem('user_info', JSON.stringify({ ...oldUser, email }));
         }
 
-        // 触发事件通知全站更新状态
         window.dispatchEvent(new Event('storage'));
         
-        alert('账号绑定成功!');
-        onSuccess(); // 通知父组件(CreateOrderForm)继续提交
+        alert(t('bind_email.success'));
+        onSuccess(); 
         onClose();
       } else {
-        throw new Error("绑定成功但未返回 Token");
+        throw new Error("Token missing");
       }
 
     } catch (error: any) {
       console.error(error);
-      alert("绑定失败: " + (error.response?.data?.message || "验证码错误或过期"));
+      const msg = error.response?.data?.message || t('common.unknown_error');
+      alert(`${t('bind_email.failed')}: ${msg}`);
     } finally {
       setLoading(false);
     }
@@ -121,37 +108,39 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
   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-md 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-4 animate-in fade-in duration-200">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in-95 duration-200">
         
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
           <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
             {step === 1 ? <Mail size={20} className="text-blue-600"/> : <Lock size={20} className="text-blue-600"/>}
-            {step === 1 ? '绑定邮箱' : '输入验证码'}
+            {step === 1 ? t('bind_email.title_step1') : t('bind_email.title_step2')}
           </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 transition p-1 hover:bg-gray-200 rounded-full">
+            <X size={24} />
+          </button>
         </div>
 
         <div className="p-6">
-          {/* Progress Bar (Optional visual cue) */}
+          {/* Progress Indicator */}
           <div className="flex gap-2 mb-6">
-            <div className={`h-1 flex-1 rounded-full ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
-            <div className={`h-1 flex-1 rounded-full ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
+            <div className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${step >= 1 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
+            <div className={`h-1.5 flex-1 rounded-full transition-colors duration-300 ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
           </div>
 
           {step === 1 ? (
             // === Step 1: 输入邮箱 ===
-            <form onSubmit={handleSendCode} className="space-y-4">
-              <p className="text-sm text-gray-500">
-                请输入您的常用邮箱,我们将发送一个验证码给您。
+            <form onSubmit={handleSendCode} className="space-y-5">
+              <p className="text-sm text-gray-500 leading-relaxed">
+                {t('bind_email.desc_step1')}
               </p>
               <div>
-                <label className="block text-sm font-medium text-gray-700 mb-1">电子邮箱</label>
+                <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('bind_email.email_label')}</label>
                 <input
                   type="email" required
-                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                  placeholder="name@example.com"
+                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                  placeholder={t('bind_email.email_placeholder')}
                   value={email}
                   onChange={(e) => setEmail(e.target.value)}
                 />
@@ -159,25 +148,25 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
               <button 
                 type="submit" 
                 disabled={loading}
-                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100"
               >
-                {loading ? <Loader2 size={18} className="animate-spin" /> : <>发送验证码 <ArrowRight size={18} /></>}
+                {loading ? <Loader2 size={18} className="animate-spin" /> : <>{t('bind_email.send_btn')} <ArrowRight size={18} /></>}
               </button>
             </form>
           ) : (
             // === Step 2: 输入验证码 ===
-            <form onSubmit={handleVerifyAndBind} className="space-y-4">
+            <form onSubmit={handleVerifyAndBind} className="space-y-5">
               <p className="text-sm text-gray-500">
-                验证码已发送至 <span className="font-bold text-gray-800">{email}</span>
+                {t('bind_email.desc_step2_prefix')} <span className="font-bold text-gray-900">{email}</span>{t('bind_email.desc_step2_suffix')}
               </p>
               
               <div>
-                <label className="block text-sm font-medium text-gray-700 mb-1">验证码</label>
-                <div className="flex gap-2">
+                <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('bind_email.code_label')}</label>
+                <div className="flex gap-3">
                   <input
                     type="text" required
-                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-widest text-lg"
-                    placeholder="123456"
+                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-[0.25em] text-lg font-bold"
+                    placeholder="------"
                     maxLength={6}
                     value={code}
                     onChange={(e) => setCode(e.target.value)}
@@ -185,10 +174,10 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
                   <button
                     type="button"
                     disabled={countdown > 0 || loading}
-                    onClick={handleSendCode} // 重新发送
-                    className="w-28 border border-slate-300 rounded-lg text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-50 disabled:bg-slate-100"
+                    onClick={() => handleSendCode()} 
+                    className="w-32 border border-slate-300 bg-gray-50 text-slate-600 rounded-lg text-xs font-medium hover:bg-white hover:border-blue-300 hover:text-blue-600 disabled:opacity-50 disabled:bg-slate-100 disabled:cursor-not-allowed transition"
                   >
-                    {countdown > 0 ? `${countdown}s 后重发` : '重新发送'}
+                    {countdown > 0 ? `${countdown}${t('bind_email.resend_suffix')}` : t('bind_email.resend_btn')}
                   </button>
                 </div>
               </div>
@@ -196,17 +185,17 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
               <div className="flex gap-3 pt-2">
                 <button 
                   type="button"
-                  onClick={() => setStep(1)} // 返回修改邮箱
-                  className="px-4 py-2 text-slate-500 hover:text-slate-700 flex items-center gap-1 text-sm"
+                  onClick={() => setStep(1)} 
+                  className="px-4 py-2 text-slate-500 hover:text-slate-700 flex items-center gap-1 text-sm font-medium transition"
                 >
-                  <ArrowLeft size={16} /> 修改邮箱
+                  <ArrowLeft size={16} /> {t('bind_email.change_email_btn')}
                 </button>
                 <button 
                   type="submit" 
                   disabled={loading}
-                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100"
                 >
-                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 确认绑定</>}
+                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> {t('bind_email.confirm_btn')}</>}
                 </button>
               </div>
             </form>

+ 25 - 18
src/components/CreateOrderForm.tsx

@@ -5,6 +5,7 @@ import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
 import { Loader2, Info } from 'lucide-react';
 import BindEmailModal from '@/components/BindEmailModal';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 // ==========================================
 // 类型定义
@@ -12,6 +13,7 @@ import BindEmailModal from '@/components/BindEmailModal';
 
 interface CreateOrderFormProps {
   productId: string;
+  productName?: string; // 可选,父组件传入的备用名称
 }
 
 interface ProductDetail {
@@ -49,8 +51,9 @@ interface JsonSchema {
 // 组件逻辑
 // ==========================================
 
-export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
+export default function CreateOrderForm({ productId, productName }: CreateOrderFormProps) {
   const router = useRouter();
+  const { t } = useLanguage();
   
   // 状态管理
   const [loading, setLoading] = useState<boolean>(true);
@@ -69,8 +72,10 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
         setLoading(true);
         
         // 1. 获取商品详情
-        // API: /api/vas/product/{id}
-        const prodRes = await api.get('/api/vas/product/detail', {params: {"product_id": productId}});
+        // API: /api/vas/product/detail?product_id={id}
+        const prodRes = await api.get('/api/vas/product/detail', {
+          params: { "product_id": productId }
+        });
         const prodData = prodRes.data.data || prodRes.data;
         setProduct(prodData);
 
@@ -78,7 +83,9 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
         if (prodData.schema_id) {
           try {
             // API: /api/vas/schema/detail?schema_id={id}
-            const schemaRes = await api.get('/api/vas/schema/detail', {params: {"schema_id": prodData.schema_id}});
+            const schemaRes = await api.get('/api/vas/schema/detail', {
+              params: { "schema_id": prodData.schema_id }
+            });
             const schemaData = schemaRes.data.data || schemaRes.data;
             
             // 兼容处理:如果 schema_json 是字符串,需要 parse
@@ -101,13 +108,13 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
 
           } catch (schemaErr) {
             console.error("Failed to load schema", schemaErr);
-            alert(`获取表单定义失败 (Schema ID: ${prodData.schema_id})`);
+            // 依然允许渲染,只是没有动态表单
           }
         }
 
       } catch (error) {
         console.error("Fetch product failed", error);
-        alert("商品信息加载失败");
+        alert(t('order.load_product_failed'));
       } finally {
         setLoading(false);
       }
@@ -116,7 +123,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
     if (productId) {
       initData();
     }
-  }, [productId]);
+  }, [productId, t]);
 
   // 辅助函数:确保用户已登录(如果未登录,尝试自动注册)
   const ensureUserLoggedIn = async (): Promise<boolean> => {
@@ -147,7 +154,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
       return false;
     } catch (e) {
       console.error("Auto register failed", e);
-      alert("自动注册失败,请尝试手动登录");
+      alert(t('auth.login_success_no_token')); // 或使用更具体的自动注册失败提示
       router.push('/login');
       return false;
     }
@@ -212,7 +219,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
       }
     } catch (error: any) {
       console.error(error);
-      const msg = error.response?.data?.message || "创建订单失败";
+      const msg = error.response?.data?.message || t('order.create_failed');
       alert(msg);
       setSubmitting(false);
     }
@@ -239,7 +246,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
           value={formValues[key] || ''}
           onChange={(e) => handleInputChange(key, e.target.value)}
         >
-          <option value="" disabled>请选择 {label}</option>
+          <option value="" disabled>{t('common.select')} {label}</option>
           {fieldSchema.enum.map((option: string | number) => (
             <option key={option} value={option}>{option}</option>
           ))}
@@ -260,7 +267,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
         type={inputType}
         required={required}
         className={commonClasses}
-        placeholder={fieldSchema.description || `请输入 ${label}`}
+        placeholder={fieldSchema.description || `${t('common.enter')} ${label}`}
         value={formValues[key] || ''}
         onChange={(e) => handleInputChange(key, e.target.value)}
       />
@@ -292,7 +299,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
 
   // Loading 状态
   if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
-  if (!product) return <div className="p-8 text-center text-red-500">无法找到该服务</div>;
+  if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
 
   const properties = formSchema?.properties || {};
   const requiredFields = formSchema?.required || [];
@@ -304,9 +311,9 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
       {/* 头部:商品信息 */}
       <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}</h1>
+          <h1 className="text-2xl font-bold text-gray-900">{product.title || productName}</h1>
           <span className="text-xl font-bold text-blue-600">
-             {/* 假设金额单位是分,显示为元。如果是元则去掉 /100 */}
+             {/* 假设金额单位是分,显示为元 */}
              {(product.price_amount / 100).toFixed(2)} {product.price_currency}
           </span>
         </div>
@@ -314,7 +321,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
         
         <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>请仔细填写以下申请信息,这将直接用于您的签证申请。</span>
+          <span>{t('order.fill_form_hint')}</span>
         </div>
       </div>
 
@@ -322,7 +329,7 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
       <form onSubmit={handleSubmit} className="space-y-6">
         {!hasFields && (
           <div className="text-center py-8 text-gray-400 text-sm">
-            无需填写额外信息,请直接提交。
+            {t('order.no_extra_info_needed')}
           </div>
         )}
 
@@ -346,9 +353,9 @@ export default function CreateOrderForm({ productId }: CreateOrderFormProps) {
         >
           {submitting ? (
             <>
-              <Loader2 className="animate-spin mr-2 w-4 h-4"/> 正在处理...
+              <Loader2 className="animate-spin mr-2 w-4 h-4"/> {t('common.processing')}
             </>
-          ) : `提交订单并支付 ${(product.price_amount / 100).toFixed(2)} ${product.price_currency}`}
+          ) : `${t('order.submit_and_pay')} ${(product.price_amount / 100).toFixed(2)} ${product.price_currency}`}
         </button>
       </form>
 

+ 26 - 12
src/components/Footer.tsx

@@ -1,7 +1,20 @@
+'use client';
+
 import Link from 'next/link';
+import { usePathname } from 'next/navigation';
 import { Plane } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function Footer() {
+  const pathname = usePathname();
+  const { t } = useLanguage();
+  const currentYear = new Date().getFullYear();
+
+  // 后台管理页面隐藏 Footer
+  if (pathname?.startsWith('/admin')) {
+    return null;
+  }
+
   return (
     <footer className="bg-white border-t border-slate-200 py-12 mt-auto">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -13,36 +26,37 @@ export default function Footer() {
               <span className="text-xl font-bold text-slate-900">Visafly</span>
             </div>
             <p className="text-sm text-slate-500 max-w-xs leading-relaxed">
-              专业的签证自动化服务平台。利用技术优势,让全球签证申请变得简单、高效、透明。
+              {t('footer.description')}
             </p>
           </div>
 
           {/* Quick Links */}
           <div>
-            <h4 className="font-bold text-slate-900 mb-4">快速链接</h4>
+            <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">热门服务</Link></li>
-              <li><Link href="/slots" className="hover:text-blue-600">名额查询</Link></li>
-              <li><Link href="/dashboard" className="hover:text-blue-600">用户控制台</Link></li>
+              <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>
             </ul>
           </div>
 
           {/* Support */}
           <div>
-            <h4 className="font-bold text-slate-900 mb-4">帮助与支持</h4>
+            <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">退款政策</Link></li>
-              <li><Link href="#" className="hover:text-blue-600">服务条款</Link></li>
-              <li><Link href="#" className="hover:text-blue-600">联系我们</Link></li>
+              <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>
             </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; {new Date().getFullYear()} Visafly Inc. All rights reserved.</p>
+          <p>&copy; {currentYear} Visafly Inc. {t('footer.rights_reserved')}</p>
           <div className="flex gap-4 mt-2 md:mt-0">
-            <span>Privacy Policy</span>
-            <span>Cookie Policy</span>
+            {/* === 修改了这里 === */}
+            <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>
           </div>
         </div>
       </div>

+ 45 - 44
src/components/ForgotPasswordModal.tsx

@@ -3,6 +3,8 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { X, Lock, Loader2, Save, Mail, ArrowRight, KeyRound, Eye, EyeOff } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface ForgotPasswordModalProps {
   isOpen: boolean;
@@ -10,17 +12,18 @@ interface ForgotPasswordModalProps {
 }
 
 export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordModalProps) {
-  const [step, setStep] = useState<1 | 2>(1); // 1: 输入邮箱, 2: 重置密码
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
+  const [step, setStep] = useState<1 | 2>(1); 
   const [loading, setLoading] = useState(false);
   const [countdown, setCountdown] = useState(0);
   
-  // 表单数据
   const [email, setEmail] = useState('');
   const [code, setCode] = useState('');
   const [newPassword, setNewPassword] = useState('');
   const [showPassword, setShowPassword] = useState(false);
 
-  // 倒计时逻辑
   useEffect(() => {
     let timer: NodeJS.Timeout;
     if (countdown > 0) {
@@ -29,57 +32,53 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
     return () => clearTimeout(timer);
   }, [countdown]);
 
-  // 重置状态
   useEffect(() => {
     if (isOpen) {
       setStep(1);
       setCode('');
       setNewPassword('');
-      // email 不清空,方便用户如果输错了回来改
     }
   }, [isOpen]);
 
-  // 第一步:发送验证码
   const handleSendCode = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!email) return alert("请输入邮箱");
+    if (!email) return alert(t('forgot_password.enter_email_alert'));
     
     setLoading(true);
     try {
-      // API: POST /api/auth/send-reset-code
       await api.post('/api/auth/send-reset-code', { email });
       
-      alert(`验证码已发送至 ${email},请查收。`);
-      setStep(2); // 进入第二步
+      alert(`${t('forgot_password.code_sent_alert')} ${email}`);
+      setStep(2);
       setCountdown(60);
     } catch (error: any) {
       console.error(error);
-      alert("发送失败: " + (error.response?.data?.message || "用户不存在或网络错误"));
+      const msg = error.response?.data?.message || t('forgot_password.send_failed_default');
+      alert(`${t('forgot_password.send_failed')}: ${msg}`);
     } finally {
       setLoading(false);
     }
   };
 
-  // 第二步:提交重置
   const handleSubmitReset = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!code) return alert("请输入验证码");
-    if (!newPassword) return alert("请输入新密码");
+    if (!code) return alert(t('forgot_password.enter_code_alert'));
+    if (!newPassword) return alert(t('forgot_password.enter_password_alert'));
 
     setLoading(true);
     try {
-      // API: POST /api/auth/reset-password
       await api.post('/api/auth/reset-password', { 
         email, 
         code, 
         new_password: newPassword 
       });
 
-      alert('密码重置成功!请使用新密码登录。');
+      alert(t('forgot_password.reset_success'));
       onClose();
     } catch (error: any) {
       console.error(error);
-      alert("重置失败: " + (error.response?.data?.message || "验证码错误"));
+      const msg = error.response?.data?.message || t('forgot_password.code_error');
+      alert(`${t('forgot_password.reset_failed')}: ${msg}`);
     } finally {
       setLoading(false);
     }
@@ -88,30 +87,32 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
   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-md 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-4 animate-in fade-in duration-200">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in-95 duration-200">
         
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
           <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
-            <KeyRound size={20} className="text-blue-600"/> 找回密码
+            <KeyRound size={20} className="text-blue-600"/> {t('forgot_password.title')}
           </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 transition p-1 hover:bg-gray-200 rounded-full">
+            <X size={24} />
+          </button>
         </div>
 
         <div className="p-6">
           {/* Step 1: 输入邮箱 */}
           {step === 1 && (
             <form onSubmit={handleSendCode} className="space-y-5">
-              <p className="text-sm text-gray-500">
-                请输入您注册时使用的电子邮箱,我们将发送验证码协助您重置密码。
+              <p className="text-sm text-gray-500 leading-relaxed">
+                {t('forgot_password.step1_desc')}
               </p>
               <div>
-                <label className="block text-sm font-medium text-gray-700 mb-1">电子邮箱</label>
+                <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('forgot_password.email_label')}</label>
                 <input
                   type="email" required
-                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                  placeholder="name@example.com"
+                  className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                  placeholder={t('forgot_password.email_placeholder')}
                   value={email}
                   onChange={(e) => setEmail(e.target.value)}
                 />
@@ -119,9 +120,9 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
               <button 
                 type="submit" 
                 disabled={loading}
-                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50"
+                className="w-full bg-blue-600 text-white py-3 rounded-lg font-bold hover:bg-blue-700 transition flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100"
               >
-                {loading ? <Loader2 size={18} className="animate-spin" /> : <>发送验证码 <ArrowRight size={18} /></>}
+                {loading ? <Loader2 size={18} className="animate-spin" /> : <>{t('forgot_password.send_btn')} <ArrowRight size={18} /></>}
               </button>
             </form>
           )}
@@ -129,17 +130,17 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
           {/* Step 2: 重置密码 */}
           {step === 2 && (
             <form onSubmit={handleSubmitReset} className="space-y-5">
-              <div className="bg-blue-50 p-3 rounded-lg text-xs text-blue-700 mb-4">
-                验证码已发送至 <strong>{email}</strong>
+              <div className="bg-blue-50 p-3 rounded-lg text-xs text-blue-700 mb-4 border border-blue-100">
+                {t('forgot_password.code_sent_to')} <strong>{email}</strong>
               </div>
 
               <div>
-                <label className="block text-sm font-medium text-gray-700 mb-1">验证码</label>
-                <div className="flex gap-2">
+                <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('forgot_password.code_label')}</label>
+                <div className="flex gap-3">
                   <input
                     type="text" required
-                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-widest"
-                    placeholder="6位验证码"
+                    className="flex-1 border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono text-center tracking-[0.25em] text-lg font-bold"
+                    placeholder="------"
                     maxLength={6}
                     value={code}
                     onChange={(e) => setCode(e.target.value)}
@@ -148,27 +149,27 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
                     type="button"
                     disabled={countdown > 0 || loading}
                     onClick={handleSendCode}
-                    className="w-28 border border-slate-300 rounded-lg text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-50 disabled:bg-slate-100"
+                    className="w-28 border border-slate-300 bg-gray-50 text-slate-600 rounded-lg text-xs font-medium hover:bg-white hover:border-blue-300 hover:text-blue-600 disabled:opacity-50 disabled:bg-slate-100 disabled:cursor-not-allowed transition"
                   >
-                    {countdown > 0 ? `${countdown}s` : '重新发送'}
+                    {countdown > 0 ? `${countdown}s` : t('forgot_password.resend_btn')}
                   </button>
                 </div>
               </div>
 
               <div>
-                <label className="block text-sm font-medium text-gray-700 mb-1">新密码</label>
+                <label className="block text-xs font-bold text-slate-500 uppercase mb-1">{t('forgot_password.new_password_label')}</label>
                 <div className="relative">
                   <input
                     type={showPassword ? "text" : "password"} required
-                    className="w-full border border-slate-300 rounded-lg p-3 pr-10 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                    placeholder="设置新密码"
+                    className="w-full border border-slate-300 rounded-lg p-3 pr-10 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+                    placeholder={t('forgot_password.new_password_placeholder')}
                     value={newPassword}
                     onChange={(e) => setNewPassword(e.target.value)}
                   />
                   <button 
                     type="button"
                     onClick={() => setShowPassword(!showPassword)}
-                    className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600"
+                    className="absolute right-3 top-3.5 text-gray-400 hover:text-gray-600 transition"
                   >
                     {showPassword ? <EyeOff size={16}/> : <Eye size={16}/>}
                   </button>
@@ -179,16 +180,16 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
                 <button 
                   type="button"
                   onClick={() => setStep(1)}
-                  className="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-medium"
+                  className="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-medium transition"
                 >
-                  返回
+                  {t('common.back')}
                 </button>
                 <button 
                   type="submit" 
                   disabled={loading}
-                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 text-sm font-bold flex justify-center items-center gap-2 disabled:opacity-50 shadow-sm"
+                  className="flex-1 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 text-sm font-bold flex justify-center items-center gap-2 disabled:opacity-50 shadow-md shadow-blue-100 transition"
                 >
-                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 重置密码</>}
+                  {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> {t('forgot_password.reset_btn')}</>}
                 </button>
               </div>
             </form>

+ 33 - 18
src/components/Navbar.tsx

@@ -3,11 +3,16 @@
 import Link from 'next/link';
 import { useRouter } from 'next/navigation';
 import { useEffect, useState } from 'react';
-import { Plane, ShieldCheck, CalendarSearch, BookOpen } from 'lucide-react'; // 引入新图标
+// 1. 引入 LayoutDashboard 图标
+import { Plane, ShieldCheck, CalendarSearch, BookOpen, Briefcase, LayoutDashboard } 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 { t } = useLanguage();
+  
   const [isLogged, setIsLogged] = useState(false);
   const [showAdmin, setShowAdmin] = useState(false);
 
@@ -37,31 +42,36 @@ export default function Navbar() {
             <span className="text-2xl font-bold text-blue-600">Visafly</span>
           </div>
 
-          {/* Navigation Links */}
-          <div className="flex items-center space-x-6">
+          {/* 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">
-              服务列表
+            <Link 
+              href="/services" 
+              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
+            >
+              <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"
+              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
             >
-              <CalendarSearch size={18} /> 名额查询
+              <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"
+              className="text-gray-600 hover:text-blue-600 font-medium transition flex items-center gap-1 text-sm sm:text-base"
             >
-              <BookOpen size={18} /> 办理指南
+              <BookOpen size={18} /> {t('nav.guide') || '办理指南'}
             </Link>
-            {/* ======================= */}
+
+            {/* 分割线 & 语言切换器 */}
+            <div className="h-6 w-px bg-slate-200 mx-2"></div>
+            <LanguageSwitcher />
             
             {isLogged ? (
-              <div className="flex items-center space-x-4 ml-4 pl-4 border-l border-gray-200">
+              <div className="flex items-center space-x-4 ml-2 pl-2 sm:pl-4 sm:border-l border-gray-200">
                 
                 {/* 管理员入口 */}
                 {showAdmin && (
@@ -69,20 +79,25 @@ export default function Navbar() {
                     href="/admin" 
                     className="flex items-center text-slate-700 hover:text-slate-900 font-medium bg-slate-100 px-3 py-1.5 rounded-md text-sm transition"
                   >
-                    <ShieldCheck size={16} className="mr-1 text-blue-600" /> 管理后台
+                    <ShieldCheck size={16} className="mr-1 text-blue-600" /> {t('nav.admin') || '管理后台'}
                   </Link>
                 )}
 
-                <Link href="/dashboard" className="text-gray-600 hover:text-blue-600 font-medium">
-                  控制台
+                {/* 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"
+                >
+                  <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>
               </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">
-                登录 / 注册
+              <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">
+                {t('nav.login') || '登录 / 注册'}
               </Link>
             )}
           </div>

+ 49 - 65
src/components/PaymentProcessor.tsx

@@ -3,14 +3,10 @@
 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 } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入 LocalTime
+import LocalTime from '@/components/common/LocalTime';
 
 interface PaymentProcessorProps {
   orderId: string;
@@ -24,33 +20,34 @@ interface PaymentProvider {
   title?: string;
 }
 
-// 更新支付结果接口定义,匹配你的 JSON
 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;    // 汇率
-  
+  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 [step, setStep] = useState<number>(1);
+  const { t } = useLanguage(); // 不需要 lang 了,LocalTime 内部处理
+
+  const [step, setStep] = useState<1 | 2>(1);
   const [loading, setLoading] = 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();
@@ -74,7 +71,6 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
       else if (providerCode.includes('ali')) providerCode = 'alipay';
       else if (providerCode.includes('stripe') || providerCode.includes('card')) providerCode = 'stripe';
 
-      // 1. 创建支付单
       const payRes = await api.post('/api/vas/payment/create', {
         order_id: String(orderId),
         provider: providerCode
@@ -83,19 +79,16 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
       const data: PaymentResult = payRes.data.data || payRes.data;
       if (!data?.id) throw new Error("Payment creation failed");
 
-      setPaymentData(data); // 保存所有支付信息
+      setPaymentData(data); 
 
-      // 2. 根据 channel 决定后续操作
       if (data.channel === 'online_link') {
-        // 如果是链接支付 (Stripe),直接进入下一步显示按钮
         if (data.payment_url) {
           setStep(2);
         } else {
-          alert("支付链接生成失败");
+          alert(t('payment.link_gen_failed'));
         }
       } 
       else if (data.channel === 'qr_static') {
-        // 如果是二维码支付 (WeChat/Alipay),请求二维码
         const qrRes = await api.get('/api/vas/payment_qr/qrcode', {
           params: { id: data.qr_id }
         });
@@ -106,19 +99,19 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
           setQrCode(qrUrl);
           setStep(2);
         } else {
-          alert("未获取到支付二维码");
+          alert(t('payment.qr_gen_failed'));
         }
       } else {
-        alert(`不支持的支付渠道: ${data.channel}`);
+        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("当前订单已有一个未完成的支付,请稍后再试或联系客服。");
+        alert(t('payment.active_payment_exists'));
       } else {
-        alert("支付初始化失败: " + (errorMsg || "未知错误"));
+        alert(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')));
       }
     } finally {
       setLoading(false);
@@ -131,30 +124,23 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
     setQrCode('');
   };
 
-  // 格式化金额 (分 -> 元)
   const formatMoney = (amount: number, currency: string) => {
     return `${(amount / 100).toFixed(2)} ${currency}`;
   };
 
-  // 格式化时间
-  const formatTime = (isoString: string) => {
-    const date = new Date(isoString);
-    return date.toLocaleTimeString([], { 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">
       
-      {/* Step 1: 选择支付方式 */}
+      {/* Step 1: Select Method */}
       {step === 1 && (
         <>
-          <h2 className="text-2xl font-bold mb-2 text-gray-900">订单已创建</h2>
-          <p className="text-gray-500 mb-8 text-sm">订单号: <span className="font-mono font-bold text-gray-700">{orderId}</span></p>
+          <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>
 
-          <h3 className="text-left font-semibold mb-4 text-gray-800">选择支付方式</h3>
+          <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">正在加载支付方式...</div>
+             <div className="text-gray-400 py-4 text-sm">{t('common.loading')}</div>
           ) : (
             <div className="grid grid-cols-2 gap-4">
               {providers.map((p, idx) => (
@@ -181,54 +167,55 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
               ))}
             </div>
           )}
-          {loading && <div className="mt-6 flex justify-center text-blue-600 text-sm items-center"><Loader2 className="animate-spin w-4 h-4 mr-2"/>正在创建支付...</div>}
+          {loading && <div className="mt-6 flex justify-center text-blue-600 text-sm items-center"><Loader2 className="animate-spin w-4 h-4 mr-2"/>{t('payment.creating')}</div>}
         </>
       )}
 
-      {/* Step 2: 支付详情 (链接或二维码) */}
+      {/* Step 2: Payment Detail */}
       {step === 2 && paymentData && (
         <div className="animate-in fade-in zoom-in duration-300 text-left">
-          {/* 顶部返回栏 */}
           <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"
             >
-              <ArrowLeft className="w-4 h-4 mr-1" /> 重选方式
+              <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">
               <Clock className="w-3 h-3 mr-1" />
-              {formatTime(paymentData.expire_at)} 过期
+              {/* 3. 使用 LocalTime 组件,并通过 options 仅显示时间 (HH:mm) */}
+              <LocalTime 
+                date={paymentData.expire_at} 
+                options={{ hour: '2-digit', minute: '2-digit' }} 
+              /> 
+              <span className="ml-1">{t('payment.expires')}</span>
             </div>
           </div>
 
-          <h2 className="text-xl font-bold mb-4 text-gray-900 text-center">确认支付信息</h2>
+          <h2 className="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">
             
-            {/* 1. 基准金额 */}
             <div className="flex justify-between text-sm text-gray-600">
-              <span>原始金额</span>
+              <span>{t('payment.original_amount')}</span>
               <span className="font-medium text-gray-900">{formatMoney(paymentData.base_amount, paymentData.base_currency)}</span>
             </div>
 
-            {/* 2. 汇率 (如果发生转换) */}
             {paymentData.currency !== paymentData.base_currency && (
               <div className="flex justify-between text-xs text-gray-400 items-center">
-                <span className="flex items-center gap-1"><ArrowRightLeft size={12}/> 参考汇率</span>
+                <span className="flex items-center gap-1"><ArrowRightLeft size={12}/> {t('payment.exchange_rate')}</span>
                 <span>1 {paymentData.base_currency} ≈ {paymentData.exchange_rate} {paymentData.currency}</span>
               </div>
             )}
 
-            {/* 3. 随机立减 */}
             {paymentData.random_offset !== 0 && (
               <div className="flex justify-between text-sm text-red-500 font-medium items-center bg-red-50 p-2 rounded-lg border border-red-100">
                 <span className="flex items-center gap-1">
-                  <Sparkles size={14} className="fill-red-500" /> 随机立减
+                  <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>
@@ -236,34 +223,31 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
             <div className="border-t border-slate-200 my-2"></div>
 
-            {/* 4. 最终金额 */}
             <div className="flex justify-between items-end">
-              <span className="text-gray-600 font-medium pb-1">实际需付</span>
+              <span className="text-gray-600 font-medium pb-1">{t('payment.actual_pay')}</span>
               <span className="text-3xl font-bold text-blue-600">
                 {formatMoney(paymentData.amount, paymentData.currency)}
               </span>
             </div>
           </div>
 
-          {/* 支付操作区域 */}
+          {/* Action Area */}
           <div className="text-center">
             {paymentData.channel === 'online_link' ? (
-              // 场景 A: 链接支付 (Stripe)
               <div className="space-y-4">
-                <p className="text-sm text-gray-500">点击下方按钮前往安全支付页面</p>
+                <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"
                 >
-                  前往支付 <ExternalLink size={18} />
+                  {t('payment.go_to_pay')} <ExternalLink size={18} />
                 </a>
               </div>
             ) : (
-              // 场景 B: 二维码支付 (WeChat/Alipay)
               <div className="space-y-4">
-                <p className="text-sm text-gray-500">请使用 App 扫码支付</p>
+                <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">
                   {qrCode ? (
                     <img 
@@ -272,7 +256,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
                       className="w-48 h-48 object-contain bg-white rounded-lg" 
                     />
                   ) : (
-                    <div className="w-48 h-48 flex items-center justify-center text-gray-400">二维码加载中...</div>
+                    <div className="w-48 h-48 flex items-center justify-center text-gray-400">{t('common.loading')}</div>
                   )}
                 </div>
               </div>
@@ -283,7 +267,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
                 onClick={() => router.push('/dashboard')} 
                 className="text-blue-600 hover:text-blue-800 hover:underline font-medium text-sm transition"
               >
-                我已完成支付,查看订单状态
+                {t('payment.completed_btn')}
               </button>
             </div>
           </div>

+ 26 - 64
src/components/ServiceList.tsx

@@ -3,8 +3,10 @@
 import { useEffect, useState } from 'react';
 import { useRouter } from 'next/navigation';
 import api from '@/lib/api';
-import { Loader2, Tag, Search, MapPin, Filter, X, Globe } from 'lucide-react';
+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 {
   id: number;
@@ -20,47 +22,24 @@ interface Product {
 
 export default function ServiceList() {
   const router = useRouter();
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
   
-  // 数据状态
   const [products, setProducts] = useState<Product[]>([]);
   const [loading, setLoading] = useState<boolean>(true);
   const [error, setError] = useState<string>('');
 
-  // 筛选状态
   const [keyword, setKeyword] = useState('');
   const [selectedCountry, setSelectedCountry] = useState('');
   const [selectedType, setSelectedType] = useState('');
   
-  // 分页状态
   const [page, setPage] = useState(1);
   const [pageSize] = useState(9);
   const [total, setTotal] = useState(0);
 
-  // 选项配置 (Mock 或从 API 获取)
-  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'
-  ];
+  // 选项配置
+  const countries = ['Austria','Croatia','Denmark','Finland','France','Germany','Greece','Hungary','Iceland','Italy','Netherlands','Poland','Spain'];
+  const visaTypes = ['Tourist','Business','Family','Student','Work','Transit','e-Visa'];
 
   useEffect(() => {
     fetchProducts(1);
@@ -71,7 +50,6 @@ export default function ServiceList() {
     setError('');
     
     try {
-      // API 请求
       const res = await api.get('/api/vas/product/list', {
         params: {
           page: targetPage,
@@ -85,7 +63,6 @@ export default function ServiceList() {
       const data = res.data.data || {};
       
       if (Array.isArray(data)) {
-        // 旧接口兼容 & 前端过滤逻辑
         let filtered = data;
         if (keyword) {
           const lowerKey = keyword.toLowerCase();
@@ -109,24 +86,9 @@ export default function ServiceList() {
 
     } catch (err) {
       console.warn("API Error, using mock data");
-      // Mock Data
-      const mockData = [
-        { id: 1, title: 'France Visa Appointment', country: 'France', city: 'Dublin', visa_type: 'Tourist', price_amount: 8000, price_currency: 'EUR', description: '15天停留,有效期3个月' },
-        { id: 2, title: 'Thailand E-Visa', country: 'Thailand', city: 'Online', visa_type: 'E-Visa', price_amount: 4500, price_currency: 'CNY', description: '极速出签,无需排队' },
-        { id: 3, title: 'Japan Tourist Visa', country: 'Japan', city: 'Shanghai', visa_type: 'Tourist', price_amount: 3000, price_currency: 'CNY', description: '单次入境,简单材料' },
-        { id: 4, title: 'US B1/B2 Interview', country: 'USA', city: 'Beijing', visa_type: 'Business', price_amount: 120000, price_currency: 'CNY', description: '包含面签培训服务' },
-      ];
-      
-      const filtered = mockData.filter(p => {
-        const lowerKey = keyword.toLowerCase();
-        const matchKey = !keyword || p.title.toLowerCase().includes(lowerKey) || p.city.toLowerCase().includes(lowerKey);
-        const matchCountry = !selectedCountry || p.country === selectedCountry;
-        const matchType = !selectedType || p.visa_type === selectedType;
-        return matchKey && matchCountry && matchType;
-      });
-      
-      setProducts(filtered);
-      setTotal(filtered.length);
+      // Mock Data Fallback
+      setProducts([]);
+      setTotal(0);
     } finally {
       setLoading(false);
     }
@@ -137,13 +99,13 @@ export default function ServiceList() {
   };
 
   const handleReset = () => {
-    window.location.reload();
+    setKeyword('');
+    setSelectedCountry('');
+    setSelectedType('');
+    fetchProducts(1);
   };
 
-  // === 核心修改点 ===
   const handleOrderClick = (id: number) => {
-    // 移除 Token 检查,直接跳转下单页
-    // 下单页 (CreateOrderForm) 会负责处理 "自动注册" 和 "绑定邮箱"
     router.push(`/create-order/${id}`);
   };
 
@@ -156,7 +118,7 @@ export default function ServiceList() {
           <div className="relative md:col-span-1">
             <input 
               type="text" 
-              placeholder="搜索国家、城市或服务..." 
+              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"
               value={keyword}
               onChange={(e) => setKeyword(e.target.value)}
@@ -171,7 +133,7 @@ export default function ServiceList() {
               value={selectedCountry}
               onChange={(e) => setSelectedCountry(e.target.value)}
             >
-              <option value="">所有国家</option>
+              <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" />
@@ -183,7 +145,7 @@ export default function ServiceList() {
               value={selectedType}
               onChange={(e) => setSelectedType(e.target.value)}
             >
-              <option value="">所有类型</option>
+              <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" />
@@ -194,13 +156,13 @@ export default function ServiceList() {
               onClick={handleSearch}
               className="flex-1 bg-slate-900 text-white rounded-lg text-sm font-bold hover:bg-slate-800 transition shadow-sm flex items-center justify-center gap-2"
             >
-              <Search size={16} /> 查询
+              <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"
-                title="重置筛选"
+                title={t('services.reset_filter')}
               >
                 <X size={18} />
               </button>
@@ -221,8 +183,8 @@ export default function ServiceList() {
           <div className="mx-auto w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
             <Search className="text-slate-300" size={32} />
           </div>
-          <h3 className="text-slate-900 font-bold mb-1">未找到相关服务</h3>
-          <p className="text-slate-500 text-sm">请尝试调整搜索关键词</p>
+          <h3 className="text-slate-900 font-bold mb-1">{t('services.no_result_title')}</h3>
+          <p className="text-slate-500 text-sm">{t('services.no_result_desc')}</p>
         </div>
       ) : (
         <div className="grid md:grid-cols-3 gap-6">
@@ -253,12 +215,12 @@ export default function ServiceList() {
               </h2>
               
               <p className="text-slate-500 mb-6 text-sm flex-grow line-clamp-2">
-                {item.description || '暂无详细描述'}
+                {item.description || t('services.no_desc')}
               </p>
               
               <div className="flex items-center justify-between mt-auto pt-4 border-t border-slate-50">
                 <div className="flex flex-col">
-                  <span className="text-xs text-slate-400 font-medium">服务费</span>
+                  <span className="text-xs text-slate-400 font-medium">{t('services.service_fee')}</span>
                   <span className="text-lg font-bold text-slate-900 leading-none">
                     <span className="text-xs font-normal mr-0.5">{item.price_currency === 'CNY' ? '¥' : item.price_currency}</span>
                     {(item.price_amount / 100).toLocaleString()}
@@ -268,7 +230,7 @@ export default function ServiceList() {
                   onClick={() => handleOrderClick(item.id)}
                   className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-bold text-sm shadow-sm shadow-blue-200 active:scale-95"
                 >
-                  立即申请
+                  {t('services.apply_now')}
                 </button>
               </div>
             </div>
@@ -279,7 +241,7 @@ export default function ServiceList() {
       <Pagination 
         currentPage={page}
         total={total}
-        pageSize={pageSize} // 这里去掉了[0],直接用值
+        pageSize={pageSize}
         onPageChange={(p) => fetchProducts(p)}
       />
     </div>

+ 3 - 1
src/components/admin/AdminSidebar.tsx

@@ -16,7 +16,8 @@ import {
   ChevronRight,
   Plane,
   LucideIcon,
-  CalendarClock
+  CalendarClock,
+  LayoutGrid
 } from 'lucide-react';
 
 interface MenuItem {
@@ -42,6 +43,7 @@ export default function AdminSidebar() {
     { name: '商品配置', href: '/admin/products', icon: Settings },
     { name: '系统任务', href: '/admin/tasks', icon: Activity },
     { name: 'TROOV Slot监控', href: '/admin/slots', icon: CalendarClock }, // 新增
+    { name: '卡片管理', href: '/admin/cards', icon: LayoutGrid }, 
   ];
 
   // 初始化:从 localStorage 读取状态

+ 301 - 0
src/components/admin/cards/CardModal.tsx

@@ -0,0 +1,301 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import api from '@/lib/api';
+import { X, Loader2, Upload, Image as ImageIcon } from 'lucide-react';
+
+// 直接定义在文件内部
+export interface CardData {
+  id?: number;
+  title: string;
+  content: string;
+  image: string; // 存储图片 ID 或 URL
+  label: string;
+  country: string;
+  additional_info?: string;
+  culture: 'english' | 'chinese';
+  created_at?: string;
+}
+
+interface CardModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  onSuccess: () => void;
+  card?: CardData | null; // 如果有值则是编辑模式
+}
+
+// 辅助函数:根据 image 字段(可能是 ID 或 URL)生成预览链接
+const getPreviewUrl = (imageField: string | null) => {
+  if (!imageField) return '';
+  if (imageField.startsWith('http') || imageField.startsWith('data:')) {
+    return imageField;
+  }
+  // 假设存的是 file_id,拼接下载链接
+  const firstFid = imageField.split(',')[0];
+  return `/api/resource/download_file?fid=${firstFid}`;
+};
+
+export default function CardModal({ isOpen, onClose, onSuccess, card }: CardModalProps) {
+  const [loading, setLoading] = useState(false);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+  
+  // 表单状态
+  const [form, setForm] = useState<CardData>({
+    title: '',
+    content: '',
+    image: '',
+    label: '',
+    country: '',
+    additional_info: '',
+    culture: 'english'
+  });
+
+  // 图片预览状态
+  const [previewUrl, setPreviewUrl] = useState('');
+
+  // 初始化:打开弹窗时填充数据
+  useEffect(() => {
+    if (isOpen) {
+      if (card) {
+        setForm(card);
+        setPreviewUrl(getPreviewUrl(card.image));
+      } else {
+        // 重置为默认值
+        setForm({
+          title: '',
+          content: '',
+          image: '',
+          label: '',
+          country: '',
+          additional_info: '',
+          culture: 'english'
+        });
+        setPreviewUrl('');
+      }
+    }
+  }, [isOpen, card]);
+
+  // 处理文件选择与上传
+  const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) return;
+
+    // 1. 简单校验大小 (2MB)
+    if (file.size > 2 * 1024 * 1024) {
+      alert("图片大小不能超过 2MB");
+      return;
+    }
+
+    // 2. 本地预览 (提升体验,不用等上传完就能看到)
+    setPreviewUrl(URL.createObjectURL(file));
+
+    // 3. 上传到服务器
+    const formData = new FormData();
+    formData.append('pdf', file); // 注意:根据 API 文档,字段名为 'pdf'
+
+    try {
+      const res = await api.post('/api/resource/upload_file', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      });
+      
+      // 假设返回结构: { code: 0, data: "fid_string" } 或 { data: { url: "..." } }
+      const result = res.data.data;
+      
+      // 如果返回的是对象且有 url/id,取对应值;如果是字符串直接用
+      const uploadedValue = (typeof result === 'object' && result !== null) ? (result.url || result.id) : result;
+      
+      setForm(prev => ({ ...prev, image: uploadedValue }));
+    } catch (error) {
+      console.error("Upload failed", error);
+      alert("图片上传失败,请检查网络或重试");
+    }
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setLoading(true);
+    try {
+      // API: POST /api/cards/publish
+      await api.post('/api/cards/publish', form);
+      
+      alert(card ? '更新成功' : '发布成功');
+      onSuccess(); // 刷新列表
+      onClose();   // 关闭弹窗
+    } catch (error: any) {
+      console.error(error);
+      const msg = error.response?.data?.message || "操作失败";
+      alert(`错误: ${msg}`);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  if (!isOpen) return null;
+
+  const labelClass = "block text-sm font-bold text-slate-700 mb-1";
+  const inputClass = "w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition";
+
+  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-3xl max-h-[90vh] 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">
+          <h3 className="font-bold text-gray-900 text-lg">
+            {card ? '编辑卡片' : '发布新卡片'}
+          </h3>
+          <button onClick={onClose} className="p-1 hover:bg-gray-200 rounded-full transition">
+            <X className="text-gray-400 hover:text-gray-600" size={24} />
+          </button>
+        </div>
+
+        {/* Body */}
+        <div className="p-6 overflow-y-auto">
+          <form onSubmit={handleSubmit} className="space-y-5">
+            
+            {/* 第一行:标题 & 语言 */}
+            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+              <div className="md:col-span-2">
+                <label className={labelClass}>标题 (Title) <span className="text-red-500">*</span></label>
+                <input 
+                  required 
+                  type="text" 
+                  className={inputClass} 
+                  value={form.title} 
+                  onChange={e => setForm({...form, title: e.target.value})} 
+                  placeholder="请输入卡片标题"
+                />
+              </div>
+              <div>
+                <label className={labelClass}>语言 (Culture)</label>
+                <select 
+                  className={inputClass} 
+                  value={form.culture} 
+                  onChange={e => setForm({...form, culture: e.target.value as any})}
+                >
+                  <option value="english">English</option>
+                  <option value="chinese">Chinese</option>
+                </select>
+              </div>
+            </div>
+
+            {/* 第二行:国家 & 标签 */}
+            <div className="grid grid-cols-2 gap-4">
+              <div>
+                <label className={labelClass}>国家 (Country)</label>
+                <input 
+                  type="text" 
+                  className={inputClass} 
+                  placeholder="e.g. France" 
+                  value={form.country} 
+                  onChange={e => setForm({...form, country: e.target.value})} 
+                />
+              </div>
+              <div>
+                <label className={labelClass}>标签 (Label)</label>
+                <input 
+                  type="text" 
+                  className={inputClass} 
+                  placeholder="e.g. Visa Guide" 
+                  value={form.label} 
+                  onChange={e => setForm({...form, label: e.target.value})} 
+                />
+              </div>
+            </div>
+
+            {/* 图片上传区域 */}
+            <div>
+              <label className={labelClass}>封面图片 (Image)</label>
+              <div className="flex items-start gap-4">
+                {/* 上传按钮/预览区 */}
+                <div 
+                  onClick={() => fileInputRef.current?.click()}
+                  className="w-32 h-24 border-2 border-dashed border-slate-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:bg-slate-50 hover:border-blue-400 transition bg-slate-50 flex-shrink-0 overflow-hidden relative"
+                >
+                  {previewUrl ? (
+                    <img src={previewUrl} alt="Preview" className="w-full h-full object-cover" />
+                  ) : (
+                    <>
+                      <Upload className="text-slate-400 mb-1" size={20} />
+                      <span className="text-xs text-slate-500">点击上传</span>
+                    </>
+                  )}
+                </div>
+
+                {/* 手动输入/提示区 */}
+                <div className="flex-1 space-y-2">
+                  <input 
+                    type="text" 
+                    className={inputClass} 
+                    placeholder="或者直接输入图片 URL / File ID" 
+                    value={form.image}
+                    onChange={e => {
+                      setForm({...form, image: e.target.value});
+                      // 如果用户手动输入 URL,尝试预览
+                      if(e.target.value.startsWith('http')) {
+                        setPreviewUrl(e.target.value);
+                      }
+                    }}
+                  />
+                  <p className="text-xs text-gray-400">
+                    推荐尺寸: 800x600 或 16:9 比例。支持上传本地图片或粘贴网络图片链接。
+                  </p>
+                  <input 
+                    type="file" 
+                    ref={fileInputRef} 
+                    className="hidden" 
+                    accept="image/*" 
+                    onChange={handleFileChange} 
+                  />
+                </div>
+              </div>
+            </div>
+
+            {/* 内容编辑器 (Textarea) */}
+            <div>
+              <label className={labelClass}>内容详情 (Content - 支持 HTML)</label>
+              <textarea 
+                className="w-full border border-slate-300 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 outline-none h-48 font-mono leading-relaxed resize-y"
+                placeholder="<p>在这里输入文章内容...</p>"
+                value={form.content}
+                onChange={e => setForm({...form, content: e.target.value})}
+              />
+            </div>
+
+            {/* 附加信息 */}
+            <div>
+              <label className={labelClass}>附加信息 (Additional Info - 可选)</label>
+              <input 
+                type="text" 
+                className={inputClass} 
+                value={form.additional_info} 
+                onChange={e => setForm({...form, additional_info: e.target.value})} 
+                placeholder="例如:备注、内部ID等"
+              />
+            </div>
+
+            {/* Footer Buttons */}
+            <div className="pt-4 flex justify-end gap-3 border-t mt-6">
+              <button 
+                type="button" 
+                onClick={onClose} 
+                className="px-4 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 text-sm font-medium text-slate-700 transition"
+              >
+                取消
+              </button>
+              <button 
+                type="submit" 
+                disabled={loading} 
+                className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-bold flex items-center gap-2 disabled:opacity-50 transition shadow-sm"
+              >
+                {loading && <Loader2 size={16} className="animate-spin" />}
+                {card ? '保存修改' : '确认发布'}
+              </button>
+            </div>
+
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 95 - 0
src/components/admin/cards/CardTable.tsx

@@ -0,0 +1,95 @@
+'use client';
+
+import { Edit, Image as ImageIcon } from 'lucide-react';
+
+export interface KnowledgeCard {
+  id: number;
+  title: string;
+  content: string;
+  image: string;
+  label: string;
+  country: string;
+  additional_info: string;
+  culture: string;
+  created_at?: string;
+  updated_at?: string;
+}
+
+interface CardTableProps {
+  cards: KnowledgeCard[];
+  loading: boolean;
+  onEdit: (card: KnowledgeCard) => void;
+}
+
+const getImageUrl = (fidString: string | null) => {
+  if (!fidString) return null;
+  if (fidString.startsWith('http')) return fidString;
+  const firstFid = fidString.split(' ')[0];
+  return `/api/resource/download_file?fid=${firstFid}`;
+};
+
+export default function CardTable({ cards, loading, onEdit }: CardTableProps) {
+  if (loading) return <div className="p-12 text-center text-gray-500">加载数据中...</div>;
+  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 || '-'}
+                    </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"
+                  >
+                    <Edit size={14} className="mr-1" /> Edit
+                  </button>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+}

+ 8 - 2
src/components/admin/orders/OrderDetailModal.tsx

@@ -3,6 +3,8 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api'; 
 import { X, CreditCard, User, FileText, Package, Loader2, Phone, Mail } from 'lucide-react';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 // === 数据结构定义 ===
 export interface OrderDetail {
@@ -154,7 +156,10 @@ export default function OrderDetailModal({ isOpen, onClose, order }: OrderDetail
             <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
               订单详情 <span className="font-mono text-blue-600">#{order.id}</span>
             </h3>
-            <p className="text-xs text-gray-500 mt-1">创建时间: {new Date(order.created_at).toLocaleString()}</p>
+            {/* 2. 使用 LocalTime 组件显示创建时间 */}
+            <p className="text-xs text-gray-500 mt-1 flex items-center gap-1">
+              创建时间: <LocalTime date={order.created_at} />
+            </p>
           </div>
           <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 rounded-full hover:bg-gray-200">
             <X size={24} />
@@ -286,7 +291,8 @@ export default function OrderDetailModal({ isOpen, onClose, order }: OrderDetail
                       </td>
                       <td className="px-4 py-2">{getStatusBadge(pay.status)}</td>
                       <td className="px-4 py-2 text-gray-500 text-xs">
-                        {new Date(pay.created_at).toLocaleString()}
+                        {/* 3. 使用 LocalTime 组件 */}
+                        <LocalTime date={pay.created_at} />
                       </td>
                       <td className="px-4 py-2 text-gray-400 text-xs font-mono">
                         {pay.external_trade_no || '-'}

+ 7 - 4
src/components/admin/orders/OrderTable.tsx

@@ -2,6 +2,8 @@
 
 import { Eye, XCircle, User, Box, Edit } from 'lucide-react';
 import { OrderDetail } from './OrderDetailModal';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 // 复用 OrderDetailModal 中的类型定义,确保一致性
 interface OrderTableProps {
@@ -9,7 +11,7 @@ interface OrderTableProps {
   loading: boolean;
   onCancel: (orderId: string) => void;
   onViewDetail: (order: OrderDetail) => void;
-  onEdit: (order: OrderDetail) => void; // <--- 新增回调:编辑
+  onEdit: (order: OrderDetail) => void;
 }
 
 export default function OrderTable({ orders, loading, onCancel, onViewDetail, onEdit }: OrderTableProps) {
@@ -76,7 +78,8 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
                 <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">
-                    {new Date(order.created_at).toLocaleDateString()} {new Date(order.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+                    {/* 2. 使用 LocalTime 组件替代 new Date().toLocaleString() */}
+                    <LocalTime date={order.created_at} />
                   </div>
                 </td>
 
@@ -121,7 +124,7 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
                 <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"
@@ -139,7 +142,7 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
                       <Eye size={16} />
                     </button>
                     
-                    {/* 取消按钮 (仅非终止状态显示) */}
+                    {/* 取消按钮 */}
                     {order.status !== 'cancelled' && order.status !== 'completed' && (
                       <button 
                         onClick={() => onCancel(order.id)}

+ 14 - 19
src/components/admin/payments/QrManager.tsx

@@ -15,10 +15,12 @@ import {
   ToggleRight,
   Upload
 } from 'lucide-react';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 interface QrManagerProps {
-  providerId: number | null; // 用于拉取列表 (保持旧逻辑,除非列表接口也变了)
-  providerName: string;      // 核心修改:用于创建时的 provider 字段 (如 'alipay', 'wechat')
+  providerId: number | null; 
+  providerName: string;      
   isOpen: boolean;
   onClose: () => void;
 }
@@ -28,7 +30,7 @@ interface PaymentQr {
   qr_code: string; 
   image_url?: string;
   priority?: number;
-  is_active: boolean | number; // 兼容后端可能返回 1/0 或 true/false
+  is_active: boolean | number; 
   description?: string;
   device?: string;
   created_at?: string;
@@ -40,7 +42,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const [submitting, setSubmitting] = useState(false);
   const [togglingId, setTogglingId] = useState<number | null>(null);
   
-  // 隐藏的文件输入框引用
   const fileInputRef = useRef<HTMLInputElement>(null);
 
   const [form, setForm] = useState({
@@ -48,7 +49,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     description: '',
     device: '', 
     priority: 10,
-    is_active: 1 // 默认为 1 (启用)
+    is_active: 1
   });
 
   useEffect(() => {
@@ -57,7 +58,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     }
   }, [isOpen, providerId]);
 
-  // 拉取列表 (假设列表接口依然支持 provider_id 查询,如果也变成了字符串请同步修改)
   const fetchQrs = async () => {
     setLoading(true);
     try {
@@ -74,7 +74,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     }
   };
 
-  // === 处理文件上传转 Base64 ===
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
@@ -99,18 +98,16 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
     }
   };
 
-  // === 核心修改:创建逻辑 ===
   const handleAdd = async () => {
     if (!form.qr_code) return alert("请上传二维码图片");
     
     setSubmitting(true);
     try {
-      // 构造符合新 API 的 Payload
       const payload = {
-        provider: providerName, // 使用字符串 (如 'alipay')
+        provider: providerName,
         qr_code: form.qr_code,
         device: form.device,
-        is_active: Number(form.is_active), // 确保是数字 1 或 0
+        is_active: Number(form.is_active),
         priority: Number(form.priority),
         description: form.description
       };
@@ -120,7 +117,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
       alert("添加成功");
       fetchQrs();
       
-      // 重置表单
       setForm({ qr_code: '', description: '', device: '', priority: 10, is_active: 1 });
       if (fileInputRef.current) fileInputRef.current.value = '';
       
@@ -135,7 +131,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/delete`, { params: {"id": id}}); // 假设删除接口还是用 ID
+      await api.delete(`/api/vas/payment_qr/${id}`); // 如果API有变动,请调整此处
       fetchQrs();
     } catch (e) {
       alert("删除失败");
@@ -145,11 +141,10 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleToggleActive = async (qr: PaymentQr) => {
     setTogglingId(qr.id);
     try {
-      // 假设更新接口没变,如果也变了请相应调整
-      await api.post(`/api/vas/payment_qr/set_enable`, {
+      await api.put(`/api/vas/payment_qr/${qr.id}`, { // 假设更新接口
         is_active: !qr.is_active 
-      }, { params: {"id": qr.id} });
-      fetchQrs(); // 刷新列表以获取最新状态
+      });
+      fetchQrs();
     } catch (e: any) {
       alert("状态更新失败");
     } finally {
@@ -303,7 +298,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
             ) : (
               <div className="grid grid-cols-1 gap-4">
                 {qrs.map((item) => {
-                  // 处理 is_active 可能是 boolean 或 number
                   const isActive = Boolean(item.is_active);
 
                   return (
@@ -367,7 +361,8 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
                         </div>
                         
                         <div className="text-[10px] text-gray-400">
-                          创建时间: {item.created_at ? new Date(item.created_at).toLocaleString() : '-'}
+                          {/* 2. 使用 LocalTime 组件替代 toLocaleString */}
+                          创建时间: {item.created_at ? <LocalTime date={item.created_at} /> : '-'}
                         </div>
                       </div>
 

+ 14 - 5
src/components/admin/tickets/TicketDetailModal.tsx

@@ -4,6 +4,8 @@ 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 { AdminTicket } from './TicketTable';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 interface TicketDetailModalProps {
   isOpen: boolean;
@@ -123,9 +125,11 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
               工单 #{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>
-            <p className="text-xs text-gray-500 mt-1">
-              用户: {ticket.user_id} | 订单: {ticket.order_id} | 创建: {new Date(ticket.created_at).toLocaleString()}
-            </p>
+            <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>
           </div>
           <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 hover:bg-gray-200 rounded-full transition">
             <X size={24} />
@@ -219,8 +223,13 @@ export default function TicketDetailModal({ isOpen, onClose, ticket, onUpdate }:
                           }`}>
                             {msg.content}
                           </div>
-                          <div className={`text-[10px] text-gray-400 mt-1 px-1 ${isAdmin ? 'text-right' : 'text-left'}`}>
-                             {msg.sender_type === 'system' ? '系统' : (isAdmin ? '客服' : '用户')} • {new Date(msg.created_at).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit'})}
+                          <div className={`text-[10px] text-gray-400 mt-1 px-1 flex items-center gap-1 ${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' }}
+                             />
                           </div>
                         </div>
                       </div>

+ 4 - 1
src/components/admin/tickets/TicketTable.tsx

@@ -1,6 +1,8 @@
 'use client';
 
 import { Eye, Clock, CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 // 定义工单数据结构 (对应 API: VasTicketOut)
 export interface AdminTicket {
@@ -99,7 +101,8 @@ export default function TicketTable({ tickets, loading, onViewDetail }: TicketTa
                   {getStatusBadge(ticket.status)}
                 </td>
                 <td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
-                  {new Date(ticket.created_at).toLocaleString()}
+                  {/* 2. 使用 LocalTime 组件显示创建时间 */}
+                  <LocalTime date={ticket.created_at} />
                 </td>
                 <td className="px-6 py-4 text-right">
                   <button 

+ 115 - 41
src/components/common/JsonEditor.tsx

@@ -1,78 +1,152 @@
 'use client';
 
 import { useState, useEffect } from 'react';
-import { Braces, Check, AlertCircle } from 'lucide-react';
+import { Braces, Check, AlertCircle, Copy } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface JsonEditorProps {
-  label: string;
+  label?: string;
   value: string; // JSON string
   onChange: (val: string) => void;
   height?: string;
   placeholder?: string;
+  readOnly?: boolean;
 }
 
-export default function JsonEditor({ label, value, onChange, height = "h-40", placeholder }: JsonEditorProps) {
-  const [text, setText] = useState(value);
+export default function JsonEditor({ 
+  label, 
+  value, 
+  onChange, 
+  height = "h-40", 
+  placeholder,
+  readOnly = false
+}: JsonEditorProps) {
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
+  const [text, setText] = useState(value || '');
   const [error, setError] = useState<string | null>(null);
+  const [isValid, setIsValid] = useState(true);
+  const [copied, setCopied] = useState(false);
 
   useEffect(() => {
-    setText(value);
+    setText(value || '');
+    validateJson(value || '');
   }, [value]);
 
-  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    const val = e.target.value;
-    setText(val);
-    onChange(val);
-    
+  const validateJson = (val: string) => {
+    if (!val.trim()) {
+      setError(null);
+      setIsValid(true);
+      return true;
+    }
     try {
-      if (val.trim()) {
-        JSON.parse(val);
-      }
+      JSON.parse(val);
       setError(null);
+      setIsValid(true);
+      return true;
     } catch (err) {
-      setError("无效的 JSON 格式");
+      // 3. 翻译错误提示
+      setError(t('common.json_invalid'));
+      setIsValid(false);
+      return false;
     }
   };
 
-  const formatJson = (e: React.MouseEvent) => {
-    e.preventDefault();
+  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    const val = e.target.value;
+    setText(val);
+    onChange(val);
+    validateJson(val);
+  };
+
+  const formatJson = () => {
+    if (!text.trim()) return;
     try {
       const obj = JSON.parse(text);
       const formatted = JSON.stringify(obj, null, 2);
       setText(formatted);
       onChange(formatted);
       setError(null);
+      setIsValid(true);
     } catch (err) {
-      alert("无法格式化:JSON 语法错误");
+      // 3. 翻译错误提示
+      setError(t('common.json_syntax_error'));
     }
   };
 
+  const copyToClipboard = () => {
+    navigator.clipboard.writeText(text);
+    setCopied(true);
+    setTimeout(() => setCopied(false), 2000);
+  };
+
   return (
-    <div>
-      <div className="flex justify-between items-center mb-1">
-        <label className="block text-sm font-medium text-slate-700">{label}</label>
-        <button 
-          onClick={formatJson}
-          className="text-xs flex items-center text-blue-600 hover:text-blue-800"
-          type="button"
-        >
-          <Braces size={12} className="mr-1" /> 格式化
-        </button>
+    <div className="w-full">
+      <div className="flex justify-between items-end mb-2">
+        {label && (
+          <label className="block text-sm font-bold text-slate-700 flex items-center gap-2">
+            {label}
+            {/* 状态指示标 */}
+            {text && (
+              <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${isValid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
+                {/* 3. 翻译状态 */}
+                {isValid ? t('common.valid') : t('common.invalid')}
+              </span>
+            )}
+          </label>
+        )}
+        
+        {/* 工具栏 */}
+        <div className="flex gap-2">
+          {!readOnly && (
+            <button 
+              onClick={formatJson}
+              className="text-xs flex items-center px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded transition"
+              type="button"
+              title={t('common.format')}
+            >
+              <Braces size={12} className="mr-1" /> {t('common.format')}
+            </button>
+          )}
+          <button 
+            onClick={copyToClipboard}
+            className="text-xs flex items-center px-2 py-1 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded transition"
+            type="button"
+            title={t('common.copy')}
+          >
+            {copied ? <Check size={12} className="mr-1 text-green-600" /> : <Copy size={12} className="mr-1" />}
+            {copied ? t('common.copied') : t('common.copy')}
+          </button>
+        </div>
+      </div>
+
+      <div className="relative">
+        <textarea
+          className={`w-full border rounded-lg p-3 font-mono text-xs leading-relaxed outline-none transition bg-slate-50
+            ${error 
+              ? 'border-red-300 focus:border-red-400 focus:ring-4 focus:ring-red-100' 
+              : 'border-slate-300 focus:border-blue-400 focus:ring-4 focus:ring-blue-100'
+            }
+            ${height}
+            ${readOnly ? 'opacity-70 cursor-not-allowed' : ''}
+          `}
+          value={text}
+          onChange={handleChange}
+          placeholder={placeholder || '{\n  "key": "value"\n}'}
+          spellCheck={false}
+          readOnly={readOnly}
+        />
+        
+        {/* 错误提示 */}
+        {error && (
+          <div className="absolute bottom-3 left-3 right-3 bg-red-50 text-red-600 text-xs px-2 py-1 rounded border border-red-100 flex items-center animate-in fade-in slide-in-from-bottom-1">
+            <AlertCircle size={12} className="mr-1.5 flex-shrink-0" /> 
+            {error}
+          </div>
+        )}
       </div>
-      <textarea
-        className={`w-full border rounded-lg p-2 font-mono text-xs leading-relaxed outline-none transition
-          ${error ? 'border-red-300 focus:ring-red-200' : 'border-slate-300 focus:ring-blue-200'}
-          ${height}
-        `}
-        value={text}
-        onChange={handleChange}
-        placeholder={placeholder || '{\n  "key": "value"\n}'}
-      />
-      {error && (
-        <p className="text-xs text-red-500 mt-1 flex items-center">
-          <AlertCircle size={12} className="mr-1" /> {error}
-        </p>
-      )}
     </div>
   );
 }

+ 19 - 0
src/components/common/LanguageSwitcher.tsx

@@ -0,0 +1,19 @@
+'use client';
+
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { Languages } from 'lucide-react';
+
+export default function LanguageSwitcher() {
+  const { lang, setLang } = useLanguage();
+
+  return (
+    <button
+      onClick={() => setLang(lang === 'zh' ? 'en' : 'zh')}
+      className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-100 text-slate-600 transition text-sm font-medium"
+      title="Switch Language"
+    >
+      <Languages size={18} />
+      <span>{lang === 'zh' ? 'English' : '中文'}</span>
+    </button>
+  );
+}

+ 68 - 0
src/components/common/LocalTime.tsx

@@ -0,0 +1,68 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+
+interface LocalTimeProps {
+  date: string | Date; // 接收 UTC 时间
+  className?: string;
+  options?: Intl.DateTimeFormatOptions;
+}
+
+export default function LocalTime({ date, className, options }: LocalTimeProps) {
+  const { lang } = useLanguage();
+  const [localTimeString, setLocalTimeString] = useState<string>('');
+
+  useEffect(() => {
+    if (!date) return;
+    
+    let dateObj: Date;
+
+    // === 核心修复逻辑 ===
+    if (typeof date === 'string') {
+      // 1. 兼容 "YYYY-MM-DD HH:mm:ss" 格式,将空格替换为 T
+      let normalizedDate = date.replace(' ', 'T');
+
+      // 2. 如果字符串末尾没有 'Z' 且没有时区偏移(如 +08:00),强制追加 'Z'
+      // 这告诉浏览器:“这个时间字符串是 UTC 时间”
+      if (!normalizedDate.endsWith('Z') && !normalizedDate.includes('+')) {
+        normalizedDate += 'Z';
+      }
+      
+      dateObj = new Date(normalizedDate);
+    } else {
+      dateObj = date;
+    }
+
+    // 检查日期是否有效
+    if (isNaN(dateObj.getTime())) {
+      setLocalTimeString('Invalid Date');
+      return;
+    }
+
+    const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
+    
+    const defaultOptions: Intl.DateTimeFormatOptions = {
+      year: 'numeric',
+      month: '2-digit',
+      day: '2-digit',
+      hour: '2-digit',
+      minute: '2-digit',
+      second: '2-digit',
+      hour12: false,
+    };
+
+    const finalOptions = { ...defaultOptions, ...options };
+    
+    // 使用浏览器的 Intl API 进行本地化格式化 (会自动转换为用户时区)
+    const formatter = new Intl.DateTimeFormat(locale, finalOptions);
+
+    setLocalTimeString(formatter.format(dateObj));
+  }, [date, lang, options]);
+
+  if (!localTimeString) {
+    return <span className={`opacity-0 ${className}`}>--:--</span>;
+  }
+
+  return <span className={className}>{localTimeString}</span>;
+}

+ 13 - 8
src/components/common/Pagination.tsx

@@ -1,6 +1,8 @@
 'use client';
 
 import { ChevronLeft, ChevronRight } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface PaginationProps {
   currentPage: number;
@@ -10,15 +12,15 @@ interface PaginationProps {
 }
 
 export default function Pagination({ currentPage, total, pageSize, onPageChange }: PaginationProps) {
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+  
   const totalPages = Math.ceil(total / pageSize);
 
-  if (totalPages <= 1) return null; // 只有一页时不显示
+  if (totalPages <= 1) return null;
 
-  // 生成页码数组 (简单的逻辑,比如显示 [1, 2, 3, 4, 5])
-  // 如果页数很多,这里可以优化为显示 [1, ... 5, 6, 7 ... 10]
   const renderPageNumbers = () => {
     const pages = [];
-    // 简单策略:最多显示前后2页
     const start = Math.max(1, currentPage - 2);
     const end = Math.min(totalPages, currentPage + 2);
 
@@ -30,28 +32,31 @@ export default function Pagination({ currentPage, total, pageSize, onPageChange
 
   return (
     <div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 mt-4 rounded-b-lg">
+      
+      {/* Mobile View Buttons */}
       <div className="flex flex-1 justify-between sm:hidden">
         <button
           onClick={() => onPageChange(Math.max(1, currentPage - 1))}
           disabled={currentPage === 1}
           className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
         >
-          上一页
+          {t('pagination.prev')}
         </button>
         <button
           onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
           disabled={currentPage === totalPages}
           className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
         >
-          下一页
+          {t('pagination.next')}
         </button>
       </div>
       
+      {/* Desktop View */}
       <div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
         <div>
           <p className="text-sm text-gray-700">
-            显示第 <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> 到 <span className="font-medium">{Math.min(currentPage * pageSize, total)}</span> 条,
-            共 <span className="font-medium">{total}</span> 条
+            {/* 3. 拼接翻译文本以适配中英文语序 */}
+            {t('pagination.showing')} <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> {t('pagination.to')} <span className="font-medium">{Math.min(currentPage * pageSize, total)}</span> {t('pagination.of')} <span className="font-medium">{total}</span> {t('pagination.results')}
           </p>
         </div>
         <div>

+ 23 - 34
src/components/dashboard/ChangePasswordModal.tsx

@@ -3,6 +3,8 @@
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import { X, Lock, Loader2, Save, Mail, KeyRound, Eye, EyeOff } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface ChangePasswordModalProps {
   isOpen: boolean;
@@ -10,6 +12,9 @@ interface ChangePasswordModalProps {
 }
 
 export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
   const [loading, setLoading] = useState(false);
   const [countdown, setCountdown] = useState(0);
   
@@ -19,10 +24,8 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
   const [newPassword, setNewPassword] = useState('');
   const [confirmPassword, setConfirmPassword] = useState('');
   
-  // 密码显示状态
   const [showPassword, setShowPassword] = useState(false);
 
-  // 初始化:获取当前用户邮箱
   useEffect(() => {
     if (isOpen) {
       const userStr = localStorage.getItem('user_info');
@@ -32,14 +35,12 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
           setEmail(user.email || '');
         } catch (e) {}
       }
-      // 重置表单
       setCode('');
       setNewPassword('');
       setConfirmPassword('');
     }
   }, [isOpen]);
 
-  // 倒计时逻辑
   useEffect(() => {
     let timer: NodeJS.Timeout;
     if (countdown > 0) {
@@ -48,48 +49,36 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
     return () => clearTimeout(timer);
   }, [countdown]);
 
-  // 发送验证码
   const handleSendCode = async () => {
-    if (!email) return alert("未找到用户邮箱");
+    if (!email) return alert(t('settings.email_not_found'));
     
     try {
-      // -----------------------------------------------------------
-      // TODO: 请替换为你真实的【发送重置密码验证码】接口
-      // API: POST /api/auth/send-reset-code (Body: { email })
-      // -----------------------------------------------------------
       await api.post('/api/auth/send-reset-code', { email });
       
-      alert(`验证码已发送至 ${email}`);
+      alert(`${t('settings.code_sent')} ${email}`);
       setCountdown(60);
     } catch (error: any) {
       console.error(error);
-      alert("发送失败: " + (error.response?.data?.message || "未知错误"));
+      alert(`${t('settings.send_failed')}: ` + (error.response?.data?.message || t('common.unknown_error')));
     }
   };
 
-  // 提交修改
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!code) return alert("请输入验证码");
-    if (!newPassword) return alert("请输入新密码");
-    if (newPassword !== confirmPassword) return alert("两次输入的密码不一致");
+    if (!code) return alert(t('settings.enter_code'));
+    if (!newPassword) return alert(t('settings.enter_password'));
+    if (newPassword !== confirmPassword) return alert(t('settings.password_mismatch'));
 
     setLoading(true);
     try {
-      // -----------------------------------------------------------
-      // TODO: 请替换为你真实的【重置密码】接口
-      // API: POST /api/auth/reset-password
-      // Body: { email, code, new_password }
-      // -----------------------------------------------------------
       await api.post('/api/auth/reset-password', { 
         email, 
         code, 
         new_password: newPassword 
       });
 
-      alert('密码修改成功!请使用新密码重新登录。');
+      alert(t('settings.password_changed_success'));
       
-      // 可选:强制退出登录
       localStorage.removeItem('rsid');
       localStorage.removeItem('user_info');
       window.location.href = '/login';
@@ -97,7 +86,7 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
       onClose();
     } catch (error: any) {
       console.error(error);
-      alert("修改失败: " + (error.response?.data?.message || error.message));
+      alert(`${t('settings.change_failed')}: ` + (error.response?.data?.message || error.message));
     } finally {
       setLoading(false);
     }
@@ -112,7 +101,7 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
           <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
-            <KeyRound size={20} className="text-blue-600"/> 修改密码
+            <KeyRound size={20} className="text-blue-600"/> {t('settings.change_password')}
           </h3>
           <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
         </div>
@@ -121,7 +110,7 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
           
           {/* 邮箱 & 验证码 */}
           <div>
-            <label className="block text-sm font-medium text-gray-700 mb-1">邮箱验证</label>
+            <label className="block text-sm font-medium text-gray-700 mb-1">{t('settings.email_verification')}</label>
             <div className="relative mb-3">
               <input 
                 type="text" disabled value={email}
@@ -134,7 +123,7 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
               <input
                 type="text" required
                 className="flex-1 border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none font-mono tracking-widest text-center"
-                placeholder="6位验证码"
+                placeholder={t('settings.code_placeholder')}
                 maxLength={6}
                 value={code}
                 onChange={(e) => setCode(e.target.value)}
@@ -145,7 +134,7 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
                 onClick={handleSendCode}
                 className="w-32 border border-blue-200 bg-blue-50 text-blue-600 rounded-lg text-xs font-medium hover:bg-blue-100 disabled:opacity-50 disabled:bg-slate-100 disabled:text-slate-400 disabled:border-slate-200 transition"
               >
-                {countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
+                {countdown > 0 ? `${countdown}s ${t('settings.resend')}` : t('settings.get_code')}
               </button>
             </div>
           </div>
@@ -154,12 +143,12 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
 
           {/* 新密码 */}
           <div>
-            <label className="block text-sm font-medium text-gray-700 mb-1">设置新密码</label>
+            <label className="block text-sm font-medium text-gray-700 mb-1">{t('settings.new_password_label')}</label>
             <div className="relative">
               <input
                 type={showPassword ? "text" : "password"} required
                 className="w-full border border-slate-300 rounded-lg p-2.5 pl-9 pr-10 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                placeholder="新密码"
+                placeholder={t('settings.new_password_placeholder')}
                 value={newPassword}
                 onChange={(e) => setNewPassword(e.target.value)}
               />
@@ -180,12 +169,12 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
               type={showPassword ? "text" : "password"} required
               className={`w-full border rounded-lg p-2.5 pl-9 text-sm focus:ring-2 outline-none transition
                 ${confirmPassword && newPassword !== confirmPassword ? 'border-red-300 focus:ring-red-200' : 'border-slate-300 focus:ring-blue-500'}`}
-              placeholder="确认新密码"
+              placeholder={t('settings.confirm_password_placeholder')}
               value={confirmPassword}
               onChange={(e) => setConfirmPassword(e.target.value)}
             />
             {confirmPassword && newPassword !== confirmPassword && (
-              <p className="text-xs text-red-500 mt-1">两次输入的密码不一致</p>
+              <p className="text-xs text-red-500 mt-1">{t('settings.password_mismatch')}</p>
             )}
           </div>
 
@@ -195,14 +184,14 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
               onClick={onClose}
               className="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-medium"
             >
-              取消
+              {t('common.cancel')}
             </button>
             <button 
               type="submit" 
               disabled={loading}
               className="flex-1 bg-blue-600 text-white py-2.5 rounded-lg hover:bg-blue-700 text-sm font-bold flex justify-center items-center gap-2 disabled:opacity-50 shadow-sm"
             >
-              {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> 确认修改</>}
+              {loading ? <Loader2 size={18} className="animate-spin" /> : <><Save size={18} /> {t('common.confirm')}</>}
             </button>
           </div>
         </form>

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

@@ -4,12 +4,14 @@ import { useEffect, useState } from 'react';
 import { useRouter } from 'next/navigation';
 import api from '@/lib/api';
 import { CreditCard, Loader2, AlertCircle, Package, Clock, Hash, Eye, Search } from 'lucide-react';
-import Pagination from '@/components/common/Pagination'; // 引入通用分页组件
+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;
+  created_at: string; // API 返回的 UTC 时间字符串
   status: string;
   base_amount: number;
   base_currency: string;
@@ -27,27 +29,22 @@ interface OrderListProps {
 
 export default function OrderList({ onRequestTicket, onViewDetail }: OrderListProps) {
   const router = useRouter();
+  const { t } = useLanguage();
   
-  // 数据状态
   const [loading, setLoading] = useState<boolean>(true);
   const [orders, setOrders] = useState<Order[]>([]);
-  
-  // 分页与搜索状态
   const [page, setPage] = useState(1);
-  const [pageSize] = useState(5); // 用户端每页显示 5 条,保持界面清爽
+  const [pageSize] = useState(5); 
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
 
-  // 初始化加载
   useEffect(() => {
     fetchOrders(1);
   }, []);
 
-  // 获取订单列表
   const fetchOrders = async (targetPage: number) => {
     setLoading(true);
     try {
-      // API: GET /api/vas/order/list?page=1&size=5&keyword=...
       const res = await api.get('/api/vas/order/list_by_user', {
         params: {
           page: targetPage,
@@ -56,17 +53,12 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
         }
       });
 
-      // 适配后端返回结构
-      // 1. 如果后端返回的是分页对象: { items: [], total: 100 }
-      // 2. 如果后端返回的是纯数组 (旧接口): []
       const data = res.data.data || {};
       
       if (Array.isArray(data)) {
-        // 兼容旧接口 (无分页)
         setOrders(data);
         setTotal(data.length);
       } else {
-        // 标准分页接口
         setOrders(data.items || []);
         setTotal(data.total || 0);
       }
@@ -75,42 +67,24 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
 
     } catch (error) {
       console.warn("API Error (Using Mock Data):", error);
-      // Mock Data 演示分页
-      const mockOrders = [
-        { id: 'ORD-2025-001', product_title: '日本单次旅游签证 (VIP)', base_amount: 30000, base_currency: 'CNY', status: 'pending', created_at: '2025-01-01T10:00:00' },
-        { id: 'ORD-2024-888', product_title: '泰国电子落地签', base_amount: 45000, base_currency: 'CNY', status: 'paid', created_at: '2024-12-20T14:30:00' },
-        { id: 'ORD-2024-777', product_title: '韩国五年多次', base_amount: 80000, base_currency: 'CNY', status: 'completed', created_at: '2024-11-15T09:00:00' },
-      ];
-      // 简单的 Mock 过滤
-      const filtered = keyword 
-        ? mockOrders.filter(o => o.id.includes(keyword) || o.product_title.includes(keyword))
-        : mockOrders;
-        
-      setOrders(filtered);
-      setTotal(filtered.length);
+      setOrders([]);
+      setTotal(0);
     } finally {
       setLoading(false);
     }
   };
 
-  // 处理搜索 (回车或点击按钮触发)
-  const handleSearch = () => {
-    fetchOrders(1); // 搜索时重置到第1页
-  };
+  const handleSearch = () => fetchOrders(1);
 
   const handleKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') {
-      handleSearch();
-    }
+    if (e.key === 'Enter') handleSearch();
   };
 
-  // 格式化金额
   const formatMoney = (amount: number, currency: string) => {
     if (isNaN(amount)) return '0.00';
     return `${(amount / 100).toFixed(2)} ${currency}`;
   };
 
-  // 状态样式
   const renderStatusBadge = (status: string) => {
     const styles: Record<string, string> = {
       pending: 'bg-yellow-50 text-yellow-700 ring-yellow-600/20',
@@ -120,25 +94,24 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
       failed: 'bg-red-50 text-red-700 ring-red-600/20',
       cancelled: 'bg-gray-50 text-gray-600 ring-gray-500/20',
     };
-    const labels: Record<string, string> = {
-      pending: '待支付', paid: '已支付', succeeded: '支付成功',
-      completed: '已完成', failed: '失败', cancelled: '已取消',
-    };
+    
+    const label = t(`status.${status}`) === `status.${status}` ? status.toUpperCase() : t(`status.${status}`);
+
     return (
       <span className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${styles[status] || 'bg-gray-50 text-gray-600 ring-gray-500/10'}`}>
-        {labels[status] || status.toUpperCase()}
+        {label}
       </span>
     );
   };
 
   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="搜索订单号或商品名称..." 
+            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"
             value={keyword}
             onChange={(e) => setKeyword(e.target.value)}
@@ -150,15 +123,15 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
           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"
         >
-          搜索
+          {t('common.search')}
         </button>
       </div>
 
-      {/* === 订单列表 === */}
+      {/* Order List */}
       <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
         <div className="px-6 py-4 border-b bg-gray-50/50 flex justify-between items-center">
-          <h2 className="text-base font-semibold leading-7 text-gray-900">申请记录</h2>
-          <span className="text-xs text-slate-500">共 {total} 条</span>
+          <h2 className="text-base font-semibold leading-7 text-gray-900">{t('order.history_title')}</h2>
+          <span className="text-xs text-slate-500">{t('common.total')}: {total}</span>
         </div>
         
         {loading ? (
@@ -168,20 +141,20 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
             <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">暂无订单</h3>
-            <p className="mt-1 text-sm text-gray-500">没有找到匹配的订单记录。</p>
+            <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>
         ) : (
           <ul className="divide-y divide-gray-100">
             {orders.map((order) => {
-              const title = order.product_title || order.product_name || '未命名服务';
+              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">
                   
-                  {/* 左侧信息 */}
+                  {/* 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">
@@ -196,12 +169,13 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
                       </div>
                       <div className="flex items-center gap-1">
                         <Clock size={12} /> 
-                        <time dateTime={order.created_at}>{new Date(order.created_at).toLocaleString()}</time>
+                        {/* 2. 使用 LocalTime 组件替换原始的 time 标签 */}
+                        <LocalTime date={order.created_at} />
                       </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>
@@ -212,7 +186,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
                       <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="查看详情"
+                        title={t('order.view_details')}
                       >
                         <Eye size={20} />
                       </button>
@@ -222,7 +196,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
                           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>
                       )}
 
@@ -231,7 +205,7 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
                           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>
@@ -242,7 +216,6 @@ export default function OrderList({ onRequestTicket, onViewDetail }: OrderListPr
           </ul>
         )}
         
-        {/* === 分页组件 === */}
         <Pagination 
           currentPage={page}
           total={total}

+ 50 - 65
src/components/dashboard/ProfileSettings.tsx

@@ -3,23 +3,26 @@
 import { useState, useEffect, useRef } from 'react';
 import api from '@/lib/api';
 import { getCurrentUser } from '@/lib/auth';
-import { Loader2, Save, Camera, User, Mail, Shield, Lock, ChevronRight, Edit2 } from 'lucide-react';
+import { Loader2, Save, Camera, User, Mail, Shield, Lock, ChevronRight, Edit2, Phone } from 'lucide-react';
 import ChangePasswordModal from '@/components/dashboard/ChangePasswordModal';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function ProfileSettings() {
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
   const [loading, setLoading] = useState(false);
   const [isDataLoaded, setIsDataLoaded] = useState(false);
   const [user, setUser] = useState<any>(null);
   
-  // 模式控制
   const [isEditing, setIsEditing] = useState(false);
   const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
 
-  // 表单状态
   const [nickname, setNickname] = useState('');
-  const [phone, setPhone] = useState(''); // 新增 phone 状态
-  const [avatarPreview, setAvatarPreview] = useState(''); // 用于 UI 预览
-  const [avatarFile, setAvatarFile] = useState<File | null>(null); // 用于实际上传
+  const [phone, setPhone] = useState(''); 
+  const [avatarPreview, setAvatarPreview] = useState(''); 
+  const [avatarFile, setAvatarFile] = useState<File | null>(null); 
   
   const fileInputRef = useRef<HTMLInputElement>(null);
 
@@ -34,46 +37,33 @@ export default function ProfileSettings() {
     setIsDataLoaded(true);
   }, []);
 
-  // 处理文件选择(仅预览,不立即上传)
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
     
-    // 限制大小 2MB
     if (file.size > 2 * 1024 * 1024) {
-      alert("图片大小不能超过 2MB");
+      alert(t('profile.image_size_limit')); // "图片大小不能超过 2MB"
       return;
     }
 
-    // 保存文件对象用于后续上传
     setAvatarFile(file);
-
-    // 生成本地预览 URL
     const previewUrl = URL.createObjectURL(file);
     setAvatarPreview(previewUrl);
   };
 
-  // 上传图片到服务器
   const uploadImage = async (file: File): Promise<string> => {
     const formData = new FormData();
-    // 注意:根据 swagger 文档,字段名为 'pdf',如果是通用接口可能兼容 'file'
-    // 这里依据文档 Body_resource_upload_file_api_resource_upload_file_post 使用 'pdf'
-    formData.append('file', file); 
+    formData.append('pdf', file); 
 
     const res = await api.post('/api/resource/upload_file', formData, {
-      headers: {
-        'Content-Type': 'multipart/form-data',
-      },
+      headers: { 'Content-Type': 'multipart/form-data' },
     });
 
-    // 假设返回结构为 { code: 0, data: { url: "http..." } } 或直接返回 url 字符串
-    // 根据实际 API 返回调整。通常 resource 接口会返回完整的 URL 对象
-    if (res.data.data && res.data.data.url) {
-      return res.data.data.url;
-    } else if (typeof res.data.data === 'string') {
-      return res.data.data;
-    }
-    throw new Error("图片上传响应格式错误");
+    const data = res.data.data;
+    if (typeof data === 'string') return data;
+    if (data && data.url) return data.url;
+    
+    throw new Error("Invalid response format");
   };
 
   const handleSubmit = async (e: React.FormEvent) => {
@@ -84,29 +74,25 @@ export default function ProfileSettings() {
     try {
       let finalAvatarUrl = user.avatar_url || '';
 
-      // 1. 如果有新选的文件,先上传文件
       if (avatarFile) {
         try {
           finalAvatarUrl = await uploadImage(avatarFile);
         } catch (uploadError) {
-          console.error("Image upload failed", uploadError);
-          alert("头像上传失败,请重试");
+          console.error("Upload failed", uploadError);
+          alert(t('profile.upload_failed')); // "头像上传失败..."
           setLoading(false);
           return;
         }
       }
 
-      // 2. 准备更新资料的 Payload
       const payload = {
-        phone: phone || user.phone || '', // 确保 phone 不为空
-        nickname: nickname,
+        phone: phone || user.phone || '', 
+        nickname: nickname || '',
         avatar_url: finalAvatarUrl
       };
 
-      // 3. 调用 set_profiles 接口
       await api.post('/api/user/set_profiles', payload);
 
-      // 4. 更新本地存储和状态
       const newUserInfo = { 
         ...user, 
         nickname: payload.nickname, 
@@ -115,17 +101,17 @@ export default function ProfileSettings() {
       };
       
       localStorage.setItem('user_info', JSON.stringify(newUserInfo));
-      // 触发 storage 事件以便 Header 等组件更新
       window.dispatchEvent(new Event('storage'));
 
-      alert("个人资料更新成功!");
+      alert(t('profile.update_success')); // "个人资料更新成功!"
       setUser(newUserInfo);
-      setAvatarFile(null); // 清空待上传文件
+      setAvatarFile(null); 
       setIsEditing(false);
       
     } catch (error: any) {
       console.error(error);
-      alert("更新失败: " + (error.response?.data?.message || "未知错误"));
+      const msg = error.response?.data?.message || error.message || t('common.unknown_error');
+      alert(`${t('profile.update_failed')}: ${msg}`);
     } finally {
       setLoading(false);
     }
@@ -141,25 +127,25 @@ export default function ProfileSettings() {
     setIsEditing(false);
   };
 
-  if (!isDataLoaded) return <div className="p-12 text-center text-gray-400">加载中...</div>;
-  if (!user) return <div className="p-8 text-center text-red-500">无法获取用户信息</div>;
+  if (!isDataLoaded) return <div className="p-12 text-center text-gray-400">{t('common.loading')}</div>;
+  if (!user) return <div className="p-8 text-center text-red-500">{t('profile.fetch_failed')}</div>;
 
   return (
     <div className="space-y-6">
       
-      {/* 卡片 1: 基础资料 */}
+      {/* 资料卡片 */}
       <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden max-w-3xl">
         <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50 flex justify-between items-center">
           <div>
-            <h2 className="text-base font-bold text-gray-900">个人资料</h2>
-            <p className="text-xs text-gray-500 mt-0.5">管理您的基本信息展示</p>
+            <h2 className="text-base font-bold text-gray-900">{t('profile.title')}</h2>
+            <p className="text-xs text-gray-500 mt-0.5">{t('profile.subtitle')}</p>
           </div>
           {!isEditing && (
             <button 
               onClick={() => setIsEditing(true)}
               className="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-300 rounded-lg text-sm text-slate-700 hover:bg-slate-50 hover:text-blue-600 transition shadow-sm"
             >
-              <Edit2 size={14} /> 编辑资料
+              <Edit2 size={14} /> {t('profile.edit_profile')}
             </button>
           )}
         </div>
@@ -177,22 +163,22 @@ export default function ProfileSettings() {
               </div>
               <div className="flex-1 space-y-4 w-full text-center sm:text-left">
                 <div>
-                  <h3 className="text-2xl font-bold text-gray-900">{user.nickname || '未设置昵称'}</h3>
+                  <h3 className="text-2xl font-bold text-gray-900">{user.nickname || t('profile.no_nickname')}</h3>
                   <p className="text-sm text-gray-500 mt-1">User ID: <span className="font-mono">{user.id}</span></p>
                 </div>
                 <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2">
                   <div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
                     <div className="p-2 bg-white rounded-full text-blue-500 shadow-sm"><Mail size={16} /></div>
-                    <div className="text-left">
+                    <div className="text-left overflow-hidden">
                       <p className="text-xs text-gray-400 font-bold uppercase">Email</p>
-                      <p className="text-sm text-gray-700 font-medium truncate max-w-[180px]" title={user.email}>{user.email}</p>
+                      <p className="text-sm text-gray-700 font-medium truncate" title={user.email}>{user.email}</p>
                     </div>
                   </div>
                   <div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg border border-slate-100">
-                    <div className="p-2 bg-white rounded-full text-purple-500 shadow-sm"><Shield size={16} /></div>
+                    <div className="p-2 bg-white rounded-full text-green-500 shadow-sm"><Phone size={16} /></div>
                     <div className="text-left">
-                      <p className="text-xs text-gray-400 font-bold uppercase">Role</p>
-                      <p className="text-sm text-gray-700 font-medium capitalize">{user.role || 'User'}</p>
+                      <p className="text-xs text-gray-400 font-bold uppercase">Phone</p>
+                      <p className="text-sm text-gray-700 font-medium">{user.phone || '-'}</p>
                     </div>
                   </div>
                 </div>
@@ -216,7 +202,7 @@ export default function ProfileSettings() {
                       <Camera className="text-white drop-shadow-md" size={28} />
                     </div>
                   </label>
-                  <p className="text-xs text-blue-600 font-medium">点击更换头像</p>
+                  <p className="text-xs text-blue-600 font-medium">{t('profile.change_avatar')}</p>
                   <input 
                     id="avatar-upload" 
                     type="file" 
@@ -230,41 +216,41 @@ export default function ProfileSettings() {
                 {/* 表单字段 */}
                 <div className="flex-1 space-y-5 max-w-lg">
                   <div>
-                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">账号邮箱 (不可改)</label>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">{t('profile.email_readonly')}</label>
                     <div className="w-full px-3 py-2.5 bg-slate-100 border border-slate-200 rounded-lg text-slate-500 text-sm">
                       {user.email}
                     </div>
                   </div>
                   
                   <div>
-                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">昵称</label>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">{t('profile.nickname_label')}</label>
                     <input 
                       type="text" 
                       className="w-full border border-slate-300 rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
                       value={nickname}
                       onChange={(e) => setNickname(e.target.value)}
-                      placeholder="请输入昵称"
+                      placeholder={t('profile.nickname_placeholder')}
                     />
                   </div>
 
                   <div>
-                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">手机号码</label>
+                    <label className="block text-sm font-semibold text-slate-700 mb-1.5">{t('profile.phone_label')}</label>
                     <input 
                       type="text" 
                       className="w-full border border-slate-300 rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
                       value={phone}
                       onChange={(e) => setPhone(e.target.value)}
-                      placeholder="请输入手机号码"
+                      placeholder={t('profile.phone_placeholder')}
                     />
                   </div>
 
                   <div className="pt-4 flex gap-3">
                     <button type="button" onClick={handleCancel} className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium transition">
-                      取消
+                      {t('common.cancel')}
                     </button>
                     <button type="submit" disabled={loading} className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium flex items-center gap-2 disabled:opacity-50 transition">
                       {loading ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />} 
-                      {loading ? '上传保存中...' : '保存更改'}
+                      {loading ? t('profile.saving') : t('common.save')}
                     </button>
                   </div>
                 </div>
@@ -274,11 +260,11 @@ export default function ProfileSettings() {
         </div>
       </div>
 
-      {/* 卡片 2: 账户安全 */}
+      {/* 账户安全卡片 */}
       {!isEditing && (
         <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden max-w-3xl">
           <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50">
-            <h2 className="text-base font-bold text-gray-900">账户安全</h2>
+            <h2 className="text-base font-bold text-gray-900">{t('settings.security_tip_title')}</h2>
           </div>
           <div className="p-6">
             <div className="flex items-center justify-between p-4 border border-slate-200 rounded-lg hover:border-slate-300 transition">
@@ -287,15 +273,15 @@ export default function ProfileSettings() {
                   <Lock size={20} />
                 </div>
                 <div>
-                  <h4 className="font-bold text-gray-800 text-sm">登录密码</h4>
-                  <p className="text-xs text-gray-500 mt-0.5">建议定期更换密码以保护账户安全</p>
+                  <h4 className="font-bold text-gray-800 text-sm">{t('profile.login_password')}</h4>
+                  <p className="text-xs text-gray-500 mt-0.5">{t('profile.password_tip')}</p>
                 </div>
               </div>
               <button 
                 onClick={() => setIsPasswordModalOpen(true)}
                 className="text-sm font-medium text-slate-600 hover:text-blue-600 flex items-center gap-1 px-3 py-1.5 rounded-lg hover:bg-slate-50 transition"
               >
-                修改 <ChevronRight size={16} />
+                {t('common.edit')} <ChevronRight size={16} />
               </button>
             </div>
           </div>
@@ -306,7 +292,6 @@ export default function ProfileSettings() {
       <ChangePasswordModal 
         isOpen={isPasswordModalOpen} 
         onClose={() => setIsPasswordModalOpen(false)} 
-        email={user.email} 
       />
     </div>
   );

+ 67 - 14
src/components/dashboard/Sidebar.tsx

@@ -1,6 +1,20 @@
 'use client';
 
-import { FileText, LifeBuoy, MessageSquare, Settings, LucideIcon } from 'lucide-react';
+import { FileText, LifeBuoy, MessageSquare, Settings, LucideIcon, Mail } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+
+// 1. 定义品牌图标组件 (无需额外安装包)
+const TelegramIcon = ({ className }: { className?: string }) => (
+  <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
+    <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 11.944 0zm4.925 8.531l-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.446 1.394c-.16.16-.295.295-.605.295l.213-3.054 5.56-5.022c.242-.213-.054-.333-.373-.121l-6.869 4.326-2.96-.924c-.643-.204-.657-.643.136-.953l11.57-4.458c.535-.196 1.006.128.832.941z"/>
+  </svg>
+);
+
+const WhatsAppIcon = ({ className }: { className?: string }) => (
+  <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
+    <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.008-.57-.008-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
+  </svg>
+);
 
 interface SidebarProps {
   activeTab: string; 
@@ -14,12 +28,16 @@ interface MenuItem {
 }
 
 export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
+  const { t } = useLanguage();
   
+  // 2. 这里配置你的联系方式
+  const TELEGRAM_USERNAME = "visafly_support"; 
+  const WHATSAPP_NUMBER = "1234567890"; 
+
   const menuItems: MenuItem[] = [
-    { id: 'orders', label: '我的订单', icon: FileText },
-    { id: 'tickets', label: '售后工单', icon: LifeBuoy },
-    // 恢复为普通 Tab
-    { id: 'settings', label: '账户设置', icon: Settings },
+    { 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 (
@@ -46,20 +64,55 @@ export default function Sidebar({ activeTab, setActiveTab }: SidebarProps) {
       {/* 客服卡片 */}
       <div className="mt-8 bg-blue-50 p-5 rounded-xl border border-blue-100">
         <h3 className="font-bold text-blue-800 flex items-center gap-2 mb-3 text-sm">
-          <MessageSquare size={16} /> 联系客服
+          <MessageSquare size={16} /> {t('sidebar.contact_support')}
         </h3>
         <p className="text-xs text-blue-600 mb-4 leading-relaxed">
-          遇到问题?您可以直接提交工单,或通过以下方式联系:
+          {t('sidebar.contact_desc')}
         </p>
-        <ul className="text-xs text-slate-600 space-y-2 font-mono">
-          <li className="flex items-center gap-2">
-            <span className="w-1.5 h-1.5 bg-blue-400 rounded-full"></span>
-            support@visafly.com
+        
+        {/* 联系方式列表 */}
+        <ul className="text-xs text-slate-700 space-y-3 font-medium">
+          
+          {/* Email */}
+          <li className="flex items-center gap-2.5">
+            <div className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 shrink-0">
+              <Mail size={12} />
+            </div>
+            <a href="mailto:support@visafly.top" className="hover:text-blue-600 hover:underline truncate">
+              support@visafly.top
+            </a>
           </li>
-          <li className="flex items-center gap-2">
-            <span className="w-1.5 h-1.5 bg-blue-400 rounded-full"></span>
-            @visafly_support
+
+          {/* Telegram */}
+          <li className="flex items-center gap-2.5">
+             <div className="w-5 h-5 rounded-full bg-sky-100 flex items-center justify-center text-sky-600 shrink-0">
+              <TelegramIcon className="w-3 h-3" />
+            </div>
+            <a 
+              href={`https://t.me/${TELEGRAM_USERNAME}`}
+              target="_blank" 
+              rel="noopener noreferrer"
+              className="hover:text-sky-600 hover:underline truncate"
+            >
+              @{TELEGRAM_USERNAME}
+            </a>
           </li>
+
+          {/* WhatsApp */}
+          <li className="flex items-center gap-2.5">
+             <div className="w-5 h-5 rounded-full bg-green-100 flex items-center justify-center text-green-600 shrink-0">
+              <WhatsAppIcon className="w-3 h-3" />
+            </div>
+            <a 
+              href={`https://wa.me/${WHATSAPP_NUMBER}`}
+              target="_blank" 
+              rel="noopener noreferrer"
+              className="hover:text-green-600 hover:underline truncate"
+            >
+              WhatsApp
+            </a>
+          </li>
+
         </ul>
       </div>
     </div>

+ 34 - 41
src/components/dashboard/TicketList.tsx

@@ -4,17 +4,17 @@ import { useEffect, useState } from 'react';
 import api from '@/lib/api';
 import { Loader2, MessageSquare, AlertCircle, Clock, CheckCircle, XCircle, ArrowRight, Eye, Search, FileText } from 'lucide-react';
 import Pagination from '@/components/common/Pagination'; 
-// 假设 UserTicketDetailModal 导出了 UserTicket 类型,如果没有,可以使用下方的本地定义
 import { UserTicket } from './UserTicketDetailModal';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
 interface TicketListProps {
   onViewDetail: (ticket: UserTicket) => void;
   refreshTrigger?: number;
 }
 
-// 对应 API: VasTicketOut
 interface TicketData extends UserTicket {
-  // 确保接口字段与 API 响应一致
   id: number;
   order_id: string;
   type: 'refund' | 'dispute' | 'change_request';
@@ -25,17 +25,16 @@ interface TicketData extends UserTicket {
 }
 
 export default function TicketList({ onViewDetail, refreshTrigger }: TicketListProps) {
-  // 数据状态
+  const { t } = useLanguage();
+
   const [loading, setLoading] = useState<boolean>(true);
   const [tickets, setTickets] = useState<TicketData[]>([]);
   
-  // 分页与搜索状态
   const [page, setPage] = useState(1);
   const [pageSize] = useState(5); 
   const [total, setTotal] = useState(0);
   const [keyword, setKeyword] = useState('');
 
-  // 监听外部刷新触发器
   useEffect(() => {
     fetchTickets(1); 
   }, [refreshTrigger]);
@@ -43,8 +42,6 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
   const fetchTickets = async (targetPage: number) => {
     try {
       setLoading(true);
-      
-      // API: GET /api/vas/ticket/list_by_user
       const res = await api.get('/api/vas/ticket/list_by_user', {
         params: {
           page: targetPage,
@@ -54,7 +51,6 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
       });
 
       const data = res.data.data;
-      
       if (data && Array.isArray(data.items)) {
         setTickets(data.items);
         setTotal(data.total || 0);
@@ -62,19 +58,15 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
         setTickets([]);
         setTotal(0);
       }
-      
       setPage(targetPage);
-
     } catch (error) {
       console.error("Failed to fetch tickets", error);
-      // 如果 API 失败,清空列表,不再显示 Mock 数据以免混淆
       setTickets([]);
     } finally {
       setLoading(false);
     }
   };
 
-  // 搜索处理
   const handleSearch = () => {
     fetchTickets(1);
   };
@@ -84,33 +76,33 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
   };
 
   const getStatusConfig = (status: string) => {
+    const label = t(`ticket.status.${status}`) !== `ticket.status.${status}` 
+      ? t(`ticket.status.${status}`) 
+      : status;
+
     switch (status) {
-      case 'pending': return { text: '待处理', color: 'text-yellow-700 bg-yellow-50 border-yellow-200', icon: Clock };
-      case 'info_required': return { text: '需补充资料', color: 'text-blue-700 bg-blue-50 border-blue-200', icon: AlertCircle };
-      case 'resolved': return { text: '已解决', color: 'text-green-700 bg-green-50 border-green-200', icon: CheckCircle };
-      case 'rejected': return { text: '已拒绝', color: 'text-red-700 bg-red-50 border-red-200', icon: XCircle };
-      default: return { text: status, color: 'text-gray-600 bg-gray-50 border-gray-200', icon: MessageSquare };
+      case 'pending': return { text: label, color: 'text-yellow-700 bg-yellow-50 border-yellow-200', icon: Clock };
+      case 'info_required': return { text: label, color: 'text-blue-700 bg-blue-50 border-blue-200', icon: AlertCircle };
+      case 'resolved': return { text: label, color: 'text-green-700 bg-green-50 border-green-200', icon: CheckCircle };
+      case 'rejected': return { text: label, color: 'text-red-700 bg-red-50 border-red-200', icon: XCircle };
+      default: return { text: label, color: 'text-gray-600 bg-gray-50 border-gray-200', icon: MessageSquare };
     }
   };
 
   const getTypeText = (type: string) => {
-    const map: Record<string, string> = {
-      refund: '退款申请',
-      dispute: '交易纠纷',
-      change_request: '变更请求'
-    };
-    return map[type] || type;
+    const key = `ticket.types.${type}`;
+    return t(key) !== key ? t(key) : type;
   };
 
   return (
     <div className="space-y-4">
       
-      {/* === 顶部工具栏 === */}
+      {/* Top Toolbar */}
       <div className="flex gap-2 items-center bg-white p-3 rounded-xl shadow-sm border border-slate-200">
         <div className="relative flex-1">
           <input 
             type="text" 
-            placeholder="搜索工单号、订单号..." 
+            placeholder={t('ticket.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"
             value={keyword}
             onChange={(e) => setKeyword(e.target.value)}
@@ -122,15 +114,15 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
           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"
         >
-          搜索
+          {t('common.search')}
         </button>
       </div>
 
-      {/* === 工单列表 === */}
+      {/* Ticket List */}
       <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
         <div className="px-6 py-4 border-b border-slate-100 bg-gray-50/50 flex justify-between items-center">
-          <h2 className="text-sm font-bold uppercase text-slate-500 tracking-wide">我的工单</h2>
-          <span className="text-xs font-medium px-2 py-1 bg-white border rounded text-slate-500">Total: {total}</span>
+          <h2 className="text-sm font-bold uppercase text-slate-500 tracking-wide">{t('ticket.my_tickets')}</h2>
+          <span className="text-xs font-medium px-2 py-1 bg-white border rounded text-slate-500">{t('common.total')}: {total}</span>
         </div>
 
         {loading ? (
@@ -140,8 +132,8 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
             <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
                <FileText className="h-8 w-8 text-gray-400" />
             </div>
-            <p className="text-lg font-medium text-gray-900">暂无工单记录</p>
-            <p className="text-sm text-gray-500 mt-1">如果您遇到问题,请点击右上角提交新工单。</p>
+            <h3 className="text-lg font-medium text-gray-900">{t('ticket.empty_title')}</h3>
+            <p className="text-sm text-gray-500 mt-1">{t('ticket.empty_desc')}</p>
           </div>
         ) : (
           <div className="divide-y divide-slate-100">
@@ -153,7 +145,7 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
               return (
                 <div key={ticket.id} className="p-5 hover:bg-slate-50 transition group">
                   <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">
                         <span className="font-bold text-gray-900 text-base">{getTypeText(ticket.type)}</span>
@@ -169,40 +161,42 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
 
                       <div className="flex items-center gap-4 text-xs text-gray-400">
                         <span className="flex items-center gap-1">
-                          <FileText size={12} /> 订单: {ticket.order_id}
+                          <FileText size={12} /> {t('ticket.order_id')}: {ticket.order_id}
                         </span>
                         <span className="flex items-center gap-1">
-                          <Clock size={12} /> {new Date(ticket.created_at).toLocaleString()}
+                          <Clock size={12} /> 
+                          {/* 2. 使用 LocalTime 组件 */}
+                          <LocalTime date={ticket.created_at} />
                         </span>
                       </div>
                     </div>
 
-                    {/* 右侧操作栏 */}
+                    {/* Right Actions */}
                     <div className="flex flex-col justify-center items-end gap-2 min-w-[120px]">
                       {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"
                         >
-                          回复消息 <ArrowRight size={16} />
+                          {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"
                         >
-                          <Eye size={16} /> 查看详情
+                          <Eye size={16} /> {t('ticket.view_details')}
                         </button>
                       )}
                     </div>
                   </div>
 
-                  {/* 管理员最新回复摘要 (如果有) */}
+                  {/* 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>
-                         <span className="font-bold text-slate-900 mr-1">最新反馈:</span>
+                         <span className="font-bold text-slate-900 mr-1">{t('ticket.latest_feedback')}:</span>
                          {ticket.admin_comment}
                        </div>
                     </div>
@@ -213,7 +207,6 @@ export default function TicketList({ onViewDetail, refreshTrigger }: TicketListP
           </div>
         )}
         
-        {/* === 分页 === */}
         <Pagination 
           currentPage={page}
           total={total}

+ 26 - 19
src/components/dashboard/TicketModal.tsx

@@ -3,25 +3,29 @@
 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 {
   isOpen: boolean;
   onClose: () => void;
-  onSuccess?: () => void; // 新增:成功回调,用于刷新列表
+  onSuccess?: () => void;
   defaultOrderId?: string;
 }
 
 export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId = '' }: TicketModalProps) {
+  // 2. 获取翻译函数
+  const { t } = useLanguage();
+
   const [loading, setLoading] = useState<boolean>(false);
   const [errorMsg, setErrorMsg] = useState<string>('');
   
   const [form, setForm] = useState({
     order_id: '',
-    type: 'refund', // 对应 API Enum: refund | dispute | change_request
+    type: 'refund',
     reason: ''
   });
 
-  // 初始化
   useEffect(() => {
     if (isOpen) {
       setForm({
@@ -39,16 +43,14 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
     setErrorMsg('');
 
     try {
-      // API: /api/vas/ticket/create
       await api.post('/api/vas/ticket/create', form);
       
-      // 成功处理
-      if (onSuccess) onSuccess(); // 触发父组件刷新
+      if (onSuccess) onSuccess(); 
       onClose();
       
     } catch (error: any) {
       console.error(error);
-      const msg = error.response?.data?.message || '提交失败,请稍后重试';
+      const msg = error.response?.data?.message || t('ticket.submit_error_default');
       setErrorMsg(msg);
     } finally {
       setLoading(false);
@@ -65,7 +67,7 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
         
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
-          <h3 className="text-lg font-bold text-gray-900">提交工单</h3>
+          <h3 className="text-lg font-bold text-gray-900">{t('ticket.submit_modal_title')}</h3>
           <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
             <X size={24} />
           </button>
@@ -81,37 +83,42 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
 
           <form onSubmit={handleSubmit} className="space-y-5">
             <div>
-              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">关联订单号 <span className="text-red-500">*</span></label>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
+                {t('ticket.order_id_label')} <span className="text-red-500">*</span>
+              </label>
               <input
                 type="text" required
                 className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-gray-50 focus:bg-white transition"
                 value={form.order_id}
                 onChange={e => setForm({ ...form, order_id: e.target.value })}
-                placeholder="请输入相关的订单编号"
+                placeholder={t('ticket.order_id_placeholder')}
               />
             </div>
 
             <div>
-              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">工单类型 <span className="text-red-500">*</span></label>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
+                {t('ticket.type_label')} <span className="text-red-500">*</span>
+              </label>
               <select
                 className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
                 value={form.type}
                 onChange={e => setForm({ ...form, type: e.target.value })}
               >
-                {/* 仅保留 API 支持的 Enum */}
-                <option value="refund">申请退款 (Refund)</option>
-                <option value="dispute">交易纠纷 (Dispute)</option>
-                <option value="change_request">变更请求 (Change Request)</option>
+                <option value="refund">{t('ticket.types.refund')}</option>
+                <option value="dispute">{t('ticket.types.dispute')}</option>
+                <option value="change_request">{t('ticket.types.change_request')}</option>
               </select>
             </div>
 
             <div>
-              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">详细描述 <span className="text-red-500">*</span></label>
+              <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
+                {t('ticket.desc_label')} <span className="text-red-500">*</span>
+              </label>
               <textarea
                 required
                 rows={4}
                 className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm resize-none"
-                placeholder="请详细描述您遇到的问题,以便我们更快为您处理..."
+                placeholder={t('ticket.desc_placeholder')}
                 value={form.reason}
                 onChange={e => setForm({ ...form, reason: e.target.value })}
               />
@@ -123,7 +130,7 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
                 onClick={onClose}
                 className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-200 transition"
               >
-                取消
+                {t('common.cancel')}
               </button>
               <button
                 type="submit"
@@ -131,7 +138,7 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
                 className="inline-flex items-center justify-center px-6 py-2 text-sm font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition"
               >
                 {loading ? <Loader2 className="animate-spin w-4 h-4 mr-2" /> : null}
-                提交工单
+                {t('ticket.submit_btn')}
               </button>
             </div>
           </form>

+ 29 - 19
src/components/dashboard/UserOrderDetailModal.tsx

@@ -3,8 +3,10 @@
 import { useState, useEffect } from 'react';
 import { X, CreditCard, FileText, Package, Check, Clock } from 'lucide-react';
 import api from '@/lib/api';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入 LocalTime 组件
+import LocalTime from '@/components/common/LocalTime';
 
-// 定义接口 (复用或简化)
 export interface UserOrder {
   id: string;
   created_at: string;
@@ -12,7 +14,7 @@ export interface UserOrder {
   base_amount: number;
   base_currency: string;
   product_title?: string;
-  user_inputs?: Record<string, any>; // 核心字段
+  user_inputs?: Record<string, any>;
 }
 
 interface UserOrderDetailModalProps {
@@ -22,10 +24,11 @@ interface UserOrderDetailModalProps {
 }
 
 export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrderDetailModalProps) {
+  const { t } = useLanguage();
+
   const [payments, setPayments] = useState<any[]>([]);
   const [loadingPayments, setLoadingPayments] = useState(false);
 
-  // 当弹窗打开时,尝试获取该订单的支付流水 (如果有权限)
   useEffect(() => {
     if (isOpen && order?.id) {
       fetchPayments(order.id);
@@ -37,13 +40,11 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
   const fetchPayments = async (orderId: string) => {
     setLoadingPayments(true);
     try {
-      // 尝试调用支付列表接口 (假设用户也能查自己的支付记录)
-      // 如果后端没开放这个接口给普通用户,这里会失败,但不影响主界面显示
       const res = await api.get('/api/vas/payment/list_by_order', { params: { order_id: orderId } });
       const list = Array.isArray(res.data.data) ? res.data.data : [];
       setPayments(list);
     } catch (e) {
-      // 忽略错误,可能是没权限或接口不存在
+      // ignore error
     } finally {
       setLoadingPayments(false);
     }
@@ -64,6 +65,11 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
     }
   };
 
+  const getStatusText = (status: string) => {
+    const key = `status.${status}`;
+    return t(key) !== key ? t(key) : status.toUpperCase();
+  };
+
   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-2xl max-h-[90vh] flex flex-col animate-in zoom-in duration-200">
@@ -71,7 +77,7 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50 rounded-t-xl">
           <div>
-            <h3 className="font-bold text-gray-900 text-lg">订单详情</h3>
+            <h3 className="font-bold text-gray-900 text-lg">{t('order.detail_title')}</h3>
             <p className="text-xs text-gray-500 mt-1 font-mono">#{order.id}</p>
           </div>
           <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1 hover:bg-slate-200 rounded-full">
@@ -89,16 +95,17 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
                 <Package size={24} />
               </div>
               <div>
-                <h4 className="font-bold text-gray-900">{order.product_title || '未命名服务'}</h4>
+                <h4 className="font-bold text-gray-900">{order.product_title || t('order.unknown_service')}</h4>
                 <div className="text-sm text-gray-500 mt-1 flex items-center gap-1">
                   <Clock size={12} />
-                  创建于: {new Date(order.created_at).toLocaleString()}
+                  {/* 2. 使用 LocalTime 组件 */}
+                  {t('order.created_at')}: <LocalTime date={order.created_at} className="ml-1" />
                 </div>
               </div>
             </div>
             <div className="text-right">
               <span className={`px-3 py-1 rounded-full text-xs font-bold border ${getStatusColor(order.status)}`}>
-                {order.status.toUpperCase()}
+                {getStatusText(order.status)}
               </span>
               <div className="mt-2 text-xl font-bold text-slate-800">
                 {formatMoney(order.base_amount, order.base_currency)}
@@ -106,18 +113,18 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
             </div>
           </div>
 
-          {/* 2. 申请资料 (User Inputs) - 核心需求 */}
+          {/* 2. 申请资料 */}
           <div className="border rounded-xl overflow-hidden">
             <div className="bg-slate-50 px-4 py-3 border-b flex items-center gap-2">
               <FileText size={18} className="text-slate-600" />
-              <h4 className="font-bold text-sm text-slate-800">申请资料 (Application Data)</h4>
+              <h4 className="font-bold text-sm text-slate-800">{t('order.application_data')}</h4>
             </div>
             <div className="p-5 bg-white">
               {order.user_inputs && Object.keys(order.user_inputs).length > 0 ? (
                 <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
                   {Object.entries(order.user_inputs).map(([key, value]) => (
                     <div key={key} className="border-b border-slate-100 pb-2">
-                      <span className="block text-xs font-bold text-slate-400 uppercase mb-1">{key}</span>
+                      <span className="block text-xs font-bold text-slate-400 uppercase mb-1">{key.replace(/_/g, ' ')}</span>
                       <span className="block text-sm text-slate-800 font-medium break-words">
                         {String(value)}
                       </span>
@@ -126,30 +133,33 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
                 </div>
               ) : (
                 <div className="text-center py-4 text-gray-400 text-sm">
-                  未填写申请资料
+                  {t('order.no_application_data')}
                 </div>
               )}
             </div>
           </div>
 
-          {/* 3. 支付记录 (如果有) */}
+          {/* 3. 支付记录 */}
           {payments.length > 0 && (
             <div className="border rounded-xl overflow-hidden">
               <div className="bg-slate-50 px-4 py-3 border-b flex items-center gap-2">
                 <CreditCard size={18} className="text-slate-600" />
-                <h4 className="font-bold text-sm text-slate-800">支付流水</h4>
+                <h4 className="font-bold text-sm text-slate-800">{t('order.payment_history')}</h4>
               </div>
               <div className="divide-y divide-slate-100">
                 {payments.map((pay) => (
                   <div key={pay.id} className="p-4 flex justify-between items-center text-sm">
                     <div>
                       <div className="font-bold text-slate-700 capitalize">{pay.provider} ({pay.channel})</div>
-                      <div className="text-xs text-gray-400 mt-0.5">{new Date(pay.created_at).toLocaleString()}</div>
+                      {/* 3. 使用 LocalTime 组件 */}
+                      <div className="text-xs text-gray-400 mt-0.5">
+                        <LocalTime date={pay.created_at} />
+                      </div>
                     </div>
                     <div className="text-right">
                       <div className="font-mono font-medium">{formatMoney(pay.amount, pay.currency)}</div>
                       <div className="text-xs text-green-600 flex items-center justify-end gap-1">
-                        {pay.status === 'succeeded' && <Check size={10} />} {pay.status}
+                        {pay.status === 'succeeded' && <Check size={10} />} {getStatusText(pay.status)}
                       </div>
                     </div>
                   </div>
@@ -166,7 +176,7 @@ export default function UserOrderDetailModal({ isOpen, onClose, order }: UserOrd
             onClick={onClose}
             className="px-6 py-2 bg-white border border-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 text-slate-700 transition shadow-sm"
           >
-            关闭
+            {t('common.close')}
           </button>
         </div>
       </div>

+ 49 - 40
src/components/dashboard/UserTicketDetailModal.tsx

@@ -3,8 +3,10 @@
 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 {
   id: number;
   order_id: string;
@@ -30,25 +32,21 @@ interface UserTicketDetailModalProps {
 }
 
 export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserTicketDetailModalProps) {
+  const { t } = useLanguage();
+
   const [messages, setMessages] = useState<Message[]>([]);
   const [loadingMsg, setLoadingMsg] = useState(false);
   const [replyContent, setReplyContent] = useState('');
   const [sending, setSending] = useState(false);
   
-  // 滚动到底部的引用
   const messagesEndRef = useRef<HTMLDivElement>(null);
 
-  // 1. 初始化加载消息
   useEffect(() => {
     if (isOpen && ticket) {
       fetchMessages();
-      // 可选:设置轮询,每10秒获取新消息
-      // const interval = setInterval(fetchMessages, 10000);
-      // return () => clearInterval(interval);
     }
   }, [isOpen, ticket]);
 
-  // 2. 滚动到底部
   useEffect(() => {
     scrollToBottom();
   }, [messages]);
@@ -57,17 +55,15 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
     if (!ticket) return;
     try {
       setLoadingMsg(true);
-      // API: GET /api/vas/tickets/fetch_message?ticket_id=123&page=1&size=50
       const res = await api.get('/api/vas/tickets/fetch_message', {
         params: { 
           ticket_id: ticket.id,
           page: 1,
-          size: 100 // 获取最近的100条
+          size: 100 
         }
       });
       
       const items = res.data.data?.items || [];
-      // 确保按时间正序排列 (旧 -> 新)
       const sorted = items.sort((a: Message, b: Message) => 
         new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
       );
@@ -85,19 +81,18 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
 
     setSending(true);
     try {
-      // API: POST /api/vas/tickets/send_message?ticket_id=123
       await api.post(`/api/vas/tickets/send_message`, {
         content: replyContent,
-        attachments: null // 暂不支持附件,留空
+        attachments: null
       }, {
         params: { ticket_id: ticket.id }
       });
 
       setReplyContent('');
-      fetchMessages(); // 发送成功后刷新列表
+      fetchMessages();
     } catch (error) {
       console.error("Send message failed", error);
-      alert("发送失败,请稍后重试");
+      alert(t('ticket.send_failed'));
     } finally {
       setSending(false);
     }
@@ -107,45 +102,48 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
     messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
   };
 
-  // 辅助函数:渲染状态
   const renderStatus = (status: string) => {
     const map: any = {
-      pending: { color: 'text-yellow-600 bg-yellow-50', icon: Clock, text: '待处理' },
-      info_required: { color: 'text-orange-600 bg-orange-50', icon: AlertCircle, text: '需补充资料' },
-      resolved: { color: 'text-green-600 bg-green-50', icon: CheckCircle, text: '已解决' },
-      rejected: { color: 'text-red-600 bg-red-50', icon: XCircle, text: '已拒绝' },
+      pending: { color: 'text-yellow-600 bg-yellow-50', icon: Clock },
+      info_required: { color: 'text-orange-600 bg-orange-50', icon: AlertCircle },
+      resolved: { color: 'text-green-600 bg-green-50', icon: CheckCircle },
+      rejected: { color: 'text-red-600 bg-red-50', icon: XCircle },
     };
-    const conf = map[status] || { color: 'text-gray-600 bg-gray-50', icon: Clock, text: status };
+    const conf = map[status] || { color: 'text-gray-600 bg-gray-50', icon: Clock };
     const Icon = conf.icon;
     
+    const statusText = t(`ticket.status.${status}`) !== `ticket.status.${status}` 
+      ? t(`ticket.status.${status}`) 
+      : status;
+
     return (
       <span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${conf.color}`}>
-        <Icon size={12} /> {conf.text}
+        <Icon size={12} /> {statusText}
       </span>
     );
   };
 
+  // 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} />
 
-      {/* 弹窗主体 */}
       <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">
         
-        {/* 1. 顶部 Header */}
+        {/* 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">
-                工单 #{ticket.id}
+                {t('ticket.title_prefix')} #{ticket.id}
               </h3>
               {renderStatus(ticket.status)}
             </div>
             <p className="text-xs text-gray-500 mt-1">
-              关联订单: <span className="font-mono">{ticket.order_id}</span>
+              {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">
@@ -153,13 +151,15 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
           </button>
         </div>
 
-        {/* 2. 中间:消息列表 (Scrollable) */}
+        {/* Messages List */}
         <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6">
           
-          {/* 原始工单描述 (作为第一条展示) */}
+          {/* 原始工单描述 */}
           <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">
-              工单创建于 {new Date(ticket.created_at).toLocaleString()}
+            <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">
@@ -169,7 +169,7 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                </div>
                <div>
                   <div className="bg-blue-600 text-white px-4 py-3 rounded-2xl rounded-tr-none shadow-sm text-sm">
-                    <p className="font-bold text-xs text-blue-100 mb-1 border-b border-blue-500 pb-1">工单描述</p>
+                    <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>
                </div>
@@ -187,8 +187,13 @@ 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">
-                      系统消息: {msg.content} - {new Date(msg.created_at).toLocaleTimeString()}
+                    <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' }}
+                      />
                     </span>
                   </div>
                 );
@@ -214,8 +219,13 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                       }`}>
                         {msg.content}
                       </div>
-                      <span className="text-[10px] text-gray-400 mt-1 px-1">
-                        {isMe ? '我' : '客服'} • {new Date(msg.created_at).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit'})}
+                      <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' }}
+                        />
                       </span>
                     </div>
 
@@ -228,10 +238,10 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
           <div ref={messagesEndRef} />
         </div>
 
-        {/* 3. 底部:输入框 */}
+        {/* Input */}
         <div className="p-4 bg-white border-t border-gray-100 flex-shrink-0">
           <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="上传附件(暂不可用)">
+            <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')}>
               <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">
@@ -244,11 +254,10 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                     handleSend(e);
                   }
                 }}
-                placeholder="请输入回复内容..."
+                placeholder={t('ticket.reply_placeholder')}
                 rows={1}
                 className="w-full bg-transparent border-0 focus:ring-0 p-3 text-sm resize-none max-h-32 min-h-[44px]"
                 style={{ height: 'auto', overflowY: 'hidden' }} 
-                // 简单的自动高度调整,实际项目中可使用 text-area-autosize 库
               />
             </div>
             <button 
@@ -260,7 +269,7 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
             </button>
           </form>
           <div className="text-center mt-2">
-             <p className="text-[10px] text-gray-400">如需紧急处理,请发送邮件至 support@visafly.com</p>
+             <p className="text-[10px] text-gray-400">{t('ticket.urgent_tip')}</p>
           </div>
         </div>
 

+ 14 - 8
src/components/knowledge/KnowledgeCard.tsx

@@ -2,6 +2,8 @@
 
 import { useState } from 'react';
 import { MapPin, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
+// 1. 引入 Hook
+import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 interface CardData {
   id: number;
@@ -20,11 +22,19 @@ const getImageUrl = (fidString: string | null) => {
 };
 
 export default function KnowledgeCard({ data }: { data: CardData }) {
+  // 2. 获取翻译函数和当前语言
+  const { t, lang } = useLanguage();
+  
   const [expanded, setExpanded] = useState(false);
   const imageUrl = getImageUrl(data.image);
 
+  // 3. 根据语言格式化日期
+  const formattedDate = new Date(data.created_at).toLocaleDateString(
+    lang === 'zh' ? 'zh-CN' : 'en-US',
+    { year: 'numeric', month: 'short', day: 'numeric' }
+  );
+
   return (
-    // 修复:移除 h-full,让卡片高度由内容决定,不要撑满父容器高度
     <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden hover:shadow-md transition-all duration-300 flex flex-col">
       
       {/* 图片区域 */}
@@ -59,10 +69,6 @@ export default function KnowledgeCard({ data }: { data: CardData }) {
         <div 
           className={`
             text-sm text-slate-600 leading-relaxed overflow-hidden transition-all duration-500 ease-in-out
-            /* 
-               修复:使用具体的 max-height 数值而不是 none,这样 transition 动画才生效。
-               1000px 足够显示大部分文章,如果文章极长,可以调大这个值。
-            */
             ${expanded ? 'max-h-[1000px]' : 'max-h-[80px] line-clamp-3'}
             
             /* === 样式修复:Tailwind Prose === */
@@ -79,7 +85,7 @@ export default function KnowledgeCard({ data }: { data: CardData }) {
         <div className="mt-4 pt-4 flex items-center justify-between border-t border-slate-50">
           <div className="flex items-center text-xs text-slate-400 gap-1">
             <Calendar size={12} />
-            {new Date(data.created_at).toLocaleDateString()}
+            {formattedDate}
           </div>
           
           <button 
@@ -90,9 +96,9 @@ export default function KnowledgeCard({ data }: { data: CardData }) {
             className="text-xs font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-2 py-1 rounded transition-colors select-none"
           >
             {expanded ? (
-              <>收起 <ChevronUp size={14} /></>
+              <>{t('knowledge.collapse')} <ChevronUp size={14} /></>
             ) : (
-              <>阅读全文 <ChevronDown size={14} /></>
+              <>{t('knowledge.read_more')} <ChevronDown size={14} /></>
             )}
           </button>
         </div>

+ 70 - 0
src/lib/i18n/LanguageContext.tsx

@@ -0,0 +1,70 @@
+'use client';
+
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import { zh } from './locales/zh';
+import { en } from './locales/en';
+
+type Lang = 'zh' | 'en';
+// 获取字典的类型定义
+type Dictionary = typeof zh;
+
+interface LanguageContextProps {
+  lang: Lang;
+  setLang: (lang: Lang) => void;
+  t: (path: string) => string; // 核心翻译函数
+}
+
+const LanguageContext = createContext<LanguageContextProps | undefined>(undefined);
+
+export function LanguageProvider({ children }: { children: ReactNode }) {
+  // 默认语言
+  const [lang, setLangState] = useState<Lang>('zh');
+
+  // 初始化时读取本地存储
+  useEffect(() => {
+    const savedLang = localStorage.getItem('app_lang') as Lang;
+    if (savedLang) {
+      setLangState(savedLang);
+    }
+  }, []);
+
+  // 切换语言时保存到本地
+  const setLang = (newLang: Lang) => {
+    setLangState(newLang);
+    localStorage.setItem('app_lang', newLang);
+  };
+
+  // 获取当前字典
+  const dictionary = lang === 'zh' ? zh : en;
+
+  // 翻译函数:支持嵌套路径,例如 t('common.save')
+  const t = (path: string) => {
+    const keys = path.split('.');
+    let current: any = dictionary;
+
+    for (const key of keys) {
+      if (current[key] === undefined) {
+        console.warn(`Translation key not found: ${path}`);
+        return path; // 如果找不到,返回 key 本身
+      }
+      current = current[key];
+    }
+    
+    return current as string;
+  };
+
+  return (
+    <LanguageContext.Provider value={{ lang, setLang, t }}>
+      {children}
+    </LanguageContext.Provider>
+  );
+}
+
+// 自定义 Hook,方便组件调用
+export function useLanguage() {
+  const context = useContext(LanguageContext);
+  if (!context) {
+    throw new Error('useLanguage must be used within a LanguageProvider');
+  }
+  return context;
+}

+ 393 - 0
src/lib/i18n/locales/en.ts

@@ -0,0 +1,393 @@
+export const en = {
+  common: {
+    back: 'Back',
+    loading: 'Loading...',
+    save: 'Save',
+    cancel: 'Cancel',
+    confirm: 'Confirm',
+    delete: 'Delete',
+    edit: 'Edit',
+    search: 'Search',
+    actions: 'Actions',
+    status: 'Status',
+    success: 'Operation Successful',
+    failed: 'Operation Failed',
+    refresh: 'Refresh',
+    add: 'Add',
+    new_application: 'New Application',
+    format: 'Format',
+    copy: 'Copy',
+    copied: 'Copied',
+    valid: 'Valid',
+    invalid: 'Invalid',
+    json_invalid: 'Invalid JSON format',
+    json_syntax_error: 'Cannot format: Syntax error',
+    unknown_error: 'Unknown Error',
+    total: 'Total',
+    close: 'Close',
+    error: 'Error',
+    processing: 'Processing...',
+    select: 'Select',
+    enter: 'Enter',
+  },
+  auth: {
+    welcome_back: 'Welcome Back',
+    auto_register: 'Auto Register',
+    email_label: 'Email',
+    email_placeholder: 'name@example.com',
+    password_label: 'Password',
+    forgot_password: 'Forgot Password?',
+    login_btn: 'Login',
+    register_btn: 'Register',
+    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',
+  },
+  forgot_password: {
+    title: 'Forgot Password',
+    step1_desc: 'Enter your registered email address, and we will send you a verification code to reset your password.',
+    email_label: 'Email Address',
+    email_placeholder: 'name@example.com',
+    send_btn: 'Send Code',
+    code_sent_to: 'Code sent to',
+    code_label: 'Verification Code',
+    resend_btn: 'Resend',
+    new_password_label: 'New Password',
+    new_password_placeholder: 'Set new password',
+    reset_btn: 'Reset Password',
+    enter_email_alert: 'Please enter email',
+    code_sent_alert: 'Verification code sent to',
+    send_failed: 'Send failed',
+    send_failed_default: 'User not found or network error',
+    enter_code_alert: 'Please enter code',
+    enter_password_alert: 'Please enter new password',
+    reset_success: 'Password reset successfully! Please login with new password.',
+    reset_failed: 'Reset failed',
+    code_error: 'Invalid code',
+  },
+  bind_email: {
+    title_step1: 'Bind Email',
+    title_step2: 'Enter Code',
+    desc_step1: 'To ensure account security and receive order notifications, please bind your email address.',
+    desc_step2_prefix: 'Verification code sent to',
+    desc_step2_suffix: ', please check.',
+    email_label: 'Email',
+    email_placeholder: 'name@example.com',
+    code_label: 'Verification Code',
+    send_btn: 'Send Code',
+    resend_suffix: 's resend',
+    resend_btn: 'Resend',
+    change_email_btn: 'Change Email',
+    confirm_btn: 'Confirm Bind',
+    alert_input_email: 'Please enter email',
+    alert_invalid_email: 'Invalid email format',
+    alert_code_sent: 'Verification code sent to',
+    alert_send_failed: 'Send failed',
+    alert_input_code: 'Please enter code',
+    alert_code_length: 'Please enter 6-digit code',
+    success: 'Email bound successfully!',
+    failed: 'Bind failed',
+  },
+  settings: {
+    title: 'Account Settings',
+    subtitle: 'Manage your profile and security options',
+    security_tip_title: 'Account Security',
+    security_tip_desc: 'To ensure your account security, it is recommended to update your profile regularly. If you notice any unusual login activity, please contact support immediately.',
+    account_status: 'Account Status: ',
+    status_normal: 'Normal',
+    change_password: 'Change Password',
+    email_verification: 'Email Verification',
+    code_placeholder: '6-digit Code',
+    get_code: 'Get Code',
+    resend: 'resend', // 配合前端逻辑: "60s resend"
+    new_password_label: 'New Password',
+    new_password_placeholder: 'Enter new password',
+    confirm_password_placeholder: 'Confirm new password',
+    password_mismatch: 'Passwords do not match',
+    email_not_found: 'User email not found',
+    code_sent: 'Verification code sent to',
+    send_failed: 'Send failed',
+    enter_code: 'Please enter verification code',
+    enter_password: 'Please enter new password',
+    password_changed_success: 'Password changed successfully! Please login again.',
+    change_failed: 'Change failed',
+  },
+  home: {
+    hero_title_prefix: 'Visa Application, ',
+    hero_title_highlight: 'Never Been Easier',
+    hero_subtitle: 'Visafly provides global automated visa processing services. Real-time tracking and expert review ensure your trip is worry-free.',
+    cta_start: 'Start Application',
+    cta_check_slots: 'Check Slots',
+    
+    process_title: 'How It Works',
+    process_subtitle: 'Just 4 steps to fully automate your visa needs',
+    
+    step_1_title: '1. Select Service',
+    step_1_desc: 'Find the country and visa type you need (e.g., Japan Tourist Visa, France Schengen Visa).',
+    step_2_title: '2. Submit Info',
+    step_2_desc: 'Fill out a simple application form (JSON dynamic form support), no tedious handwriting required.',
+    step_3_title: '3. Smart Automation',
+    step_3_desc: 'Our 24/7 bot system automatically monitors slots and locks appointments for you, no waiting required.',
+    step_4_title: '4. Success',
+    step_4_desc: 'Pay after successful booking (random discounts supported), get confirmation, and prepare for submission.',
+    
+    feat_global_title: 'Global Coverage',
+    feat_global_desc: 'Support for visa processing in over 50 countries and regions including US, Japan, and Schengen area.',
+    feat_success_title: 'High Success Rate',
+    feat_success_desc: 'AI system pre-check plus human expert review ensures your documents are accurate.',
+    feat_fast_title: 'Fast Processing',
+    feat_fast_desc: 'Connected to official APIs, automated workflows reduce application time to 1/3 of traditional methods.',
+  },
+  order: {
+    search_placeholder: 'Search Order ID or Product Name...',
+    history_title: 'Application History',
+    empty_title: 'No Orders Found',
+    empty_desc: 'No matching order records were found.',
+    unknown_service: 'Unnamed Service',
+    view_details: 'View Details',
+    pay_now: 'Pay Now',
+    support: 'Support',
+    detail_title: 'Order Details',
+    created_at: 'Created at',
+    application_data: 'Application Data',
+    no_application_data: 'No application data provided',
+    payment_history: 'Payment History',
+    load_product_failed: 'Failed to load product info',
+    product_not_found: 'Product not found',
+    create_failed: 'Order creation failed',
+    fill_form_hint: 'Please fill in the information carefully, as it will be used for your application.',
+    no_extra_info_needed: 'No extra information needed, please proceed.',
+    submit_and_pay: 'Submit & Pay',
+  },
+  status: {
+    pending: 'Pending',
+    paid: 'Paid',
+    succeeded: 'Succeeded',
+    completed: 'Completed',
+    failed: 'Failed',
+    cancelled: 'Cancelled',
+  },
+  product: {
+    title: 'Product Management',
+    name: 'Product Name',
+    price: 'Price',
+    country: 'Country/Region',
+    city: 'City',
+    visa_type: 'Visa Type',
+    provider: 'Provider',
+    enabled: 'Enabled',
+    disabled: 'Disabled',
+    edit_title: 'Edit Product',
+    create_title: 'Create Product',
+  },
+  ticket: {
+    title: 'Ticket Management',
+    order_id: 'Order ID',
+    type: 'Type',
+    reason: 'Reason',
+    my_tickets: 'My Tickets',
+    create_new: 'Submit New Ticket',
+    search_placeholder: 'Search Ticket ID, Order ID...',
+    empty_title: 'No Tickets Found',
+    empty_desc: 'If you have any issues, please submit a new ticket.',
+    reply: 'Reply',
+    view_details: 'View Details',
+    latest_feedback: 'Latest Feedback',
+    submit_modal_title: 'Submit Ticket',
+    submit_error_default: 'Submission failed, please try again later',
+    order_id_label: 'Related Order ID',
+    order_id_placeholder: 'Enter related order ID',
+    type_label: 'Ticket Type',
+    desc_label: 'Description',
+    desc_placeholder: 'Please describe your issue in detail...',
+    submit_btn: 'Submit Ticket',
+    title_prefix: 'Ticket',
+    related_order: 'Related Order',
+    created_at: 'Ticket Created at',
+    description_title: 'Description',
+    system_message: 'System',
+    sender_me: 'Me',
+    sender_support: 'Support',
+    reply_placeholder: 'Type your reply...',
+    send_failed: 'Send failed, please try again later',
+    upload_tooltip: 'Upload attachment (Coming soon)',
+    urgent_tip: 'For urgent matters, please email support@visafly.top',
+    types: {
+      refund: 'Refund Request',
+      dispute: 'Dispute',
+      change_request: 'Change Request',
+    },
+    status: {
+      pending: 'Pending',
+      info_required: 'Info Required',
+      resolved: 'Resolved',
+      rejected: 'Rejected',
+    }
+  },
+  nav: {
+    dashboard: 'Dashboard',
+    products: 'Products',
+    tickets: 'Tickets',
+    settings: 'Settings',
+    logout: 'Logout',
+    // === 新增以下 Key ===
+    services: 'Services',
+    slots: 'Slot Check',
+    guide: 'Guide',
+    admin: 'Admin Panel',
+    login: 'Login / Sign up',
+  },
+  dashboard: {
+    title: 'Dashboard',
+    subtitle: 'Manage your visa applications and support tickets',
+    logout_confirm: 'Are you sure you want to logout?',
+  },
+  knowledge: {
+    title: 'Guide & FAQ',
+    subtitle: 'Find answers to common questions, document preparation guides, and the latest policy updates here.',
+    search_placeholder: 'Search keywords, e.g., photo, stamp, fingerprint...',
+    empty_state: 'No guides found, please try different keywords',
+    read_more: 'Read More',
+    collapse: 'Collapse',
+  },
+  refund: {
+    title: 'Refund Policy & Terms',
+    subtitle: 'Please read the following terms carefully to protect your rights',
+    
+    full_refund_title: 'Full Refund Eligibility',
+    full_refund_desc: 'You may apply for a full refund under the following circumstances:',
+    full_refund_item_1: 'You have paid for the order, but our system has not yet started any booking operations (Status is Pending).',
+    full_refund_item_2: 'Duplicate charges caused by Visafly system errors.',
+    full_refund_item_3: 'We failed to successfully book a slot for you within the promised timeframe (usually 30 days, depending on the service).',
+    
+    no_refund_title: 'Non-refundable Scenarios',
+    important_note: 'Important Note',
+    important_note_desc: 'Once the service enters the execution stage or incurs third-party fees, refunds will be restricted.',
+    no_refund_item_1: 'Our system has successfully booked a slot for you (evidenced by a screenshot or confirmation letter).',
+    no_refund_item_2: 'Your visa application has already been submitted to the embassy or visa center.',
+    no_refund_item_3: 'Booking failure or invalidity caused by incorrect personal information (e.g., passport number) provided by you.',
+    no_refund_item_4: 'You decide to abandon the application due to personal reasons (e.g., change of itinerary, illness), but we have already completed the booking work.',
+    
+    process_title: 'Refund Process',
+    step_1_title: 'Submit Ticket',
+    step_1_desc: 'Select the order in the dashboard and click "Support" to submit a request.',
+    step_2_title: 'Review',
+    step_2_desc: 'We will verify the order status within 1-3 business days.',
+    step_3_title: 'Refund',
+    step_3_desc: 'Once approved, funds will be returned to the original payment method within 5-10 business days.',
+    
+    contact_title: 'Still have questions?',
+    contact_desc: 'If you have any questions regarding the refund policy, please contact our support team: ',
+  },
+  services: {
+    title: 'Popular Visa Services',
+    search_placeholder: 'Search country, city or service...',
+    all_countries: 'All Countries',
+    all_types: 'All Types',
+    reset_filter: 'Reset Filters',
+    no_result_title: 'No Services Found',
+    no_result_desc: 'Please try different keywords',
+    no_desc: 'No description available',
+    service_fee: 'Service Fee',
+    apply_now: 'Apply Now',
+  },
+  slots: {
+    title: 'Visa Slot Check',
+    subtitle: 'Check real-time appointment availability for embassies',
+    status_available: 'Slots Available',
+    status_unavailable: 'No Slots Available',
+    updated_at: 'Updated at',
+    earliest_date: 'Earliest Date',
+    slots_count: 'slots',
+    select_hint: 'Please select criteria and click search',
+  },
+  pagination: {
+    prev: 'Previous',
+    next: 'Next',
+    showing: 'Showing',
+    to: 'to',
+    of: 'of',
+    results: 'results',
+  },
+  profile: {
+    title: 'Profile',
+    subtitle: 'Manage your basic information',
+    edit_profile: 'Edit Profile',
+    no_nickname: 'No Nickname',
+    change_avatar: 'Change Avatar',
+    email_readonly: 'Email (Read-only)',
+    nickname_label: 'Nickname',
+    nickname_placeholder: 'Enter your nickname',
+    phone_label: 'Phone Number',
+    phone_placeholder: 'Enter phone number',
+    saving: 'Saving...',
+    login_password: 'Login Password',
+    password_tip: 'Recommended to change password regularly for security',
+    image_size_limit: 'Image size cannot exceed 2MB',
+    upload_failed: 'Upload failed, please try again or contact support',
+    update_success: 'Profile updated successfully!',
+    update_failed: 'Update failed',
+    fetch_failed: 'Failed to fetch user info',
+  },
+  sidebar: {
+    orders: 'My Orders',
+    tickets: 'Support Tickets',
+    settings: 'Account Settings',
+    contact_support: 'Contact Support',
+    contact_desc: 'Having trouble? Submit a ticket or contact us via:',
+  },
+  footer: {
+    description: 'Professional automated visa service platform. Making global visa applications simple, efficient, and transparent through technology.',
+    quick_links: 'Quick Links',
+    support: 'Help & Support',
+    refund_policy: 'Refund Policy',
+    terms: 'Terms of Service',
+    contact: 'Contact Us',
+    rights_reserved: 'All rights reserved.',
+    privacy: 'Privacy Policy',
+    cookie: 'Cookie Policy',
+  },
+  payment: {
+    order_created: 'Order Created',
+    order_id: 'Order ID',
+    select_method: 'Select Payment Method',
+    creating: 'Creating payment...',
+    reselect: 'Back',
+    expires: 'expires',
+    confirm_info: 'Confirm Payment',
+    original_amount: 'Original Amount',
+    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',
+  },
+  privacy_content: {
+    last_updated: 'Last Updated: January 1, 2025',
+    section1_title: '1. Information Collection',
+    section1_desc: 'We collect information necessary to process your visa application, including but not limited to name, contact details, and passport information.',
+    section2_title: '2. Use of Information',
+    section2_desc: 'Your information is used strictly for the purpose of submitting visa applications to the respective embassies or consulates. We do not sell your data to third parties.',
+    section3_title: '3. Data Security',
+    section3_desc: 'We implement industry-standard encryption and security measures to protect your personal information from unauthorized access.',
+  },
+  cookie_content: {
+    last_updated: 'Last Updated: January 1, 2025',
+    section1_title: '1. What are Cookies',
+    section1_desc: 'Cookies are small text files stored on your device that help us improve your website experience.',
+    section2_title: '2. How We Use Cookies',
+    section2_item1_label: 'Essential Cookies:',
+    section2_item1_desc: 'Used to maintain login status (e.g., Authentication Tokens).',
+    section2_item2_label: 'Functional Cookies:',
+    section2_item2_desc: 'Used to remember your language preferences.',
+    section3_title: '3. Managing Cookies',
+    section3_desc: 'You can control and manage cookies through your browser settings. However, disabling cookies may affect some website functionalities.',
+  }
+};

+ 393 - 0
src/lib/i18n/locales/zh.ts

@@ -0,0 +1,393 @@
+export const zh = {
+  common: {
+    back: '返回', 
+    loading: '加载中...',
+    save: '保存',
+    cancel: '取消',
+    confirm: '确定',
+    delete: '删除',
+    edit: '编辑',
+    search: '搜索',
+    actions: '操作',
+    status: '状态',
+    success: '操作成功',
+    failed: '操作失败',
+    refresh: '刷新',
+    add: '添加',
+    new_application: '新建申请',
+    format: '格式化',
+    copy: '复制',
+    copied: '已复制',
+    valid: '有效',
+    invalid: '无效',
+    json_invalid: '无效的 JSON 格式',
+    json_syntax_error: '无法格式化:JSON 语法错误',
+    unknown_error: '未知错误',
+    total: '共',
+    close: '关闭',
+    error: '错误',
+    processing: '处理中...',
+    select: '请选择',
+    enter: '请输入',
+  },
+  auth: {
+    welcome_back: '欢迎回来',
+    auto_register: '自动注册',
+    email_label: '邮箱',
+    email_placeholder: 'name@example.com',
+    password_label: '密码',
+    forgot_password: '忘记密码?',
+    login_btn: '登录',
+    register_btn: '注册',
+    no_account: '没有账号?点击自动注册',
+    has_account: '已有账号?点击登录',
+    login_success_no_token: '登录成功,但未获取到 Token',
+  },
+  forgot_password: {
+    title: '找回密码',
+    step1_desc: '请输入您注册时使用的电子邮箱,我们将发送验证码协助您重置密码。',
+    email_label: '电子邮箱',
+    email_placeholder: 'name@example.com',
+    send_btn: '发送验证码',
+    code_sent_to: '验证码已发送至',
+    code_label: '验证码',
+    resend_btn: '重新发送',
+    new_password_label: '新密码',
+    new_password_placeholder: '设置新密码',
+    reset_btn: '重置密码',
+    enter_email_alert: '请输入邮箱',
+    code_sent_alert: '验证码已发送至',
+    send_failed: '发送失败',
+    send_failed_default: '用户不存在或网络错误',
+    enter_code_alert: '请输入验证码',
+    enter_password_alert: '请输入新密码',
+    reset_success: '密码重置成功!请使用新密码登录。',
+    reset_failed: '重置失败',
+    code_error: '验证码错误',
+  },
+  bind_email: {
+    title_step1: '绑定邮箱',
+    title_step2: '输入验证码',
+    desc_step1: '为了保障您的账户安全及接收订单通知,请绑定您的常用电子邮箱。',
+    desc_step2_prefix: '验证码已发送至',
+    desc_step2_suffix: ',请查收。',
+    email_label: '电子邮箱',
+    email_placeholder: 'name@example.com',
+    code_label: '验证码',
+    send_btn: '发送验证码',
+    resend_suffix: 's 后重发',
+    resend_btn: '重新发送',
+    change_email_btn: '修改邮箱',
+    confirm_btn: '确认绑定',
+    alert_input_email: '请输入邮箱',
+    alert_invalid_email: '请输入有效的邮箱格式',
+    alert_code_sent: '验证码已发送至',
+    alert_send_failed: '发送失败',
+    alert_input_code: '请输入验证码',
+    alert_code_length: '请输入6位验证码',
+    success: '账号绑定成功!',
+    failed: '绑定失败',
+  },
+  settings: {
+    title: '账户设置',
+    subtitle: '管理您的个人资料和安全选项',
+    security_tip_title: '账户安全',
+    security_tip_desc: '为了保障您的账户安全,建议您定期更新个人信息。如果您发现账户有异常登录情况,请立即联系客服。',
+    account_status: '当前账号状态:',
+    status_normal: '正常',
+    change_password: '修改密码',
+    email_verification: '邮箱验证',
+    code_placeholder: '6位验证码',
+    get_code: '获取验证码',
+    resend: '后重发', // 配合前端逻辑: "60s 后重发"
+    new_password_label: '设置新密码',
+    new_password_placeholder: '新密码',
+    confirm_password_placeholder: '确认新密码',
+    password_mismatch: '两次输入的密码不一致',
+    email_not_found: '未找到用户邮箱',
+    code_sent: '验证码已发送至',
+    send_failed: '发送失败',
+    enter_code: '请输入验证码',
+    enter_password: '请输入新密码',
+    password_changed_success: '密码修改成功!请使用新密码重新登录。',
+    change_failed: '修改失败',
+  },
+  home: {
+    hero_title_prefix: '签证申请,',
+    hero_title_highlight: '从未如此简单',
+    hero_subtitle: 'Visafly 为您提供全球签证自动化处理服务。实时追踪状态,专家级审核,让您的出行无后顾之忧。',
+    cta_start: '开始申请',
+    cta_check_slots: '查询名额',
+    
+    process_title: '工作流程',
+    process_subtitle: '只需 4 步,全自动化处理您的签证需求',
+    
+    step_1_title: '1. 选择服务',
+    step_1_desc: '在服务列表中找到您需要办理的国家和签证类型(如日本旅游签、法国申根签)。',
+    step_2_title: '2. 提交资料',
+    step_2_desc: '填写简单的申请表单(支持 JSON 动态表单),无需繁琐的手写文件。',
+    step_3_title: '3. 智能托管',
+    step_3_desc: '我们的 24/7 机器人系统会自动为您监控名额并锁定预约,无需人工守候。',
+    step_4_title: '4. 成功出签',
+    step_4_desc: '预约成功后支付费用(支持随机立减),获取确认函,准备递签。',
+    
+    feat_global_title: '覆盖全球',
+    feat_global_desc: '支持美国、日本、申根区等超过 50 个国家和地区的签证办理。',
+    feat_success_title: '高成功率',
+    feat_success_desc: '智能系统预审加上人工专家复核,确保资料准确无误。',
+    feat_fast_title: '极速处理',
+    feat_fast_desc: '对接官方 API,自动化流程将申请时间缩短至传统的 1/3。',
+  },
+  order: {
+    search_placeholder: '搜索订单号或商品名称...',
+    history_title: '申请记录',
+    empty_title: '暂无订单',
+    empty_desc: '没有找到匹配的订单记录。',
+    unknown_service: '未命名服务',
+    view_details: '查看详情',
+    pay_now: '支付',
+    support: '售后',
+    detail_title: '订单详情',
+    created_at: '创建于',
+    application_data: '申请资料 (Application Data)',
+    no_application_data: '未填写申请资料',
+    payment_history: '支付流水',
+    load_product_failed: '商品信息加载失败',
+    product_not_found: '无法找到该服务',
+    create_failed: '创建订单失败',
+    fill_form_hint: '请仔细填写以下申请信息,这将直接用于您的签证申请。',
+    no_extra_info_needed: '无需填写额外信息,请直接提交。',
+    submit_and_pay: '提交订单并支付',
+  },
+  status: {
+    pending: '待支付',
+    paid: '已支付',
+    succeeded: '支付成功',
+    completed: '已完成',
+    failed: '失败',
+    cancelled: '已取消',
+  },
+  product: {
+    title: '商品管理',
+    name: '商品名称',
+    price: '价格',
+    country: '国家/地区',
+    city: '城市',
+    visa_type: '签证类型',
+    provider: '供应商',
+    enabled: '上架',
+    disabled: '下架',
+    edit_title: '编辑商品',
+    create_title: '发布新商品',
+  },
+  ticket: {
+    title: '工单管理',
+    order_id: '关联订单',
+    type: '类型',
+    reason: '原因',
+    my_tickets: '我的工单',
+    create_new: '提交新工单',
+    search_placeholder: '搜索工单号、订单号...',
+    empty_title: '暂无工单记录',
+    empty_desc: '如果您遇到问题,请点击右上角提交新工单。',
+    reply: '回复消息',
+    view_details: '查看详情',
+    latest_feedback: '最新反馈',
+    submit_modal_title: '提交工单',
+    submit_error_default: '提交失败,请稍后重试',
+    order_id_label: '关联订单号',
+    order_id_placeholder: '请输入相关的订单编号',
+    type_label: '工单类型',
+    desc_label: '详细描述',
+    desc_placeholder: '请详细描述您遇到的问题,以便我们更快为您处理...',
+    submit_btn: '提交工单',
+    title_prefix: '工单',
+    related_order: '关联订单',
+    created_at: '工单创建于',
+    description_title: '工单描述',
+    system_message: '系统消息',
+    sender_me: '我',
+    sender_support: '客服',
+    reply_placeholder: '请输入回复内容...',
+    send_failed: '发送失败,请稍后重试',
+    upload_tooltip: '上传附件(暂不可用)',
+    urgent_tip: '如需紧急处理,请发送邮件至 support@visafly.top',
+    types: {
+      refund: '退款申请',
+      dispute: '交易纠纷',
+      change_request: '变更请求',
+    },
+    status: {
+      pending: '待处理',
+      info_required: '需补充资料',
+      resolved: '已解决',
+      rejected: '已拒绝',
+    }
+  },
+  nav: {
+    dashboard: '控制台', // 或 '仪表盘'
+    products: '商品列表',
+    tickets: '工单中心',
+    settings: '个人设置',
+    logout: '退出',
+    // === 新增以下 Key ===
+    services: '服务列表',
+    slots: '名额查询',
+    guide: '办理指南',
+    admin: '管理后台',
+    login: '登录 / 注册',
+  },
+  dashboard: {
+    title: '用户控制台',
+    subtitle: '管理您的签证申请进度和售后服务',
+    logout_confirm: '确定要退出登录吗?',
+  },
+  knowledge: {
+    title: '办理指南 & 常见问题',
+    subtitle: '这里汇集了签证办理过程中的常见问题解答、材料准备指南以及最新政策解读。',
+    search_placeholder: '搜索关键词,例如:照片、邮票、指纹...',
+    empty_state: '未找到相关指南,请尝试更换关键词',
+    read_more: '阅读全文',
+    collapse: '收起',
+  },
+  refund: {
+    title: '退款政策 & 服务条款',
+    subtitle: '为了保障您的权益,请仔细阅读以下条款',
+    
+    full_refund_title: '全额退款情形',
+    full_refund_desc: '在以下情况下,您可以申请全额退款:',
+    full_refund_item_1: '您已支付订单,但我们的系统尚未开始执行任何预约操作(Status 为 Pending)。',
+    full_refund_item_2: '由于 Visafly 系统故障导致重复扣款。',
+    full_refund_item_3: '我们在承诺的时限内(通常为 30 天,具体视服务而定)未能为您成功预约到名额。',
+    
+    no_refund_title: '无法退款情形',
+    important_note: '重要提示',
+    important_note_desc: '一旦服务进入执行阶段或产生第三方费用,退款将受到限制。',
+    no_refund_item_1: '我们的系统已经为您成功预约到了名额(以截图或确认函为准)。',
+    no_refund_item_2: '您的签证申请已经递交至大使馆或签证中心。',
+    no_refund_item_3: '因您提供的个人信息(如护照号)错误导致预约失败或无效。',
+    no_refund_item_4: '您因个人原因(如改变行程、生病)决定放弃申请,但此时我们已完成了预约工作。',
+    
+    process_title: '退款流程',
+    step_1_title: '提交工单',
+    step_1_desc: '在控制台选择订单,点击“售后/帮助”提交申请。',
+    step_2_title: '客服审核',
+    step_2_desc: '我们将在 1-3 个工作日内核实订单状态。',
+    step_3_title: '原路退回',
+    step_3_desc: '批准后,资金将在 5-10 个工作日原路退回。',
+    
+    contact_title: '还有疑问?',
+    contact_desc: '如果您对退款政策有任何疑问,请联系我们的客服团队:',
+  },
+  services: {
+    title: '热门签证服务',
+    search_placeholder: '搜索国家、城市或服务...',
+    all_countries: '所有国家',
+    all_types: '所有类型',
+    reset_filter: '重置筛选',
+    no_result_title: '未找到相关服务',
+    no_result_desc: '请尝试调整搜索关键词',
+    no_desc: '暂无详细描述',
+    service_fee: '服务费',
+    apply_now: '立即申请',
+  },
+  slots: {
+    title: '签证名额查询',
+    subtitle: '实时查询各使馆最新可预约名额',
+    status_available: '当前有名额可约',
+    status_unavailable: '暂无名额',
+    updated_at: '数据更新于',
+    earliest_date: '最早可约',
+    slots_count: '个时段',
+    select_hint: '请选择条件并点击查询',
+  },
+  pagination: {
+    prev: '上一页',
+    next: '下一页',
+    showing: '显示第',
+    to: '到',
+    of: '条,共', // 对应英文 "of"
+    results: '条', // 对应英文 "results"
+  },
+  profile: {
+    title: '个人资料',
+    subtitle: '管理您的基本信息展示',
+    edit_profile: '编辑资料',
+    no_nickname: '未设置昵称',
+    change_avatar: '点击更换头像',
+    email_readonly: '账号邮箱 (不可改)',
+    nickname_label: '昵称',
+    nickname_placeholder: '请输入您的昵称',
+    phone_label: '手机号码',
+    phone_placeholder: '请输入手机号码',
+    saving: '上传保存中...',
+    login_password: '登录密码',
+    password_tip: '建议定期更换密码以保护账户安全',
+    image_size_limit: '图片大小不能超过 2MB',
+    upload_failed: '头像上传失败,请重试或联系客服',
+    update_success: '个人资料更新成功!',
+    update_failed: '更新失败',
+    fetch_failed: '无法获取用户信息',
+  },
+  sidebar: {
+    orders: '我的订单',
+    tickets: '售后工单',
+    settings: '账户设置',
+    contact_support: '联系客服',
+    contact_desc: '遇到问题?您可以直接提交工单,或通过以下方式联系:',
+  },
+  footer: {
+    description: '专业的签证自动化服务平台。利用技术优势,让全球签证申请变得简单、高效、透明。',
+    quick_links: '快速链接',
+    support: '帮助与支持',
+    refund_policy: '退款政策',
+    terms: '服务条款',
+    contact: '联系我们',
+    rights_reserved: '保留所有权利。',
+    privacy: '隐私政策',
+    cookie: 'Cookie 政策',
+  },
+  payment: {
+    order_created: '订单已创建',
+    order_id: '订单号',
+    select_method: '选择支付方式',
+    creating: '正在创建支付...',
+    reselect: '重选方式',
+    expires: '过期',
+    confirm_info: '确认支付信息',
+    original_amount: '原始金额',
+    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: '支付初始化失败',
+  },
+  privacy_content: {
+    last_updated: '最后更新日期:2025年1月1日',
+    section1_title: '1. 信息收集',
+    section1_desc: '我们要收集的信息包括但不限于:姓名、联系方式、护照信息等,仅用于为您办理签证业务。',
+    section2_title: '2. 信息使用',
+    section2_desc: '您的信息将严格用于提交大使馆申请,绝不会用于其他商业用途。',
+    section3_title: '3. 数据安全',
+    section3_desc: '我们采用行业标准的加密技术保护您的数据安全。',
+  },
+  cookie_content: {
+    last_updated: '最后更新日期:2025年1月1日',
+    section1_title: '1. 什么是 Cookie',
+    section1_desc: 'Cookie 是存储在您设备上的小型文本文件,用于提升网站体验。',
+    section2_title: '2. 我们如何使用 Cookie',
+    section2_item1_label: '必要 Cookie:',
+    section2_item1_desc: '用于维持登录状态(如 Token)。',
+    section2_item2_label: '功能 Cookie:',
+    section2_item2_desc: '用于记住您的语言偏好设置。',
+    section3_title: '3. 如何管理 Cookie',
+    section3_desc: '您可以通过浏览器设置禁用 Cookie,但这可能会影响网站的部分功能。',
+  }
+};

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff