mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
fix(frontend): hover cards, balancer load, routing dnd, modal a11y, outbound crash
- ClientsPage/SettingsPage/XrayPage: add hoverable to bottom card/tabs so hover affordance matches the top card - BalancerFormModal: lazy-init useState from props + destroyOnHidden so the form mounts with saved values instead of relying on a useEffect sync that could miss the first open - RoutingTab: rewrite pointer drag — handlers are now defined inside the pointerdown closure so addEventListener/removeEventListener match; drag state lives on a ref (from/to/moved) so onUp reads the real indices, not stale closure values. Adds setPointerCapture so Windows and touch keep delivering events when the cursor leaves the handle. - OutboundFormModal/InboundFormModal: blur the focused input before switching tabs to silence the aria-hidden-on-focused-element warning - utils.isArrEmpty: return true for undefined/null arrays — the old form treated undefined as "not empty" which crashed VLESSSettings.fromJson when json.vnext was missing
This commit is contained in:
parent
2a96ac9721
commit
20a3d00bf1
8 changed files with 68 additions and 45 deletions
|
|
@ -687,6 +687,7 @@ export default function ClientsPage() {
|
|||
<Col span={24}>
|
||||
<Card
|
||||
size="small"
|
||||
hoverable
|
||||
title={
|
||||
<div className="card-toolbar">
|
||||
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={onAdd}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,12 +320,14 @@ export default function SettingsPage() {
|
|||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
activeKey={activeTabKey}
|
||||
onChange={onTabChange}
|
||||
className={isMobile ? 'icons-only' : ''}
|
||||
items={tabItems}
|
||||
/>
|
||||
<Card hoverable>
|
||||
<Tabs
|
||||
activeKey={activeTabKey}
|
||||
onChange={onTabChange}
|
||||
className={isMobile ? 'icons-only' : ''}
|
||||
items={tabItems}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
const [fallbackTag, setFallbackTag] = useState('');
|
||||
const [tag, setTag] = useState(() => balancer?.tag || '');
|
||||
const [strategy, setStrategy] = useState(() => balancer?.strategy || 'random');
|
||||
const [selector, setSelector] = useState<string[]>(() => [...(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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,12 @@ export default function RoutingTab({
|
|||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(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) {
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@ export default function XrayPage() {
|
|||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Card hoverable>
|
||||
<Tabs
|
||||
activeKey={activeTabKey}
|
||||
onChange={onTabChange}
|
||||
|
|
@ -444,6 +445,7 @@ export default function XrayPage() {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue