TicketModal.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import api from '@/lib/api';
  4. import { Loader2, X, AlertTriangle } from 'lucide-react';
  5. // 1. 引入 Hook
  6. import { useLanguage } from '@/lib/i18n/LanguageContext';
  7. interface TicketModalProps {
  8. isOpen: boolean;
  9. onClose: () => void;
  10. onSuccess?: () => void;
  11. defaultOrderId?: string;
  12. }
  13. export default function TicketModal({ isOpen, onClose, onSuccess, defaultOrderId = '' }: TicketModalProps) {
  14. // 2. 获取翻译函数
  15. const { t } = useLanguage();
  16. const [loading, setLoading] = useState<boolean>(false);
  17. const [errorMsg, setErrorMsg] = useState<string>('');
  18. const [form, setForm] = useState({
  19. order_id: '',
  20. type: 'refund',
  21. reason: ''
  22. });
  23. useEffect(() => {
  24. if (isOpen) {
  25. setForm({
  26. order_id: defaultOrderId || '',
  27. type: 'refund',
  28. reason: ''
  29. });
  30. setErrorMsg('');
  31. }
  32. }, [isOpen, defaultOrderId]);
  33. const handleSubmit = async (e: React.FormEvent) => {
  34. e.preventDefault();
  35. setLoading(true);
  36. setErrorMsg('');
  37. try {
  38. await api.post('/api/vas/ticket/create', form);
  39. if (onSuccess) onSuccess();
  40. onClose();
  41. } catch (error: any) {
  42. console.error(error);
  43. const msg = error.response?.data?.message || t('ticket.submit_error_default');
  44. setErrorMsg(msg);
  45. } finally {
  46. setLoading(false);
  47. }
  48. };
  49. if (!isOpen) return null;
  50. return (
  51. <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
  52. <div className="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity" onClick={onClose} />
  53. <div className="relative w-full max-w-lg transform overflow-hidden rounded-xl bg-white text-left shadow-2xl transition-all animate-in zoom-in duration-200">
  54. {/* Header */}
  55. <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
  56. <h3 className="text-lg font-bold text-gray-900">{t('ticket.submit_modal_title')}</h3>
  57. <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
  58. <X size={24} />
  59. </button>
  60. </div>
  61. <div className="p-6">
  62. {errorMsg && (
  63. <div className="mb-4 p-3 bg-red-50 text-red-700 text-sm rounded-lg flex items-center">
  64. <AlertTriangle size={16} className="mr-2 flex-shrink-0" />
  65. {errorMsg}
  66. </div>
  67. )}
  68. <form onSubmit={handleSubmit} className="space-y-5">
  69. <div>
  70. <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
  71. {t('ticket.order_id_label')} <span className="text-red-500">*</span>
  72. </label>
  73. <input
  74. type="text" required
  75. className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-gray-50 focus:bg-white transition"
  76. value={form.order_id}
  77. onChange={e => setForm({ ...form, order_id: e.target.value })}
  78. placeholder={t('ticket.order_id_placeholder')}
  79. />
  80. </div>
  81. <div>
  82. <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
  83. {t('ticket.type_label')} <span className="text-red-500">*</span>
  84. </label>
  85. <select
  86. className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm bg-white"
  87. value={form.type}
  88. onChange={e => setForm({ ...form, type: e.target.value })}
  89. >
  90. <option value="refund">{t('ticket.types.refund')}</option>
  91. <option value="dispute">{t('ticket.types.dispute')}</option>
  92. <option value="change_request">{t('ticket.types.change_request')}</option>
  93. </select>
  94. </div>
  95. <div>
  96. <label className="block text-xs font-bold uppercase text-gray-500 mb-1">
  97. {t('ticket.desc_label')} <span className="text-red-500">*</span>
  98. </label>
  99. <textarea
  100. required
  101. rows={4}
  102. className="w-full rounded-lg border border-gray-300 py-2.5 px-3 text-gray-900 shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm resize-none"
  103. placeholder={t('ticket.desc_placeholder')}
  104. value={form.reason}
  105. onChange={e => setForm({ ...form, reason: e.target.value })}
  106. />
  107. </div>
  108. <div className="pt-2 flex items-center justify-end gap-3 border-t mt-6">
  109. <button
  110. type="button"
  111. onClick={onClose}
  112. className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-200 transition"
  113. >
  114. {t('common.cancel')}
  115. </button>
  116. <button
  117. type="submit"
  118. disabled={loading}
  119. className="inline-flex items-center justify-center px-6 py-2 text-sm font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed shadow-md transition"
  120. >
  121. {loading ? <Loader2 className="animate-spin w-4 h-4 mr-2" /> : null}
  122. {t('ticket.submit_btn')}
  123. </button>
  124. </div>
  125. </form>
  126. </div>
  127. </div>
  128. </div>
  129. );
  130. }