| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- import { Loader2, Trash2, Plus, Save, X, Network } from 'lucide-react';
- import JsonEditor from '@/components/common/JsonEditor';
- interface RoutingManagerProps {
- productId: number | null;
- productTitle: string;
- isOpen: boolean;
- onClose: () => void;
- }
- interface ProductRouting {
- id: number;
- routing_key: string;
- script_version: string;
- priority: number;
- config?: any; // JSON object or string
- meta?: any;
- }
- export default function RoutingManager({ productId, productTitle, isOpen, onClose }: RoutingManagerProps) {
- const [routings, setRoutings] = useState<ProductRouting[]>([]);
- const [loading, setLoading] = useState(false);
- const [submitting, setSubmitting] = useState(false);
-
- // 移动端 Tab 状态: 'list' | 'add'
- const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
- // 新增 Routing 的表单状态
- const [newRouting, setNewRouting] = useState({
- routing_key: '',
- script_version: 'latest',
- priority: 10,
- config: '{}',
- });
- // 加载路由列表
- useEffect(() => {
- if (isOpen && productId) {
- fetchRoutings();
- setActiveTab('list'); // 默认显示列表
- }
- }, [isOpen, productId]);
- const fetchRoutings = async () => {
- setLoading(true);
- try {
- const res = await api.get('/api/vas/product_routing/list', { params: { product_id: productId } });
- setRoutings(Array.isArray(res.data.data) ? res.data.data : []);
- } catch (e) {
- console.warn("Fetch routings failed");
- setRoutings([]);
- } finally {
- setLoading(false);
- }
- };
- const handleAddRouting = async () => {
- if (!newRouting.routing_key) return alert("Routing Key 必填");
-
- setSubmitting(true);
- try {
- let configObj = {};
- try {
- configObj = JSON.parse(newRouting.config);
- } catch (err) {
- alert("Config JSON 格式错误,请检查");
- setSubmitting(false);
- return;
- }
- await api.post('/api/vas/product_routing/create', {
- ...newRouting,
- config: configObj,
- product_id: productId
- });
-
- alert("路由添加成功");
- fetchRoutings();
- setNewRouting({ routing_key: '', script_version: 'latest', priority: 10, config: '{}' });
- setActiveTab('list'); // 提交成功后切回列表
- } catch (e: any) {
- alert("添加失败: " + (e.response?.data?.message || e.message));
- } finally {
- setSubmitting(false);
- }
- };
- const handleDeleteRouting = async (id: number) => {
- if (!confirm("确定删除此路由配置吗?")) return;
- try {
- await api.delete('/api/vas/product_routing/delete', {params: {"id": id}});
- fetchRoutings();
- } catch (e) {
- alert("删除失败");
- }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
- {/*
- 容器:
- - 移动端:w-full h-full (全屏), rounded-none
- - 桌面端:max-w-5xl h-[90vh], rounded-xl
- */}
- <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">
-
- {/* Header */}
- <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">
- <div className="flex items-center gap-3">
- <div className="bg-purple-100 p-2 rounded-lg text-purple-600 hidden md:block">
- <Network size={24} />
- </div>
- <div>
- <h3 className="font-bold text-gray-900 text-base md:text-lg">路由策略配置</h3>
- <p className="text-xs md:text-sm text-gray-500">商品: <span className="font-medium text-gray-700">{productTitle}</span></p>
- </div>
- </div>
- <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition p-1">
- <X size={24} />
- </button>
- </div>
- {/* Mobile Tabs */}
- <div className="flex md:hidden border-b border-gray-200">
- <button
- onClick={() => setActiveTab('list')}
- 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'}`}
- >
- 规则列表 ({routings.length})
- </button>
- <button
- onClick={() => setActiveTab('add')}
- 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'}`}
- >
- 添加新规则
- </button>
- </div>
- {/* Content */}
- <div className="flex flex-1 overflow-hidden relative">
-
- {/* 左侧:添加表单 (Desktop: 30%, Mobile: 根据 Tab 显示) */}
- <div className={`
- flex-col bg-slate-50 overflow-y-auto border-r border-slate-200 p-5
- ${activeTab === 'add' ? 'flex w-full' : 'hidden'}
- md:flex md:w-1/3
- `}>
- <h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
- <Plus size={16} /> 添加新规则
- </h4>
-
- <div className="space-y-4">
- <div>
- <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Routing Key (Queue)</label>
- <input
- type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
- placeholder="e.g. fr_visabot_vip"
- value={newRouting.routing_key}
- onChange={e => setNewRouting({...newRouting, routing_key: e.target.value})}
- />
- </div>
-
- <div className="grid grid-cols-2 gap-3">
- <div>
- <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Version</label>
- <input
- type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
- value={newRouting.script_version}
- onChange={e => setNewRouting({...newRouting, script_version: e.target.value})}
- />
- </div>
- <div>
- <label className="text-xs font-bold text-slate-500 mb-1 block uppercase">Priority</label>
- <input
- type="number" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
- value={newRouting.priority}
- onChange={e => setNewRouting({...newRouting, priority: parseInt(e.target.value)})}
- />
- </div>
- </div>
- {/* JSON Config Editor */}
- <div className="pt-2">
- <JsonEditor
- label="Config (JSON 参数)"
- value={newRouting.config}
- onChange={(val) => setNewRouting({...newRouting, config: val})}
- height="h-64"
- placeholder='{"headless": true, "timeout": 30000}'
- />
- </div>
- <button
- onClick={handleAddRouting}
- disabled={submitting}
- 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"
- >
- {submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
- 保存规则
- </button>
- </div>
- </div>
- {/* 右侧:列表 (Desktop: 70%, Mobile: 根据 Tab 显示) */}
- <div className={`
- flex-col bg-white p-4 md:p-6 overflow-y-auto
- ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
- md:flex md:w-2/3
- `}>
- <h4 className="text-sm font-bold text-slate-800 mb-4 hidden md:block">现有路由列表</h4>
-
- {loading ? (
- <div className="text-center py-20 text-slate-400"><Loader2 className="animate-spin mx-auto mb-2" /> 加载中...</div>
- ) : routings.length === 0 ? (
- <div className="text-center py-20 border-2 border-dashed border-slate-100 rounded-lg text-slate-400">
- 暂无路由配置,请添加。
- </div>
- ) : (
- <div className="space-y-4">
- {routings.map((r) => (
- <div key={r.id} className="border border-slate-200 rounded-lg p-4 hover:border-blue-300 transition group">
- <div className="flex justify-between items-start mb-3">
- <div>
- <div className="flex items-center gap-2 flex-wrap">
- <span className="font-mono text-sm font-bold text-blue-700 bg-blue-50 px-2 py-0.5 rounded">
- {r.routing_key}
- </span>
- <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
- v{r.script_version}
- </span>
- <span className="text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
- P{r.priority}
- </span>
- </div>
- <div className="text-xs text-slate-400 mt-1 font-mono">ID: {r.id}</div>
- </div>
- <button
- onClick={() => handleDeleteRouting(r.id)}
- className="text-slate-400 hover:text-red-600 p-2 rounded-md hover:bg-red-50 transition"
- title="删除此规则"
- >
- <Trash2 size={18} />
- </button>
- </div>
-
- {/* Config Preview */}
- <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">
- {typeof r.config === 'string' ? r.config : JSON.stringify(r.config, null, 2)}
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- }
|