jerry 4 miesięcy temu
rodzic
commit
7a1dc96400

+ 12 - 10
next.config.js

@@ -1,13 +1,15 @@
 /** @type {import('next').NextConfig} */
 /** @type {import('next').NextConfig} */
 const nextConfig = {
 const nextConfig = {
-    async rewrites() {
-      return [
-        {
-          source: '/api/:path*',
-          destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
-        },
-      ];
-    },
-  };
+
+  async rewrites() {
+    return [
+      {
+        source: '/api/:path*',
+        destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
+      },
+    ];
+  },
   
   
-  module.exports = nextConfig;
+};
+
+module.exports = nextConfig;

+ 14 - 1
package-lock.json

@@ -14,7 +14,8 @@
                 "react": "^18",
                 "react": "^18",
                 "react-dom": "^18",
                 "react-dom": "^18",
                 "react-is": "^19.2.3",
                 "react-is": "^19.2.3",
-                "recharts": "^3.6.0"
+                "recharts": "^3.6.0",
+                "use-debounce": "^10.0.6"
             },
             },
             "devDependencies": {
             "devDependencies": {
                 "@types/node": "25.0.3",
                 "@types/node": "25.0.3",
@@ -2074,6 +2075,18 @@
                 "browserslist": ">= 4.21.0"
                 "browserslist": ">= 4.21.0"
             }
             }
         },
         },
