diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx
index 2bf1a262..c2f61a18 100644
--- a/frontend/src/pages/clients/ClientsPage.tsx
+++ b/frontend/src/pages/clients/ClientsPage.tsx
@@ -687,6 +687,7 @@ export default function ClientsPage() {
} onClick={onAdd}>
diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx
index 0444e839..ef3ad247 100644
--- a/frontend/src/pages/inbounds/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/InboundFormModal.tsx
@@ -701,6 +701,9 @@ export default function InboundFormModal({
}, [t, refresh, parseAdvancedSliceWithLabel, messageApi]);
const handleTabChange = (next: string) => {
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
if (activeTabKey === 'advanced' && next !== 'advanced') {
if (!applyAdvancedJsonToBasic()) return;
}
diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx
index 5f2ee688..141e59dc 100644
--- a/frontend/src/pages/settings/SettingsPage.tsx
+++ b/frontend/src/pages/settings/SettingsPage.tsx
@@ -320,12 +320,14 @@ export default function SettingsPage() {
-
+
+
+
>
diff --git a/frontend/src/pages/xray/BalancerFormModal.tsx b/frontend/src/pages/xray/BalancerFormModal.tsx
index e1fd29e3..8008ab40 100644
--- a/frontend/src/pages/xray/BalancerFormModal.tsx
+++ b/frontend/src/pages/xray/BalancerFormModal.tsx
@@ -34,10 +34,10 @@ export default function BalancerFormModal({
onConfirm,
}: BalancerFormModalProps) {
const { t } = useTranslation();
- const [tag, setTag] = useState('');
- const [strategy, setStrategy] = useState('random');
- const [selector, setSelector] = useState([]);
- const [fallbackTag, setFallbackTag] = useState('');
+ const [tag, setTag] = useState(() => balancer?.tag || '');
+ const [strategy, setStrategy] = useState(() => balancer?.strategy || 'random');
+ const [selector, setSelector] = useState(() => [...(balancer?.selector || [])]);
+ const [fallbackTag, setFallbackTag] = useState(() => balancer?.fallbackTag || '');
const isEdit = balancer != null;
@@ -98,6 +98,7 @@ export default function BalancerFormModal({
cancelText={t('close')}
okButtonProps={{ disabled: !isValid }}
mask={{ closable: false }}
+ destroyOnHidden
onOk={submit}
onCancel={onClose}
>
diff --git a/frontend/src/pages/xray/OutboundFormModal.tsx b/frontend/src/pages/xray/OutboundFormModal.tsx
index 58ce3747..6ffb1703 100644
--- a/frontend/src/pages/xray/OutboundFormModal.tsx
+++ b/frontend/src/pages/xray/OutboundFormModal.tsx
@@ -138,6 +138,9 @@ export default function OutboundFormModal({
}
function onTabChange(key: string) {
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
if (revertingTabRef.current) {
revertingTabRef.current = false;
setActiveKey(key);
diff --git a/frontend/src/pages/xray/RoutingTab.tsx b/frontend/src/pages/xray/RoutingTab.tsx
index 57882c5a..66916e72 100644
--- a/frontend/src/pages/xray/RoutingTab.tsx
+++ b/frontend/src/pages/xray/RoutingTab.tsx
@@ -76,7 +76,12 @@ export default function RoutingTab({
const [editingIndex, setEditingIndex] = useState(null);
const [draggedIndex, setDraggedIndex] = useState(null);
const [dropTargetIndex, setDropTargetIndex] = useState(null);
- const dragStateRef = useRef<{ startY: number; moved: boolean }>({ startY: 0, moved: false });
+ const dragRef = useRef<{ from: number | null; to: number | null; startY: number; moved: boolean }>({
+ from: null,
+ to: null,
+ startY: 0,
+ moved: false,
+ });
const rules = useMemo(
() => (templateSettings?.routing?.rules || []) as RoutingRule[],
@@ -214,43 +219,49 @@ export default function RoutingTab({
function onHandlePointerDown(idx: number, ev: React.PointerEvent) {
if (ev.button != null && ev.button !== 0) return;
ev.preventDefault();
+ try {
+ (ev.currentTarget as Element).setPointerCapture(ev.pointerId);
+ } catch { /* ignore */ }
+ dragRef.current = { from: idx, to: idx, startY: ev.clientY, moved: false };
setDraggedIndex(idx);
setDropTargetIndex(idx);
- dragStateRef.current = { startY: ev.clientY, moved: false };
- document.addEventListener('pointermove', onDragMove);
- document.addEventListener('pointerup', onDragUp);
- document.addEventListener('pointercancel', onDragUp);
- }
- function onDragMove(ev: PointerEvent) {
- setDraggedIndex((dragged) => {
- if (dragged == null) return dragged;
- const state = dragStateRef.current;
- if (!state.moved && Math.abs(ev.clientY - state.startY) < 5) return dragged;
+
+ const onMove = (e: PointerEvent) => {
+ const state = dragRef.current;
+ if (state.from == null) return;
+ if (!state.moved && Math.abs(e.clientY - state.startY) < 5) return;
state.moved = true;
- const el = document.elementFromPoint(ev.clientX, ev.clientY);
- if (!el) return dragged;
+ const el = document.elementFromPoint(e.clientX, e.clientY);
+ if (!el) return;
const target = el.closest('[data-row-key]');
- if (!target) return dragged;
- const idx = Number(target.getAttribute('data-row-key'));
- if (Number.isFinite(idx)) setDropTargetIndex(idx);
- return dragged;
- });
- }
- function onDragUp() {
- document.removeEventListener('pointermove', onDragMove);
- document.removeEventListener('pointerup', onDragUp);
- document.removeEventListener('pointercancel', onDragUp);
- const from = draggedIndex;
- const to = dropTargetIndex;
- setDraggedIndex(null);
- setDropTargetIndex(null);
- if (!dragStateRef.current.moved || from == null || to == null || from === to) return;
- mutate((tt) => {
- const list = tt.routing?.rules;
- if (!list) return;
- const [moved] = list.splice(from, 1);
- list.splice(to, 0, moved);
- });
+ if (!target) return;
+ const newIdx = Number(target.getAttribute('data-row-key'));
+ if (Number.isFinite(newIdx) && newIdx !== state.to) {
+ state.to = newIdx;
+ setDropTargetIndex(newIdx);
+ }
+ };
+
+ const onUp = () => {
+ document.removeEventListener('pointermove', onMove);
+ document.removeEventListener('pointerup', onUp);
+ document.removeEventListener('pointercancel', onUp);
+ const { from, to, moved } = dragRef.current;
+ dragRef.current = { from: null, to: null, startY: 0, moved: false };
+ setDraggedIndex(null);
+ setDropTargetIndex(null);
+ if (!moved || from == null || to == null || from === to) return;
+ mutate((tt) => {
+ const list = tt.routing?.rules;
+ if (!list) return;
+ const [movedItem] = list.splice(from, 1);
+ list.splice(to, 0, movedItem);
+ });
+ };
+
+ document.addEventListener('pointermove', onMove);
+ document.addEventListener('pointerup', onUp);
+ document.addEventListener('pointercancel', onUp);
}
function ruleCriteriaChips(rule: RuleRow) {
diff --git a/frontend/src/pages/xray/XrayPage.tsx b/frontend/src/pages/xray/XrayPage.tsx
index 37bbf741..cc3d34ba 100644
--- a/frontend/src/pages/xray/XrayPage.tsx
+++ b/frontend/src/pages/xray/XrayPage.tsx
@@ -305,6 +305,7 @@ export default function XrayPage() {
+
+
)}
diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js
index dd8cb6a2..5788acb7 100644
--- a/frontend/src/utils/index.js
+++ b/frontend/src/utils/index.js
@@ -238,7 +238,7 @@ export class ObjectUtil {
}
static isArrEmpty(arr) {
- return !this.isEmpty(arr) && arr.length === 0;
+ return !Array.isArray(arr) || arr.length === 0;
}
static copyArr(dest, src) {