mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-24 13:36:24 +00:00
8f408d2d6a
* feat(routing): show tag (remark) in routing rules list Rules table and mobile cards showed raw inboundTag while the form already used remarks. Display "tag (remark)" when a remark exists; saved rules still store tags only. Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> * feat(inbounds): show "tag (remark)" consistently wherever an inbound is listed Add a shared formatInboundLabel/formatInboundTag helper and apply the "tag (remark)" format across the routing rules table, mobile cards, the rule form and route tester, plus the client attach/detach/filter modals and the attached-inbounds column. Falls back to the bare tag when no distinct remark exists. Also fix the routing rules list mis-rendering inbounds whose remark contains a comma: formatted entries are now carried as an array end to end instead of being joined and re-split on commas. --------- Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Alert, Modal, Select, Typography, message } from 'antd';
|
|
|
|
import type { InboundOption } from '@/hooks/useClients';
|
|
import { formatInboundLabel } from '@/lib/inbounds/label';
|
|
import type { BulkAttachResult } from '@/schemas/client';
|
|
|
|
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
|
|
|
|
interface BulkAttachInboundsModalProps {
|
|
open: boolean;
|
|
count: number;
|
|
inbounds: InboundOption[];
|
|
onOpenChange: (open: boolean) => void;
|
|
onSubmit: (inboundIds: number[]) => Promise<BulkAttachResult | null>;
|
|
}
|
|
|
|
export default function BulkAttachInboundsModal({
|
|
open,
|
|
count,
|
|
inbounds,
|
|
onOpenChange,
|
|
onSubmit,
|
|
}: BulkAttachInboundsModalProps) {
|
|
const { t } = useTranslation();
|
|
const [messageApi, messageContextHolder] = message.useMessage();
|
|
const [targetIds, setTargetIds] = useState<number[]>([]);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) setTargetIds([]);
|
|
}, [open]);
|
|
|
|
const targetOptions = useMemo(() => {
|
|
return (inbounds || [])
|
|
.filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
|
|
.map((ib) => ({
|
|
value: ib.id,
|
|
label: formatInboundLabel(ib.tag, ib.remark),
|
|
}));
|
|
}, [inbounds]);
|
|
|
|
async function submit() {
|
|
if (targetIds.length === 0 || count === 0) return;
|
|
setSubmitting(true);
|
|
try {
|
|
const result = await onSubmit(targetIds);
|
|
if (!result) return;
|
|
const attached = result.attached?.length ?? 0;
|
|
const skipped = result.skipped?.length ?? 0;
|
|
const errors = result.errors?.length ?? 0;
|
|
if (errors > 0) {
|
|
messageApi.warning(
|
|
t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }),
|
|
);
|
|
} else {
|
|
messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped }));
|
|
}
|
|
onOpenChange(false);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{messageContextHolder}
|
|
<Modal
|
|
open={open}
|
|
title={t('pages.clients.attachToInboundsTitle', { count })}
|
|
okText={t('pages.inbounds.attachClients')}
|
|
cancelText={t('cancel')}
|
|
okButtonProps={{ disabled: targetIds.length === 0, loading: submitting }}
|
|
onCancel={() => onOpenChange(false)}
|
|
onOk={submit}
|
|
destroyOnHidden
|
|
>
|
|
<Typography.Paragraph type="secondary">
|
|
{t('pages.clients.attachToInboundsDesc', { count })}
|
|
</Typography.Paragraph>
|
|
{targetOptions.length === 0 ? (
|
|
<Alert type="info" showIcon message={t('pages.clients.attachToInboundsNoTargets')} />
|
|
) : (
|
|
<Select
|
|
mode="multiple"
|
|
style={{ width: '100%' }}
|
|
value={targetIds}
|
|
onChange={setTargetIds}
|
|
options={targetOptions}
|
|
placeholder={t('pages.clients.attachToInboundsTargets')}
|
|
optionFilterProp="label"
|
|
autoFocus
|
|
/>
|
|
)}
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|