+        "node_modules/use-debounce": {
+            "version": "10.0.6",
+            "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.6.tgz",
+            "integrity": "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 16.0.0"
+            },
+            "peerDependencies": {
+                "react": "*"
+            }
+        },
         "node_modules/use-sync-external-store": {
         "node_modules/use-sync-external-store": {
             "version": "1.6.0",
             "version": "1.6.0",
             "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
             "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",

+ 2 - 1
package.json

@@ -15,7 +15,8 @@
         "react": "^18",
         "react": "^18",
         "react-dom": "^18",
         "react-dom": "^18",
         "react-is": "^19.2.3",
         "react-is": "^19.2.3",
-        "recharts": "^3.6.0"
+        "recharts": "^3.6.0",
+        "use-debounce": "^10.0.6"
     },
     },
     "devDependencies": {
     "devDependencies": {
         "@types/node": "25.0.3",
         "@types/node": "25.0.3",

+ 262 - 171
src/app/admin/orders/new/page.tsx

@@ -3,252 +3,343 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
-import { Loader2, ArrowLeft, Search, CheckCircle, User } from 'lucide-react';
+import { Loader2, ArrowLeft, Search, CheckCircle, Package, Info } from 'lucide-react';
+import { useDebounce } from 'use-debounce'; 
+
+// 定义 Schema 属性接口,方便类型提示
+interface SchemaProperty {
+  type: string;
+  title?: string;
+  description?: string;
+  default?: any;
+  enum?: string[];
+  'x-order'?: number; // 关键排序字段
+  order?: number;     // 兼容标准排序字段
+  [key: string]: any;
+}
 
 
 export default function AdminCreateOrderPage() {
 export default function AdminCreateOrderPage() {
   const router = useRouter();
   const router = useRouter();
-  
-  // 步骤控制
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
-  const [products, setProducts] = useState<any[]>([]);
   
   
-  // 表单状态
-  const [targetEmail, setTargetEmail] = useState('');
-  const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
+  // 1. 商品搜索状态
+  const [keyword, setKeyword] = useState('');
+  const [debouncedKeyword] = useDebounce(keyword, 500);
+  const [productList, setProductList] = useState<any[]>([]);
+  const [isSearchingProduct, setIsSearchingProduct] = useState(false);
+  const [selectedProduct, setSelectedProduct] = useState<any>(null);
+
+  // 2. 表单状态
   const [formSchema, setFormSchema] = useState<any>(null);
   const [formSchema, setFormSchema] = useState<any>(null);
   const [formValues, setFormValues] = useState<Record<string, any>>({});
   const [formValues, setFormValues] = useState<Record<string, any>>({});
-  const [skipPayment, setSkipPayment] = useState(true); // 默认跳过支付
+  const [sortedFields, setSortedFields] = useState<string[]>([]); // 存储排序后的字段名
 
 
-  // 1. 加载商品列表
+  // --- 商品搜索逻辑 ---
   useEffect(() => {
   useEffect(() => {
-    async function loadProducts() {
+    if (!debouncedKeyword) {
+      setProductList([]);
+      return;
+    }
+    
+    async function searchProducts() {
+      setIsSearchingProduct(true);
       try {
       try {
-        const res = await api.get('/api/vas/product/list');
-        setProducts(Array.isArray(res.data.data) ? res.data.data : []);
+        const res = await api.get('/api/vas/product/list', {
+          params: { keyword: debouncedKeyword, page: 1, size: 20 }
+        });
+        const data = res.data.data;
+        if (Array.isArray(data)) setProductList(data);
+        else if (data.items) setProductList(data.items);
       } catch (e) {
       } catch (e) {
-        console.error(e);
+        console.error("Search failed", e);
+      } finally {
+        setIsSearchingProduct(false);
       }
       }
     }
     }
-    loadProducts();
-  }, []);
+    searchProducts();
+  }, [debouncedKeyword]);
 
 
-  // 2. 当选择了商品,加载 Schema
+  // --- 加载 Schema 并排序 ---
   useEffect(() => {
   useEffect(() => {
     async function loadSchema() {
     async function loadSchema() {
-      if (!selectedProductId) {
+      if (!selectedProduct?.schema_id) {
         setFormSchema(null);
         setFormSchema(null);
+        setSortedFields([]);
         return;
         return;
       }
       }
-      
-      const product = products.find(p => p.id === Number(selectedProductId));
-      if (!product?.schema_id) return;
-
       try {
       try {
-        // API: 获取 Schema 定义
-        const res = await api.get(`/api/vas/schema/${product.schema_id}`);
+        const res = await api.get('/api/vas/schema/detail', { 
+            params: { schema_id: selectedProduct.schema_id }
+        });
         const data = res.data.data || res.data;
         const data = res.data.data || res.data;
-        // 解析 JSON
         const schemaJson = typeof data.schema_json === 'string' 
         const schemaJson = typeof data.schema_json === 'string' 
           ? JSON.parse(data.schema_json) 
           ? JSON.parse(data.schema_json) 
           : data.schema_json;
           : data.schema_json;
         
         
         setFormSchema(schemaJson);
         setFormSchema(schemaJson);
-        setFormValues({}); // 重置表单
+        setFormValues({});
+
+        // === 核心修改:字段排序逻辑 ===
+        if (schemaJson.properties) {
+          const keys = Object.keys(schemaJson.properties);
+          
+          // 优先检查 ui:order (如果 Schema 顶层有定义)
+          if (Array.isArray(schemaJson['ui:order'])) {
+             setSortedFields(schemaJson['ui:order']);
+          } else {
+             // 否则根据 x-order 或 order 属性排序
+             const sorted = keys.sort((a, b) => {
+               const propA = schemaJson.properties[a] as SchemaProperty;
+               const propB = schemaJson.properties[b] as SchemaProperty;
+               
+               // 获取权重,默认 999 放到最后
+               const orderA = propA['x-order'] ?? propA.order ?? 999;
+               const orderB = propB['x-order'] ?? propB.order ?? 999;
+               
+               return orderA - orderB;
+             });
+             setSortedFields(sorted);
+          }
+        }
+
       } catch (e) {
       } catch (e) {
-        alert("加载表单定义失败");
+        alert("表单定义加载失败");
       }
       }
     }
     }
     loadSchema();
     loadSchema();
-  }, [selectedProductId, products]);
+  }, [selectedProduct]);
 
 
-  // 处理动态表单输入
-  const handleInputChange = (key: string, value: any) => {
-    setFormValues(prev => ({ ...prev, [key]: value }));
-  };
-
-  // 3. 提交订单
+  // --- 提交逻辑 ---
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    if (!targetEmail) return alert("请输入目标用户邮箱");
-    if (!selectedProductId) return alert("请选择商品");
+    if (!selectedProduct) return alert("请先搜索并选择商品");
 
 
     setLoading(true);
     setLoading(true);
     try {
     try {
-      // Step A: 创建订单
-      // 注意:这里假设后端 create 接口支持通过 extra 参数指定 user_email (管理员权限)
-      // 如果后端不支持直接指定 user,可能需要先根据 email 查 user_id,然后传 user_id
+      // 1. 构造 Payload
       const payload = {
       const payload = {
-        product_id: Number(selectedProductId),
+        product_id: selectedProduct.id,
         user_inputs: formValues,
         user_inputs: formValues,
-        // 传递目标用户标识 (需要后端支持处理,或者后端从 user_inputs 里提取 email)
-        target_user_email: targetEmail, 
-        // 标记为后台创建
-        is_admin_created: true 
       };
       };
 
 
-      const res = await api.post('/api/vas/order/create', payload);
-      const orderData = res.data.data || res.data;
-      const orderId = orderData.id;
-
-      // Step B: 如果勾选了“跳过支付”,直接将订单更新为已支付
-      if (skipPayment && orderId) {
-        // 调用更新接口将状态改为 paid
-        // API: PUT /api/vas/order/{id}
-        await api.put(`/api/vas/order/${orderId}`, {
-          status: 'paid', // 强制标记为已支付
-          admin_note: '管理员后台代下单,免支付'
-        });
-      }
+      // 2. === 修改点:调用 create_by_admin 接口 ===
+      // 该接口应当包含:创建订单 + 自动标记支付成功 + 触发后续逻辑
+      const res = await api.post('/api/vas/order/create_by_admin', payload);
+      
+      // 兼容不同后端返回结构
+      const orderId = res.data.data?.id || res.data?.id || res.data?.order_id;
 
 
-      alert("订单创建成功!");
+      alert(`代下单成功!订单号: ${orderId}`);
       router.push('/admin/orders');
       router.push('/admin/orders');
 
 
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
-      alert("创建失败: " + (error.response?.data?.message || error.message));
+      alert("下单失败: " + (error.response?.data?.message || error.message));
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
   };
   };
 
 
-  // 动态字段渲染器 (复用之前逻辑)
-  const renderField = (key: string, fieldSchema: any) => {
-    const commonClass = "w-full border rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none";
-    
-    if (fieldSchema.enum) {
+  const handleInputChange = (key: string, value: any) => {
+    setFormValues(prev => ({ ...prev, [key]: value }));
+  };
+
+  const renderField = (key: string, fieldSchema: SchemaProperty) => {
+    const commonClass = "w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition";
+    const label = fieldSchema.title || key;
+    const placeholder = fieldSchema.description || `请输入 ${label}`;
+
+    // 枚举类型 (Select)
+    if (fieldSchema.enum && fieldSchema.enum.length > 0) {
       return (
       return (
-        <select className={commonClass} onChange={e => handleInputChange(key, e.target.value)}>
-          <option value="">请选择</option>
+        <select 
+          className={commonClass} 
+          onChange={e => handleInputChange(key, e.target.value)}
+          value={formValues[key] || ''}
+        >
+          <option value="">请选择 {label}</option>
           {fieldSchema.enum.map((v: string) => <option key={v} value={v}>{v}</option>)}
           {fieldSchema.enum.map((v: string) => <option key={v} value={v}>{v}</option>)}
         </select>
         </select>
       );
       );
     }
     }
+
+    // 日期类型 (原生 Date Picker)
+    if (fieldSchema.format === 'date') {
+      return (
+        <input 
+          type="date"
+          className={commonClass}
+          onChange={e => handleInputChange(key, e.target.value)}
+          value={formValues[key] || ''}
+        />
+      );
+    }
+
+    // 普通输入框
     return (
     return (
       <input 
       <input 
-        type={fieldSchema.type === 'integer' ? 'number' : 'text'} 
+        type={fieldSchema.type === 'integer' || fieldSchema.type === 'number' ? 'number' : 'text'} 
         className={commonClass}
         className={commonClass}
+        placeholder={placeholder}
         onChange={e => handleInputChange(key, e.target.value)}
         onChange={e => handleInputChange(key, e.target.value)}
+        value={formValues[key] || ''}
       />
       />
     );
     );
   };
   };
 
 
   return (
   return (
-    <div className="max-w-3xl mx-auto">
-      <button onClick={() => router.back()} className="flex items-center text-slate-500 hover:text-slate-800 mb-6 text-sm">
-        <ArrowLeft size={16} className="mr-1" /> 返回订单列表
-      </button>
-
-      <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
-        <div className="px-6 py-4 border-b bg-slate-50">
-          <h1 className="text-lg font-bold text-slate-800">管理员代下单 (后台创建)</h1>
-          <p className="text-xs text-slate-500 mt-1">为用户创建订单并可选择直接跳过支付流程</p>
-        </div>
+    <div className="max-w-4xl mx-auto p-4 md:p-8">
+      
+      {/* 头部导航 */}
+      <div className="flex items-center justify-between mb-6">
+        <button onClick={() => router.back()} className="flex items-center text-slate-500 hover:text-slate-900 transition text-sm font-medium">
+          <ArrowLeft size={18} className="mr-1" /> 返回列表
+        </button>
+        <h1 className="text-xl font-bold text-slate-800">管理员代客下单</h1>
+      </div>
 
 
-        <form onSubmit={handleSubmit} className="p-6 space-y-8">
-          
-          {/* 1. 用户与商品选择 */}
-          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
-            <div>
-              <label className="block text-sm font-bold text-slate-700 mb-2">
-                目标用户邮箱 <span className="text-red-500">*</span>
-              </label>
-              <div className="relative">
-                <input 
-                  type="email" required
-                  className="w-full border border-slate-300 rounded-lg pl-9 p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
-                  placeholder="user@example.com"
-                  value={targetEmail}
-                  onChange={e => setTargetEmail(e.target.value)}
-                />
-                <User size={16} className="absolute left-3 top-3 text-slate-400" />
-              </div>
-              <p className="text-xs text-slate-400 mt-1">如果用户不存在,系统将尝试自动创建或报错。</p>
+      <div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden">
+        
+        {/* Step 1: 搜索选择商品 */}
+        <div className="p-6 border-b border-slate-100 bg-slate-50">
+          <label className="block text-sm font-bold text-slate-700 mb-2">
+            1. 搜索服务商品 <span className="text-red-500">*</span>
+          </label>
+          <div className="relative">
+            <input 
+              type="text" 
+              className="w-full pl-10 pr-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+              placeholder="输入关键词搜索 (例如: 日本, France...)"
+              value={keyword}
+              onChange={e => {
+                setKeyword(e.target.value);
+                if(!e.target.value) {
+                    setProductList([]);
+                    setSelectedProduct(null);
+                }
+              }}
+            />
+            <Search className="absolute left-3 top-3.5 text-slate-400" size={20} />
+            {isSearchingProduct && (
+              <Loader2 className="absolute right-3 top-3.5 text-blue-500 animate-spin" size={20} />
+            )}
+          </div>
+
+          {/* 搜索结果下拉列表 */}
+          {productList.length > 0 && !selectedProduct && (
+            <div className="mt-2 bg-white border border-slate-200 rounded-xl shadow-lg max-h-60 overflow-y-auto divide-y divide-slate-100 animate-in fade-in zoom-in-95 duration-200">
+              {productList.map(p => (
+                <div 
+                  key={p.id} 
+                  onClick={() => {
+                    setSelectedProduct(p);
+                    setKeyword(p.title); // 回填输入框
+                    setProductList([]);  // 收起列表
+                  }}
+                  className="p-3 hover:bg-blue-50 cursor-pointer transition flex justify-between items-center group"
+                >
+                  <div className="flex items-center gap-3">
+                    <div className="p-2 bg-slate-100 rounded-lg group-hover:bg-blue-100 text-slate-500 group-hover:text-blue-600 transition">
+                      <Package size={16} />
+                    </div>
+                    <div>
+                      <div className="font-bold text-sm text-slate-800">{p.title}</div>
+                      <div className="text-xs text-slate-500">{p.country} • {p.visa_type}</div>
+                    </div>
+                  </div>
+                  <div className="text-sm font-mono font-bold text-slate-600">
+                    {p.price_amount / 100} {p.price_currency}
+                  </div>
+                </div>
+              ))}
             </div>
             </div>
+          )}
 
 
-            <div>
-              <label className="block text-sm font-bold text-slate-700 mb-2">
-                选择商品 <span className="text-red-500">*</span>
-              </label>
-              <select 
-                required
-                className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white"
-                value={selectedProductId || ''}
-                onChange={e => setSelectedProductId(Number(e.target.value))}
+          {/* 已选中状态展示 */}
+          {selectedProduct && (
+            <div className="mt-4 p-4 bg-blue-50 border border-blue-100 rounded-xl flex justify-between items-center animate-in slide-in-from-top-2">
+              <div className="flex items-center gap-3">
+                <div className="p-2 bg-blue-600 text-white rounded-lg shadow-sm">
+                  <CheckCircle size={20} />
+                </div>
+                <div>
+                  <div className="font-bold text-slate-800 text-sm">已选择: {selectedProduct.title}</div>
+                  <div className="text-xs text-slate-500 mt-0.5">ID: {selectedProduct.id}</div>
+                </div>
+              </div>
+              <button 
+                type="button"
+                onClick={() => { setSelectedProduct(null); setKeyword(''); setFormSchema(null); setSortedFields([]); }}
+                className="text-xs text-blue-600 hover:text-blue-800 font-medium hover:underline"
               >
               >
-                <option value="">-- 请选择服务 --</option>
-                {products.map(p => (
-                  <option key={p.id} value={p.id}>{p.title} ({p.price_amount/100} {p.price_currency})</option>
-                ))}
-              </select>
+                重新选择
+              </button>
             </div>
             </div>
-          </div>
+          )}
+        </div>
 
 
-          <div className="border-t border-slate-100"></div>
-
-          {/* 2. 动态表单区域 */}
-          <div>
-            <h3 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
-              填写申请资料
-              {formSchema && <span className="text-xs font-normal text-slate-500 bg-slate-100 px-2 py-0.5 rounded">Schema Loaded</span>}
-            </h3>
-            
-            {!selectedProductId ? (
-              <div className="text-center py-8 bg-slate-50 rounded-lg text-slate-400 text-sm">
-                请先选择左侧的商品,以加载对应的表单字段
-              </div>
-            ) : !formSchema ? (
-              <div className="text-center py-8 text-slate-400 text-sm">正在加载表单定义...</div>
-            ) : (
-              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
-                {formSchema.properties && Object.keys(formSchema.properties).map(key => (
-                  <div key={key}>
-                    <label className="block text-sm font-medium text-slate-700 mb-1">
-                      {formSchema.properties[key].title || key}
-                    </label>
-                    {renderField(key, formSchema.properties[key])}
-                  </div>
-                ))}
-              </div>
-            )}
-          </div>
+        <form onSubmit={handleSubmit}>
+          {/* Step 2: 填写表单 (仅当选中商品后显示) */}
+          {selectedProduct && (
+            <div className="p-6">
+              <h3 className="text-sm font-bold text-slate-800 mb-6 flex items-center gap-2 pb-2 border-b border-slate-100">
+                <span className="w-6 h-6 rounded-full bg-slate-800 text-white flex items-center justify-center text-xs">2</span>
+                填写申请资料
+              </h3>
 
 
-          <div className="border-t border-slate-100"></div>
-
-          {/* 3. 支付设置 */}
-          <div className="bg-green-50 border border-green-100 rounded-lg p-4 flex items-start gap-3">
-            <div className="mt-0.5">
-              <input 
-                type="checkbox" 
-                id="skipPayment"
-                className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
-                checked={skipPayment}
-                onChange={e => setSkipPayment(e.target.checked)}
-              />
-            </div>
-            <div>
-              <label htmlFor="skipPayment" className="block text-sm font-bold text-green-800 cursor-pointer">
-                免支付 (直接标记为已支付)
-              </label>
-              <p className="text-xs text-green-700 mt-1">
-                选中后,订单创建后将自动把状态更为 <strong>PAID</strong>,并触发后续业务流程(如机器人任务)。
-                <br/>如果不选中,订单将保持 Pending 状态,需要用户自行登录付款。
-              </p>
+              {!formSchema ? (
+                <div className="text-center py-8 text-slate-400 text-sm flex flex-col items-center">
+                   <Loader2 className="animate-spin mb-2" /> 正在加载表单定义...
+                </div>
+              ) : (
+                <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                  {/* === 使用 sortedFields 进行渲染 === */}
+                  {sortedFields.map(key => {
+                    const fieldSchema = formSchema.properties[key] as SchemaProperty;
+                    return (
+                      <div key={key}>
+                        <label className="block text-xs font-bold text-slate-500 uppercase mb-1.5 ml-1">
+                          {fieldSchema.title || key}
+                        </label>
+                        {renderField(key, fieldSchema)}
+                        {fieldSchema.description && (
+                          <p className="text-xs text-gray-400 mt-1">{fieldSchema.description}</p>
+                        )}
+                      </div>
+                    );
+                  })}
+                  
+                  {sortedFields.length === 0 && (
+                     <div className="col-span-2 text-center text-slate-400 text-sm py-4 bg-slate-50 rounded-lg border border-dashed border-slate-200">
+                        此商品无需填写额外信息
+                     </div>
+                  )}
+                </div>
+              )}
             </div>
             </div>
-          </div>
+          )}
 
 
-          {/* 提交按钮 */}
-          <div className="flex justify-end pt-4">
-            <button 
-              type="submit" 
-              disabled={loading}
-              className="px-8 py-3 bg-slate-900 text-white rounded-lg font-bold hover:bg-slate-800 transition flex items-center gap-2 disabled:opacity-50 shadow-lg"
-            >
-              {loading ? <Loader2 className="animate-spin" /> : <CheckCircle size={18} />}
-              {skipPayment ? '创建并标记成功' : '创建待支付订单'}
-            </button>
+          {/* Footer Action */}
+          <div className="p-6 bg-slate-50 border-t border-slate-100 flex items-center justify-between">
+             <div className="text-xs text-slate-500 max-w-md flex items-start gap-2">
+                <Info size={16} className="shrink-0 mt-0.5" />
+                <div>
+                  <span className="font-bold text-slate-700">说明:</span>
+                  订单将直接创建在您的账号下,状态为 <span className="font-bold text-green-600">已支付 (Paid)</span>,并自动进入待处理队列。
+                </div>
+             </div>
+             
+             <button 
+                type="submit" 
+                disabled={loading || !selectedProduct}
+                className="px-8 py-3 bg-slate-900 text-white rounded-xl font-bold hover:bg-slate-800 transition flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-slate-300 transform active:scale-95"
+              >
+                {loading ? <Loader2 className="animate-spin" /> : <CheckCircle size={18} />}
+                确认创建
+              </button>
           </div>
           </div>
-
         </form>
         </form>
+
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 59 - 42
src/app/admin/orders/page.tsx

@@ -7,6 +7,7 @@ import { RefreshCw, Search, Plus } from 'lucide-react';
 import OrderTable from '@/components/admin/orders/OrderTable';
 import OrderTable from '@/components/admin/orders/OrderTable';
 import OrderDetailModal, { OrderDetail } from '@/components/admin/orders/OrderDetailModal';
 import OrderDetailModal, { OrderDetail } from '@/components/admin/orders/OrderDetailModal';
 import OrderEditModal from '@/components/admin/orders/OrderEditModal';
 import OrderEditModal from '@/components/admin/orders/OrderEditModal';
+import OrderPaymentModal from '@/components/admin/orders/OrderPaymentModal'; // 1. 引入支付管理弹窗
 import Pagination from '@/components/common/Pagination';
 import Pagination from '@/components/common/Pagination';
 
 
 export default function AdminOrdersPage() {
 export default function AdminOrdersPage() {
@@ -23,15 +24,15 @@ export default function AdminOrdersPage() {
   const [keyword, setKeyword] = useState('');
   const [keyword, setKeyword] = useState('');
   
   
   // 弹窗状态
   // 弹窗状态
-  const [isDetailOpen, setIsDetailOpen] = useState<boolean>(false);
   const [selectedOrder, setSelectedOrder] = useState<OrderDetail | null>(null);
   const [selectedOrder, setSelectedOrder] = useState<OrderDetail | null>(null);
-  const [isEditOpen, setIsEditOpen] = useState<boolean>(false);
-  const [editingOrder, setEditingOrder] = useState<OrderDetail | null>(null);
+  
+  const [isDetailOpen, setIsDetailOpen] = useState(false);
+  const [isEditOpen, setIsEditOpen] = useState(false);
+  const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false); // 2. 新增支付弹窗状态
 
 
-  useEffect(() => {
-    fetchOrders(1);
-  }, []);
+  const [editingOrder, setEditingOrder] = useState<OrderDetail | null>(null);
 
 
+  // 获取订单列表
   const fetchOrders = async (targetPage: number = page) => {
   const fetchOrders = async (targetPage: number = page) => {
     setLoading(true);
     setLoading(true);
     try {
     try {
@@ -44,22 +45,33 @@ export default function AdminOrdersPage() {
       });
       });
       
       
       const data = res.data.data;
       const data = res.data.data;
+      
       if (data && Array.isArray(data.items)) {
       if (data && Array.isArray(data.items)) {
         setOrders(data.items);
         setOrders(data.items);
         setTotal(data.total || 0);
         setTotal(data.total || 0);
+      } else if (Array.isArray(data)) {
+        setOrders(data);
+        setTotal(data.length);
       } else {
       } else {
         setOrders([]);
         setOrders([]);
         setTotal(0);
         setTotal(0);
       }
       }
+      
       setPage(targetPage);
       setPage(targetPage);
+
     } catch (e) {
     } catch (e) {
-      console.error("Fetch orders failed", e);
+      console.warn("API Error", e);
       setOrders([]);
       setOrders([]);
+      setTotal(0);
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
   };
   };
 
 
+  useEffect(() => {
+    fetchOrders(1);
+  }, []);
+
   const handleSearch = () => fetchOrders(1);
   const handleSearch = () => fetchOrders(1);
   const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
   const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleSearch(); };
 
 
@@ -84,6 +96,17 @@ export default function AdminOrdersPage() {
     setIsEditOpen(true);
     setIsEditOpen(true);
   };
   };
 
 
+  // 3. 处理人工核销/检查支付
+  const handleCheckPayments = (order: OrderDetail) => {
+    setSelectedOrder(order);
+    setIsPaymentModalOpen(true);
+  };
+
+  // 4. 支付状态变更后的回调
+  const handlePaymentSuccess = () => {
+    fetchOrders(page); // 刷新列表,订单状态应变为 paid
+  };
+
   const handleSubmitEdit = async (orderId: string, data: any) => {
   const handleSubmitEdit = async (orderId: string, data: any) => {
     try {
     try {
       await api.post(`/api/vas/order/patch_user_inputs`, data, { params: { order_id: orderId } });
       await api.post(`/api/vas/order/patch_user_inputs`, data, { params: { order_id: orderId } });
@@ -96,26 +119,18 @@ export default function AdminOrdersPage() {
   };
   };
 
 
   return (
   return (
-    <div className="p-4 md:p-6">
-      
-      {/* === 头部区域:响应式布局 === */}
-      <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
-        
-        {/* 标题 */}
+    <div className="p-4 md:p-8">
+      <div className="flex flex-col md:flex-row justify-between md:items-center gap-4 mb-6">
         <div>
         <div>
           <h1 className="text-2xl font-bold text-slate-800">订单管理</h1>
           <h1 className="text-2xl font-bold text-slate-800">订单管理</h1>
           <p className="text-sm text-slate-500 mt-1">查看及管理所有用户提交的签证申请</p>
           <p className="text-sm text-slate-500 mt-1">查看及管理所有用户提交的签证申请</p>
         </div>
         </div>
-        
-        {/* 操作区:移动端垂直,桌面端水平 */}
-        <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
-          
-          {/* 搜索框:移动端全宽 */}
-          <div className="relative w-full sm:w-auto md:w-64">
+        <div className="flex flex-wrap gap-3">
+          <div className="relative flex-grow md:flex-grow-0">
             <input 
             <input 
               type="text" 
               type="text" 
               placeholder="搜索订单号/邮箱..." 
               placeholder="搜索订单号/邮箱..." 
-              className="w-full pl-9 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
+              className="w-full md:w-64 pl-10 pr-4 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none transition"
               value={keyword}
               value={keyword}
               onChange={e => setKeyword(e.target.value)}
               onChange={e => setKeyword(e.target.value)}
               onKeyDown={handleKeyDown}
               onKeyDown={handleKeyDown}
@@ -123,38 +138,32 @@ export default function AdminOrdersPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
           </div>
 
 
-          <div className="flex gap-2 w-full sm:w-auto">
-            {/* 刷新按钮:移动端平分宽度 */}
-            <button 
-              onClick={handleSearch} 
-              className="flex-1 sm:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition whitespace-nowrap"
-            >
-              <RefreshCw size={16} /> 
-              <span className="md:hidden xl:inline">刷新</span>
-            </button>
-
-            {/* 下单按钮:移动端平分宽度 */}
-            <button 
-              onClick={() => router.push('/admin/orders/new')} 
-              className="flex-[2] sm:flex-none flex items-center justify-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition whitespace-nowrap"
-            >
-              <Plus size={16} /> 
-              <span>代客下单</span>
-            </button>
-          </div>
+          <button 
+            onClick={handleSearch} 
+            className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-lg hover:bg-slate-50 text-slate-700 font-medium shadow-sm transition"
+          >
+            <RefreshCw size={16} /> <span className="hidden sm:inline">刷新</span>
+          </button>
+
+          <button 
+            onClick={() => router.push('/admin/orders/new')} 
+            className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg hover:bg-slate-800 font-medium shadow-sm transition"
+          >
+            <Plus size={16} /> <span className="hidden sm:inline">管理员下单</span>
+          </button>
         </div>
         </div>
       </div>
       </div>
       
       
-      {/* 订单列表表格 (内部已适配卡片视图) */}
+      {/* 5. 传递 onCheckPayments */}
       <OrderTable 
       <OrderTable 
         orders={orders} 
         orders={orders} 
         loading={loading} 
         loading={loading} 
         onCancel={handleCancelOrder} 
         onCancel={handleCancelOrder} 
         onViewDetail={handleViewDetail} 
         onViewDetail={handleViewDetail} 
-        onEdit={handleEditOrder} 
+        onEdit={handleEditOrder}
+        onCheckPayments={handleCheckPayments} // <--- 关键修复点
       />
       />
 
 
-      {/* 分页组件 */}
       <div className="mt-4">
       <div className="mt-4">
         <Pagination 
         <Pagination 
           currentPage={page}
           currentPage={page}
@@ -178,6 +187,14 @@ export default function AdminOrdersPage() {
         order={editingOrder}
         order={editingOrder}
         onSubmit={handleSubmitEdit}
         onSubmit={handleSubmitEdit}
       />
       />
+
+      {/* 6. 挂载支付管理弹窗 */}
+      <OrderPaymentModal 
+        isOpen={isPaymentModalOpen}
+        onClose={() => setIsPaymentModalOpen(false)}
+        order={selectedOrder}
+        onSuccess={handlePaymentSuccess}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 11 - 27
src/app/admin/payment-confirmations/page.tsx

@@ -61,14 +61,20 @@ export default function PaymentConfirmationsPage() {
     setIsDetailOpen(true);
     setIsDetailOpen(true);
   };
   };
 
 
+  // 确认收款
   const handleApprove = async (item: PaymentConfirmation) => {
   const handleApprove = async (item: PaymentConfirmation) => {
     const moneyStr = `${(item.amount / 100).toFixed(2)} ${item.currency}`;
     const moneyStr = `${(item.amount / 100).toFixed(2)} ${item.currency}`;
     if (!confirm(`确认已收到支付单 #${item.payment_id} 的款项 (${moneyStr}) 吗?`)) return;
     if (!confirm(`确认已收到支付单 #${item.payment_id} 的款项 (${moneyStr}) 吗?`)) return;
 
 
     try {
     try {
-      await api.post('/api/vas/payment_confirmation/approve', null, {
-        params: { id: item.id }
+      // 使用新的确认接口
+      await api.post('/api/vas/payment/confirm_by_admin', {
+        status: 'confirmed',
+        admin_confirmed_at: new Date().toISOString()
+      }, {
+        params: { id: item.id } // 这里的 id 是 confirmation record id
       });
       });
+      
       alert('已确认收款');
       alert('已确认收款');
       fetchData(page);
       fetchData(page);
     } catch (e: any) {
     } catch (e: any) {
@@ -76,25 +82,10 @@ export default function PaymentConfirmationsPage() {
     }
     }
   };
   };
 
 
-  const handleReject = async (item: PaymentConfirmation) => {
-    const reason = prompt("请输入驳回原因:");
-    if (reason === null) return;
-
-    try {
-      await api.post('/api/vas/payment_confirmation/reject', { reason }, {
-        params: { id: item.id }
-      });
-      alert('已驳回');
-      fetchData(page);
-    } catch (e: any) {
-      alert('操作失败: ' + (e.response?.data?.message || '未知错误'));
-    }
-  };
-
   return (
   return (
     <div className="p-4 md:p-6">
     <div className="p-4 md:p-6">
       
       
-      {/* === 头部区域:响应式布局 === */}
+      {/* Header */}
       <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
       <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
         <div>
         <div>
           <h1 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
           <h1 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
@@ -103,10 +94,7 @@ export default function PaymentConfirmationsPage() {
           <p className="text-sm text-slate-500 mt-1">处理用户提交的“我已付款”确认请求</p>
           <p className="text-sm text-slate-500 mt-1">处理用户提交的“我已付款”确认请求</p>
         </div>
         </div>
         
         
-        {/* 操作区:移动端垂直,桌面端水平 */}
         <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
         <div className="flex flex-col sm:flex-row gap-3 w-full md:w-auto">
-          
-          {/* 搜索框:移动端全宽 */}
           <div className="relative w-full sm:w-auto md:w-64">
           <div className="relative w-full sm:w-auto md:w-64">
             <input 
             <input 
               type="text" 
               type="text" 
@@ -119,7 +107,6 @@ export default function PaymentConfirmationsPage() {
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
             <Search size={16} className="absolute left-3 top-2.5 text-gray-400" />
           </div>
           </div>
           
           
-          {/* 刷新按钮:移动端拉伸 */}
           <button 
           <button 
             onClick={() => fetchData(page)} 
             onClick={() => fetchData(page)} 
             className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition w-full sm:w-auto"
             className="flex items-center justify-center gap-2 px-4 py-2 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 text-slate-600 transition w-full sm:w-auto"
@@ -131,16 +118,14 @@ export default function PaymentConfirmationsPage() {
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* 表格区域 (内部已适配卡片视图) */}
+      {/* Table */}
       <ConfirmationTable 
       <ConfirmationTable 
         data={list} 
         data={list} 
         loading={loading}
         loading={loading}
         onApprove={handleApprove}
         onApprove={handleApprove}
-        onReject={handleReject}
         onViewDetail={handleViewDetail} 
         onViewDetail={handleViewDetail} 
       />
       />
 
 
-      {/* 分页区域 */}
       <div className="mt-4">
       <div className="mt-4">
         <Pagination 
         <Pagination 
           currentPage={page} 
           currentPage={page} 
@@ -150,13 +135,12 @@ export default function PaymentConfirmationsPage() {
         />
         />
       </div>
       </div>
 
 
-      {/* 挂载详情弹窗 */}
+      {/* Modal */}
       <ConfirmationDetailModal 
       <ConfirmationDetailModal 
         isOpen={isDetailOpen}
         isOpen={isDetailOpen}
         onClose={() => setIsDetailOpen(false)}
         onClose={() => setIsDetailOpen(false)}
         data={selectedItem}
         data={selectedItem}
         onApprove={handleApprove}
         onApprove={handleApprove}
-        onReject={handleReject}
       />
       />
     </div>
     </div>
   );
   );

+ 99 - 28
src/app/dashboard/page.tsx

@@ -2,7 +2,7 @@
 
 
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
-import { LogOut, Plus, FileText, LifeBuoy, Settings } from 'lucide-react'; // 引入图标
+import { LogOut, Plus, FileText, LifeBuoy, Settings } from 'lucide-react';
 
 
 import Sidebar from '@/components/dashboard/Sidebar';
 import Sidebar from '@/components/dashboard/Sidebar';
 import OrderList from '@/components/dashboard/OrderList';
 import OrderList from '@/components/dashboard/OrderList';
@@ -14,13 +14,17 @@ import UserTicketDetailModal, { UserTicket } from '@/components/dashboard/UserTi
 import BindEmailModal from '@/components/BindEmailModal';
 import BindEmailModal from '@/components/BindEmailModal';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 
+// 1. 引入通用弹窗组件
+import ConfirmModal from '@/components/common/ConfirmModal';
+import MessageModal from '@/components/common/MessageModal';
+
 export default function DashboardPage() {
 export default function DashboardPage() {
   const router = useRouter();
   const router = useRouter();
   const { t } = useLanguage();
   const { t } = useLanguage();
   
   
   const [activeTab, setActiveTab] = useState<string>('orders');
   const [activeTab, setActiveTab] = useState<string>('orders');
   
   
-  // 状态管理
+  // --- 业务弹窗状态 ---
   const [isTicketModalOpen, setIsTicketModalOpen] = useState<boolean>(false);
   const [isTicketModalOpen, setIsTicketModalOpen] = useState<boolean>(false);
   const [ticketDefaultOrderId, setTicketDefaultOrderId] = useState<string>('');
   const [ticketDefaultOrderId, setTicketDefaultOrderId] = useState<string>('');
   const [isOrderDetailOpen, setIsOrderDetailOpen] = useState<boolean>(false);
   const [isOrderDetailOpen, setIsOrderDetailOpen] = useState<boolean>(false);
@@ -29,10 +33,39 @@ export default function DashboardPage() {
   const [selectedTicket, setSelectedTicket] = useState<UserTicket | null>(null);
   const [selectedTicket, setSelectedTicket] = useState<UserTicket | null>(null);
   const [refreshTickets, setRefreshTickets] = useState<number>(0);
   const [refreshTickets, setRefreshTickets] = useState<number>(0);
   
   
-  // 强制绑定
+  // 强制绑定邮箱状态
   const [isForceBindEmailOpen, setIsForceBindEmailOpen] = useState(false);
   const [isForceBindEmailOpen, setIsForceBindEmailOpen] = useState(false);
   const [isCheckingAuth, setIsCheckingAuth] = useState(true);
   const [isCheckingAuth, setIsCheckingAuth] = useState(true);
 
 
+  // --- 2. 新增:通用交互弹窗状态 ---
+  const [isLogoutConfirmOpen, setIsLogoutConfirmOpen] = useState(false); // 退出确认
+  const [msgModal, setMsgModal] = useState({ // 通用消息提示
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null, // 关闭后的回调
+  });
+
+  // 辅助函数:显示消息
+  const showMessage = (message: string, type: 'info'|'error'|'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: t('common.notice') || '提示',
+      message,
+      type,
+      onOk: onOk || null
+    });
+  };
+
+  // 处理消息弹窗关闭
+  const handleMessageClose = () => {
+    const callback = msgModal.onOk;
+    setMsgModal(prev => ({ ...prev, isOpen: false }));
+    if (callback) callback(); // 如果有回调,执行它(例如跳转首页)
+  };
+
+  // 初始化检查
   useEffect(() => {
   useEffect(() => {
     const checkUserStatus = () => {
     const checkUserStatus = () => {
       const token = localStorage.getItem('rsid');
       const token = localStorage.getItem('rsid');
@@ -60,18 +93,29 @@ export default function DashboardPage() {
     checkUserStatus();
     checkUserStatus();
   }, [router]);
   }, [router]);
 
 
-  const handleLogout = () => {
-    if (confirm(t('dashboard.logout_confirm'))) {
-      localStorage.removeItem('rsid');
-      localStorage.removeItem('user_info');
-      window.dispatchEvent(new Event('storage'));
-      router.push('/login');
-    }
+  // --- 事件处理 ---
+
+  // 点击退出按钮 -> 打开确认框
+  const handleLogoutClick = () => {
+    setIsLogoutConfirmOpen(true);
   };
   };
 
 
+  // 确认退出逻辑
+  const performLogout = () => {
+    localStorage.removeItem('rsid');
+    localStorage.removeItem('user_info');
+    window.dispatchEvent(new Event('storage'));
+    setIsLogoutConfirmOpen(false);
+    router.push('/login');
+  };
+
+  // 强制绑定 - 用户点击关闭 -> 提示并跳转
   const handleForceBindClose = () => {
   const handleForceBindClose = () => {
-    alert("为了保障账户安全,访问控制台前必须绑定邮箱。");
-    router.push('/'); 
+    showMessage(
+      "为了保障账户安全,访问控制台前必须绑定邮箱。", 
+      "error", 
+      () => router.push('/') // 关闭弹窗后跳转首页
+    );
   };
   };
 
 
   const handleForceBindSuccess = () => {
   const handleForceBindSuccess = () => {
@@ -96,10 +140,6 @@ export default function DashboardPage() {
 
 
   const handleTicketUpdate = () => setRefreshTickets(prev => prev + 1);
   const handleTicketUpdate = () => setRefreshTickets(prev => prev + 1);
 
 
-  if (isCheckingAuth) {
-    return <div className="min-h-screen bg-slate-50 flex items-center justify-center text-gray-400">Loading...</div>;
-  }
-
   // 移动端 Tab 定义
   // 移动端 Tab 定义
   const mobileTabs = [
   const mobileTabs = [
     { id: 'orders', label: t('sidebar.orders'), icon: FileText },
     { id: 'orders', label: t('sidebar.orders'), icon: FileText },
@@ -107,16 +147,21 @@ export default function DashboardPage() {
     { id: 'settings', label: t('sidebar.settings'), icon: Settings },
     { id: 'settings', label: t('sidebar.settings'), icon: Settings },
   ];
   ];
 
 
+  if (isCheckingAuth) {
+    return <div className="min-h-screen bg-slate-50 flex items-center justify-center text-gray-400">Loading...</div>;
+  }
+
   return (
   return (
-    <div className="min-h-screen bg-slate-50 pb-20 lg:pb-10"> {/* 移动端底部留空给 TabBar */}
+    <div className="min-h-screen bg-slate-50 pb-24 lg:pb-10">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-10">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 lg:py-10">
         
         
-        {/* === Header: 响应式布局 === */}
+        {/* Header */}
         <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 lg:mb-8">
         <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 lg:mb-8">
           <div>
           <div>
             <h1 className="text-2xl font-bold text-gray-900">{t('dashboard.title')}</h1>
             <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>
             <p className="text-sm text-gray-500 mt-1">{t('dashboard.subtitle')}</p>
           </div>
           </div>
+          
           <div className="flex gap-3 w-full sm:w-auto">
           <div className="flex gap-3 w-full sm:w-auto">
              <button 
              <button 
                onClick={() => router.push('/services')} 
                onClick={() => router.push('/services')} 
@@ -125,7 +170,7 @@ export default function DashboardPage() {
                <Plus size={16} /> {t('common.new_application')}
                <Plus size={16} /> {t('common.new_application')}
              </button>
              </button>
              <button 
              <button 
-               onClick={handleLogout} 
+               onClick={handleLogoutClick} // 修改:绑定打开弹窗函数
                className="flex-1 sm:flex-none flex justify-center items-center gap-2 bg-white border border-gray-300 px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition active:scale-95"
                className="flex-1 sm:flex-none flex justify-center items-center gap-2 bg-white border border-gray-300 px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-gray-50 text-gray-700 transition active:scale-95"
              >
              >
                <LogOut size={16} /> {t('nav.logout')}
                <LogOut size={16} /> {t('nav.logout')}
@@ -133,15 +178,15 @@ export default function DashboardPage() {
           </div>
           </div>
         </div>
         </div>
 
 
-        {/* === Main Content === */}
+        {/* Content Grid */}
         <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
         <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
           
           
-          {/* 左侧侧边栏 (仅在桌面端显示) */}
+          {/* Sidebar (Desktop) */}
           <div className="hidden lg:block lg:col-span-1">
           <div className="hidden lg:block lg:col-span-1">
             <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
             <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
           </div>
           </div>
 
 
-          {/* 右侧内容区 */}
+          {/* Main Area */}
           <div className="lg:col-span-3">
           <div className="lg:col-span-3">
             {activeTab === 'orders' && (
             {activeTab === 'orders' && (
               <div className="animate-in fade-in slide-in-from-right-4 duration-300">
               <div className="animate-in fade-in slide-in-from-right-4 duration-300">
@@ -156,7 +201,7 @@ export default function DashboardPage() {
                 <div className="flex justify-end">
                 <div className="flex justify-end">
                   <button 
                   <button 
                     onClick={() => openCreateTicketModal()}
                     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 w-full sm:w-auto justify-center"
+                    className="flex items-center justify-center gap-2 bg-slate-900 text-white px-4 py-2.5 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm w-full sm:w-auto active:scale-95"
                   >
                   >
                     <Plus size={16} /> {t('ticket.create_new')}
                     <Plus size={16} /> {t('ticket.create_new')}
                   </button>
                   </button>
@@ -173,8 +218,8 @@ export default function DashboardPage() {
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* === 移动端底部导航栏 (TabBar) === */}
-      <div className="fixed bottom-0 left-0 w-full bg-white border-t border-gray-200 lg:hidden z-40 pb-safe">
+      {/* Mobile TabBar */}
+      <div className="fixed bottom-0 left-0 w-full bg-white border-t border-gray-200 lg:hidden z-40 pb-[env(safe-area-inset-bottom)]">
         <div className="grid grid-cols-3 h-16">
         <div className="grid grid-cols-3 h-16">
           {mobileTabs.map((tab) => {
           {mobileTabs.map((tab) => {
             const Icon = tab.icon;
             const Icon = tab.icon;
@@ -184,9 +229,9 @@ export default function DashboardPage() {
                 key={tab.id}
                 key={tab.id}
                 onClick={() => {
                 onClick={() => {
                   setActiveTab(tab.id);
                   setActiveTab(tab.id);
-                  window.scrollTo({ top: 0, behavior: 'smooth' }); // 切换时回到顶部
+                  window.scrollTo({ top: 0, behavior: 'smooth' });
                 }}
                 }}
-                className={`flex flex-col items-center justify-center gap-1 transition ${
+                className={`flex flex-col items-center justify-center gap-1 transition active:bg-slate-50 ${
                   isActive ? 'text-blue-600' : 'text-gray-400 hover:text-gray-600'
                   isActive ? 'text-blue-600' : 'text-gray-400 hover:text-gray-600'
                 }`}
                 }`}
               >
               >
@@ -199,7 +244,33 @@ export default function DashboardPage() {
       </div>
       </div>
 
 
       {/* === 全局弹窗 === */}
       {/* === 全局弹窗 === */}
-      <BindEmailModal isOpen={isForceBindEmailOpen} onClose={handleForceBindClose} onSuccess={handleForceBindSuccess} />
+      
+      {/* 1. 强制绑定邮箱 */}
+      <BindEmailModal 
+        isOpen={isForceBindEmailOpen} 
+        onClose={handleForceBindClose} 
+        onSuccess={handleForceBindSuccess} 
+      />
+      
+      {/* 2. 退出登录确认框 (新增) */}
+      <ConfirmModal 
+        isOpen={isLogoutConfirmOpen}
+        title={t('nav.logout')}
+        message={t('dashboard.logout_confirm')}
+        onConfirm={performLogout}
+        onClose={() => setIsLogoutConfirmOpen(false)}
+      />
+
+      {/* 3. 通用消息提示框 (新增) */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleMessageClose}
+      />
+
+      {/* 4. 业务弹窗 */}
       <TicketModal isOpen={isTicketModalOpen} onClose={() => setIsTicketModalOpen(false)} defaultOrderId={ticketDefaultOrderId} />
       <TicketModal isOpen={isTicketModalOpen} onClose={() => setIsTicketModalOpen(false)} defaultOrderId={ticketDefaultOrderId} />
       <UserOrderDetailModal isOpen={isOrderDetailOpen} onClose={() => setIsOrderDetailOpen(false)} order={selectedOrder} />
       <UserOrderDetailModal isOpen={isOrderDetailOpen} onClose={() => setIsOrderDetailOpen(false)} order={selectedOrder} />
       <UserTicketDetailModal isOpen={isTicketDetailOpen} onClose={() => setIsTicketDetailOpen(false)} ticket={selectedTicket} onUpdate={handleTicketUpdate} />
       <UserTicketDetailModal isOpen={isTicketDetailOpen} onClose={() => setIsTicketDetailOpen(false)} ticket={selectedTicket} onUpdate={handleTicketUpdate} />

+ 46 - 12
src/components/AuthForm.tsx

@@ -5,7 +5,9 @@ import api from '@/lib/api';
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
 import ForgotPasswordModal from '@/components/ForgotPasswordModal';
 import ForgotPasswordModal from '@/components/ForgotPasswordModal';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
-import { Zap } from 'lucide-react'; // 引入一个图标增加视觉效果
+import { Zap } from 'lucide-react';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 export default function AuthForm() {
 export default function AuthForm() {
   const router = useRouter();
   const router = useRouter();
@@ -16,6 +18,33 @@ export default function AuthForm() {
   const [formData, setFormData] = useState({ email: '', password: '' });
   const [formData, setFormData] = useState({ email: '', password: '' });
   const [isForgotOpen, setIsForgotOpen] = useState(false);
   const [isForgotOpen, setIsForgotOpen] = useState(false);
 
 
+  // 2. 新增:控制消息弹窗的状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null, // 关闭弹窗后的回调函数
+  });
+
+  // 辅助函数:显示消息弹窗
+  const showMessage = (message: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  // 处理弹窗关闭
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     setLoading(true);
     setLoading(true);
@@ -24,11 +53,8 @@ export default function AuthForm() {
       let res;
       let res;
       
       
       if (isLoginMode) {
       if (isLoginMode) {
-        // === 登录逻辑 ===
         res = await api.post('/api/auth/login', formData);
         res = await api.post('/api/auth/login', formData);
       } else {
       } else {
-        // === 自动注册逻辑 ===
-        // 不需要传 email/password,通常传一个标识来源的字段即可
         res = await api.post('/api/auth/auto-register', {});
         res = await api.post('/api/auth/auto-register', {});
       }
       }
       
       
@@ -42,19 +68,20 @@ export default function AuthForm() {
         }
         }
         window.dispatchEvent(new Event('storage'));
         window.dispatchEvent(new Event('storage'));
         
         
-        // 如果是自动注册,给个提示
         if (!isLoginMode) {
         if (!isLoginMode) {
-          alert(t('auth.auto_register_success'));
+          showMessage(t('auth.auto_register_success'), 'success', () => {
+            router.push('/dashboard');
+          });
+        } else {
+          router.push('/dashboard');
         }
         }
-
-        router.push('/dashboard');
       } else {
       } else {
-        alert(t('auth.login_success_no_token'));
+        showMessage(t('auth.login_success_no_token'), 'error');
       }
       }
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('common.unknown_error');
       const msg = error.response?.data?.message || t('common.unknown_error');
-      alert(`${t('common.error')}: ${msg}`);
+      showMessage(msg, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -68,7 +95,6 @@ export default function AuthForm() {
       
       
       <form onSubmit={handleSubmit} className="space-y-5">
       <form onSubmit={handleSubmit} className="space-y-5">
         
         
-        {/* 只有在登录模式下才显示输入框 */}
         {isLoginMode ? (
         {isLoginMode ? (
           <>
           <>
             <div>
             <div>
@@ -102,7 +128,6 @@ export default function AuthForm() {
             </div>
             </div>
           </>
           </>
         ) : (
         ) : (
-          // 自动注册模式下的提示信息
           <div className="bg-blue-50 p-6 rounded-lg text-center border border-blue-100">
           <div className="bg-blue-50 p-6 rounded-lg text-center border border-blue-100">
             <div className="flex justify-center mb-3">
             <div className="flex justify-center mb-3">
               <div className="bg-blue-100 p-3 rounded-full text-blue-600">
               <div className="bg-blue-100 p-3 rounded-full text-blue-600">
@@ -139,6 +164,15 @@ export default function AuthForm() {
         isOpen={isForgotOpen} 
         isOpen={isForgotOpen} 
         onClose={() => setIsForgotOpen(false)} 
         onClose={() => setIsForgotOpen(false)} 
       />
       />
+
+      {/* 4. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 49 - 20
src/components/BindEmailModal.tsx

@@ -3,8 +3,9 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import { X, Mail, Loader2, Save, Lock, ArrowRight, ArrowLeft } from 'lucide-react';
 import { X, Mail, Loader2, Save, Lock, ArrowRight, ArrowLeft } from 'lucide-react';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 interface BindEmailModalProps {
 interface BindEmailModalProps {
   isOpen: boolean;
   isOpen: boolean;
@@ -13,7 +14,6 @@ interface BindEmailModalProps {
 }
 }
 
 
 export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmailModalProps) {
 export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmailModalProps) {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
   const { t } = useLanguage();
 
 
   const [step, setStep] = useState<1 | 2>(1); 
   const [step, setStep] = useState<1 | 2>(1); 
@@ -24,6 +24,31 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
   
   
   const [countdown, setCountdown] = useState(0);
   const [countdown, setCountdown] = useState(0);
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null,
+  });
+
+  const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message: msg,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal(prev => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   useEffect(() => {
   useEffect(() => {
     let timer: NodeJS.Timeout;
     let timer: NodeJS.Timeout;
     if (countdown > 0) {
     if (countdown > 0) {
@@ -41,20 +66,20 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
 
 
   const handleSendCode = async (e?: React.FormEvent) => {
   const handleSendCode = async (e?: React.FormEvent) => {
     if (e) e.preventDefault();
     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'));
+    if (!email) return showMessage(t('bind_email.alert_input_email'), 'error');
+    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return showMessage(t('bind_email.alert_invalid_email'), 'error');
     
     
     setLoading(true);
     setLoading(true);
     try {
     try {
       await api.post('/api/auth/send-bind-code', { email });
       await api.post('/api/auth/send-bind-code', { email });
       
       
-      alert(`${t('bind_email.alert_code_sent')} ${email}`);
+      showMessage(`${t('bind_email.alert_code_sent')} ${email}`, 'success');
       setStep(2);
       setStep(2);
       setCountdown(60); 
       setCountdown(60); 
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('common.unknown_error');
       const msg = error.response?.data?.message || t('common.unknown_error');
-      alert(`${t('bind_email.alert_send_failed')}: ${msg}`);
+      showMessage(`${t('bind_email.alert_send_failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -62,24 +87,19 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
 
 
   const handleVerifyAndBind = async (e: React.FormEvent) => {
   const handleVerifyAndBind = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    if (!code) return alert(t('bind_email.alert_input_code'));
-    if (code.length !== 6) return alert(t('bind_email.alert_code_length'));
+    if (!code) return showMessage(t('bind_email.alert_input_code'), 'error');
+    if (code.length !== 6) return showMessage(t('bind_email.alert_code_length'), 'error');
 
 
     setLoading(true);
     setLoading(true);
     try {
     try {
-      const res = await api.post('/api/auth/bind-email', { 
-        email, 
-        code 
-      });
+      const res = await api.post('/api/auth/bind-email', { email, code });
 
 
       const data = res.data.data || res.data;
       const data = res.data.data || res.data;
       const newToken = data.token || data.access_token;
       const newToken = data.token || data.access_token;
       const newUser = data.user;
       const newUser = data.user;
 
 
       if (newToken) {
       if (newToken) {
-        console.log("Bind success, updating token...");
         localStorage.setItem('rsid', newToken);
         localStorage.setItem('rsid', newToken);
-        
         if (newUser) {
         if (newUser) {
           localStorage.setItem('user_info', JSON.stringify(newUser));
           localStorage.setItem('user_info', JSON.stringify(newUser));
         } else {
         } else {
@@ -89,9 +109,11 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
 
 
         window.dispatchEvent(new Event('storage'));
         window.dispatchEvent(new Event('storage'));
         
         
-        alert(t('bind_email.success'));
-        onSuccess(); 
-        onClose();
+        // 绑定成功后提示,点击确认后关闭弹窗并执行 onSuccess
+        showMessage(t('bind_email.success'), 'success', () => {
+          onSuccess(); 
+          onClose();
+        });
       } else {
       } else {
         throw new Error("Token missing");
         throw new Error("Token missing");
       }
       }
@@ -99,7 +121,7 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('common.unknown_error');
       const msg = error.response?.data?.message || t('common.unknown_error');
-      alert(`${t('bind_email.failed')}: ${msg}`);
+      showMessage(`${t('bind_email.failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -130,7 +152,6 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
           </div>
           </div>
 
 
           {step === 1 ? (
           {step === 1 ? (
-            // === Step 1: 输入邮箱 ===
             <form onSubmit={handleSendCode} className="space-y-5">
             <form onSubmit={handleSendCode} className="space-y-5">
               <p className="text-sm text-gray-500 leading-relaxed">
               <p className="text-sm text-gray-500 leading-relaxed">
                 {t('bind_email.desc_step1')}
                 {t('bind_email.desc_step1')}
@@ -154,7 +175,6 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
               </button>
               </button>
             </form>
             </form>
           ) : (
           ) : (
-            // === Step 2: 输入验证码 ===
             <form onSubmit={handleVerifyAndBind} className="space-y-5">
             <form onSubmit={handleVerifyAndBind} className="space-y-5">
               <p className="text-sm text-gray-500">
               <p className="text-sm text-gray-500">
                 {t('bind_email.desc_step2_prefix')} <span className="font-bold text-gray-900">{email}</span>{t('bind_email.desc_step2_suffix')}
                 {t('bind_email.desc_step2_prefix')} <span className="font-bold text-gray-900">{email}</span>{t('bind_email.desc_step2_suffix')}
@@ -202,6 +222,15 @@ export default function BindEmailModal({ isOpen, onClose, onSuccess }: BindEmail
           )}
           )}
         </div>
         </div>
       </div>
       </div>
+
+      {/* 3. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 168 - 81
src/components/CreateOrderForm.tsx

@@ -6,6 +6,9 @@ import { useRouter } from 'next/navigation';
 import { Loader2, Info } from 'lucide-react';
 import { Loader2, Info } from 'lucide-react';
 import BindEmailModal from '@/components/BindEmailModal';
 import BindEmailModal from '@/components/BindEmailModal';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 引入通用弹窗组件
+import ConfirmModal from '@/components/common/ConfirmModal';
+import MessageModal from '@/components/common/MessageModal';
 
 
 // ==========================================
 // ==========================================
 // 类型定义
 // 类型定义
@@ -55,40 +58,58 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
   const router = useRouter();
   const router = useRouter();
   const { t } = useLanguage();
   const { t } = useLanguage();
   
   
-  // 状态管理
+  // --- 状态管理 ---
   const [loading, setLoading] = useState<boolean>(true);
   const [loading, setLoading] = useState<boolean>(true);
   const [submitting, setSubmitting] = useState<boolean>(false);
   const [submitting, setSubmitting] = useState<boolean>(false);
+  
+  // 数据状态
   const [product, setProduct] = useState<ProductDetail | null>(null);
   const [product, setProduct] = useState<ProductDetail | null>(null);
   const [formSchema, setFormSchema] = useState<JsonSchema | null>(null);
   const [formSchema, setFormSchema] = useState<JsonSchema | null>(null);
   const [formValues, setFormValues] = useState<Record<string, any>>({});
   const [formValues, setFormValues] = useState<Record<string, any>>({});
 
 
-  // 绑定邮箱弹窗控制
+  // 弹窗控制状态
   const [isBindEmailOpen, setIsBindEmailOpen] = useState(false);
   const [isBindEmailOpen, setIsBindEmailOpen] = useState(false);
+  const [showConfirmModal, setShowConfirmModal] = useState(false);
+  
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'error' as 'error' | 'info' | 'success'
+  });
+
+  // --- 辅助函数:显示消息 ---
+  const showMessage = (msg: string, title?: string, type: 'error' | 'info' = 'error') => {
+    setMsgModal({
+      isOpen: true,
+      message: msg,
+      title: title || t('common.notice'),
+      type
+    });
+  };
 
 
-  // 初始化数据
+  // --- 初始化数据 ---
   useEffect(() => {
   useEffect(() => {
     async function initData() {
     async function initData() {
       try {
       try {
         setLoading(true);
         setLoading(true);
         
         
         // 1. 获取商品详情
         // 1. 获取商品详情
-        // API: /api/vas/product/detail?product_id={id}
         const prodRes = await api.get('/api/vas/product/detail', {
         const prodRes = await api.get('/api/vas/product/detail', {
           params: { "product_id": productId }
           params: { "product_id": productId }
         });
         });
         const prodData = prodRes.data.data || prodRes.data;
         const prodData = prodRes.data.data || prodRes.data;
         setProduct(prodData);
         setProduct(prodData);
 
 
-        // 2. 获取动态表单定义 (如果有 schema_id)
+        // 2. 获取动态表单定义
         if (prodData.schema_id) {
         if (prodData.schema_id) {
           try {
           try {
-            // API: /api/vas/schema/detail?schema_id={id}
             const schemaRes = await api.get('/api/vas/schema/detail', {
             const schemaRes = await api.get('/api/vas/schema/detail', {
               params: { "schema_id": prodData.schema_id }
               params: { "schema_id": prodData.schema_id }
             });
             });
             const schemaData = schemaRes.data.data || schemaRes.data;
             const schemaData = schemaRes.data.data || schemaRes.data;
             
             
-            // 兼容处理:如果 schema_json 是字符串,需要 parse
+            // 兼容处理 JSON 字符串
             const schemaJson = typeof schemaData.schema_json === 'string' 
             const schemaJson = typeof schemaData.schema_json === 'string' 
               ? JSON.parse(schemaData.schema_json) 
               ? JSON.parse(schemaData.schema_json) 
               : schemaData.schema_json;
               : schemaData.schema_json;
@@ -100,7 +121,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
             if (schemaJson.properties) {
             if (schemaJson.properties) {
               Object.keys(schemaJson.properties).forEach(key => {
               Object.keys(schemaJson.properties).forEach(key => {
                 const prop = schemaJson.properties[key];
                 const prop = schemaJson.properties[key];
-                // 如果有默认值就使用,否则为空字符串
                 initialValues[key] = prop.default !== undefined ? prop.default : '';
                 initialValues[key] = prop.default !== undefined ? prop.default : '';
               });
               });
             }
             }
@@ -108,13 +128,12 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
 
 
           } catch (schemaErr) {
           } catch (schemaErr) {
             console.error("Failed to load schema", schemaErr);
             console.error("Failed to load schema", schemaErr);
-            // 依然允许渲染,只是没有动态表单
           }
           }
         }
         }
 
 
       } catch (error) {
       } catch (error) {
         console.error("Fetch product failed", error);
         console.error("Fetch product failed", error);
-        alert(t('order.load_product_failed'));
+        showMessage(t('order.load_product_failed'));
       } finally {
       } finally {
         setLoading(false);
         setLoading(false);
       }
       }
@@ -125,15 +144,17 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     }
     }
   }, [productId, t]);
   }, [productId, t]);
 
 
-  // 辅助函数:确保用户已登录(如果未登录,尝试自动注册)
-  const ensureUserLoggedIn = async (): Promise<boolean> => {
-    // 1. 如果已有 Token,直接通过
-    const token = localStorage.getItem('rsid');
-    if (token) return true;
+  // --- 辅助函数:用户状态检查 ---
+
+  // 确保用户已登录(如果未登录,尝试自动注册)
+  const ensureUserLoggedIn = async (forceNew = false): Promise<boolean> => {
+    if (!forceNew) {
+      const token = localStorage.getItem('rsid');
+      if (token) return true;
+    }
 
 
     try {
     try {
       console.log("Detecting no session, auto-registering...");
       console.log("Detecting no session, auto-registering...");
-      // 2. 调用自动注册
       const res = await api.post('/api/auth/auto-register', {
       const res = await api.post('/api/auth/auto-register', {
         register_ip: 'client-lazy-init'
         register_ip: 'client-lazy-init'
       });
       });
@@ -142,25 +163,23 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       const newToken = data.token || data.access_token;
       const newToken = data.token || data.access_token;
 
 
       if (newToken) {
       if (newToken) {
-        // 3. 保存登录状态
         localStorage.setItem('rsid', newToken);
         localStorage.setItem('rsid', newToken);
         if (data.user) {
         if (data.user) {
           localStorage.setItem('user_info', JSON.stringify(data.user));
           localStorage.setItem('user_info', JSON.stringify(data.user));
         }
         }
-        // 通知 Navbar 变化
         window.dispatchEvent(new Event('storage'));
         window.dispatchEvent(new Event('storage'));
         return true;
         return true;
       }
       }
       return false;
       return false;
     } catch (e) {
     } catch (e) {
       console.error("Auto register failed", e);
       console.error("Auto register failed", e);
-      alert(t('auth.login_success_no_token')); 
+      showMessage(t('auth.login_success_no_token')); 
       router.push('/login');
       router.push('/login');
       return false;
       return false;
     }
     }
   };
   };
 
 
-  // 辅助函数:检查用户是否已绑定邮箱
+  // 检查用户是否已绑定邮箱
   const checkUserEmail = (): boolean => {
   const checkUserEmail = (): boolean => {
     if (typeof window === 'undefined') return false;
     if (typeof window === 'undefined') return false;
     const userStr = localStorage.getItem('user_info');
     const userStr = localStorage.getItem('user_info');
@@ -168,7 +187,6 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     
     
     try {
     try {
       const user = JSON.parse(userStr);
       const user = JSON.parse(userStr);
-      // 判断逻辑:必须有 email 且包含 @ 符号
       if (user.email && user.email.includes('@')) {
       if (user.email && user.email.includes('@')) {
         return true; 
         return true; 
       }
       }
@@ -178,18 +196,33 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
     }
     }
   };
   };
 
 
-  // 处理输入变更
   const handleInputChange = (key: string, value: any) => {
   const handleInputChange = (key: string, value: any) => {
     setFormValues(prev => ({ ...prev, [key]: value }));
     setFormValues(prev => ({ ...prev, [key]: value }));
   };
   };
 
 
-  // 提交表单
+  // --- 提交逻辑 ---
+
+  // 封装 API 请求
+  const performOrderCreation = async () => {
+    const payload = {
+      product_id: parseInt(productId),
+      user_inputs: formValues
+    };
+
+    const res = await api.post('/api/vas/order/create', payload);
+    const orderId = res.data.data?.id || res.data.id;
+    
+    if (!orderId) throw new Error("Missing Order ID in response");
+    return orderId;
+  };
+
+  // 表单提交入口
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     setSubmitting(true);
     setSubmitting(true);
 
 
     try {
     try {
-      // 1. 确保已登录
+      // 1. 初步确保已登录
       const isLoggedIn = await ensureUserLoggedIn();
       const isLoggedIn = await ensureUserLoggedIn();
       if (!isLoggedIn) {
       if (!isLoggedIn) {
         setSubmitting(false);
         setSubmitting(false);
@@ -198,44 +231,81 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
 
 
       // 2. 检查邮箱
       // 2. 检查邮箱
       if (!checkUserEmail()) {
       if (!checkUserEmail()) {
-        setIsBindEmailOpen(true); // 打开绑定弹窗
-        setSubmitting(false); // 暂停提交
+        setIsBindEmailOpen(true);
+        setSubmitting(false);
         return;
         return;
       }
       }
 
 
-      // 3. 提交订单
-      const payload = {
-        product_id: parseInt(productId),
-        user_inputs: formValues // 动态表单数据
-      };
+      // 3. 弹出确认框 (不直接提交)
+      setShowConfirmModal(true);
+      // setSubmitting 状态会在确认框关闭或确认后处理
 
 
-      const res = await api.post('/api/vas/order/create', payload);
-      const orderId = res.data.data?.id || res.data.id;
-      
-      if (orderId) {
-        router.push(`/payment/${orderId}`);
-      } else {
-        throw new Error("Missing Order ID in response");
-      }
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
-      const msg = error.response?.data?.message || t('order.create_failed');
-      alert(msg);
       setSubmitting(false);
       setSubmitting(false);
     }
     }
   };
   };
 
 
-  // 绑定成功回调
+  // 确认后的实际提交 (含 Token 过期重试)
+  const handleConfirmSubmit = async () => {
+    setShowConfirmModal(false);
+    setSubmitting(true); // 重新设置 loading,因为关闭弹窗可能导致状态丢失
+
+    try {
+      const orderId = await performOrderCreation();
+      router.push(`/payment/${orderId}`);
+    } catch (firstError: any) {
+      const errorMsg = firstError.response?.data?.message || "";
+      const status = firstError.response?.status;
+
+      // 如果是 Token 失效
+      if (status === 401 || errorMsg.toLowerCase().includes('token') || errorMsg.toLowerCase().includes('expired')) {
+        console.log("Token expired, retrying...");
+        
+        // A. 强制获取新 Token
+        const newSessionSuccess = await ensureUserLoggedIn(true); 
+        
+        if (newSessionSuccess) {
+           // B. 新账号需绑定邮箱
+           if (!checkUserEmail()) {
+              setIsBindEmailOpen(true);
+              setSubmitting(false);
+              return;
+           }
+
+           // C. 重试提交
+           try {
+             const retryOrderId = await performOrderCreation();
+             router.push(`/payment/${retryOrderId}`);
+           } catch (retryErr: any) {
+             const msg = retryErr.response?.data?.message || t('order.create_failed');
+             showMessage(`${t('common.error')}: ${msg}`);
+             setSubmitting(false);
+           }
+        } else {
+           showMessage("Session refresh failed");
+           setSubmitting(false);
+        }
+      } else {
+        // 普通错误
+        const msg = firstError.response?.data?.message || t('order.create_failed');
+        showMessage(`${t('common.error')}: ${msg}`);
+        setSubmitting(false);
+      }
+    }
+  };
+
   const handleBindSuccess = () => {
   const handleBindSuccess = () => {
-    // 绑定成功后,保持在当前页面,用户可再次点击提交
+    // 绑定成功后,仅关闭弹窗,用户需再次点击提交
+    setIsBindEmailOpen(false);
   };
   };
 
 
-  // 渲染单个字段
   const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
   const renderField = (key: string, fieldSchema: SchemaProperty, required: boolean = false) => {
-    const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-sm";
+    // 公共样式
+    const commonClasses = "w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition text-base md:text-sm bg-white min-h-[46px]";
     const label = fieldSchema.title || key;
     const label = fieldSchema.title || key;
-    // 使用翻译后的 Placeholder
     const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
     const placeholderText = fieldSchema.description || `${t('common.enter')} ${label}`;
+    const currentValue = formValues[key] || '';
 
 
     // 1. 枚举类型 (Select)
     // 1. 枚举类型 (Select)
     if (fieldSchema.enum && fieldSchema.enum.length > 0) {
     if (fieldSchema.enum && fieldSchema.enum.length > 0) {
@@ -243,8 +313,8 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
         <select
         <select
           key={key}
           key={key}
           required={required}
           required={required}
-          className={commonClasses}
-          value={formValues[key] || ''}
+          className={`${commonClasses} appearance-none`}
+          value={currentValue}
           onChange={(e) => handleInputChange(key, e.target.value)}
           onChange={(e) => handleInputChange(key, e.target.value)}
         >
         >
           <option value="" disabled>{t('common.select')} {label}</option>
           <option value="" disabled>{t('common.select')} {label}</option>
@@ -255,30 +325,35 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
       );
       );
     }
     }
 
 
-    // 2. 日期/时间类型 (使用原生 input + 焦点切换技巧以显示 placeholder)
+    // 2. 日期/时间类型 (修复:聚焦时隐藏 Placeholder)
     if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
     if (fieldSchema.format === 'date' || fieldSchema.format === 'date-time') {
       const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
       const realType = fieldSchema.format === 'date' ? 'date' : 'datetime-local';
-      
       return (
       return (
-        <input
-          key={key}
-          type="text" // 默认为 text 以显示 placeholder
-          required={required}
-          className={commonClasses}
-          placeholder={placeholderText}
-          value={formValues[key] || ''}
-          onChange={(e) => handleInputChange(key, e.target.value)}
-          // 聚焦时切换为日期选择器
-          onFocus={(e) => (e.target.type = realType)} 
-          // 失焦且无值时切回文本
-          onBlur={(e) => {
-            if (!e.target.value) e.target.type = 'text'; 
-          }}
-        />
+        <div className="relative w-full">
+          <input
+            key={key}
+            type={realType}
+            required={required}
+            className={`
+              ${commonClasses}
+              peer 
+              ${!currentValue ? 'text-transparent focus:text-gray-900' : 'text-gray-900'} 
+            `}
+            style={{ lineHeight: 'normal' }}
+            value={currentValue}
+            onChange={(e) => handleInputChange(key, e.target.value)}
+          />
+          {/* 仅当无值时渲染,且聚焦时通过 CSS 隐藏 */}
+          {!currentValue && (
+            <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-base md:text-sm pointer-events-none truncate pr-8 w-full h-fit transition-opacity peer-focus:opacity-0">
+              {placeholderText}
+            </span>
+          )}
+        </div>
       );
       );
     }
     }
 
 
-    // 3. 其他 Input 类型推断
+    // 3. 其他 Input 类型
     let inputType = 'text';
     let inputType = 'text';
     if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
     if (fieldSchema.type === 'integer' || fieldSchema.type === 'number') inputType = 'number';
     if (fieldSchema.format === 'email') inputType = 'email';
     if (fieldSchema.format === 'email') inputType = 'email';
@@ -290,35 +365,31 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
         required={required}
         required={required}
         className={commonClasses}
         className={commonClasses}
         placeholder={placeholderText}
         placeholder={placeholderText}
-        value={formValues[key] || ''}
+        value={currentValue}
         onChange={(e) => handleInputChange(key, e.target.value)}
         onChange={(e) => handleInputChange(key, e.target.value)}
       />
       />
     );
     );
   };
   };
 
 
-  // 获取排序后的字段 Keys
   const getSortedKeys = () => {
   const getSortedKeys = () => {
     const properties = formSchema?.properties || {};
     const properties = formSchema?.properties || {};
     const keys = Object.keys(properties);
     const keys = Object.keys(properties);
 
 
-    // 优先使用 ui:order 数组排序
     if (Array.isArray(formSchema?.['ui:order'])) {
     if (Array.isArray(formSchema?.['ui:order'])) {
       return formSchema!['ui:order'];
       return formSchema!['ui:order'];
     }
     }
 
 
-    // 否则根据字段内的 order 或 x-order 属性排序
     return keys.sort((a, b) => {
     return keys.sort((a, b) => {
       const propA = properties[a];
       const propA = properties[a];
       const propB = properties[b];
       const propB = properties[b];
-      
       const orderA = propA.order ?? propA['x-order'] ?? 999;
       const orderA = propA.order ?? propA['x-order'] ?? 999;
       const orderB = propB.order ?? propB['x-order'] ?? 999;
       const orderB = propB.order ?? propB['x-order'] ?? 999;
-      
       return orderA - orderB;
       return orderA - orderB;
     });
     });
   };
   };
 
 
-  // Loading 状态
+  // --- Render ---
+
   if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
   if (loading) return <div className="p-12 flex justify-center"><Loader2 className="animate-spin text-blue-600"/></div>;
   if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
   if (!product) return <div className="p-8 text-center text-red-500">{t('order.product_not_found')}</div>;
 
 
@@ -328,31 +399,27 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
   const hasFields = sortedKeys.length > 0;
   const hasFields = sortedKeys.length > 0;
 
 
   return (
   return (
-    // 响应式内边距
     <div className="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
     <div className="bg-white p-5 md:p-8 rounded-xl shadow-sm border border-slate-200">
       
       
-      {/* 头部:商品信息 */}
+      {/* Header */}
       <div className="mb-6 md:mb-8 pb-4 md:pb-6 border-b border-gray-100">
       <div className="mb-6 md:mb-8 pb-4 md:pb-6 border-b border-gray-100">
-        {/* 移动端垂直布局,桌面端水平布局 */}
         <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2 md:gap-4">
         <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2 md:gap-4">
           <h1 className="text-xl md:text-2xl font-bold text-gray-900 leading-tight">
           <h1 className="text-xl md:text-2xl font-bold text-gray-900 leading-tight">
             {product.title || productName}
             {product.title || productName}
           </h1>
           </h1>
           <span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
           <span className="text-lg md:text-xl font-bold text-blue-600 flex-shrink-0">
-             {/* 金额显示:假设单位是分 */}
              {(product.price_amount / 100).toFixed(2)} {product.price_currency}
              {(product.price_amount / 100).toFixed(2)} {product.price_currency}
           </span>
           </span>
         </div>
         </div>
         <p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
         <p className="text-gray-500 mt-2 text-sm leading-relaxed">{product.description}</p>
         
         
-        {/* 提示信息:移动端全宽 */}
         <div className="mt-4 flex items-start gap-2 text-xs text-amber-700 bg-amber-50 px-3 py-2.5 rounded-lg w-full md:w-fit border border-amber-100">
         <div className="mt-4 flex items-start gap-2 text-xs text-amber-700 bg-amber-50 px-3 py-2.5 rounded-lg w-full md:w-fit border border-amber-100">
           <Info size={16} className="flex-shrink-0 mt-0.5" />
           <Info size={16} className="flex-shrink-0 mt-0.5" />
           <span className="leading-snug">{t('order.fill_form_hint')}</span>
           <span className="leading-snug">{t('order.fill_form_hint')}</span>
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* 动态表单区域 */}
+      {/* Form Fields */}
       <form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
       <form onSubmit={handleSubmit} className="space-y-5 md:space-y-6">
         {!hasFields && (
         {!hasFields && (
           <div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
           <div className="text-center py-8 text-gray-400 text-sm bg-slate-50 rounded-lg border border-dashed border-slate-200">
@@ -393,17 +460,37 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
             )}
             )}
           </button>
           </button>
           <p className="text-center text-xs text-gray-400 mt-3">
           <p className="text-center text-xs text-gray-400 mt-3">
-            点击提交即代表同意服务条款与隐私政策
+            {t('common.agree_agreement') || "点击提交即代表同意服务条款与隐私政策"}
           </p>
           </p>
         </div>
         </div>
       </form>
       </form>
 
 
-      {/* 绑定邮箱弹窗 */}
+      {/* === 全局弹窗挂载 === */}
+
       <BindEmailModal 
       <BindEmailModal 
         isOpen={isBindEmailOpen}
         isOpen={isBindEmailOpen}
         onClose={() => setIsBindEmailOpen(false)}
         onClose={() => setIsBindEmailOpen(false)}
         onSuccess={handleBindSuccess}
         onSuccess={handleBindSuccess}
       />
       />
+
+      <ConfirmModal 
+        isOpen={showConfirmModal}
+        title={t('common.confirm_title') || "请确认您的操作"}
+        message={t('order.confirm_submit_msg') || "请核对信息无误后,点击确认提交。"}
+        onConfirm={handleConfirmSubmit}
+        onClose={() => {
+            setShowConfirmModal(false);
+            setSubmitting(false); // 取消后恢复按钮
+        }}
+      />
+
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={() => setMsgModal(prev => ({ ...prev, isOpen: false }))}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 48 - 10
src/components/ForgotPasswordModal.tsx

@@ -3,8 +3,9 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import { X, Lock, Loader2, Save, Mail, ArrowRight, KeyRound, Eye, EyeOff } from 'lucide-react';
 import { X, Lock, Loader2, Save, Mail, ArrowRight, KeyRound, Eye, EyeOff } from 'lucide-react';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 interface ForgotPasswordModalProps {
 interface ForgotPasswordModalProps {
   isOpen: boolean;
   isOpen: boolean;
@@ -12,7 +13,6 @@ interface ForgotPasswordModalProps {
 }
 }
 
 
 export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordModalProps) {
 export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordModalProps) {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
   const { t } = useLanguage();
 
 
   const [step, setStep] = useState<1 | 2>(1); 
   const [step, setStep] = useState<1 | 2>(1); 
@@ -24,6 +24,33 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
   const [newPassword, setNewPassword] = useState('');
   const [newPassword, setNewPassword] = useState('');
   const [showPassword, setShowPassword] = useState(false);
   const [showPassword, setShowPassword] = useState(false);
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null,
+  });
+
+  // 辅助函数:显示消息
+  const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message: msg,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  // 处理消息关闭(如果有回调则执行)
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   useEffect(() => {
   useEffect(() => {
     let timer: NodeJS.Timeout;
     let timer: NodeJS.Timeout;
     if (countdown > 0) {
     if (countdown > 0) {
@@ -42,19 +69,19 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
 
 
   const handleSendCode = async (e: React.FormEvent) => {
   const handleSendCode = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    if (!email) return alert(t('forgot_password.enter_email_alert'));
+    if (!email) return showMessage(t('forgot_password.enter_email_alert'), 'error');
     
     
     setLoading(true);
     setLoading(true);
     try {
     try {
       await api.post('/api/auth/send-reset-code', { email });
       await api.post('/api/auth/send-reset-code', { email });
       
       
-      alert(`${t('forgot_password.code_sent_alert')} ${email}`);
+      showMessage(`${t('forgot_password.code_sent_alert')} ${email}`, 'success');
       setStep(2);
       setStep(2);
       setCountdown(60);
       setCountdown(60);
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('forgot_password.send_failed_default');
       const msg = error.response?.data?.message || t('forgot_password.send_failed_default');
-      alert(`${t('forgot_password.send_failed')}: ${msg}`);
+      showMessage(`${t('forgot_password.send_failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -62,8 +89,8 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
 
 
   const handleSubmitReset = async (e: React.FormEvent) => {
   const handleSubmitReset = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    if (!code) return alert(t('forgot_password.enter_code_alert'));
-    if (!newPassword) return alert(t('forgot_password.enter_password_alert'));
+    if (!code) return showMessage(t('forgot_password.enter_code_alert'), 'error');
+    if (!newPassword) return showMessage(t('forgot_password.enter_password_alert'), 'error');
 
 
     setLoading(true);
     setLoading(true);
     try {
     try {
@@ -73,12 +100,14 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
         new_password: newPassword 
         new_password: newPassword 
       });
       });
 
 
-      alert(t('forgot_password.reset_success'));
-      onClose();
+      // 成功后显示提示,点击“知道了”之后关闭找回密码弹窗
+      showMessage(t('forgot_password.reset_success'), 'success', () => {
+        onClose();
+      });
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('forgot_password.code_error');
       const msg = error.response?.data?.message || t('forgot_password.code_error');
-      alert(`${t('forgot_password.reset_failed')}: ${msg}`);
+      showMessage(`${t('forgot_password.reset_failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -196,6 +225,15 @@ export default function ForgotPasswordModal({ isOpen, onClose }: ForgotPasswordM
           )}
           )}
         </div>
         </div>
       </div>
       </div>
+
+      {/* 4. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 51 - 13
src/components/PaymentProcessor.tsx

@@ -6,6 +6,8 @@ import { useRouter } from 'next/navigation';
 import { Loader2, ArrowLeft, Sparkles, ExternalLink, Clock, ArrowRightLeft, CheckCircle2, AlertCircle } from 'lucide-react';
 import { Loader2, ArrowLeft, Sparkles, ExternalLink, Clock, ArrowRightLeft, CheckCircle2, AlertCircle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import LocalTime from '@/components/common/LocalTime';
 import LocalTime from '@/components/common/LocalTime';
+// 1. 引入消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 // ... 接口定义保持不变 ...
 // ... 接口定义保持不变 ...
 interface PaymentProcessorProps { orderId: string; }
 interface PaymentProcessorProps { orderId: string; }
@@ -24,6 +26,31 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
   const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
   const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
   const [qrCode, setQrCode] = useState<string>('');
   const [qrCode, setQrCode] = useState<string>('');
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null,
+  });
+
+  const showMessage = (message: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   useEffect(() => { fetchProviders(); }, []);
   useEffect(() => { fetchProviders(); }, []);
 
 
   const fetchProviders = async () => {
   const fetchProviders = async () => {
@@ -50,20 +77,20 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
 
       if (data.channel === 'online_link') {
       if (data.channel === 'online_link') {
         if (data.payment_url) setStep(2);
         if (data.payment_url) setStep(2);
-        else alert(t('payment.link_gen_failed'));
+        else showMessage(t('payment.link_gen_failed'), 'error');
       } else if (data.channel === 'qr_static') {
       } else if (data.channel === 'qr_static') {
         const qrRes = await api.get('/api/vas/payment_qr/qrcode', { params: { id: data.qr_id } });
         const qrRes = await api.get('/api/vas/payment_qr/qrcode', { params: { id: data.qr_id } });
         const qrData = qrRes.data.data || qrRes.data;
         const qrData = qrRes.data.data || qrRes.data;
         const qrUrl = qrData?.qr_code || qrData?.qrcode_url;
         const qrUrl = qrData?.qr_code || qrData?.qrcode_url;
         if (qrUrl) { setQrCode(qrUrl); setStep(2); } 
         if (qrUrl) { setQrCode(qrUrl); setStep(2); } 
-        else alert(t('payment.qr_gen_failed'));
+        else showMessage(t('payment.qr_gen_failed'), 'error');
       } else {
       } else {
-        alert(`${t('payment.unsupported_channel')}: ${data.channel}`);
+        showMessage(`${t('payment.unsupported_channel')}: ${data.channel}`, 'error');
       }
       }
     } catch (error: any) {
     } catch (error: any) {
       const errorMsg = error.response?.data?.message || error.response?.data?.detail || "";
       const errorMsg = error.response?.data?.message || error.response?.data?.detail || "";
-      if (errorMsg.includes("active payment")) alert(t('payment.active_payment_exists'));
-      else alert(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')));
+      if (errorMsg.includes("active payment")) showMessage(t('payment.active_payment_exists'), 'info');
+      else showMessage(`${t('payment.init_failed')}: ` + (errorMsg || t('common.unknown_error')), 'error');
     } finally { setLoading(false); }
     } finally { setLoading(false); }
   };
   };
 
 
@@ -79,11 +106,15 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
         confirmed_at: new Date().toISOString()
         confirmed_at: new Date().toISOString()
       };
       };
       await api.post('/api/vas/payment/confirm_by_user', payload);
       await api.post('/api/vas/payment/confirm_by_user', payload);
-      alert(t('payment.confirm_success_alert')); 
-      router.push('/dashboard');
+      
+      // 成功后弹窗,点击确定跳转 Dashboard
+      showMessage(t('payment.confirm_success_alert'), 'success', () => {
+        router.push('/dashboard');
+      });
+      
     } catch (error: any) {
     } catch (error: any) {
       const msg = error.response?.data?.message || t('common.unknown_error');
       const msg = error.response?.data?.message || t('common.unknown_error');
-      alert(`${t('payment.confirm_failed')}: ${msg}`);
+      showMessage(`${t('payment.confirm_failed')}: ${msg}`, 'error');
     } finally { setConfirming(false); }
     } finally { setConfirming(false); }
   };
   };
 
 
@@ -95,7 +126,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
 
 
   const formatMoney = (amount: number, currency: string) => `${(amount / 100).toFixed(2)} ${currency}`;
   const formatMoney = (amount: number, currency: string) => `${(amount / 100).toFixed(2)} ${currency}`;
 
 
-  // 动态时间格式化
+  // ... formatTime ...
   const formatTime = (isoString: string) => {
   const formatTime = (isoString: string) => {
     const date = new Date(isoString);
     const date = new Date(isoString);
     const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
     const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
@@ -103,10 +134,9 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
   };
   };
 
 
   return (
   return (
-    // 调整 1: 响应式 Padding
     <div className="bg-white p-6 md:p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
     <div className="bg-white p-6 md:p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
       
       
-      {/* === Step 1: Select Method === */}
+      {/* ... Step 1 (保持不变) ... */}
       {step === 1 && (
       {step === 1 && (
         <>
         <>
           <h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
           <h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
@@ -117,7 +147,6 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
           {providers.length === 0 ? (
           {providers.length === 0 ? (
              <div className="text-gray-400 py-4 text-sm">{t('common.loading')}</div>
              <div className="text-gray-400 py-4 text-sm">{t('common.loading')}</div>
           ) : (
           ) : (
-            // 调整 2: 移动端单列,桌面端双列
             <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
             <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
               {providers.map((p, idx) => (
               {providers.map((p, idx) => (
                 <button
                 <button
@@ -147,7 +176,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
         </>
         </>
       )}
       )}
 
 
-      {/* === Step 2: Payment Detail & Confirm === */}
+      {/* ... Step 2 (保持不变) ... */}
       {step === 2 && paymentData && (
       {step === 2 && paymentData && (
         <div className="animate-in fade-in zoom-in duration-300 text-left">
         <div className="animate-in fade-in zoom-in duration-300 text-left">
           
           
@@ -269,6 +298,15 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
           </div>
           </div>
         </div>
         </div>
       )}
       )}
+
+      {/* 3. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 6 - 11
src/components/admin/dashboard/OverviewCharts.tsx

@@ -2,7 +2,7 @@
 
 
 import { 
 import { 
   LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, 
   LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, 
-  PieChart, Pie, Cell, Legend 
+  PieChart, Pie, Cell 
 } from 'recharts';
 } from 'recharts';
 
 
 interface OverviewChartsProps {
 interface OverviewChartsProps {
@@ -75,8 +75,8 @@ export default function OverviewCharts({ revenueData, productData }: OverviewCha
                 data={productData}
                 data={productData}
                 cx="50%"
                 cx="50%"
                 cy="50%"
                 cy="50%"
-                innerRadius={60}
-                outerRadius={80}
+                innerRadius={70} // 稍微调大内径
+                outerRadius={100} // 调大外径以填充空间
                 paddingAngle={5}
                 paddingAngle={5}
                 dataKey="value"
                 dataKey="value"
               >
               >
@@ -84,19 +84,14 @@ export default function OverviewCharts({ revenueData, productData }: OverviewCha
                   <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                   <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                 ))}
                 ))}
               </Pie>
               </Pie>
+              {/* 移除了 Legend 组件,防止溢出 */}
               <Tooltip />
               <Tooltip />
-              <Legend 
-                verticalAlign="bottom" 
-                height={36} 
-                iconType="circle"
-                formatter={(value) => <span className="text-slate-600 text-xs ml-1">{value}</span>}
-              />
             </PieChart>
             </PieChart>
           </ResponsiveContainer>
           </ResponsiveContainer>
           
           
-          {/* 中间显示总数 (可选装饰) */}
+          {/* 中间显示文字装饰 */}
           <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none">
           <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none">
-            <p className="text-xs text-slate-400">Top Products</p>
+            <p className="text-xs text-slate-400 font-medium">Top Products</p>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>

+ 184 - 0
src/components/admin/orders/OrderPaymentModal.tsx

@@ -0,0 +1,184 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { X, CreditCard, CheckCircle, AlertCircle, Loader2, Ban } from 'lucide-react';
+import api from '@/lib/api';
+import LocalTime from '@/components/common/LocalTime';
+import { OrderDetail, PaymentRecord } from './OrderDetailModal'; // 复用类型
+
+interface OrderPaymentModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  order: OrderDetail | null;
+  onSuccess: () => void; // 操作成功后刷新列表
+}
+
+export default function OrderPaymentModal({ isOpen, onClose, order, onSuccess }: OrderPaymentModalProps) {
+  const [payments, setPayments] = useState<PaymentRecord[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [processingId, setProcessingId] = useState<number | null>(null);
+
+  useEffect(() => {
+    if (isOpen && order) {
+      fetchPayments();
+    }
+  }, [isOpen, order]);
+
+  const fetchPayments = async () => {
+    setLoading(true);
+    try {
+      // 获取该订单的所有支付记录
+      const res = await api.get('/api/vas/payment/list_by_order', {
+        params: { order_id: order?.id }
+      });
+      const list = Array.isArray(res.data.data) ? res.data.data : [];
+      setPayments(list);
+    } catch (error) {
+      console.error(error);
+      setPayments([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 管理员强制将某条支付记录标记为成功
+  const handleForceSuccess = async (payment: PaymentRecord) => {
+    const amountStr = `${(payment.amount / 100).toFixed(2)} ${payment.currency}`;
+    if (!confirm(`【高风险操作】\n\n您确定要将支付单 #${payment.id} (${payment.provider}) 强制标记为“成功”吗?\n\n金额: ${amountStr}\n\n请务必确认您的银行/支付后台确实收到了这笔款项。`)) {
+      return;
+    }
+
+    setProcessingId(payment.id);
+    try {
+      // API: POST /api/vas/payment/admin_update_status
+      // 您需要确保后端有这个接口,用于管理员强制修改支付单状态
+      await api.post('/api/vas/payment/admin_update_status', {
+        status: 'succeeded',
+        remark: 'Admin manually confirmed via Order Console'
+      }, {
+        params: { payment_id: payment.id }
+      });
+
+      alert("操作成功:支付单已更新,订单状态将同步变更。");
+      onSuccess(); // 刷新外部列表
+      onClose();   // 关闭弹窗
+    } catch (e: any) {
+      alert("操作失败: " + (e.response?.data?.message || "未知错误"));
+    } finally {
+      setProcessingId(null);
+    }
+  };
+
+  // 标记为失败/关闭
+  const handleForceFail = async (payment: PaymentRecord) => {
+    if (!confirm(`确定要废弃这条支付记录 #${payment.id} 吗?`)) return;
+    
+    setProcessingId(payment.id);
+    try {
+      await api.post('/api/vas/payment/admin_update_status', { status: 'failed' }, {
+        params: { payment_id: payment.id }
+      });
+      fetchPayments(); // 刷新内部列表
+    } catch (e) {
+      alert("操作失败");
+    } finally {
+      setProcessingId(null);
+    }
+  };
+
+  if (!isOpen || !order) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl overflow-hidden flex flex-col max-h-[80vh]">
+        
+        <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
+          <div>
+            <h3 className="font-bold text-gray-900 text-lg">支付记录管理</h3>
+            <p className="text-xs text-gray-500 mt-1">订单号: <span className="font-mono text-blue-600">{order.id}</span></p>
+          </div>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={24} /></button>
+        </div>
+
+        <div className="flex-1 overflow-y-auto p-6">
+          {loading ? (
+            <div className="text-center py-10 text-gray-400"><Loader2 className="animate-spin mx-auto mb-2"/>加载中...</div>
+          ) : payments.length === 0 ? (
+            <div className="text-center py-10 bg-slate-50 rounded-lg border border-dashed border-slate-200">
+              <AlertCircle className="mx-auto text-slate-300 mb-2" size={32} />
+              <p className="text-slate-500 text-sm">该用户尚未发起过任何支付尝试。</p>
+              <p className="text-xs text-slate-400 mt-1">如果是线下转账,请使用“代客下单”或手动创建支付单功能。</p>
+            </div>
+          ) : (
+            <table className="min-w-full text-sm text-left">
+              <thead className="bg-slate-50 border-b border-slate-200">
+                <tr>
+                  <th className="px-4 py-3 font-medium text-slate-500">ID / 时间</th>
+                  <th className="px-4 py-3 font-medium text-slate-500">渠道</th>
+                  <th className="px-4 py-3 font-medium text-slate-500">金额</th>
+                  <th className="px-4 py-3 font-medium text-slate-500">流水号</th>
+                  <th className="px-4 py-3 font-medium text-slate-500">状态</th>
+                  <th className="px-4 py-3 font-medium text-slate-500 text-right">管理操作</th>
+                </tr>
+              </thead>
+              <tbody className="divide-y divide-slate-100">
+                {payments.map((pay) => (
+                  <tr key={pay.id} className="hover:bg-slate-50 group">
+                    <td className="px-4 py-3">
+                      <div className="font-mono font-bold text-slate-700">#{pay.id}</div>
+                      <div className="text-xs text-gray-400 mt-0.5"><LocalTime date={pay.created_at}/></div>
+                    </td>
+                    <td className="px-4 py-3 uppercase text-xs font-bold text-slate-600">
+                      {pay.provider} <span className="font-normal text-gray-400">({pay.channel})</span>
+                    </td>
+                    <td className="px-4 py-3 font-mono font-bold">
+                      {(pay.amount / 100).toFixed(2)} {pay.currency}
+                    </td>
+                    <td className="px-4 py-3 font-mono text-xs text-slate-500 break-all max-w-[150px]">
+                      {pay.external_trade_no || '-'}
+                    </td>
+                    <td className="px-4 py-3">
+                      <span className={`px-2 py-0.5 rounded text-xs font-bold uppercase
+                        ${pay.status === 'succeeded' ? 'bg-green-100 text-green-700' : 
+                          pay.status === 'pending' ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-500'}
+                      `}>
+                        {pay.status}
+                      </span>
+                    </td>
+                    <td className="px-4 py-3 text-right">
+                      {pay.status === 'pending' && (
+                        <div className="flex justify-end gap-2">
+                           <button 
+                             onClick={() => handleForceFail(pay)}
+                             disabled={!!processingId}
+                             className="p-1.5 bg-red-50 text-red-600 rounded border border-red-100 hover:bg-red-100 transition"
+                             title="标记为失败/废弃"
+                           >
+                             <Ban size={14} />
+                           </button>
+                           <button 
+                             onClick={() => handleForceSuccess(pay)}
+                             disabled={!!processingId}
+                             className="flex items-center gap-1 px-3 py-1.5 bg-green-600 text-white rounded hover:bg-green-700 text-xs font-bold shadow-sm transition disabled:opacity-50"
+                           >
+                             {processingId === pay.id ? <Loader2 size={14} className="animate-spin"/> : <CheckCircle size={14} />}
+                             强制入账
+                           </button>
+                        </div>
+                      )}
+                    </td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          )}
+        </div>
+        
+        <div className="p-4 bg-slate-50 border-t text-xs text-slate-500 flex justify-between items-center">
+          <span>注意:强制入账将直接触发订单状态变更及后续机器人任务。</span>
+          <button onClick={onClose} className="px-4 py-2 bg-white border rounded text-slate-700 hover:bg-gray-50">关闭</button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 50 - 31
src/components/admin/orders/OrderTable.tsx

@@ -1,20 +1,26 @@
 'use client';
 'use client';
 
 
-import { Eye, XCircle, User, Box, Edit, Clock, FileText } from 'lucide-react';
+import { Eye, XCircle, User, Box, Edit, Clock, FileText, Wallet } from 'lucide-react';
 import { OrderDetail } from './OrderDetailModal';
 import { OrderDetail } from './OrderDetailModal';
-// 1. 引入 LocalTime 组件
 import LocalTime from '@/components/common/LocalTime';
 import LocalTime from '@/components/common/LocalTime';
 
 
-// 复用 OrderDetailModal 中的类型定义
 interface OrderTableProps {
 interface OrderTableProps {
   orders: OrderDetail[]; 
   orders: OrderDetail[]; 
   loading: boolean;
   loading: boolean;
   onCancel: (orderId: string) => void;
   onCancel: (orderId: string) => void;
   onViewDetail: (order: OrderDetail) => void;
   onViewDetail: (order: OrderDetail) => void;
   onEdit: (order: OrderDetail) => void;
   onEdit: (order: OrderDetail) => void;
+  onCheckPayments: (order: OrderDetail) => void; // 新增:查看/核销支付记录
 }
 }
 
 
-export default function OrderTable({ orders, loading, onCancel, onViewDetail, onEdit }: OrderTableProps) {
+export default function OrderTable({ 
+  orders, 
+  loading, 
+  onCancel, 
+  onViewDetail, 
+  onEdit, 
+  onCheckPayments 
+}: OrderTableProps) {
   
   
   if (loading) {
   if (loading) {
     return (
     return (
@@ -38,7 +44,7 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
       case 'paid': return 'bg-green-100 text-green-800 border-green-200';
       case 'paid': return 'bg-green-100 text-green-800 border-green-200';
       case 'succeeded': return 'bg-green-100 text-green-800 border-green-200';
       case 'succeeded': return 'bg-green-100 text-green-800 border-green-200';
       case 'pending': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
       case 'pending': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
-      case 'cancelled': return 'bg-red-50 text-red-600 border-red-100'; // 红色更明显
+      case 'cancelled': return 'bg-red-50 text-red-600 border-red-100';
       case 'failed': return 'bg-red-100 text-red-800 border-red-200';
       case 'failed': return 'bg-red-100 text-red-800 border-red-200';
       default: return 'bg-gray-100 text-gray-800 border-gray-200';
       default: return 'bg-gray-100 text-gray-800 border-gray-200';
     }
     }
@@ -51,7 +57,7 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
     <div className="space-y-4">
     <div className="space-y-4">
       
       
       {/* =========================== */}
       {/* =========================== */}
-      {/* 1. Desktop View (Table) - 仅在中大屏幕显示 */}
+      {/* 1. Desktop View (Table) */}
       {/* =========================== */}
       {/* =========================== */}
       <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
       <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
         <div className="overflow-x-auto">
         <div className="overflow-x-auto">
@@ -102,8 +108,21 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
                   </td>
                   </td>
                   <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                   <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                     <div className="flex justify-end gap-2">
                     <div className="flex justify-end gap-2">
+                      
+                      {/* 待支付状态显示人工核销按钮 */}
+                      {order.status === 'pending' && (
+                        <button 
+                          onClick={() => onCheckPayments(order)}
+                          className="group flex items-center justify-center p-1.5 rounded-md text-emerald-600 hover:text-emerald-900 bg-emerald-50 hover:bg-emerald-100 transition border border-transparent hover:border-emerald-200"
+                          title="查看/核销支付记录 (处理漏单)"
+                        >
+                          <Wallet size={16} />
+                        </button>
+                      )}
+
                       <button onClick={() => onEdit(order)} className="p-1.5 rounded-md text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-200" title="修改"><Edit size={16} /></button>
                       <button onClick={() => onEdit(order)} className="p-1.5 rounded-md text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-200" title="修改"><Edit size={16} /></button>
                       <button onClick={() => onViewDetail(order)} className="p-1.5 rounded-md text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-200" title="详情"><Eye size={16} /></button>
                       <button onClick={() => onViewDetail(order)} className="p-1.5 rounded-md text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-200" title="详情"><Eye size={16} /></button>
+                      
                       {canCancel(order.status) && (
                       {canCancel(order.status) && (
                         <button onClick={() => onCancel(order.id)} className="p-1.5 rounded-md text-red-600 hover:bg-red-50 border border-transparent hover:border-red-200" title="取消"><XCircle size={16} /></button>
                         <button onClick={() => onCancel(order.id)} className="p-1.5 rounded-md text-red-600 hover:bg-red-50 border border-transparent hover:border-red-200" title="取消"><XCircle size={16} /></button>
                       )}
                       )}
@@ -117,7 +136,7 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
       </div>
       </div>
 
 
       {/* =========================== */}
       {/* =========================== */}
-      {/* 2. Mobile View (Cards) - 仅在小屏幕显示 */}
+      {/* 2. Mobile View (Cards) */}
       {/* =========================== */}
       {/* =========================== */}
       <div className="md:hidden space-y-4">
       <div className="md:hidden space-y-4">
         {orders.map((order) => (
         {orders.map((order) => (
@@ -171,32 +190,32 @@ export default function OrderTable({ orders, loading, onCancel, onViewDetail, on
             </div>
             </div>
 
 
             {/* Actions */}
             {/* Actions */}
-            <div className="grid grid-cols-3 gap-2 mt-4 pt-3 border-t border-slate-100">
-              <button 
-                onClick={() => onEdit(order)}
-                className="flex items-center justify-center gap-1py-2 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-medium active:scale-95 transition"
-              >
-                <Edit size={16} /> 编辑
-              </button>
+            <div className="grid grid-cols-4 gap-2 mt-4 pt-3 border-t border-slate-100">
               
               
-              <button 
-                onClick={() => onViewDetail(order)}
-                className="flex items-center justify-center gap-1 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-medium active:scale-95 transition"
-              >
-                <Eye size={16} /> 详情
-              </button>
-
-              {canCancel(order.status) ? (
-                <button 
-                  onClick={() => onCancel(order.id)}
-                  className="flex items-center justify-center gap-1 py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium active:scale-95 transition"
-                >
-                  <XCircle size={16} /> 取消
-                </button>
+              {order.status === 'pending' ? (
+                <>
+                  <button onClick={() => onCheckPayments(order)} className="col-span-1 flex items-center justify-center py-2 bg-emerald-50 text-emerald-700 rounded-lg text-sm font-medium active:scale-95 transition border border-emerald-200">
+                    <Wallet size={16} />
+                  </button>
+                  <button onClick={() => onEdit(order)} className="col-span-1 flex items-center justify-center py-2 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-medium active:scale-95 transition border border-indigo-200">
+                    <Edit size={16} />
+                  </button>
+                  <button onClick={() => onViewDetail(order)} className="col-span-1 flex items-center justify-center py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-medium active:scale-95 transition border border-blue-200">
+                    <Eye size={16} />
+                  </button>
+                  <button onClick={() => onCancel(order.id)} className="col-span-1 flex items-center justify-center py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium active:scale-95 transition border border-red-200">
+                    <XCircle size={16} />
+                  </button>
+                </>
               ) : (
               ) : (
-                <button disabled className="flex items-center justify-center gap-1 py-2 bg-gray-50 text-gray-400 rounded-lg text-sm font-medium cursor-not-allowed">
-                  <XCircle size={16} /> 取消
-                </button>
+                <>
+                  <button onClick={() => onEdit(order)} className="col-span-2 flex items-center justify-center gap-1 py-2 bg-indigo-50 text-indigo-700 rounded-lg text-sm font-medium active:scale-95 transition">
+                    <Edit size={16} /> 编辑
+                  </button>
+                  <button onClick={() => onViewDetail(order)} className="col-span-2 flex items-center justify-center gap-1 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm font-medium active:scale-95 transition">
+                    <Eye size={16} /> 详情
+                  </button>
+                </>
               )}
               )}
             </div>
             </div>
 
 

+ 12 - 42
src/components/admin/payments/ConfirmationDetailModal.tsx

@@ -1,35 +1,20 @@
 'use client';
 'use client';
 
 
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
-import { X, User, CreditCard, ShoppingBag, Loader2, CheckCircle, XCircle, ArrowUpRight } from 'lucide-react';
+import { X, User, CreditCard, ShoppingBag, Loader2, CheckCircle } from 'lucide-react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import LocalTime from '@/components/common/LocalTime';
 import LocalTime from '@/components/common/LocalTime';
-
-export interface PaymentConfirmation {
-    id: number;              // 确认单 ID
-    payment_id: number;      // 关联的支付单 ID
-    user_id: string;         // 用户 ID
-    status: string;          // pending, approved, rejected
-    confirmed_at: string;    // 用户点击确认的时间 (ISO String)
-    created_at: string;      // 记录创建时间 (ISO String)
-    admin_id?: string | null;
-    admin_confirmed_at?: string | null;
-    
-    // === 新增字段 (用于对账) ===
-    amount: number;          // 用户确认的金额 (分)
-    currency: string;        // 货币 (如 CNY)
-    random_offset: number;   // 随机立减金额 (分)
-}
+import { PaymentConfirmation } from '@/types/payment';
 
 
 interface ConfirmationDetailModalProps {
 interface ConfirmationDetailModalProps {
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   data: PaymentConfirmation | null;
   data: PaymentConfirmation | null;
   onApprove: (item: PaymentConfirmation) => void;
   onApprove: (item: PaymentConfirmation) => void;
-  onReject: (item: PaymentConfirmation) => void;
+  // onReject: (item: PaymentConfirmation) => void; // 移除
 }
 }
 
 
-export default function ConfirmationDetailModal({ isOpen, onClose, data, onApprove, onReject }: ConfirmationDetailModalProps) {
+export default function ConfirmationDetailModal({ isOpen, onClose, data, onApprove }: ConfirmationDetailModalProps) {
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   
   
   // 详情数据状态
   // 详情数据状态
@@ -57,15 +42,12 @@ export default function ConfirmationDetailModal({ isOpen, onClose, data, onAppro
       setUserInfo(userRes.data.data);
       setUserInfo(userRes.data.data);
 
 
       // 2. 获取支付详情 (已有 payment_id)
       // 2. 获取支付详情 (已有 payment_id)
-      // 假设 API: GET /api/vas/payment/detail?payment_id=...
       const payRes = await api.get('/api/vas/payment/detail', { params: { payment_id: data.payment_id } });
       const payRes = await api.get('/api/vas/payment/detail', { params: { payment_id: data.payment_id } });
       const paymentData = payRes.data.data;
       const paymentData = payRes.data.data;
       setPaymentInfo(paymentData);
       setPaymentInfo(paymentData);
 
 
       // 3. 获取订单详情 (通过支付详情里的 order_id)
       // 3. 获取订单详情 (通过支付详情里的 order_id)
       if (paymentData && paymentData.order_id) {
       if (paymentData && paymentData.order_id) {
-        // 假设 API: GET /api/vas/order/detail?order_id=...
-        // 如果只有 list_all,可能需要 filter,这里假设有 detail 接口
         const orderRes = await api.get('/api/vas/order/detail', { params: { order_id: paymentData.order_id } });
         const orderRes = await api.get('/api/vas/order/detail', { params: { order_id: paymentData.order_id } });
         setOrderInfo(orderRes.data.data);
         setOrderInfo(orderRes.data.data);
       }
       }
@@ -239,28 +221,16 @@ export default function ConfirmationDetailModal({ isOpen, onClose, data, onAppro
 
 
         {/* Footer Actions */}
         {/* Footer Actions */}
         <div className="p-4 bg-white border-t flex justify-end gap-3">
         <div className="p-4 bg-white border-t flex justify-end gap-3">
-          <button 
-            onClick={onClose}
-            className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium"
-          >
-            关闭
-          </button>
+          <button onClick={onClose} className="px-4 py-2 border border-slate-300 rounded-lg text-slate-700 hover:bg-slate-50 text-sm font-medium">关闭</button>
           
           
           {data.status === 'pending' && (
           {data.status === 'pending' && (
-            <>
-              <button 
-                onClick={() => { onReject(data); onClose(); }}
-                className="px-4 py-2 bg-red-50 text-red-600 border border-red-200 rounded-lg hover:bg-red-100 text-sm font-medium flex items-center gap-2"
-              >
-                <XCircle size={16} /> 驳回
-              </button>
-              <button 
-                onClick={() => { onApprove(data); onClose(); }}
-                className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-bold flex items-center gap-2 shadow-sm"
-              >
-                <CheckCircle size={16} /> 确认已收款
-              </button>
-            </>
+            // 移除了驳回按钮
+            <button 
+              onClick={() => { onApprove(data); onClose(); }}
+              className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-bold flex items-center gap-2 shadow-sm"
+            >
+              <CheckCircle size={16} /> 确认已收款
+            </button>
           )}
           )}
         </div>
         </div>
 
 

+ 86 - 102
src/components/admin/payments/ConfirmationTable.tsx

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

+ 2 - 2
src/components/admin/payments/QrManager.tsx

@@ -104,7 +104,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleDelete = async (id: number) => {
   const handleDelete = async (id: number) => {
     if (!confirm("确定删除此二维码吗?")) return;
     if (!confirm("确定删除此二维码吗?")) return;
     try {
     try {
-      await api.delete(`/api/vas/payment_qr/${id}`);
+      await api.delete('/api/vas/payment_qr/delete', {params: {"id": id}});
       fetchQrs();
       fetchQrs();
     } catch (e) {
     } catch (e) {
       alert("删除失败");
       alert("删除失败");
@@ -114,7 +114,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
   const handleToggleActive = async (qr: PaymentQr) => {
   const handleToggleActive = async (qr: PaymentQr) => {
     setTogglingId(qr.id);
     setTogglingId(qr.id);
     try {
     try {
-      await api.put(`/api/vas/payment_qr/${qr.id}`, { is_active: !qr.is_active });
+      await api.post('/api/vas/payment_qr/set_enable', { is_active: !qr.is_active }, { params: {"id": qr.id}});
       fetchQrs();
       fetchQrs();
     } catch (e: any) {
     } catch (e: any) {
       alert("状态更新失败");
       alert("状态更新失败");

+ 1 - 1
src/components/admin/products/SchemaManager.tsx

@@ -2,7 +2,7 @@
 
 
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
-import { Loader2, Plus, Save, X, FileJson, List, Edit3 } from 'lucide-react';
+import { Loader2, Plus, Save, X, FileJson, List, Edit3, Trash2 } from 'lucide-react';
 import JsonEditor from '@/components/common/JsonEditor';
 import JsonEditor from '@/components/common/JsonEditor';
 
 
 interface SchemaManagerProps {
 interface SchemaManagerProps {

+ 113 - 83
src/components/admin/tasks/TaskTable.tsx

@@ -3,8 +3,9 @@
 import { useState } from 'react';
 import { useState } from 'react';
 import { 
 import { 
   RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson, 
   RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson, 
-  User, History, Edit, AlertCircle, FileText
+  User, History, Edit
 } from 'lucide-react';
 } from 'lucide-react';
+import LocalTime from '@/components/common/LocalTime';
 
 
 export interface VasTask {
 export interface VasTask {
   id: number;
   id: number;
@@ -17,8 +18,11 @@ export interface VasTask {
   notify_count: number;
   notify_count: number;
   config?: any;
   config?: any;
   user_inputs?: any;
   user_inputs?: any;
-  grabbed_history?: string[];
-  meta?: string[];
+  
+  // 两者结构一致:可能是 Object, Array 或 Null
+  grabbed_history?: any; 
+  meta?: any;
+
   created_at: string;
   created_at: string;
   updated_at: string;
   updated_at: string;
   expire_at: string;
   expire_at: string;
@@ -43,25 +47,41 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
     setExpandedRows(newSet);
     setExpandedRows(newSet);
   };
   };
 
 
-  const getLastItem = (arr: any) => {
-    if (!Array.isArray(arr) || arr.length === 0) return null;
-    const last = arr[arr.length - 1];
-    return typeof last === 'string' ? last : JSON.stringify(last);
-  };
-
   const getUserSummary = (inputs: any) => {
   const getUserSummary = (inputs: any) => {
     if (!inputs) return '-';
     if (!inputs) return '-';
-    const name = inputs.name || inputs.applicant_name || inputs.first_name || inputs.full_name;
-    const passport = inputs.passport || inputs.passport_no || inputs.passport_number;
-    if (name && passport) return `${name}`;
-    if (name) return name;
-    return 'User Data';
+    const name = inputs.last_name ? `${inputs.first_name} ${inputs.last_name}` : (inputs.name || inputs.applicant_name);
+    return name || 'User Data';
+  };
+
+  // === 通用摘要函数:用于表格预览 ===
+  const getComplexSummary = (data: any, fallbackText: string = "No info") => {
+    if (!data) return <span className="text-xs text-gray-300 italic">{fallbackText}</span>;
+
+    // 1. 如果是对象 (且不是数组)
+    if (typeof data === 'object' && !Array.isArray(data)) {
+      // 优先展示 slot_date (针对 history) 或 msg (针对 meta) 等关键字段
+      if (data.slot_date) return <div className="text-xs font-medium text-slate-700">Slot: {data.slot_date} {data.slot_time}</div>;
+      if (data.msg || data.message) return <div className="text-xs text-slate-600 truncate" title={data.msg}>{data.msg}</div>;
+      if (data.error) return <div className="text-xs text-red-600 truncate" title={data.error}>Error: {data.error}</div>;
+      
+      // 否则显示 JSON 字符串
+      return <div className="text-xs text-slate-500 truncate" title={JSON.stringify(data)}>{JSON.stringify(data)}</div>;
+    }
+
+    // 2. 如果是数组,显示最后一条
+    if (Array.isArray(data) && data.length > 0) {
+      const last = data[data.length - 1];
+      const text = typeof last === 'object' ? JSON.stringify(last) : String(last);
+      return <div className="text-xs text-slate-600 truncate" title={text}>{text}</div>;
+    }
+
+    return <span className="text-xs text-gray-300 italic">{fallbackText}</span>;
   };
   };
 
 
-  // 提取状态徽章渲染逻辑
   const renderStatus = (status: string) => (
   const renderStatus = (status: string) => (
     <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
     <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase whitespace-nowrap
       ${status === 'failed' ? 'bg-red-100 text-red-700' : 
       ${status === 'failed' ? 'bg-red-100 text-red-700' : 
+        status === 'grabbed' ? 'bg-purple-100 text-purple-700 animate-pulse' : 
         status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' : 
         status === 'running' ? 'bg-blue-100 text-blue-700 animate-pulse' : 
         status === 'completed' ? 'bg-green-100 text-green-700' :
         status === 'completed' ? 'bg-green-100 text-green-700' :
         'bg-gray-100 text-gray-600'}`}>
         'bg-gray-100 text-gray-600'}`}>
@@ -75,27 +95,23 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
   return (
   return (
     <div className="space-y-4">
     <div className="space-y-4">
       
       
-      {/* ======================= */}
-      {/* 1. Desktop View (Table) */}
-      {/* ======================= */}
+      {/* Desktop View */}
       <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
       <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
         <div className="overflow-x-auto">
         <div className="overflow-x-auto">
           <table className="min-w-full text-sm text-left table-fixed">
           <table className="min-w-full text-sm text-left table-fixed">
             <thead className="bg-slate-50 border-b border-slate-200">
             <thead className="bg-slate-50 border-b border-slate-200">
               <tr>
               <tr>
                 <th className="w-12 px-4 py-3"></th>
                 <th className="w-12 px-4 py-3"></th>
-                <th className="w-[160px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
-                <th className="w-[180px] px-4 py-3 font-medium text-slate-500">Order / User</th>
+                <th className="w-[180px] px-4 py-3 font-medium text-slate-500">ID / Route</th>
+                <th className="w-[200px] px-4 py-3 font-medium text-slate-500">Order / User</th>
                 <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
                 <th className="px-4 py-3 font-medium text-slate-500">业务进度 (History)</th>
                 <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
                 <th className="px-4 py-3 font-medium text-slate-500">系统调试 (Meta)</th>
-                <th className="w-[100px] px-4 py-3 font-medium text-slate-500">状态</th>
-                <th className="w-[140px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
+                <th className="w-[120px] px-4 py-3 font-medium text-slate-500">状态</th>
+                <th className="w-[150px] px-4 py-3 font-medium text-slate-500 text-right">操作</th>
               </tr>
               </tr>
             </thead>
             </thead>
             {tasks.map((task) => {
             {tasks.map((task) => {
               const isExpanded = expandedRows.has(task.id);
               const isExpanded = expandedRows.has(task.id);
-              const lastHistory = getLastItem(task.grabbed_history);
-              const lastMeta = getLastItem(task.meta);
               
               
               return (
               return (
                 <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
                 <tbody key={task.id} className="group hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0">
@@ -111,42 +127,37 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                     </td>
                     </td>
                     <td className="px-4 py-4 align-top">
                     <td className="px-4 py-4 align-top">
                       <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
                       <div className="text-slate-900 font-bold text-xs mb-1 break-all font-mono">{task.order_id}</div>
-                      <div className="text-xs text-slate-500 flex items-center gap-1 truncate" title={JSON.stringify(task.user_inputs)}>
+                      <div className="text-xs text-slate-500 flex items-center gap-1 truncate">
                         <User size={12} className="text-slate-400 flex-shrink-0" /> {getUserSummary(task.user_inputs)}
                         <User size={12} className="text-slate-400 flex-shrink-0" /> {getUserSummary(task.user_inputs)}
                       </div>
                       </div>
                     </td>
                     </td>
+                    
+                    {/* History Column */}
                     <td className="px-4 py-4 align-top">
                     <td className="px-4 py-4 align-top">
-                      {lastHistory ? (
-                        <div className="text-xs text-slate-700 font-medium break-words line-clamp-3" title={lastHistory}>
-                          <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mr-2 mb-0.5"></span>
-                          {lastHistory}
-                        </div>
-                      ) : <span className="text-xs text-gray-300 italic">No history</span>}
+                      {getComplexSummary(task.grabbed_history, 'No history')}
                     </td>
                     </td>
+
+                    {/* Meta Column */}
                     <td className="px-4 py-4 align-top">
                     <td className="px-4 py-4 align-top">
-                      {lastMeta ? (
-                        <div className="text-[10px] text-slate-600 font-mono break-all line-clamp-3 bg-slate-100 p-1.5 rounded border border-slate-200" title={lastMeta}>
-                          {lastMeta}
-                        </div>
-                      ) : <span className="text-xs text-gray-300 italic">No logs</span>}
-                      <div className="text-[10px] text-slate-300 mt-1 text-right">
-                        {new Date(task.updated_at).toLocaleTimeString()}
+                      {getComplexSummary(task.meta, 'No logs')}
+                      <div className="text-[10px] text-slate-400 mt-1">
+                         <LocalTime date={task.updated_at} options={{hour:'2-digit', minute:'2-digit', second:'2-digit'}} />
                       </div>
                       </div>
                     </td>
                     </td>
+
                     <td className="px-4 py-4 align-top">
                     <td className="px-4 py-4 align-top">
                       {renderStatus(task.status)}
                       {renderStatus(task.status)}
                       {task.attempt_count > 0 && <div className="text-[10px] text-slate-400 mt-1 pl-1">Retry: {task.attempt_count}</div>}
                       {task.attempt_count > 0 && <div className="text-[10px] text-slate-400 mt-1 pl-1">Retry: {task.attempt_count}</div>}
                     </td>
                     </td>
                     <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
                     <td className="px-4 py-4 text-right align-top" onClick={(e) => e.stopPropagation()}>
                       <div className="flex justify-end gap-1 flex-wrap">
                       <div className="flex justify-end gap-1 flex-wrap">
-                        <button onClick={() => onEdit(task)} className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-100"><Edit size={16} /></button>
-                        <button onClick={() => onRetry(task.id)} className="p-1.5 rounded text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-100"><RotateCcw size={16} /></button>
-                        <button onClick={() => onManualConfirm(task.id)} className="p-1.5 rounded text-green-600 hover:bg-green-50 border border-transparent hover:border-green-100"><CheckCircle size={16} /></button>
+                        <button onClick={() => onEdit(task)} className="p-1.5 rounded text-indigo-600 hover:bg-indigo-50 border border-transparent hover:border-indigo-100" title="编辑"><Edit size={16} /></button>
+                        <button onClick={() => onRetry(task.id)} className="p-1.5 rounded text-blue-600 hover:bg-blue-50 border border-transparent hover:border-blue-100" title="重置"><RotateCcw size={16} /></button>
+                        <button onClick={() => onManualConfirm(task.id)} className="p-1.5 rounded text-green-600 hover:bg-green-50 border border-transparent hover:border-green-100" title="完成"><CheckCircle size={16} /></button>
                       </div>
                       </div>
                     </td>
                     </td>
                   </tr>
                   </tr>
                   
                   
-                  {/* Expanded Detail (Desktop Only) */}
                   {isExpanded && (
                   {isExpanded && (
                     <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
                     <tr className="bg-slate-50 shadow-inner border-t border-slate-100">
                       <td colSpan={7} className="px-4 py-4">
                       <td colSpan={7} className="px-4 py-4">
@@ -161,17 +172,12 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* ======================= */}
-      {/* 2. Mobile View (Cards)  */}
-      {/* ======================= */}
+      {/* Mobile View */}
       <div className="md:hidden space-y-4">
       <div className="md:hidden space-y-4">
         {tasks.map((task) => {
         {tasks.map((task) => {
           const isExpanded = expandedRows.has(task.id);
           const isExpanded = expandedRows.has(task.id);
-          const lastHistory = getLastItem(task.grabbed_history);
-
           return (
           return (
             <div key={task.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
             <div key={task.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
-              {/* Header: ID, Status, Route */}
               <div className="flex justify-between items-start mb-3">
               <div className="flex justify-between items-start mb-3">
                 <div className="flex flex-col gap-1">
                 <div className="flex flex-col gap-1">
                   <div className="flex items-center gap-2">
                   <div className="flex items-center gap-2">
@@ -180,14 +186,12 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                   </div>
                   </div>
                   <span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit">{task.routing_key}</span>
                   <span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit">{task.routing_key}</span>
                 </div>
                 </div>
-                {/* Mobile Actions */}
                 <div className="flex gap-2">
                 <div className="flex gap-2">
                    <button onClick={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw size={18}/></button>
                    <button onClick={() => onRetry(task.id)} className="p-2 bg-blue-50 text-blue-600 rounded-lg active:scale-95"><RotateCcw size={18}/></button>
                    <button onClick={() => onManualConfirm(task.id)} className="p-2 bg-green-50 text-green-600 rounded-lg active:scale-95"><CheckCircle size={18}/></button>
                    <button onClick={() => onManualConfirm(task.id)} className="p-2 bg-green-50 text-green-600 rounded-lg active:scale-95"><CheckCircle size={18}/></button>
                 </div>
                 </div>
               </div>
               </div>
 
 
-              {/* Info Grid */}
               <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
               <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
                  <div className="flex justify-between">
                  <div className="flex justify-between">
                    <span className="text-xs text-slate-400">Order ID</span>
                    <span className="text-xs text-slate-400">Order ID</span>
@@ -197,13 +201,19 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                    <span className="text-xs text-slate-400">Applicant</span>
                    <span className="text-xs text-slate-400">Applicant</span>
                    <span className="font-medium truncate max-w-[150px]">{getUserSummary(task.user_inputs)}</span>
                    <span className="font-medium truncate max-w-[150px]">{getUserSummary(task.user_inputs)}</span>
                  </div>
                  </div>
-                 <div className="pt-2 border-t border-slate-200 mt-1">
-                   <p className="text-xs text-slate-400 mb-1">Latest History:</p>
-                   <p className="text-xs text-slate-700 line-clamp-2">{lastHistory || 'No history'}</p>
+                 {/* Mobile Preview for History/Meta */}
+                 <div className="pt-2 border-t border-slate-200 mt-1 grid grid-cols-1 gap-1">
+                   <div className="flex justify-between items-start text-xs">
+                      <span className="text-slate-400">History:</span>
+                      <div className="text-right flex-1 ml-2">{getComplexSummary(task.grabbed_history)}</div>
+                   </div>
+                   <div className="flex justify-between items-start text-xs">
+                      <span className="text-slate-400">Meta:</span>
+                      <div className="text-right flex-1 ml-2">{getComplexSummary(task.meta)}</div>
+                   </div>
                  </div>
                  </div>
               </div>
               </div>
 
 
-              {/* Expand Button */}
               <button 
               <button 
                 onClick={() => toggleRow(task.id)}
                 onClick={() => toggleRow(task.id)}
                 className="w-full flex items-center justify-center py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-xs font-medium active:bg-slate-50 transition"
                 className="w-full flex items-center justify-center py-2 bg-white border border-slate-200 text-slate-600 rounded-lg text-xs font-medium active:bg-slate-50 transition"
@@ -211,7 +221,6 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                 {isExpanded ? <>收起详情 <ChevronUp size={14} className="ml-1"/></> : <>查看日志 & 配置 <ChevronDown size={14} className="ml-1"/></>}
                 {isExpanded ? <>收起详情 <ChevronUp size={14} className="ml-1"/></> : <>查看日志 & 配置 <ChevronDown size={14} className="ml-1"/></>}
               </button>
               </button>
 
 
-              {/* Expanded Detail (Mobile) */}
               {isExpanded && (
               {isExpanded && (
                 <div className="mt-3 pt-3 border-t border-slate-100">
                 <div className="mt-3 pt-3 border-t border-slate-100">
                   <TaskDetailContent task={task} />
                   <TaskDetailContent task={task} />
@@ -230,12 +239,51 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
   );
   );
 }
 }
 
 
-// === 提取详情内容组件,供 Desktop 和 Mobile 复用 ===
+// === 核心修改:通用的数据可视化组件 (支持 Meta 和 History) ===
+function DataVisualizer({ data, emptyText, isDark = false }: { data: any, emptyText: string, isDark?: boolean }) {
+  if (!data) return <span className={`italic ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>{emptyText}</span>;
+
+  // 1. 如果是对象:渲染为 Key-Value 列表
+  if (typeof data === 'object' && !Array.isArray(data)) {
+    return (
+      <div className="space-y-1">
+          {Object.entries(data).map(([key, val]) => (
+            <div key={key} className={`flex flex-col border-b pb-1 last:border-0 ${isDark ? 'border-slate-800' : 'border-slate-50'}`}>
+              <span className={`text-[10px] uppercase font-bold ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>{key.replace(/_/g, ' ')}</span>
+              <span className={`break-all font-medium ${isDark ? 'text-slate-300' : 'text-slate-700'}`}>
+                {typeof val === 'object' ? JSON.stringify(val) : String(val)}
+              </span>
+            </div>
+          ))}
+      </div>
+    );
+  }
+
+  // 2. 如果是数组:渲染为列表
+  if (Array.isArray(data)) {
+    if (data.length === 0) return <span className={`italic ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>{emptyText}</span>;
+    return (
+      <div className="space-y-1">
+        {data.map((line, i) => (
+          <div key={i} className={`flex gap-2 border-b pb-1 last:border-0 ${isDark ? 'border-slate-800' : 'border-slate-50'}`}>
+            <span className={`select-none w-5 text-right ${isDark ? 'text-slate-600' : 'text-slate-300'}`}>{i + 1}</span>
+            <span className="break-all">{typeof line === 'object' ? JSON.stringify(line) : String(line)}</span>
+          </div>
+        ))}
+      </div>
+    );
+  }
+
+  // 3. 其他:直接渲染字符串
+  return <span className="break-all">{String(data)}</span>;
+}
+
+// === 详情内容组件 ===
 function TaskDetailContent({ task }: { task: VasTask }) {
 function TaskDetailContent({ task }: { task: VasTask }) {
   return (
   return (
     <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
     <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 text-xs">
       
       
-      {/* Config & Inputs */}
+      {/* Col 1: Config & Inputs */}
       <div className="space-y-4">
       <div className="space-y-4">
         <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
         <div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
           <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
           <div className="px-3 py-2 bg-slate-100 border-b border-slate-200 font-bold text-slate-600 flex items-center gap-2">
@@ -255,41 +303,23 @@ function TaskDetailContent({ task }: { task: VasTask }) {
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* History */}
+      {/* Col 2: History (Light Theme) */}
       <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
       <div className="bg-white border border-slate-200 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px]">
         <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
         <div className="px-3 py-2 bg-green-50 border-b border-green-100 font-bold text-green-800 flex items-center gap-2">
-          <History size={14} /> Grabbed History
+          <History size={14} /> Grabbed Info / History
         </div>
         </div>
-        <div className="p-3 overflow-y-auto flex-1 font-mono text-slate-700 space-y-2">
-          {Array.isArray(task.grabbed_history) && task.grabbed_history.length > 0 ? (
-            task.grabbed_history.map((line, i) => (
-              <div key={i} className="border-b border-slate-50 last:border-0 pb-1 flex gap-2">
-                <span className="text-slate-300 select-none w-5 text-right">{i+1}</span>
-                <span className="break-all">{line}</span>
-              </div>
-            ))
-          ) : (
-            <span className="text-slate-400 italic">暂无业务日志</span>
-          )}
+        <div className="p-3 overflow-y-auto flex-1 font-mono">
+          <DataVisualizer data={task.grabbed_history} emptyText="暂无业务日志" isDark={false} />
         </div>
         </div>
       </div>
       </div>
 
 
-      {/* Meta/Debug */}
+      {/* Col 3: Meta/Debug (Dark Theme) */}
       <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
       <div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden flex flex-col h-full max-h-[400px] text-slate-300">
         <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
         <div className="px-3 py-2 bg-slate-950 border-b border-slate-800 font-bold text-slate-100 flex items-center gap-2">
-          <Terminal size={14} /> Debug Logs
+          <Terminal size={14} /> Debug Logs (Meta)
         </div>
         </div>
-        <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px] space-y-1">
-          {Array.isArray(task.meta) && task.meta.length > 0 ? (
-            task.meta.map((line, i) => (
-              <div key={i} className="break-all border-b border-slate-800/30 pb-1 flex gap-2">
-                <span className="text-slate-600 mr-1">$</span>
-                <span>{line}</span>
-              </div>
-            ))
-          ) : (
-            <span className="text-slate-600 italic">暂无调试信息</span>
-          )}
+        <div className="p-3 overflow-y-auto flex-1 font-mono text-[11px]">
+           <DataVisualizer data={task.meta} emptyText="暂无调试信息" isDark={true} />
         </div>
         </div>
       </div>
       </div>
 
 

+ 54 - 0
src/components/common/ConfirmModal.tsx

@@ -0,0 +1,54 @@
+'use client';
+
+import { X } from 'lucide-react';
+
+interface ConfirmModalProps {
+  isOpen: boolean;
+  title: string;
+  message: string;
+  onConfirm: () => void;
+  onCancel: () => void;
+}
+
+export default function ConfirmModal({ isOpen, onClose, onConfirm, message, title }: ConfirmModalProps) {
+  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 flex flex-col 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">{title}</h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-100 transition">
+            <X size={24} />
+          </button>
+        </div>
+
+        {/* Body */}
+        <div className="p-6 flex-1 flex flex-col justify-center">
+          <p className="text-sm text-gray-600 mb-6 text-center">{message}</p>
+        </div>
+
+        {/* Footer Buttons */}
+        <div className="px-6 py-4 border-t bg-gray-50 flex justify-end gap-3">
+          <button 
+            onClick={onClose}
+            className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm font-medium transition"
+          >
+            取消
+          </button>
+          <button 
+            onClick={() => {
+              onConfirm(); // 调用确认回调
+              onClose();   // 关闭弹窗
+            }}
+            className="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-bold transition"
+          >
+            确认
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 53 - 0
src/components/common/MessageModal.tsx

@@ -0,0 +1,53 @@
+'use client';
+
+import { X, AlertCircle } from 'lucide-react';
+import { useLanguage } from '@/lib/i18n/LanguageContext';
+
+interface MessageModalProps {
+  isOpen: boolean;
+  title?: string;
+  message: string;
+  onClose: () => void;
+  type?: 'info' | 'error' | 'success'; // 可选:用于显示不同颜色的图标
+}
+
+export default function MessageModal({ isOpen, onClose, message, title, type = 'error' }: MessageModalProps) {
+  const { t } = useLanguage();
+  
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-[60] 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-sm flex flex-col overflow-hidden transform transition-all scale-100">
+        
+        {/* Header */}
+        <div className="px-5 py-4 border-b flex justify-between items-center bg-slate-50">
+          <h3 className={`font-bold text-lg flex items-center gap-2 ${type === 'error' ? 'text-red-600' : 'text-slate-800'}`}>
+            {type === 'error' && <AlertCircle size={20} />}
+            {title || t('common.notice')}
+          </h3>
+          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-100 transition">
+            <X size={20} />
+          </button>
+        </div>
+
+        {/* Body */}
+        <div className="p-6">
+          <p className="text-sm text-gray-600 leading-relaxed text-center">
+            {message}
+          </p>
+        </div>
+
+        {/* Footer */}
+        <div className="px-5 py-3 border-t bg-gray-50 flex justify-center">
+          <button 
+            onClick={onClose}
+            className="w-full bg-slate-900 text-white py-2.5 rounded-lg hover:bg-slate-800 text-sm font-bold transition shadow-sm active:scale-95"
+          >
+            {t('common.confirm_known') || '知道了'}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 54 - 16
src/components/dashboard/ChangePasswordModal.tsx

@@ -3,8 +3,9 @@
 import { useState, useEffect } from 'react';
 import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import { X, Lock, Loader2, Save, Mail, KeyRound, Eye, EyeOff } from 'lucide-react';
 import { X, Lock, Loader2, Save, Mail, KeyRound, Eye, EyeOff } from 'lucide-react';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 interface ChangePasswordModalProps {
 interface ChangePasswordModalProps {
   isOpen: boolean;
   isOpen: boolean;
@@ -12,13 +13,11 @@ interface ChangePasswordModalProps {
 }
 }
 
 
 export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
 export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
   const { t } = useLanguage();
 
 
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [countdown, setCountdown] = useState(0);
   const [countdown, setCountdown] = useState(0);
   
   
-  // 表单数据
   const [email, setEmail] = useState('');
   const [email, setEmail] = useState('');
   const [code, setCode] = useState('');
   const [code, setCode] = useState('');
   const [newPassword, setNewPassword] = useState('');
   const [newPassword, setNewPassword] = useState('');
@@ -26,6 +25,33 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
   
   
   const [showPassword, setShowPassword] = useState(false);
   const [showPassword, setShowPassword] = useState(false);
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null,
+  });
+
+  // 辅助函数:显示消息
+  const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'info', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message: msg,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  // 处理消息关闭(如果有回调则执行)
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   useEffect(() => {
   useEffect(() => {
     if (isOpen) {
     if (isOpen) {
       const userStr = localStorage.getItem('user_info');
       const userStr = localStorage.getItem('user_info');
@@ -50,24 +76,25 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
   }, [countdown]);
   }, [countdown]);
 
 
   const handleSendCode = async () => {
   const handleSendCode = async () => {
-    if (!email) return alert(t('settings.email_not_found'));
+    if (!email) return showMessage(t('settings.email_not_found'), 'error');
     
     
     try {
     try {
       await api.post('/api/auth/send-reset-code', { email });
       await api.post('/api/auth/send-reset-code', { email });
       
       
-      alert(`${t('settings.code_sent')} ${email}`);
+      showMessage(`${t('settings.code_sent')} ${email}`, 'success');
       setCountdown(60);
       setCountdown(60);
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
-      alert(`${t('settings.send_failed')}: ` + (error.response?.data?.message || t('common.unknown_error')));
+      const msg = error.response?.data?.message || t('common.unknown_error');
+      showMessage(`${t('settings.send_failed')}: ${msg}`, 'error');
     }
     }
   };
   };
 
 
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    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'));
+    if (!code) return showMessage(t('settings.enter_code'), 'error');
+    if (!newPassword) return showMessage(t('settings.enter_password'), 'error');
+    if (newPassword !== confirmPassword) return showMessage(t('settings.password_mismatch'), 'error');
 
 
     setLoading(true);
     setLoading(true);
     try {
     try {
@@ -77,16 +104,18 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
         new_password: newPassword 
         new_password: newPassword 
       });
       });
 
 
-      alert(t('settings.password_changed_success'));
-      
-      localStorage.removeItem('rsid');
-      localStorage.removeItem('user_info');
-      window.location.href = '/login';
+      // 成功后显示提示,点击“知道了”之后执行登出跳转
+      showMessage(t('settings.password_changed_success'), 'success', () => {
+        localStorage.removeItem('rsid');
+        localStorage.removeItem('user_info');
+        window.location.href = '/login';
+        onClose();
+      });
       
       
-      onClose();
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
-      alert(`${t('settings.change_failed')}: ` + (error.response?.data?.message || error.message));
+      const msg = error.response?.data?.message || error.message;
+      showMessage(`${t('settings.change_failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -196,6 +225,15 @@ export default function ChangePasswordModal({ isOpen, onClose }: ChangePasswordM
           </div>
           </div>
         </form>
         </form>
       </div>
       </div>
+
+      {/* 3. 挂载消息弹窗 (z-index 自动更高) */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 39 - 7
src/components/dashboard/ProfileSettings.tsx

@@ -5,11 +5,11 @@ import api from '@/lib/api';
 import { getCurrentUser } from '@/lib/auth';
 import { getCurrentUser } from '@/lib/auth';
 import { Loader2, Save, Camera, User, Mail, Shield, Lock, ChevronRight, Edit2, Phone } from 'lucide-react';
 import { Loader2, Save, Camera, User, Mail, Shield, Lock, ChevronRight, Edit2, Phone } from 'lucide-react';
 import ChangePasswordModal from '@/components/dashboard/ChangePasswordModal';
 import ChangePasswordModal from '@/components/dashboard/ChangePasswordModal';
-// 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 export default function ProfileSettings() {
 export default function ProfileSettings() {
-  // 2. 获取翻译函数
   const { t } = useLanguage();
   const { t } = useLanguage();
 
 
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
@@ -26,6 +26,28 @@ export default function ProfileSettings() {
   
   
   const fileInputRef = useRef<HTMLInputElement>(null);
   const fileInputRef = useRef<HTMLInputElement>(null);
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+  });
+
+  // 辅助函数:显示消息
+  const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'info') => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message: msg,
+      type,
+    });
+  };
+
+  const handleCloseMsg = () => {
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+  };
+
   useEffect(() => {
   useEffect(() => {
     const currentUser = getCurrentUser();
     const currentUser = getCurrentUser();
     if (currentUser) {
     if (currentUser) {
@@ -42,7 +64,7 @@ export default function ProfileSettings() {
     if (!file) return;
     if (!file) return;
     
     
     if (file.size > 2 * 1024 * 1024) {
     if (file.size > 2 * 1024 * 1024) {
-      alert(t('profile.image_size_limit')); // "图片大小不能超过 2MB"
+      showMessage(t('profile.image_size_limit'), 'error'); 
       return;
       return;
     }
     }
 
 
@@ -53,7 +75,7 @@ export default function ProfileSettings() {
 
 
   const uploadImage = async (file: File): Promise<string> => {
   const uploadImage = async (file: File): Promise<string> => {
     const formData = new FormData();
     const formData = new FormData();
-    formData.append('pdf', file); 
+    formData.append('file', file); 
 
 
     const res = await api.post('/api/resource/upload_file', formData, {
     const res = await api.post('/api/resource/upload_file', formData, {
       headers: { 'Content-Type': 'multipart/form-data' },
       headers: { 'Content-Type': 'multipart/form-data' },
@@ -79,7 +101,7 @@ export default function ProfileSettings() {
           finalAvatarUrl = await uploadImage(avatarFile);
           finalAvatarUrl = await uploadImage(avatarFile);
         } catch (uploadError) {
         } catch (uploadError) {
           console.error("Upload failed", uploadError);
           console.error("Upload failed", uploadError);
-          alert(t('profile.upload_failed')); // "头像上传失败..."
+          showMessage(t('profile.upload_failed'), 'error');
           setLoading(false);
           setLoading(false);
           return;
           return;
         }
         }
@@ -103,7 +125,8 @@ export default function ProfileSettings() {
       localStorage.setItem('user_info', JSON.stringify(newUserInfo));
       localStorage.setItem('user_info', JSON.stringify(newUserInfo));
       window.dispatchEvent(new Event('storage'));
       window.dispatchEvent(new Event('storage'));
 
 
-      alert(t('profile.update_success')); // "个人资料更新成功!"
+      showMessage(t('profile.update_success'), 'success');
+      
       setUser(newUserInfo);
       setUser(newUserInfo);
       setAvatarFile(null); 
       setAvatarFile(null); 
       setIsEditing(false);
       setIsEditing(false);
@@ -111,7 +134,7 @@ export default function ProfileSettings() {
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || error.message || t('common.unknown_error');
       const msg = error.response?.data?.message || error.message || t('common.unknown_error');
-      alert(`${t('profile.update_failed')}: ${msg}`);
+      showMessage(`${t('profile.update_failed')}: ${msg}`, 'error');
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
     }
     }
@@ -293,6 +316,15 @@ export default function ProfileSettings() {
         isOpen={isPasswordModalOpen} 
         isOpen={isPasswordModalOpen} 
         onClose={() => setIsPasswordModalOpen(false)} 
         onClose={() => setIsPasswordModalOpen(false)} 
       />
       />
+
+      {/* 4. 挂载消息弹窗 (z-index 自动比上面的 modal 高) */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 44 - 5
src/components/dashboard/TicketModal.tsx

@@ -4,6 +4,8 @@ import { useState, useEffect } from 'react';
 import api from '@/lib/api';
 import api from '@/lib/api';
 import { Loader2, X, AlertTriangle } from 'lucide-react';
 import { Loader2, X, AlertTriangle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+// 1. 引入通用消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 interface TicketModalProps {
 interface TicketModalProps {
   isOpen: boolean;
   isOpen: boolean;
@@ -24,6 +26,33 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
     reason: ''
     reason: ''
   });
   });
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+    onOk: null as (() => void) | null,
+  });
+
+  // 辅助函数:显示消息
+  const showMessage = (message: string, type: 'info' | 'error' | 'success', onOk?: () => void) => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message,
+      type,
+      onOk: onOk || null,
+    });
+  };
+
+  // 处理消息关闭
+  const handleCloseMsg = () => {
+    const callback = msgModal.onOk;
+    setMsgModal((prev) => ({ ...prev, isOpen: false }));
+    if (callback) callback();
+  };
+
   useEffect(() => {
   useEffect(() => {
     if (isOpen) {
     if (isOpen) {
       setForm({
       setForm({
@@ -43,15 +72,16 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
     try {
     try {
       await api.post('/api/vas/ticket/create', form);
       await api.post('/api/vas/ticket/create', form);
       
       
-      // === 新增:成功提示 ===
-      alert(t('ticket.create_success'));
-
-      if (onSuccess) onSuccess(); 
-      onClose();
+      // 成功后显示弹窗,点击确认后执行 onSuccess 和 onClose
+      showMessage(t('ticket.create_success'), 'success', () => {
+        if (onSuccess) onSuccess(); 
+        onClose();
+      });
       
       
     } catch (error: any) {
     } catch (error: any) {
       console.error(error);
       console.error(error);
       const msg = error.response?.data?.message || t('ticket.submit_error_default');
       const msg = error.response?.data?.message || t('ticket.submit_error_default');
+      // 错误依然保留在 Modal 内部显示(UX 更好),或者你也可以改为弹窗
       setErrorMsg(msg);
       setErrorMsg(msg);
     } finally {
     } finally {
       setLoading(false);
       setLoading(false);
@@ -145,6 +175,15 @@ export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId
           </form>
           </form>
         </div>
         </div>
       </div>
       </div>
+
+      {/* 4. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 34 - 10
src/components/dashboard/UserTicketDetailModal.tsx

@@ -5,6 +5,8 @@ import api from '@/lib/api';
 import { X, Send, User, Headset, Paperclip, Loader2, Clock, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
 import { X, Send, User, Headset, Paperclip, Loader2, Clock, CheckCircle, AlertCircle, XCircle } from 'lucide-react';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 import LocalTime from '@/components/common/LocalTime';
 import LocalTime from '@/components/common/LocalTime';
+// 1. 引入消息弹窗
+import MessageModal from '@/components/common/MessageModal';
 
 
 export interface UserTicket {
 export interface UserTicket {
   id: number;
   id: number;
@@ -40,6 +42,27 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
   
   
   const messagesEndRef = useRef<HTMLDivElement>(null);
   const messagesEndRef = useRef<HTMLDivElement>(null);
 
 
+  // 2. 消息弹窗状态
+  const [msgModal, setMsgModal] = useState({
+    isOpen: false,
+    title: '',
+    message: '',
+    type: 'info' as 'info' | 'error' | 'success',
+  });
+
+  const showMessage = (msg: string, type: 'info' | 'error' | 'success' = 'error') => {
+    setMsgModal({
+      isOpen: true,
+      title: type === 'error' ? t('common.error') : t('common.notice'),
+      message: msg,
+      type,
+    });
+  };
+
+  const handleCloseMsg = () => {
+    setMsgModal(prev => ({ ...prev, isOpen: false }));
+  };
+
   useEffect(() => {
   useEffect(() => {
     if (isOpen && ticket) {
     if (isOpen && ticket) {
       fetchMessages();
       fetchMessages();
@@ -91,7 +114,7 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
       fetchMessages();
       fetchMessages();
     } catch (error) {
     } catch (error) {
       console.error("Send message failed", error);
       console.error("Send message failed", error);
-      alert(t('ticket.send_failed'));
+      showMessage(t('ticket.send_failed'), 'error');
     } finally {
     } finally {
       setSending(false);
       setSending(false);
     }
     }
@@ -125,13 +148,10 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
   if (!isOpen || !ticket) return null;
   if (!isOpen || !ticket) return null;
 
 
   return (
   return (
-    // 修改 1: 移动端全屏容器 (p-0, h-full, w-full)
     <div className="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-6 bg-black/50 backdrop-blur-sm">
     <div className="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-6 bg-black/50 backdrop-blur-sm">
       
       
-      {/* 遮罩层 (点击关闭) */}
       <div className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity hidden sm:block" onClick={onClose} />
       <div className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity hidden sm:block" onClick={onClose} />
 
 
-      {/* 弹窗主体 */}
       <div className="relative w-full h-full sm:max-w-2xl sm:h-[80vh] bg-white sm:rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in duration-200">
       <div className="relative w-full h-full sm:max-w-2xl sm:h-[80vh] bg-white sm:rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in zoom-in duration-200">
         
         
         {/* Header */}
         {/* Header */}
@@ -155,7 +175,6 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
         {/* Messages List */}
         {/* Messages List */}
         <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6 overscroll-contain">
         <div className="flex-1 overflow-y-auto p-4 sm:p-6 bg-slate-50 space-y-6 overscroll-contain">
           
           
-          {/* 原始工单描述 */}
           <div className="flex justify-center">
           <div className="flex justify-center">
             <div className="bg-white border border-gray-200 text-gray-600 text-xs px-4 py-2 rounded-full shadow-sm flex items-center gap-1">
             <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')} 
               {t('ticket.created_at')} 
@@ -176,7 +195,6 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
             </div>
             </div>
           </div>
           </div>
 
 
-          {/* 会话记录 */}
           {loadingMsg ? (
           {loadingMsg ? (
             <div className="flex justify-center py-8"><Loader2 className="animate-spin text-gray-400" /></div>
             <div className="flex justify-center py-8"><Loader2 className="animate-spin text-gray-400" /></div>
           ) : (
           ) : (
@@ -199,14 +217,12 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
                 <div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
                 <div key={msg.id} className={`flex ${isMe ? 'justify-end' : 'justify-start'}`}>
                   <div className={`flex items-end gap-2 max-w-[90%] sm:max-w-[80%] ${isMe ? 'flex-row-reverse' : 'flex-row'}`}>
                   <div className={`flex items-end gap-2 max-w-[90%] sm:max-w-[80%] ${isMe ? 'flex-row-reverse' : 'flex-row'}`}>
                     
                     
-                    {/* 头像 */}
                     <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
                     <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
                       isMe ? 'bg-blue-100' : 'bg-purple-100'
                       isMe ? 'bg-blue-100' : 'bg-purple-100'
                     }`}>
                     }`}>
                       {isMe ? <User size={14} className="text-blue-600" /> : <Headset size={14} className="text-purple-600" />}
                       {isMe ? <User size={14} className="text-blue-600" /> : <Headset size={14} className="text-purple-600" />}
                     </div>
                     </div>
 
 
-                    {/* 气泡 */}
                     <div className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} min-w-0`}>
                     <div className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} min-w-0`}>
                       <div className={`px-4 py-2.5 text-sm shadow-sm break-words ${
                       <div className={`px-4 py-2.5 text-sm shadow-sm break-words ${
                         isMe 
                         isMe 
@@ -233,7 +249,7 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
           <div ref={messagesEndRef} />
           <div ref={messagesEndRef} />
         </div>
         </div>
 
 
-        {/* Input - 修改 2: 移动端底部适配 (pb-safe) */}
+        {/* Input */}
         <div className="p-3 sm:p-4 bg-white border-t border-gray-100 flex-shrink-0 safe-area-bottom">
         <div className="p-3 sm:p-4 bg-white border-t border-gray-100 flex-shrink-0 safe-area-bottom">
           <form onSubmit={handleSend} className="flex items-end gap-2">
           <form onSubmit={handleSend} className="flex items-end gap-2">
             <button type="button" className="p-2 sm:p-3 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-50 transition" title={t('ticket.upload_tooltip')}>
             <button type="button" className="p-2 sm:p-3 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-50 transition" title={t('ticket.upload_tooltip')}>
@@ -263,13 +279,21 @@ export default function UserTicketDetailModal({ isOpen, onClose, ticket }: UserT
               {sending ? <Loader2 className="animate-spin w-5 h-5" /> : <Send size={20} />}
               {sending ? <Loader2 className="animate-spin w-5 h-5" /> : <Send size={20} />}
             </button>
             </button>
           </form>
           </form>
-          {/* 移动端提示简化 */}
           <div className="text-center mt-2 hidden sm:block">
           <div className="text-center mt-2 hidden sm:block">
              <p className="text-[10px] text-gray-400">{t('ticket.urgent_tip')}</p>
              <p className="text-[10px] text-gray-400">{t('ticket.urgent_tip')}</p>
           </div>
           </div>
         </div>
         </div>
 
 
       </div>
       </div>
+
+      {/* 4. 挂载消息弹窗 */}
+      <MessageModal 
+        isOpen={msgModal.isOpen}
+        title={msgModal.title}
+        message={msgModal.message}
+        type={msgModal.type}
+        onClose={handleCloseMsg}
+      />
     </div>
     </div>
   );
   );
 }
 }

+ 42 - 14
src/lib/api.js

@@ -1,34 +1,62 @@
 import axios from 'axios';
 import axios from 'axios';
 
 
-// 留空,通过 next.config.js 转发
-const API_BASE_URL = ''; 
-
+// 1. 创建 axios 实例
 const api = axios.create({
 const api = axios.create({
-  baseURL: API_BASE_URL,
+  // 根据你的环境变量或硬编码地址
+  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://45.137.220.138:8888',
   headers: {
   headers: {
     'Content-Type': 'application/json',
     'Content-Type': 'application/json',
   },
   },
-  timeout: 15000,
 });
 });
 
 
-api.interceptors.request.use((config) => {
-  if (typeof window !== 'undefined') {
-    const token = localStorage.getItem('rsid');
-    if (token) {
-      config.headers.Authorization = `Bearer ${token}`;
+// 2. 请求拦截器:自动携带 Token
+api.interceptors.request.use(
+  (config) => {
+    // 仅在客户端执行时获取 token
+    if (typeof window !== 'undefined') {
+      const token = localStorage.getItem('rsid');
+      if (token) {
+        config.headers.Authorization = `Bearer ${token}`;
+      }
     }
     }
+    return config;
+  },
+  (error) => {
+    return Promise.reject(error);
   }
   }
-  return config;
-});
+);
 
 
+// 3. 响应拦截器:全局处理 401 自动跳转
 api.interceptors.response.use(
 api.interceptors.response.use(
-  (response) => response,
+  (response) => {
+    // 请求成功,直接返回
+    return response;
+  },
   (error) => {
   (error) => {
-    if (error.response?.status === 401) {
+    // 检查响应是否存在,且状态码是否为 401
+    if (error.response && error.response.status === 401) {
+      console.warn('Session expired or unauthorized (401). Cleaning up...');
+
+      // 确保在浏览器端执行
       if (typeof window !== 'undefined') {
       if (typeof window !== 'undefined') {
+        // A. 清除本地存储的 Token 和用户信息
         localStorage.removeItem('rsid');
         localStorage.removeItem('rsid');
+        localStorage.removeItem('user_info');
+
+        // B. 触发 storage 事件,通知 Navbar 等组件更新状态 (变为未登录)
+        window.dispatchEvent(new Event('storage'));
+
+        // C. 强制跳转到登录页
+        // 增加判断:如果当前已经在 /login 页面,就不需要跳转了,防止登录失败时死循环刷新
+        if (!window.location.pathname.startsWith('/login')) {
+          // 使用 window.location.href 强制跳转,确保页面状态彻底重置
+          // 也可以使用 router.push('/login'),但 axios 外部拿不到 router 实例,href 最通用
+          window.location.href = '/login';
+        }
       }
       }
     }
     }
+    
+    // 继续抛出错误,以便组件内具体的 try/catch 也能捕获(比如显示"用户名密码错误")
     return Promise.reject(error);
     return Promise.reject(error);
   }
   }
 );
 );

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

@@ -29,6 +29,11 @@ export const en = {
     processing: 'Processing...',
     processing: 'Processing...',
     select: 'Select',
     select: 'Select',
     enter: 'Enter',
     enter: 'Enter',
+    notice: 'Notice',
+    confirm_known: 'Got it',
+    confirm_title: 'Confirm Action',
+    agree_agreement: "By clicking submit, you agree to the Terms of Service and Privacy Policy"
+
   },
   },
   auth: {
   auth: {
     welcome_back: 'Welcome Back',
     welcome_back: 'Welcome Back',
@@ -162,6 +167,7 @@ export const en = {
     fill_form_hint: 'Please fill in the information carefully, as it will be used for your application.',
     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.',
     no_extra_info_needed: 'No extra information needed, please proceed.',
     submit_and_pay: 'Submit & Pay',
     submit_and_pay: 'Submit & Pay',
+    confirm_submit_msg: 'Please verify your information. It cannot be easily changed after submission.',
   },
   },
   status: {
   status: {
     pending: 'Pending',
     pending: 'Pending',

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

@@ -29,6 +29,10 @@ export const zh = {
     processing: '处理中...',
     processing: '处理中...',
     select: '请选择',
     select: '请选择',
     enter: '请输入',
     enter: '请输入',
+    notice: '提示',
+    confirm_known: '知道了',
+    confirm_title: '确认操作',
+    agree_agreement: '点击提交即代表同意服务条款与隐私政策'
   },
   },
   auth: {
   auth: {
     welcome_back: '欢迎回来',
     welcome_back: '欢迎回来',
@@ -162,6 +166,7 @@ export const zh = {
     fill_form_hint: '请仔细填写以下申请信息,这将直接用于您的签证申请。',
     fill_form_hint: '请仔细填写以下申请信息,这将直接用于您的签证申请。',
     no_extra_info_needed: '无需填写额外信息,请直接提交。',
     no_extra_info_needed: '无需填写额外信息,请直接提交。',
     submit_and_pay: '提交订单并支付',
     submit_and_pay: '提交订单并支付',
+    confirm_submit_msg: '请核对填写的信息是否准确,提交后将无法直接修改。',
   },
   },
   status: {
   status: {
     pending: '待支付',
     pending: '待支付',