jerry před 3 týdny
rodič
revize
6de6e63611

+ 120 - 12
src/app/admin/accounts/page.tsx

@@ -1,7 +1,7 @@
 'use client';
 
 import { useState, useEffect } from 'react';
-import { Plus, Search, RefreshCw, X, Lock } from 'lucide-react';
+import { Plus, Search, RefreshCw, X, Lock, User, Pencil } from 'lucide-react';
 import api from '@/lib/api';
 import { toast } from 'react-hot-toast';
 
@@ -23,6 +23,8 @@ export default function AdminAccountsPage() {
   // 模态框状态
   const [showAddModal, setShowAddModal] = useState(false);
   const [showLockModal, setShowLockModal] = useState(false);
+  const [showRegistrationModal, setShowRegistrationModal] = useState(false);
+  const [showEditModal, setShowEditModal] = useState(false);
   const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
 
   // 表单状态
@@ -33,6 +35,10 @@ export default function AdminAccountsPage() {
     extra_data_str: '{}'
   });
   const [lockDuration, setLockDuration] = useState<number>(3600);
+  const [editForm, setEditForm] = useState({
+    password: '',
+    extra_data_str: '{}'
+  });
 
   // === 2. 数据获取 ===
   const fetchAccounts = async (targetPage: number = page) => {
@@ -110,19 +116,50 @@ export default function AdminAccountsPage() {
     }
   };
 
