File size: 9,819 Bytes
a21c316 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { X, RefreshCw, Link2, Unlink } from 'lucide-react';
import { request } from '../../../utils/request';
import { showToast } from '../../common/ToastContainer';
import { useAccountStore } from '../../../stores/useAccountStore';
import { ProxyEntry } from '../../../types/config';
interface ProxyBindingManagerProps {
isOpen: boolean;
onClose: () => void;
proxies: ProxyEntry[];
}
export default function ProxyBindingManager({ isOpen, onClose, proxies }: ProxyBindingManagerProps) {
const { t } = useTranslation();
const { accounts, fetchAccounts } = useAccountStore();
const [bindings, setBindings] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState<string | null>(null);
// Filter enabled proxies for selection
const availableProxies = proxies.filter(p => p.enabled);
useEffect(() => {
if (isOpen) {
refreshData();
}
}, [isOpen]);
const refreshData = async () => {
setIsLoading(true);
try {
await fetchAccounts();
const currentBindings = await request<Record<string, string>>('get_all_account_bindings');
setBindings(currentBindings || {});
} catch (error) {
console.error('Failed to load bindings:', error);
showToast(t('settings.proxy_pool.binding.load_failed', 'Failed to load bindings'), 'error');
} finally {
setIsLoading(false);
}
};
const handleBind = async (accountId: string, proxyId: string) => {
setIsSaving(accountId);
try {
if (proxyId === '') {
// Unbind
await request('unbind_account_proxy', { accountId });
const newBindings = { ...bindings };
delete newBindings[accountId];
setBindings(newBindings);
showToast(t('settings.proxy_pool.binding.unbind_success', 'Unbound successfully'), 'success');
} else {
// Bind
await request('bind_account_proxy', { accountId, proxyId });
setBindings({ ...bindings, [accountId]: proxyId });
showToast(t('settings.proxy_pool.binding.bind_success', 'Bound successfully'), 'success');
}
} catch (error) {
console.error('Failed to update binding:', error);
showToast(t('settings.proxy_pool.binding.update_failed', 'Failed to update binding'), 'error');
} finally {
setIsSaving(null);
}
};
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Link2 className="w-5 h-5" />
{t('settings.proxy_pool.binding.title', 'Account Proxy Bindings')}
</h3>
<div className="flex items-center gap-2">
<button
onClick={refreshData}
disabled={isLoading}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title={t('common.refresh', 'Refresh')}
>
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
</button>
<button
onClick={onClose}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<X size={20} />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
{isLoading && accounts.length === 0 ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="space-y-3">
<div className="grid grid-cols-12 gap-4 px-3 py-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
<div className="col-span-5">{t('settings.account.email', 'Email')}</div>
<div className="col-span-7">{t('settings.proxy_pool.binding.assigned_proxy', 'Assigned Proxy')}</div>
</div>
{accounts.map(account => {
const currentProxyId = bindings[account.id] || '';
return (
<div key={account.id} className="grid grid-cols-12 gap-4 items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors">
<div className="col-span-5 truncate font-medium text-gray-900 dark:text-gray-200" title={account.email}>
{account.email}
</div>
<div className="col-span-7 relative">
<select
value={currentProxyId}
onChange={(e) => handleBind(account.id, e.target.value)}
disabled={isSaving === account.id}
className={`w-full appearance-none pl-3 pr-8 py-2 border rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white transition-colors
${bindings[account.id]
? 'border-blue-300 dark:border-blue-700 ring-1 ring-blue-100 dark:ring-blue-900/20'
: 'border-gray-300 dark:border-gray-600'
}`}
>
<option value="">{t('settings.proxy_pool.binding.default_strategy', 'Default (Follow Strategy)')}</option>
<optgroup label={t('settings.proxy_pool.proxies', 'Proxies')}>
{availableProxies.map(proxy => (
<option key={proxy.id} value={proxy.id}>
{proxy.name || proxy.url}
</option>
))}
</optgroup>
</select>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none text-gray-500">
{isSaving === account.id ? (
<div className="animate-spin h-4 w-4 border-2 border-gray-500 border-t-transparent rounded-full" />
) : (
bindings[account.id] ? <Link2 size={16} className="text-blue-500" /> : <Unlink size={16} className="opacity-50" />
)}
</div>
</div>
</div>
);
})}
{accounts.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
{t('settings.account.no_accounts', 'No accounts found')}
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-xl flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
{t('common.close', 'Close')}
</button>
</div>
</div>
</div>,
document.body
);
}
|