OrderTable.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. 'use client';
  2. import { Eye, XCircle, User, Box, Edit, Clock, FileText, Wallet } from 'lucide-react';
  3. import { OrderDetail } from './OrderDetailModal';
  4. import LocalTime from '@/components/common/LocalTime';
  5. interface OrderTableProps {
  6. orders: OrderDetail[];
  7. loading: boolean;
  8. onCancel: (orderId: string) => void;
  9. onViewDetail: (order: OrderDetail) => void;
  10. onEdit: (order: OrderDetail) => void;
  11. onCheckPayments: (order: OrderDetail) => void; // 新增:查看/核销支付记录
  12. }
  13. export default function OrderTable({
  14. orders,
  15. loading,
  16. onCancel,
  17. onViewDetail,
  18. onEdit,
  19. onCheckPayments
  20. }: OrderTableProps) {
  21. if (loading) {
  22. return (
  23. <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
  24. <div className="text-gray-500 text-sm">加载订单数据中...</div>
  25. </div>
  26. );
  27. }
  28. if (orders.length === 0) {
  29. return (
  30. <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
  31. <div className="text-gray-500 text-sm">暂无订单记录</div>
  32. </div>
  33. );
  34. }
  35. // 状态颜色映射
  36. const getStatusColor = (status: string) => {
  37. switch (status) {
  38. case 'paid': return 'bg-green-100 text-green-800 border-green-200';
  39. case 'succeeded': return 'bg-green-100 text-green-800 border-green-200';
  40. case 'pending': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
  41. case 'cancelled': return 'bg-red-50 text-red-600 border-red-100';
  42. case 'failed': return 'bg-red-100 text-red-800 border-red-200';
  43. default: return 'bg-gray-100 text-gray-800 border-gray-200';
  44. }
  45. };
  46. // 辅助函数:是否可以取消
  47. const canCancel = (status: string) => status !== 'cancelled' && status !== 'completed' && status !== 'failed';
  48. return (
  49. <div className="space-y-4">
  50. {/* =========================== */}
  51. {/* 1. Desktop View (Table) */}
  52. {/* =========================== */}
  53. <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
  54. <div className="overflow-x-auto">
  55. <table className="min-w-full divide-y divide-slate-200">
  56. <thead className="bg-slate-50">
  57. <tr>
  58. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">订单号 / 创建时间</th>
  59. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">商品信息</th>
  60. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">用户信息</th>
  61. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">金额</th>
  62. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
  63. <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
  64. </tr>
  65. </thead>
  66. <tbody className="bg-white divide-y divide-slate-200">
  67. {orders.map((order) => (
  68. <tr key={order.id} className="hover:bg-slate-50 transition-colors">
  69. <td className="px-6 py-4 whitespace-nowrap">
  70. <div className="text-sm font-medium text-slate-900 font-mono">{order.id}</div>
  71. <div className="text-xs text-slate-400 mt-1">
  72. <LocalTime date={order.created_at} />
  73. </div>
  74. </td>
  75. <td className="px-6 py-4">
  76. <div className="flex items-center">
  77. <Box size={16} className="text-slate-400 mr-2 flex-shrink-0" />
  78. <span className="text-sm text-slate-700 truncate max-w-[150px]" title={order.product_title}>
  79. {order.product_name || order.product_title || '未知商品'}
  80. </span>
  81. </div>
  82. </td>
  83. <td className="px-6 py-4">
  84. <div className="flex items-center">
  85. <User size={16} className="text-slate-400 mr-2 flex-shrink-0" />
  86. <div className="flex flex-col">
  87. <span className="text-sm text-slate-700 font-medium">{order.user_name || order.applicant_name || '未填写'}</span>
  88. <span className="text-xs text-slate-400">{order.user_email || order.user_id}</span>
  89. </div>
  90. </div>
  91. </td>
  92. <td className="px-6 py-4 whitespace-nowrap text-sm text-slate-900 font-bold font-mono">
  93. {(order.base_amount / 100).toFixed(2)} {order.base_currency}
  94. </td>
  95. <td className="px-6 py-4 whitespace-nowrap">
  96. <span className={`px-2 py-0.5 inline-flex text-xs leading-5 font-semibold rounded-full border ${getStatusColor(order.status)}`}>
  97. {order.status}
  98. </span>
  99. </td>
  100. <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
  101. <div className="flex justify-end gap-2">
  102. {/* 待支付状态显示人工核销按钮 */}
  103. {order.status === 'pending' && (
  104. <button
  105. onClick={() => onCheckPayments(order)}
  106. 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"
  107. title="查看/核销支付记录 (处理漏单)"
  108. >
  109. <Wallet size={16} />
  110. </button>
  111. )}
  112. <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>
  113. <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>
  114. {canCancel(order.status) && (
  115. <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>
  116. )}
  117. </div>
  118. </td>
  119. </tr>
  120. ))}
  121. </tbody>
  122. </table>
  123. </div>
  124. </div>
  125. {/* =========================== */}
  126. {/* 2. Mobile View (Cards) */}
  127. {/* =========================== */}
  128. <div className="md:hidden space-y-4">
  129. {orders.map((order) => (
  130. <div key={order.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
  131. {/* Header: ID & Status */}
  132. <div className="flex justify-between items-start mb-3 border-b border-slate-100 pb-2">
  133. <div className="flex items-center gap-2 text-slate-500">
  134. <FileText size={14} />
  135. <span className="text-xs font-mono font-bold">{order.id}</span>
  136. </div>
  137. <span className={`px-2 py-0.5 text-xs font-bold rounded border ${getStatusColor(order.status)}`}>
  138. {order.status.toUpperCase()}
  139. </span>
  140. </div>
  141. {/* Info Grid */}
  142. <div className="space-y-3">
  143. {/* Product */}
  144. <div className="flex items-start gap-3">
  145. <div className="p-1.5 bg-blue-50 rounded text-blue-600 shrink-0 mt-0.5">
  146. <Box size={16} />
  147. </div>
  148. <div>
  149. <div className="text-sm font-bold text-slate-800 line-clamp-2">
  150. {order.product_name || order.product_title || '未知商品'}
  151. </div>
  152. <div className="text-xs text-slate-500 mt-1 flex items-center gap-1">
  153. <Clock size={12} />
  154. <LocalTime date={order.created_at} />
  155. </div>
  156. </div>
  157. </div>
  158. {/* User & Price */}
  159. <div className="bg-slate-50 rounded p-3 grid grid-cols-2 gap-2 text-sm border border-slate-100">
  160. <div>
  161. <span className="text-xs text-slate-400 block mb-0.5">申请人</span>
  162. <div className="flex items-center gap-1.5 text-slate-700">
  163. <User size={12} />
  164. <span className="truncate font-medium">{order.user_name || order.applicant_name || '-'}</span>
  165. </div>
  166. </div>
  167. <div className="text-right">
  168. <span className="text-xs text-slate-400 block mb-0.5">金额</span>
  169. <div className="font-mono font-bold text-slate-900">
  170. {(order.base_amount / 100).toFixed(2)} <span className="text-xs font-normal">{order.base_currency}</span>
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. {/* Actions */}
  176. <div className="grid grid-cols-4 gap-2 mt-4 pt-3 border-t border-slate-100">
  177. {order.status === 'pending' ? (
  178. <>
  179. <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">
  180. <Wallet size={16} />
  181. </button>
  182. <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">
  183. <Edit size={16} />
  184. </button>
  185. <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">
  186. <Eye size={16} />
  187. </button>
  188. <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">
  189. <XCircle size={16} />
  190. </button>
  191. </>
  192. ) : (
  193. <>
  194. <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">
  195. <Edit size={16} /> 编辑
  196. </button>
  197. <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">
  198. <Eye size={16} /> 详情
  199. </button>
  200. </>
  201. )}
  202. </div>
  203. </div>
  204. ))}
  205. </div>
  206. </div>
  207. );
  208. }