-  const handleLockConfirm = async () => {
+  const handleEditClick = (account: Account) => {
+    setSelectedAccount(account);
+    setEditForm({
+      password: account.password || '',
+      extra_data_str: JSON.stringify(account.extra_data || {}, null, 2)
+    });
+    setShowEditModal(true);
+  };
+
+  const handleEditAccount = async () => {
     if (!selectedAccount) return;
     try {
-      await api.post('/api/account/lock', {
+      let extraDataJson = {};
+      try {
+        extraDataJson = JSON.parse(editForm.extra_data_str);
+      } catch (e) {
+        toast.error('Extra Data 必须是 JSON 格式');
+        return;
+      }
+      await api.post('/api/account/add', {
         pool_name: selectedAccount.pool_name,
         username: selectedAccount.username,
-        duration: Number(lockDuration)
+        password: editForm.password,
+        extra_data: extraDataJson
       });
-      toast.success('锁定成功');
-      setShowLockModal(false);
-      fetchAccounts(page); // 刷新当前页
+      toast.success('修改成功');
+      setShowEditModal(false);
+      fetchAccounts(page);
     } catch (error) {
-      toast.error('锁定失败');
+      toast.error('修改失败');
+    }
+  };
+
+  const handleLockClick = async (account: Account, unlock: boolean) => {
+    try {
+      await api.post('/api/account/lock', {
+        pool_name: account.pool_name,
+        username: account.username,
+        duration: unlock ? 0 : Number(lockDuration)
+      });
+      toast.success(unlock ? '解锁成功' : '锁定成功');
+      fetchAccounts(page);
+    } catch (error) {
+      toast.error(unlock ? '解锁失败' : '锁定失败');
     }
   };
 
@@ -190,6 +227,8 @@ export default function AdminAccountsPage() {
         loading={loading}
         onLock={(acc) => { setSelectedAccount(acc); setShowLockModal(true); }}
         onDisable={handleDisable}
+        onViewRegistration={(acc) => { setSelectedAccount(acc); setShowRegistrationModal(true); }}
+        onEdit={handleEditClick}
       />
 
       {/* === 分页组件 === */}
@@ -239,7 +278,7 @@ export default function AdminAccountsPage() {
         </div>
       )}
 
-      {/* 锁定 Modal */}
+{/* 锁定 Modal */}
       {showLockModal && selectedAccount && (
         <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in zoom-in-95 duration-200">
           <div className="bg-white rounded-xl shadow-2xl w-full max-w-sm overflow-hidden">
@@ -253,7 +292,7 @@ export default function AdminAccountsPage() {
             </div>
             <div className="p-6 space-y-4">
               <p className="text-sm text-slate-600 bg-orange-50 p-3 rounded-lg border border-orange-100">
-                正在锁定: <span className="font-bold text-slate-900">{selectedAccount.username}</span>
+                账号: <span className="font-bold text-slate-900">{selectedAccount.username}</span>
               </p>
               <div>
                 <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">持续时间 (秒)</label>
@@ -264,9 +303,78 @@ export default function AdminAccountsPage() {
                 </div>
               </div>
             </div>
+            <div className="px-6 py-4 bg-slate-50 flex justify-between gap-3 border-t">
+              <button onClick={async () => { await handleLockClick(selectedAccount, true); setShowLockModal(false); }} className="px-4 py-2 text-sm text-green-600 hover:bg-green-100 rounded-lg">
+                直接解锁
+              </button>
+              <div className="flex gap-3 ml-auto">
+                <button onClick={() => setShowLockModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
+                <button onClick={async () => { await handleLockClick(selectedAccount, false); setShowLockModal(false); }} className="px-4 py-2 text-sm font-bold bg-orange-600 text-white rounded-lg hover:bg-orange-700">确认锁定</button>
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 注册信息 Modal */}
+      {showRegistrationModal && selectedAccount && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in zoom-in-95 duration-200">
+          <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg overflow-hidden">
+             <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-blue-50/50">
+              <h3 className="font-bold text-lg text-blue-900 flex items-center gap-2">
+                <User size={18} /> 注册信息
+              </h3>
+              <button onClick={() => setShowRegistrationModal(false)} className="text-slate-400 hover:text-slate-600">
+                <X size={20} />
+              </button>
+            </div>
+            <div className="p-6 space-y-4">
+              <p className="text-sm text-slate-600 bg-blue-50 p-3 rounded-lg border border-blue-100">
+                账号: <span className="font-bold text-slate-900">{selectedAccount.username}</span>
+              </p>
+              <div className="bg-slate-50 p-4 rounded-lg border border-slate-200">
+                <pre className="text-xs font-mono text-slate-700 whitespace-pre-wrap overflow-x-auto">
+                  {JSON.stringify(selectedAccount.extra_data || {}, null, 2)}
+                </pre>
+              </div>
+            </div>
+            <div className="px-6 py-4 bg-slate-50 flex justify-end gap-3 border-t">
+              <button onClick={() => setShowRegistrationModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">关闭</button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 编辑账号 Modal */}
+      {showEditModal && selectedAccount && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in zoom-in-95 duration-200">
+          <div className="bg-white rounded-xl shadow-2xl w-full max-w-md overflow-hidden">
+             <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-blue-50/50">
+              <h3 className="font-bold text-lg text-blue-900 flex items-center gap-2">
+                <Pencil size={18} /> 编辑账号
+              </h3>
+              <button onClick={() => setShowEditModal(false)} className="text-slate-400 hover:text-slate-600">
+                <X size={20} />
+              </button>
+            </div>
+            <div className="p-6 space-y-4 max-h-[80vh] overflow-y-auto">
+              <div className="text-sm text-slate-600 bg-blue-50 p-3 rounded-lg border border-blue-100">
+                Pool: <span className="font-bold text-slate-900">{selectedAccount.pool_name}</span>
+                <br />
+                Username: <span className="font-bold text-slate-900">{selectedAccount.username}</span>
+              </div>
+              <div>
+                <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Password</label>
+                <input type="text" className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm font-mono" value={editForm.password} onChange={e => setEditForm({...editForm, password: e.target.value})} />
+              </div>
+              <div>
+                <label className="block text-xs font-bold uppercase text-slate-500 mb-1.5">Extra Data (JSON)</label>
+                <textarea className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-mono text-xs bg-slate-50" rows={6} value={editForm.extra_data_str} onChange={e => setEditForm({...editForm, extra_data_str: e.target.value})} />
+              </div>
+            </div>
             <div className="px-6 py-4 bg-slate-50 flex justify-end gap-3 border-t">
-              <button onClick={() => setShowLockModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
-              <button onClick={handleLockConfirm} className="px-4 py-2 text-sm font-bold bg-orange-600 text-white rounded-lg hover:bg-orange-700">确认锁定</button>
+              <button onClick={() => setShowEditModal(false)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-200 rounded-lg">取消</button>
+              <button onClick={handleEditAccount} className="px-4 py-2 text-sm font-bold bg-blue-600 text-white rounded-lg hover:bg-blue-700">保存修改</button>
             </div>
           </div>
         </div>

+ 15 - 1
src/app/admin/tasks/page.tsx

@@ -106,6 +106,18 @@ export default function AdminTasksPage() {
     }
   };
 
+  // 暂停任务
+  const handlePause = async (taskId: number) => {
+    if(!confirm("确定要暂停该任务吗?")) return;
+    try {
+      await api.post('/api/vas/task/pause', null, { params: { task_id: taskId } });
+      alert("操作成功");
+      fetchTasks(page);
+    } catch (e) {
+      alert("操作失败");
+    }
+  };
+
   // 打开详情弹窗
   const handleViewDetail = (task: VasTask) => {
     setSelectedTask(task);
@@ -154,6 +166,7 @@ export default function AdminTasksPage() {
             <option value="all">所有状态 (All)</option>
             <option value="pending">等待中 (Pending)</option>
             <option value="running">运行中 (Running)</option>
+            <option value="pause">已暂停 (Paused)</option>
             <option value="grabbed">待确认 (Grabbed)</option>
             <option value="completed">已完成 (Completed)</option>
             <option value="cancelled">已取消 (Cancelled)</option>
@@ -197,7 +210,8 @@ export default function AdminTasksPage() {
         tasks={tasks} 
         loading={loading} 
         onRetry={handleRetry} 
-        onManualConfirm={handleConfirm} 
+        onManualConfirm={handleConfirm}
+        onPause={handlePause}
         onViewDetail={handleViewDetail} 
         onEdit={handleEdit} 
       />

+ 30 - 12
src/components/admin/accounts/AccountTable.tsx

@@ -1,4 +1,4 @@
-import { Lock, Unlock, Timer, Loader2, Ban } from 'lucide-react';
+import { Lock, Unlock, Timer, Loader2, Ban, Pencil } from 'lucide-react';
 
 export interface Account {
   id: number;
@@ -7,7 +7,7 @@ export interface Account {
   password?: string;
   lock_until: number;
   extra_data: Record<string, any>;
-  status: string; // 后端返回的状态字符串,如 "active", "disabled"
+  status: string;
 }
 
 interface AccountTableProps {
@@ -15,9 +15,11 @@ interface AccountTableProps {
   loading: boolean;
   onLock: (account: Account) => void;
   onDisable: (account: Account) => void;
+  onViewRegistration: (account: Account) => void;
+  onEdit: (account: Account) => void;
 }
 
-export default function AccountTable({ data, loading, onLock, onDisable }: AccountTableProps) {
+export default function AccountTable({ data, loading, onLock, onDisable, onViewRegistration, onEdit }: AccountTableProps) {
   // 辅助函数:计算剩余时间
   const getRemainingTime = (until: number): string | null => {
     if (!until) return null;
@@ -64,12 +66,13 @@ export default function AccountTable({ data, loading, onLock, onDisable }: Accou
           <thead className="bg-slate-50 border-b border-slate-200">
             <tr>
               <th className="px-6 py-4 font-semibold text-slate-700">ID</th>
-              <th className="px-6 py-4 font-semibold text-slate-700">Pool Name</th>
-              <th className="px-6 py-4 font-semibold text-slate-700">Username</th>
-              <th className="px-6 py-4 font-semibold text-slate-700">Password</th>
-              <th className="px-6 py-4 font-semibold text-slate-700">Status</th>
-              <th className="px-6 py-4 font-semibold text-slate-700">Unlock In</th>
-              <th className="px-6 py-4 font-semibold text-slate-700 text-right">Actions</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">账号池</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">用户名</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">密码</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">状态</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">解锁剩余时间</th>
+              <th className="px-6 py-4 font-semibold text-slate-700">注册信息</th>
+              <th className="px-6 py-4 font-semibold text-slate-700 text-right">操作</th>
             </tr>
           </thead>
           <tbody className="divide-y divide-slate-100">
@@ -125,11 +128,27 @@ export default function AccountTable({ data, loading, onLock, onDisable }: Accou
                     )}
                   </td>
 
-                  <td className="px-6 py-4 text-right">
+                  <td className="px-6 py-4">
+                    <button
+                      onClick={() => onViewRegistration(item)}
+                      className="text-blue-600 hover:text-blue-800 text-xs underline"
+                      title="查看注册信息"
+                    >
+                      查看
+                    </button>
+                  </td>
+
+<td className="px-6 py-4 text-right">
                     <div className="flex items-center justify-end gap-2">
+                      <button
+                        onClick={() => onEdit(item)}
+                        className="p-2 text-slate-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition"
+                        title="编辑账号"
+                      >
+                        <Pencil size={16} />
+                      </button>
                       <button
                         onClick={() => onLock(item)}
-                        // 如果已经被禁用,通常不需要再锁定了,可以禁用按钮
                         disabled={isDisabled} 
                         className={`p-2 rounded-lg transition ${
                           isDisabled 
@@ -142,7 +161,6 @@ export default function AccountTable({ data, loading, onLock, onDisable }: Accou
                       </button>
                       <button
                         onClick={() => onDisable(item)}
-                        // 如果已经是禁用状态,也许需要变成“启用”按钮?这里暂时只显示禁用操作
                         className="p-2 text-slate-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition"
                         title="禁用账号"
                       >

+ 114 - 26
src/components/admin/tasks/TaskTable.tsx

@@ -3,7 +3,7 @@
 import { useState } from 'react';
 import { 
   RotateCcw, CheckCircle, ChevronDown, ChevronUp, Terminal, FileJson, 
-  User, History, Edit, MessageCircle, MessageSquare, X
+  User, History, Edit, MessageCircle, MessageSquare, X, Pause, Mail
 } from 'lucide-react';
 import LocalTime from '@/components/common/LocalTime';
 import api from '@/lib/api';
@@ -31,15 +31,17 @@ interface TaskTableProps {
   loading: boolean;
   onRetry: (taskId: number) => void;
   onManualConfirm: (taskId: number) => void;
+  onPause: (taskId: number) => void;
   onEdit: (task: VasTask) => void;
   onViewDetail: (task: VasTask) => void;
 }
 
-export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onEdit }: TaskTableProps) {
+export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, onPause, onEdit }: TaskTableProps) {
   const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
   const [isMessageOpen, setIsMessageOpen] = useState(false);
-  const [messageChannel, setMessageChannel] = useState<'sms' | 'whatsapp'>('sms');
+  const [messageChannel, setMessageChannel] = useState<'sms' | 'whatsapp' | 'email'>('sms');
   const [messageTask, setMessageTask] = useState<VasTask | null>(null);
+  const [contactDropdownOpen, setContactDropdownOpen] = useState<number | null>(null);
   const [messageText, setMessageText] = useState('');
   const [messageSending, setMessageSending] = useState(false);
   const [messageResult, setMessageResult] = useState<'idle' | 'success' | 'error'>('idle');
@@ -52,10 +54,15 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
     setExpandedRows(newSet);
   };
 
-  const openMessageModal = (task: VasTask, channel: 'sms' | 'whatsapp') => {
-    const template = channel === 'whatsapp'
-      ? 'Visafly: Your appointment status changed. Please review updates and confirm next steps.'
-      : 'Your appointment status changed. Please review updates and confirm next steps.';
+  const openMessageModal = (task: VasTask, channel: 'sms' | 'whatsapp' | 'email') => {
+    let template = '';
+    if (channel === 'whatsapp') {
+      template = 'Visafly: Your appointment status changed. Please review updates and confirm next steps.';
+    } else if (channel === 'sms') {
+      template = 'Your appointment status changed. Please review updates and confirm next steps.';
+    } else {
+      template = 'Your appointment status changed. Please review updates and confirm next steps.';
+    }
     setMessageTask(task);
     setMessageChannel(channel);
     setMessageText(template);
@@ -73,17 +80,19 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
   const handleSendMessage = async () => {
     if (!messageTask) return;
     const target = getRecipientPhoneRaw(messageTask);
-    if (!target) {
-      setMessageResult('error');
-      setMessageResultText('Missing recipient phone number.');
-      return;
-    }
+    const email = getRecipientEmail(messageTask);
     setMessageSending(true);
     setMessageResult('idle');
     setMessageResultText('');
 
     try {
       if (messageChannel === 'whatsapp') {
+        if (!target) {
+          setMessageResult('error');
+          setMessageResultText('Missing WhatsApp number.');
+          setMessageSending(false);
+          return;
+        }
         const res = await api.post('/api/whatsapp/send_no_token', {
           chat_id: target,
           message: messageText,
@@ -91,7 +100,13 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
         const ok = res.data?.code === 0;
         setMessageResult(ok ? 'success' : 'error');
         setMessageResultText(ok ? 'WhatsApp sent successfully.' : (res.data?.message || 'Failed to send WhatsApp.'));
-      } else {
+      } else if (messageChannel === 'sms') {
+        if (!target) {
+          setMessageResult('error');
+          setMessageResultText('Missing phone number.');
+          setMessageSending(false);
+          return;
+        }
         const res = await api.post('/api/sms/send', {
           send_to: target,
           sender: getSmsSender(messageTask),
@@ -100,9 +115,24 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
         const ok = res.data?.code === 0;
         setMessageResult(ok ? 'success' : 'error');
         setMessageResultText(ok ? 'SMS sent successfully.' : (res.data?.message || 'Failed to send SMS.'));
+      } else {
+        if (!email) {
+          setMessageResult('error');
+          setMessageResultText('Missing email address.');
+          setMessageSending(false);
+          return;
+        }
+        const res = await api.post('/api/email/send', {
+          send_to: email,
+          subject: 'Visafly Notification',
+          content: messageText,
+        });
+        const ok = res.data?.code === 0;
+        setMessageResult(ok ? 'success' : 'error');
+        setMessageResultText(ok ? 'Email sent successfully.' : (res.data?.message || 'Failed to send email.'));
       }
     } catch (error) {
-      const fallback = messageChannel === 'whatsapp' ? 'Failed to send WhatsApp.' : 'Failed to send SMS.';
+      const fallback = messageChannel === 'whatsapp' ? 'Failed to send WhatsApp.' : messageChannel === 'sms' ? 'Failed to send SMS.' : 'Failed to send email.';
       setMessageResult('error');
       setMessageResultText(fallback);
     } finally {
@@ -138,6 +168,11 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
     return 'Visafly';
   };
 
+  const getRecipientEmail = (task: VasTask) => {
+    if (!task.user_inputs) return '';
+    return task.user_inputs.email || '';
+  };
+
   const getUserSummary = (inputs: any) => {
     if (!inputs) return '-';
     const name = inputs.social_media_account ? (inputs.social_media_account): `${inputs.first_name} ${inputs.last_name}`;
@@ -179,7 +214,8 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
     <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 === '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 === 'pause' ? 'bg-orange-100 text-orange-700' :
         status === 'completed' ? 'bg-green-100 text-green-700' :
         'bg-gray-100 text-gray-600'}`}>
       {status}
@@ -252,9 +288,39 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                       <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" 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={() => onPause(task.id)} className="p-1.5 rounded text-orange-600 hover:bg-orange-50 border border-transparent hover:border-orange-100" title="暂停"><Pause 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>
-                        <button onClick={() => openMessageModal(task, 'sms')} className="p-1.5 rounded text-amber-600 hover:bg-amber-50 border border-transparent hover:border-amber-100" title="短信提醒"><MessageSquare size={16} /></button>
-                        <button onClick={() => openMessageModal(task, 'whatsapp')} className="p-1.5 rounded text-emerald-600 hover:bg-emerald-50 border border-transparent hover:border-emerald-100" title="WhatsApp 提醒"><MessageCircle size={16} /></button>
+                        <div className="relative">
+                          <button 
+                            onClick={() => setContactDropdownOpen(contactDropdownOpen === task.id ? null : task.id)}
+                            className="p-1.5 rounded text-cyan-600 hover:bg-cyan-50 border border-transparent hover:border-cyan-100"
+                            title="联系客户"
+                          >
+                            <Mail size={16} />
+                          </button>
+                          {contactDropdownOpen === task.id && (
+                            <div className="absolute right-0 top-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg z-10 min-w-[120px]">
+                              <button 
+                                onClick={() => { openMessageModal(task, 'sms'); setContactDropdownOpen(null); }}
+                                className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-amber-50 rounded-t-lg"
+                              >
+                                <MessageSquare size={14} /> 短信
+                              </button>
+                              <button 
+                                onClick={() => { openMessageModal(task, 'whatsapp'); setContactDropdownOpen(null); }}
+                                className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50"
+                              >
+                                <MessageCircle size={14} /> WhatsApp
+                              </button>
+                              <button 
+                                onClick={() => { openMessageModal(task, 'email'); setContactDropdownOpen(null); }}
+                                className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-blue-50 rounded-b-lg"
+                              >
+                                <Mail size={14} /> 邮件
+                              </button>
+                            </div>
+                          )}
+                        </div>
                       </div>
                     </td>
                   </tr>
@@ -289,12 +355,27 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                   </div>
                   <span className="text-xs font-mono text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded w-fit truncate max-w-full">{task.routing_key}</span>
                 </div>
-                <div className="flex gap-2 flex-shrink-0">
+<div className="flex gap-2 flex-shrink-0">
                    <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={() => onPause(task.id)} className="p-2 bg-orange-50 text-orange-600 rounded-lg active:scale-95"><Pause 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={() => openMessageModal(task, 'sms')} className="p-2 bg-amber-50 text-amber-600 rounded-lg active:scale-95"><MessageSquare size={18} /></button>
-                   <button onClick={() => openMessageModal(task, 'whatsapp')} className="p-2 bg-emerald-50 text-emerald-600 rounded-lg active:scale-95"><MessageCircle size={18} /></button>
-                </div>
+                   <div className="relative">
+                     <button onClick={() => setContactDropdownOpen(contactDropdownOpen === task.id ? null : task.id)} className="p-2 bg-cyan-50 text-cyan-600 rounded-lg active:scale-95"><Mail size={18}/></button>
+                     {contactDropdownOpen === task.id && (
+                       <div className="absolute right-0 top-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg z-10 min-w-[100px]">
+                         <button onClick={() => { openMessageModal(task, 'sms'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-amber-50 rounded-t-lg">
+                           <MessageSquare size={14} /> 短信
+                         </button>
+                         <button onClick={() => { openMessageModal(task, 'whatsapp'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-emerald-50">
+                           <MessageCircle size={14} /> WhatsApp
+                         </button>
+                         <button onClick={() => { openMessageModal(task, 'email'); setContactDropdownOpen(null); }} className="flex items-center gap-2 w-full px-3 py-2 text-sm text-slate-700 hover:bg-blue-50 rounded-b-lg">
+                           <Mail size={14} /> 邮件
+                         </button>
+                       </div>
+                     )}
+                   </div>
+                 </div>
               </div>
 
               <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm space-y-2 mb-3">
@@ -339,7 +420,7 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
             <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
               <div>
                 <h3 className="font-bold text-slate-900 text-lg">
-                  {messageChannel === 'sms' ? 'Send SMS' : 'Send WhatsApp'}
+                  {messageChannel === 'sms' ? '发送短信' : messageChannel === 'whatsapp' ? '发送 WhatsApp' : '发送邮件'}
                 </h3>
                 <p className="text-xs text-slate-500 mt-1">Order #{messageTask.order_id}</p>
               </div>
@@ -353,10 +434,17 @@ export default function TaskTable({ tasks, loading, onRetry, onManualConfirm, on
                   <span className="font-semibold text-slate-500">Recipient</span>
                   <span className="text-slate-700">{getRecipientName(messageTask)}</span>
                 </div>
-                <div className="flex justify-between">
-                  <span className="font-semibold text-slate-500">{messageChannel === 'sms' ? 'SMS To' : 'WhatsApp To'}</span>
-                  <span className="text-slate-700">{getRecipientPhone(messageTask)}</span>
-                </div>
+                {messageChannel === 'email' ? (
+                  <div className="flex justify-between">
+                    <span className="font-semibold text-slate-500">Email To</span>
+                    <span className="text-slate-700">{getRecipientEmail(messageTask) || '-'}</span>
+                  </div>
+                ) : (
+                  <div className="flex justify-between">
+                    <span className="font-semibold text-slate-500">{messageChannel === 'sms' ? 'SMS To' : 'WhatsApp To'}</span>
+                    <span className="text-slate-700">{getRecipientPhone(messageTask)}</span>
+                  </div>
+                )}
                 {messageChannel === 'sms' && (
                   <div className="flex justify-between">
                     <span className="font-semibold text-slate-500">Sender</span>