TicketTable.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. 'use client';
  2. import { Eye, Clock, CheckCircle, XCircle, AlertCircle, HelpCircle, User, FileText } from 'lucide-react';
  3. // 1. 引入 LocalTime 组件
  4. import LocalTime from '@/components/common/LocalTime';
  5. // 定义工单数据结构 (对应 API: VasTicketOut)
  6. export interface AdminTicket {
  7. id: number;
  8. order_id: string;
  9. user_id: string;
  10. type: string; // refund, dispute, change_request
  11. status: string; // pending, info_required, resolved, rejected
  12. reason: string;
  13. admin_comment?: string;
  14. created_at: string;
  15. updated_at: string;
  16. }
  17. interface TicketTableProps {
  18. tickets: AdminTicket[];
  19. loading: boolean;
  20. onViewDetail: (ticket: AdminTicket) => void;
  21. }
  22. export default function TicketTable({ tickets, loading, onViewDetail }: TicketTableProps) {
  23. if (loading) {
  24. return (
  25. <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
  26. <div className="text-gray-500 text-sm">加载工单数据中...</div>
  27. </div>
  28. );
  29. }
  30. if (tickets.length === 0) {
  31. return (
  32. <div className="bg-white rounded-lg shadow p-12 text-center border border-slate-200">
  33. <div className="text-gray-500 text-sm">暂无工单记录</div>
  34. </div>
  35. );
  36. }
  37. // 状态徽章
  38. const getStatusBadge = (status: string) => {
  39. switch (status) {
  40. case 'pending':
  41. return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"><Clock size={12}/> 待处理</span>;
  42. case 'info_required':
  43. return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"><HelpCircle size={12}/> 需补充资料</span>;
  44. case 'resolved':
  45. return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"><CheckCircle size={12}/> 已解决</span>;
  46. case 'rejected':
  47. return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"><XCircle size={12}/> 已拒绝</span>;
  48. default:
  49. return <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">{status}</span>;
  50. }
  51. };
  52. const getTypeText = (type: string) => {
  53. const map: Record<string, string> = {
  54. refund: '退款申请',
  55. dispute: '交易纠纷',
  56. change_request: '变更请求'
  57. };
  58. return map[type] || type;
  59. };
  60. return (
  61. <div className="space-y-4">
  62. {/* ======================= */}
  63. {/* 1. Desktop View (Table) - 仅在中大屏幕显示 */}
  64. {/* ======================= */}
  65. <div className="hidden md:block bg-white rounded-lg shadow overflow-hidden border border-slate-200">
  66. <div className="overflow-x-auto">
  67. <table className="min-w-full divide-y divide-slate-200">
  68. <thead className="bg-slate-50">
  69. <tr>
  70. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">ID</th>
  71. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">类型</th>
  72. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">关联订单 / 用户</th>
  73. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">描述摘要</th>
  74. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">状态</th>
  75. <th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">创建时间</th>
  76. <th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">操作</th>
  77. </tr>
  78. </thead>
  79. <tbody className="bg-white divide-y divide-slate-200">
  80. {tickets.map((ticket) => (
  81. <tr key={ticket.id} className="hover:bg-slate-50 transition-colors">
  82. <td className="px-6 py-4 text-sm font-mono text-gray-500">#{ticket.id}</td>
  83. <td className="px-6 py-4 text-sm font-bold text-gray-800">{getTypeText(ticket.type)}</td>
  84. <td className="px-6 py-4">
  85. <div className="text-sm font-medium text-blue-600 font-mono">{ticket.order_id}</div>
  86. <div className="text-xs text-gray-500 mt-0.5">{ticket.user_id}</div>
  87. </td>
  88. <td className="px-6 py-4">
  89. <div className="text-sm text-gray-600 max-w-[200px] truncate" title={ticket.reason}>
  90. {ticket.reason}
  91. </div>
  92. </td>
  93. <td className="px-6 py-4 whitespace-nowrap">
  94. {getStatusBadge(ticket.status)}
  95. </td>
  96. <td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
  97. <LocalTime date={ticket.created_at} />
  98. </td>
  99. <td className="px-6 py-4 text-right">
  100. <button
  101. onClick={() => onViewDetail(ticket)}
  102. className="text-blue-600 hover:text-blue-900 inline-flex items-center text-sm font-medium"
  103. >
  104. <Eye size={16} className="mr-1" /> 处理/详情
  105. </button>
  106. </td>
  107. </tr>
  108. ))}
  109. </tbody>
  110. </table>
  111. </div>
  112. </div>
  113. {/* ======================= */}
  114. {/* 2. Mobile View (Cards) - 仅在小屏幕显示 */}
  115. {/* ======================= */}
  116. <div className="md:hidden space-y-4">
  117. {tickets.map((ticket) => (
  118. <div key={ticket.id} className="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
  119. {/* Header: ID + Status */}
  120. <div className="flex justify-between items-start mb-3">
  121. <div className="flex items-center gap-2">
  122. <span className="text-sm font-mono text-slate-500 font-bold">#{ticket.id}</span>
  123. <span className="text-sm font-bold text-slate-900">{getTypeText(ticket.type)}</span>
  124. </div>
  125. <div>{getStatusBadge(ticket.status)}</div>
  126. </div>
  127. {/* Info Grid */}
  128. <div className="space-y-2 text-sm text-slate-600 mb-4 bg-slate-50 p-3 rounded-lg border border-slate-100">
  129. <div className="flex justify-between">
  130. <span className="flex items-center text-xs text-slate-400">
  131. <FileText size={12} className="mr-1"/> 订单号
  132. </span>
  133. <span className="font-mono text-blue-600">{ticket.order_id}</span>
  134. </div>
  135. <div className="flex justify-between">
  136. <span className="flex items-center text-xs text-slate-400">
  137. <User size={12} className="mr-1"/> 用户
  138. </span>
  139. <span className="truncate max-w-[150px]">{ticket.user_id}</span>
  140. </div>
  141. <div className="flex justify-between">
  142. <span className="flex items-center text-xs text-slate-400">
  143. <Clock size={12} className="mr-1"/> 时间
  144. </span>
  145. <span><LocalTime date={ticket.created_at} /></span>
  146. </div>
  147. {/* 描述摘要 */}
  148. <div className="pt-2 border-t border-slate-200 mt-2">
  149. <span className="text-xs text-slate-400 block mb-1">描述:</span>
  150. <p className="text-slate-800 line-clamp-2">{ticket.reason}</p>
  151. </div>
  152. </div>
  153. {/* Action Button */}
  154. <button
  155. onClick={() => onViewDetail(ticket)}
  156. className="w-full flex items-center justify-center py-2.5 bg-blue-600 text-white rounded-lg text-sm font-bold active:scale-95 transition-transform shadow-sm shadow-blue-200"
  157. >
  158. <Eye size={16} className="mr-2" /> 处理工单
  159. </button>
  160. </div>
  161. ))}
  162. </div>
  163. </div>
  164. );
  165. }