RoutingManager.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import api from '@/lib/api';
  4. import { Loader2, Trash2, Plus, Save, X, Network } from 'lucide-react';
  5. import JsonEditor from '@/components/common/JsonEditor';
  6. interface RoutingManagerProps {
  7. productId: number | null;
  8. productTitle: string;
  9. isOpen: boolean;
  10. onClose: () => void;
  11. }
  12. interface ProductRouting {
  13. id: number;
  14. routing_key: string;
  15. script_version: string;
  16. priority: number;
  17. config?: any; // JSON object or string
  18. meta?: any;
  19. }
  20. export default function RoutingManager({ productId, productTitle, isOpen, onClose }: RoutingManagerProps) {
  21. const [routings, setRoutings] = useState<ProductRouting[]>([]);
  22. const [loading, setLoading] = useState(false);
  23. const [submitting, setSubmitting] = useState(false);
  24. // 移动端 Tab 状态: 'list' | 'add'
  25. const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
  26. // 新增 Routing 的表单状态
  27. const [newRouting, setNewRouting] = useState({
  28. routing_key: '',
  29. script_version: 'latest',
  30. priority: 10,
  31. config: '{}',
  32. });
  33. // 加载路由列表
  34. useEffect(() => {
  35. if (isOpen && productId) {
  36. fetchRoutings();
  37. setActiveTab('list'); // 默认显示列表
  38. }
  39. }, [isOpen, productId]);
  40. const fetchRoutings = async () => {
  41. setLoading(true);
  42. try {
  43. const res = await api.get('/api/vas/product_routing/list', { params: { product_id: productId } });
  44. setRoutings(Array.isArray(res.data.data) ? res.data.data : []);
  45. } catch (e) {
  46. console.warn("Fetch routings failed");
  47. setRoutings([]);
  48. } finally {
  49. setLoading(false);
  50. }
  51. };
  52. const handleAddRouting = async () => {
  53. if (!newRouting.routing_key) return alert("Routing Key 必填");
  54. setSubmitting(true);
  55. try {
  56. let configObj = {};
  57. try {
  58. configObj = JSON.parse(newRouting.config);
  59. } catch (err) {
  60. alert("Config JSON 格式错误,请检查");
  61. setSubmitting(false);
  62. return;
  63. }
  64. await api.post('/api/vas/product_routing/create', {
  65. ...newRouting,
  66. config: configObj,
  67. product_id: productId
  68. });
  69. alert("路由添加成功");
  70. fetchRoutings();
  71. setNewRouting({ routing_key: '', script_version: 'latest', priority: 10, config: '{}' });
  72. setActiveTab('list'); // 提交成功后切回列表
  73. } catch (e: any) {
  74. alert("添加失败: " + (e.response?.data?.message || e.message));
  75. } finally {
  76. setSubmitting(false);
  77. }
  78. };
  79. const handleDeleteRouting = async (id: number) => {
  80. if (!confirm("确定删除此路由配置吗?")) return;
  81. try {
  82. await api.delete('/api/vas/product_routing/delete', {params: {"id": id}});
  83. fetchRoutings();
  84. } catch (e) {
  85. alert("删除失败");
  86. }
  87. };
  88. if (!isOpen) return null;
  89. return (
  90. <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
  91. {/*
  92. 容器:
  93. - 移动端:w-full h-full (全屏), rounded-none
  94. - 桌面端:max-w-5xl h-[90vh], rounded-xl
  95. */}
  96. <div className="bg-white w-full h-full md:w-full md:max-w-5xl md:h-[90vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
  97. {/* Header */}
  98. <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-slate-50 flex-shrink-0">
  99. <div className="flex items-center gap-3">
  100. <div className="bg-purple-100 p-2 rounded-lg text-purple-600 hidden md:block">
  101. <Network size={24} />
  102. </div>
  103. <div>
  104. <h3 className="font-bold text-gray-900 text-base md:text-lg">路由策略配置</h3>
  105. <p className="text-xs md:text-sm text-gray-500">商品: <span className="font-medium text-gray-700">{productTitle}</span></p>
  106. </div>
  107. </div>
  108. <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1">
  109. <X size={24} />
  110. </button>
  111. </div>
  112. {/* Mobile Tabs */}
  113. <div className="flex md:hidden border-b border-gray-200">
  114. <button
  115. onClick={() => setActiveTab('list')}
  116. className={`flex-1 py-3 text-sm font-medium ${activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
  117. >
  118. 规则列表 ({routings.length})
  119. </button>
  120. <button
  121. onClick={() => setActiveTab('add')}
  122. className={`flex-1 py-3 text-sm font-medium ${activeTab === 'add' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
  123. >
  124. 添加新规则
  125. </button>
  126. </div>
  127. {/* Content */}
  128. <div className="flex flex-1 overflow-hidden relative">
  129. {/* 左侧:添加表单 (Desktop: 30%, Mobile: 根据 Tab 显示) */}
  130. <div className={`
  131. flex-col bg-slate-50 overflow-y-auto border-r border-slate-200 p-5
  132. ${activeTab === 'add' ? 'flex w-full' : 'hidden'}
  133. md:flex md:w-1/3
  134. `}>
  135. <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
  136. <Plus size={16} /> 添加新规则
  137. </h4>
  138. <div className="space-y-4">
  139. <div>
  140. <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Routing Key (Queue)</label>
  141. <input
  142. type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
  143. placeholder="e.g. fr_visabot_vip"
  144. value={newRouting.routing_key}
  145. onChange={e => setNewRouting({...newRouting, routing_key: e.target.value})}
  146. />
  147. </div>
  148. <div className="grid grid-cols-2 gap-3">
  149. <div>
  150. <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Version</label>
  151. <input
  152. type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
  153. value={newRouting.script_version}
  154. onChange={e => setNewRouting({...newRouting, script_version: e.target.value})}
  155. />
  156. </div>
  157. <div>
  158. <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Priority</label>
  159. <input
  160. type="number" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
  161. value={newRouting.priority}
  162. onChange={e => setNewRouting({...newRouting, priority: parseInt(e.target.value)})}
  163. />
  164. </div>
  165. </div>
  166. {/* JSON Config Editor */}
  167. <div className="pt-2">
  168. <JsonEditor
  169. label="Config (JSON 参数)"
  170. value={newRouting.config}
  171. onChange={(val) => setNewRouting({...newRouting, config: val})}
  172. height="h-64"
  173. placeholder='{"headless": true, "timeout": 30000}'
  174. />
  175. </div>
  176. <button
  177. onClick={handleAddRouting}
  178. disabled={submitting}
  179. className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 transition flex items-center justify-center gap-2 shadow-sm disabled:opacity-50 mt-4"
  180. >
  181. {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
  182. 保存规则
  183. </button>
  184. </div>
  185. </div>
  186. {/* 右侧:列表 (Desktop: 70%, Mobile: 根据 Tab 显示) */}
  187. <div className={`
  188. flex-col bg-white p-4 md:p-6 overflow-y-auto
  189. ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
  190. md:flex md:w-2/3
  191. `}>
  192. <h4 className="text-sm font-bold text-slate-800 mb-4 hidden md:block">现有路由列表</h4>
  193. {loading ? (
  194. <div className="text-center py-20 text-slate-400"><Loader2 className="animate-spin mx-auto mb-2" /> 加载中...</div>
  195. ) : routings.length === 0 ? (
  196. <div className="text-center py-20 border-2 border-dashed border-slate-100 rounded-lg text-slate-400">
  197. 暂无路由配置,请添加。
  198. </div>
  199. ) : (
  200. <div className="space-y-4">
  201. {routings.map((r) => (
  202. <div key={r.id} className="border border-slate-200 rounded-lg p-4 hover:border-blue-300 transition group">
  203. <div className="flex justify-between items-start mb-3">
  204. <div>
  205. <div className="flex items-center gap-2 flex-wrap">
  206. <span className="font-mono text-sm font-bold text-blue-700 bg-blue-50 px-2 py-0.5 rounded">
  207. {r.routing_key}
  208. </span>
  209. <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
  210. v{r.script_version}
  211. </span>
  212. <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
  213. P{r.priority}
  214. </span>
  215. </div>
  216. <div className="text-xs text-slate-400 mt-1 font-mono">ID: {r.id}</div>
  217. </div>
  218. <button
  219. onClick={() => handleDeleteRouting(r.id)}
  220. className="text-slate-400 hover:text-red-600 p-2 rounded-md hover:bg-red-50 transition"
  221. title="删除此规则"
  222. >
  223. <Trash2 size={18} />
  224. </button>
  225. </div>
  226. {/* Config Preview */}
  227. <div className="bg-slate-50 rounded p-3 text-xs font-mono text-slate-600 break-all border border-slate-100 max-h-32 overflow-y-auto">
  228. {typeof r.config === 'string' ? r.config : JSON.stringify(r.config, null, 2)}
  229. </div>
  230. </div>
  231. ))}
  232. </div>
  233. )}
  234. </div>
  235. </div>
  236. </div>
  237. </div>
  238. );
  239. }