feat(inbounds): add multi-select and bulk delete

Mirror the clients page: checkbox selection on the desktop table and on
mobile cards, with a danger Delete button in the toolbar that removes all
selected inbounds in one call.

Backend adds POST /panel/api/inbounds/bulkDel, which loops the existing
DelInbound per id (xray restarts at most once) and returns {deleted,
skipped}. Frontend shows a confirm modal plus a result toast, clears the
selection on success, adds bulk-delete i18n keys across all 13 languages,
and documents the endpoint in the in-panel API docs.
This commit is contained in:
MHSanaei 2026-05-31 00:29:24 +02:00
parent 6bb5a3b56b
commit cf50952921
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
21 changed files with 315 additions and 4 deletions

View file

@ -615,6 +615,65 @@
} }
} }
}, },
"/panel/api/inbounds/bulkDel": {
"post": {
"tags": [
"Inbounds"
],
"summary": "Delete many inbounds in one call. Processes the list sequentially; failures are reported per id and the rest still proceed. Restarts xray at most once.",
"operationId": "post_panel_api_inbounds_bulkDel",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object"
},
"example": {
"ids": [
1,
2,
3
]
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"msg": {
"type": "string"
},
"obj": {}
}
},
"example": {
"success": true,
"obj": {
"deleted": 2,
"skipped": [
{
"id": 3,
"reason": "..."
}
]
}
}
}
}
}
}
}
},
"/panel/api/inbounds/update/{id}": { "/panel/api/inbounds/update/{id}": {
"post": { "post": {
"tags": [ "tags": [

View file

@ -149,6 +149,13 @@ export const sections: readonly Section[] = [
{ name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' },
], ],
}, },
{
method: 'POST',
path: '/panel/api/inbounds/bulkDel',
summary: 'Delete many inbounds in one call. Processes the list sequentially; failures are reported per id and the rest still proceed. Restarts xray at most once.',
body: '{\n "ids": [1, 2, 3]\n}',
response: '{\n "success": true,\n "obj": {\n "deleted": 2,\n "skipped": [\n { "id": 3, "reason": "..." }\n ]\n }\n}',
},
{ {
method: 'POST', method: 'POST',
path: '/panel/api/inbounds/update/:id', path: '/panel/api/inbounds/update/:id',

View file

@ -357,6 +357,36 @@ export default function InboundsPage() {
}); });
}, [modal, refresh, t]); }, [modal, refresh, t]);
const confirmBulkDelete = useCallback((ids: number[]) => new Promise<boolean>((resolve) => {
if (ids.length === 0) {
resolve(false);
return;
}
modal.confirm({
title: t('pages.inbounds.bulkDeleteConfirmTitle', { count: ids.length }),
content: t('pages.inbounds.bulkDeleteConfirmContent'),
okText: t('delete'),
okType: 'danger',
cancelText: t('cancel'),
onOk: async () => {
const msg = await HttpUtil.post('/panel/api/inbounds/bulkDel', { ids }, { headers: { 'Content-Type': 'application/json' } });
const obj = (msg?.obj ?? {}) as { deleted?: number; skipped?: { id: number; reason: string }[] };
const ok = obj.deleted ?? 0;
const skipped = obj.skipped ?? [];
if (msg?.success && skipped.length === 0) {
messageApi.success(t('pages.inbounds.toasts.bulkDeleted', { count: ok }));
} else {
const firstError = skipped[0]?.reason ?? msg?.msg ?? '';
const base = t('pages.inbounds.toasts.bulkDeletedMixed', { ok, failed: skipped.length });
messageApi.warning(firstError ? `${base}${firstError}` : base);
}
await refresh();
resolve(true);
},
onCancel: () => resolve(false),
});
}), [modal, refresh, t, messageApi]);
const confirmResetTraffic = useCallback((dbInbound: DBInbound) => { const confirmResetTraffic = useCallback((dbInbound: DBInbound) => {
modal.confirm({ modal.confirm({
title: t('pages.inbounds.resetConfirmTitle', { remark: dbInbound.remark }), title: t('pages.inbounds.resetConfirmTitle', { remark: dbInbound.remark }),
@ -567,6 +597,7 @@ export default function InboundsPage() {
onAddInbound={onAddInbound} onAddInbound={onAddInbound}
onGeneralAction={onGeneralAction} onGeneralAction={onGeneralAction}
onRowAction={({ key, dbInbound }) => onRowAction({ key, dbInbound: dbInbound as unknown as DBInbound })} onRowAction={({ key, dbInbound }) => onRowAction({ key, dbInbound: dbInbound as unknown as DBInbound })}
onBulkDelete={confirmBulkDelete}
/> />
</Col> </Col>
</Row> </Row>

View file

@ -75,6 +75,26 @@
gap: 8px; gap: 8px;
} }
.inbound-card.is-selected {
border-color: var(--ant-color-primary);
background: color-mix(in srgb, var(--ant-color-primary) 6%, transparent);
}
.card-bulk-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 4px 8px;
}
.bulk-count {
font-size: 12px;
background: color-mix(in srgb, var(--ant-color-primary) 12%, transparent);
color: var(--ant-color-primary);
padding: 1px 8px;
border-radius: 10px;
}
.card-head { .card-head {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -1,12 +1,14 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState, type Key } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
Button, Button,
Card, Card,
Checkbox,
Dropdown, Dropdown,
Space, Space,
Switch, Switch,
Table, Table,
Tag,
Tooltip, Tooltip,
type MenuProps, type MenuProps,
} from 'antd'; } from 'antd';
@ -18,6 +20,7 @@ import {
ImportOutlined, ImportOutlined,
ReloadOutlined, ReloadOutlined,
InfoCircleOutlined, InfoCircleOutlined,
DeleteOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { HttpUtil } from '@/utils'; import { HttpUtil } from '@/utils';
@ -43,11 +46,13 @@ export default function InboundList({
onAddInbound, onAddInbound,
onGeneralAction, onGeneralAction,
onRowAction, onRowAction,
onBulkDelete,
}: InboundListProps) { }: InboundListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [sortKey, setSortKey] = useState<SortKey | null>(null); const [sortKey, setSortKey] = useState<SortKey | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null); const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const [statsRecord, setStatsRecord] = useState<DBInboundRecord | null>(null); const [statsRecord, setStatsRecord] = useState<DBInboundRecord | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => { const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => {
const previous = dbInbound.enable; const previous = dbInbound.enable;
@ -75,6 +80,26 @@ export default function InboundList({
[dbInbounds], [dbInbounds],
); );
const toggleSelect = useCallback((id: number, checked: boolean) => {
setSelectedRowKeys((prev) => {
const next = new Set(prev);
if (checked) next.add(id); else next.delete(id);
return Array.from(next);
});
}, []);
const selectAll = useCallback((checked: boolean) => {
setSelectedRowKeys(checked ? sortedInbounds.map((i) => i.id) : []);
}, [sortedInbounds]);
const allSelected = sortedInbounds.length > 0 && selectedRowKeys.length === sortedInbounds.length;
const someSelected = selectedRowKeys.length > 0 && selectedRowKeys.length < sortedInbounds.length;
const handleBulkDelete = useCallback(async () => {
const ok = await onBulkDelete(selectedRowKeys);
if (ok) setSelectedRowKeys([]);
}, [onBulkDelete, selectedRowKeys]);
const columns = useInboundColumns({ const columns = useInboundColumns({
hasAnyRemark, hasAnyRemark,
hasActiveNode, hasActiveNode,
@ -119,6 +144,16 @@ export default function InboundList({
{!isMobile && t('pages.inbounds.generalActions')} {!isMobile && t('pages.inbounds.generalActions')}
</Button> </Button>
</Dropdown> </Dropdown>
{selectedRowKeys.length > 0 && (
<>
<Tag color="blue" closable onClose={() => setSelectedRowKeys([])} style={{ marginInlineEnd: 0 }}>
{t('pages.inbounds.selectedCount', { count: selectedRowKeys.length })}
</Tag>
<Button danger icon={<DeleteOutlined />} onClick={handleBulkDelete}>
{!isMobile && t('delete')}
</Button>
</>
)}
</Space> </Space>
)} )}
> >
@ -131,9 +166,26 @@ export default function InboundList({
<div>{t('noData')}</div> <div>{t('noData')}</div>
</div> </div>
) : ( ) : (
sortedInbounds.map((record) => ( <>
<div key={record.id} className="inbound-card"> <div className="card-bulk-bar">
<Checkbox
checked={allSelected}
indeterminate={someSelected}
onChange={(e) => selectAll(e.target.checked)}
>
{t('pages.inbounds.selectAll')}
</Checkbox>
{selectedRowKeys.length > 0 && (
<span className="bulk-count">{selectedRowKeys.length}</span>
)}
</div>
{sortedInbounds.map((record) => (
<div key={record.id} className={`inbound-card${selectedRowKeys.includes(record.id) ? ' is-selected' : ''}`}>
<div className="card-head"> <div className="card-head">
<Checkbox
checked={selectedRowKeys.includes(record.id)}
onChange={(e) => toggleSelect(record.id, e.target.checked)}
/>
<span className="card-id">#{record.id}</span> <span className="card-id">#{record.id}</span>
<span className="tag-name">{record.remark}</span> <span className="tag-name">{record.remark}</span>
<div className="card-actions" onClick={(e) => e.stopPropagation()}> <div className="card-actions" onClick={(e) => e.stopPropagation()}>
@ -158,7 +210,8 @@ export default function InboundList({
</div> </div>
</div> </div>
</div> </div>
)) ))}
</>
)} )}
</div> </div>
) : ( ) : (
@ -166,6 +219,10 @@ export default function InboundList({
columns={columns} columns={columns}
dataSource={sortedInbounds} dataSource={sortedInbounds}
rowKey={(r) => r.id} rowKey={(r) => r.id}
rowSelection={{
selectedRowKeys,
onChange: (keys: Key[]) => setSelectedRowKeys(keys as number[]),
}}
pagination={paginationFor(sortedInbounds)} pagination={paginationFor(sortedInbounds)}
scroll={{ x: 1000 }} scroll={{ x: 1000 }}
style={{ marginTop: 10 }} style={{ marginTop: 10 }}

View file

@ -72,6 +72,7 @@ export interface InboundListProps {
onAddInbound: () => void; onAddInbound: () => void;
onGeneralAction: (key: GeneralAction) => void; onGeneralAction: (key: GeneralAction) => void;
onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void; onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
onBulkDelete: (ids: number[]) => Promise<boolean>;
} }
export type SortKey = export type SortKey =

View file

@ -69,6 +69,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/add", a.addInbound) g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound) g.POST("/del/:id", a.delInbound)
g.POST("/bulkDel", a.bulkDelInbounds)
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/setEnable/:id", a.setInboundEnable) g.POST("/setEnable/:id", a.setInboundEnable)
g.POST("/:id/resetTraffic", a.resetInboundTraffic) g.POST("/:id/resetTraffic", a.resetInboundTraffic)
@ -179,6 +180,32 @@ func (a *InboundController) delInbound(c *gin.Context) {
notifyClientsChanged() notifyClientsChanged()
} }
type bulkDelInboundsRequest struct {
Ids []int `json:"ids"`
}
// bulkDelInbounds deletes several inbounds in one call. Failures are
// reported per id and the rest still proceed; xray restarts at most once.
func (a *InboundController) bulkDelInbounds(c *gin.Context) {
var req bulkDelInboundsRequest
if err := c.ShouldBindJSON(&req); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
result, needRestart, err := a.inboundService.DelInbounds(req.Ids)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonObj(c, result, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
user := session.GetLoginUser(c)
a.broadcastInboundsUpdate(user.Id)
notifyClientsChanged()
}
// updateInbound updates an existing inbound configuration. // updateInbound updates an existing inbound configuration.
func (a *InboundController) updateInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))

View file

@ -617,6 +617,37 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
return needRestart, db.Delete(model.Inbound{}, id).Error return needRestart, db.Delete(model.Inbound{}, id).Error
} }
type BulkDelInboundResult struct {
Deleted int `json:"deleted"`
Skipped []BulkDelInboundReport `json:"skipped,omitempty"`
}
type BulkDelInboundReport struct {
Id int `json:"id"`
Reason string `json:"reason"`
}
// DelInbounds removes every inbound in the list, reusing the single-delete
// path per id. Failures are recorded in Skipped and processing continues for
// the rest; the aggregated needRestart is returned so the caller restarts
// xray at most once.
func (s *InboundService) DelInbounds(ids []int) (BulkDelInboundResult, bool, error) {
result := BulkDelInboundResult{}
needRestart := false
for _, id := range ids {
r, err := s.DelInbound(id)
if err != nil {
result.Skipped = append(result.Skipped, BulkDelInboundReport{Id: id, Reason: err.Error()})
continue
}
result.Deleted++
if r {
needRestart = true
}
}
return result, needRestart, nil
}
func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
inbound := &model.Inbound{} inbound := &model.Inbound{}

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "سيؤدي هذا إلى إزالة الإدخال وجميع عملائه. لا يمكن التراجع.", "deleteConfirmContent": "سيؤدي هذا إلى إزالة الإدخال وجميع عملائه. لا يمكن التراجع.",
"resetConfirmTitle": "إعادة تعيين ترافيك \"{remark}\"؟", "resetConfirmTitle": "إعادة تعيين ترافيك \"{remark}\"؟",
"resetConfirmContent": "يعيد عدادات الإرسال/الاستقبال لهذا الإدخال إلى 0.", "resetConfirmContent": "يعيد عدادات الإرسال/الاستقبال لهذا الإدخال إلى 0.",
"selectedCount": "{count} محدد",
"selectAll": "تحديد الكل",
"bulkDeleteConfirmTitle": "حذف {count} إدخال؟",
"bulkDeleteConfirmContent": "سيؤدي هذا إلى إزالة الإدخالات المحددة وجميع عملائها. لا يمكن التراجع.",
"cloneConfirmTitle": "نسخ الإدخال \"{remark}\"؟", "cloneConfirmTitle": "نسخ الإدخال \"{remark}\"؟",
"cloneConfirmContent": "ينشئ نسخة بمنفذ جديد وقائمة عملاء فارغة.", "cloneConfirmContent": "ينشئ نسخة بمنفذ جديد وقائمة عملاء فارغة.",
"delAllClients": "حذف جميع العملاء", "delAllClients": "حذف جميع العملاء",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "تم تحديث الواردات بنجاح", "inboundsUpdateSuccess": "تم تحديث الواردات بنجاح",
"inboundUpdateSuccess": "تم تحديث الوارد بنجاح", "inboundUpdateSuccess": "تم تحديث الوارد بنجاح",
"inboundCreateSuccess": "تم إنشاء الوارد بنجاح", "inboundCreateSuccess": "تم إنشاء الوارد بنجاح",
"bulkDeleted": "تم حذف {count} إدخال",
"bulkDeletedMixed": "تم حذف {ok}, وفشل {failed}",
"inboundDeleteSuccess": "تم حذف الوارد بنجاح", "inboundDeleteSuccess": "تم حذف الوارد بنجاح",
"inboundClientAddSuccess": "تمت إضافة عميل(عملاء) وارد", "inboundClientAddSuccess": "تمت إضافة عميل(عملاء) وارد",
"inboundClientDeleteSuccess": "تم حذف عميل وارد", "inboundClientDeleteSuccess": "تم حذف عميل وارد",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "This removes the inbound and all its clients. This cannot be undone.", "deleteConfirmContent": "This removes the inbound and all its clients. This cannot be undone.",
"resetConfirmTitle": "Reset traffic for \"{remark}\"?", "resetConfirmTitle": "Reset traffic for \"{remark}\"?",
"resetConfirmContent": "Resets up/down counters to 0 for this inbound.", "resetConfirmContent": "Resets up/down counters to 0 for this inbound.",
"selectedCount": "{count} selected",
"selectAll": "Select all",
"bulkDeleteConfirmTitle": "Delete {count} inbounds?",
"bulkDeleteConfirmContent": "This removes the selected inbounds and all their clients. This cannot be undone.",
"cloneConfirmTitle": "Clone inbound \"{remark}\"?", "cloneConfirmTitle": "Clone inbound \"{remark}\"?",
"cloneConfirmContent": "Creates a copy with a new port and an empty client list.", "cloneConfirmContent": "Creates a copy with a new port and an empty client list.",
"delAllClients": "Delete All Clients", "delAllClients": "Delete All Clients",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Inbounds have been successfully updated.", "inboundsUpdateSuccess": "Inbounds have been successfully updated.",
"inboundUpdateSuccess": "Inbound has been successfully updated.", "inboundUpdateSuccess": "Inbound has been successfully updated.",
"inboundCreateSuccess": "Inbound has been successfully created.", "inboundCreateSuccess": "Inbound has been successfully created.",
"bulkDeleted": "{count} inbounds deleted",
"bulkDeletedMixed": "{ok} deleted, {failed} failed",
"inboundDeleteSuccess": "Inbound has been successfully deleted.", "inboundDeleteSuccess": "Inbound has been successfully deleted.",
"inboundClientAddSuccess": "Inbound client(s) have been added.", "inboundClientAddSuccess": "Inbound client(s) have been added.",
"inboundClientDeleteSuccess": "Inbound client has been deleted.", "inboundClientDeleteSuccess": "Inbound client has been deleted.",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Esto elimina el inbound y todos sus clientes. No se puede deshacer.", "deleteConfirmContent": "Esto elimina el inbound y todos sus clientes. No se puede deshacer.",
"resetConfirmTitle": "¿Restablecer el tráfico de \"{remark}\"?", "resetConfirmTitle": "¿Restablecer el tráfico de \"{remark}\"?",
"resetConfirmContent": "Restablece los contadores de subida/bajada a 0 para este inbound.", "resetConfirmContent": "Restablece los contadores de subida/bajada a 0 para este inbound.",
"selectedCount": "{count} seleccionado(s)",
"selectAll": "Seleccionar todo",
"bulkDeleteConfirmTitle": "¿Eliminar {count} inbounds?",
"bulkDeleteConfirmContent": "Esto elimina los inbounds seleccionados y todos sus clientes. No se puede deshacer.",
"cloneConfirmTitle": "¿Clonar el inbound \"{remark}\"?", "cloneConfirmTitle": "¿Clonar el inbound \"{remark}\"?",
"cloneConfirmContent": "Crea una copia con un puerto nuevo y una lista de clientes vacía.", "cloneConfirmContent": "Crea una copia con un puerto nuevo y una lista de clientes vacía.",
"delAllClients": "Eliminar todos los clientes", "delAllClients": "Eliminar todos los clientes",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Entradas actualizadas correctamente", "inboundsUpdateSuccess": "Entradas actualizadas correctamente",
"inboundUpdateSuccess": "Entrada actualizada correctamente", "inboundUpdateSuccess": "Entrada actualizada correctamente",
"inboundCreateSuccess": "Entrada creada correctamente", "inboundCreateSuccess": "Entrada creada correctamente",
"bulkDeleted": "{count} inbounds eliminados",
"bulkDeletedMixed": "{ok} eliminados, {failed} fallidos",
"inboundDeleteSuccess": "Entrada eliminada correctamente", "inboundDeleteSuccess": "Entrada eliminada correctamente",
"inboundClientAddSuccess": "Cliente(s) de entrada añadido(s)", "inboundClientAddSuccess": "Cliente(s) de entrada añadido(s)",
"inboundClientDeleteSuccess": "Cliente de entrada eliminado", "inboundClientDeleteSuccess": "Cliente de entrada eliminado",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "این اینباند و تمام کلاینت‌های آن حذف می‌شود. این عمل غیرقابل بازگشت است.", "deleteConfirmContent": "این اینباند و تمام کلاینت‌های آن حذف می‌شود. این عمل غیرقابل بازگشت است.",
"resetConfirmTitle": "ترافیک اینباند «{remark}» صفر شود؟", "resetConfirmTitle": "ترافیک اینباند «{remark}» صفر شود؟",
"resetConfirmContent": "شمارنده‌های ارسال/دریافت این اینباند به صفر برمی‌گردد.", "resetConfirmContent": "شمارنده‌های ارسال/دریافت این اینباند به صفر برمی‌گردد.",
"selectedCount": "{count} انتخاب‌شده",
"selectAll": "انتخاب همه",
"bulkDeleteConfirmTitle": "حذف {count} اینباند؟",
"bulkDeleteConfirmContent": "اینباندهای انتخاب‌شده و تمام کلاینت‌های آن‌ها حذف می‌شوند. این عمل غیرقابل بازگشت است.",
"cloneConfirmTitle": "اینباند «{remark}» کپی شود؟", "cloneConfirmTitle": "اینباند «{remark}» کپی شود؟",
"cloneConfirmContent": "یک نسخه با پورت جدید و لیست کلاینت خالی ساخته می‌شود.", "cloneConfirmContent": "یک نسخه با پورت جدید و لیست کلاینت خالی ساخته می‌شود.",
"delAllClients": "حذف همه کلاینت‌ها", "delAllClients": "حذف همه کلاینت‌ها",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "ورودی‌ها با موفقیت به‌روزرسانی شدند", "inboundsUpdateSuccess": "ورودی‌ها با موفقیت به‌روزرسانی شدند",
"inboundUpdateSuccess": "ورودی با موفقیت به‌روزرسانی شد", "inboundUpdateSuccess": "ورودی با موفقیت به‌روزرسانی شد",
"inboundCreateSuccess": "ورودی با موفقیت ایجاد شد", "inboundCreateSuccess": "ورودی با موفقیت ایجاد شد",
"bulkDeleted": "{count} اینباند حذف شد",
"bulkDeletedMixed": "{ok} حذف، {failed} ناموفق",
"inboundDeleteSuccess": "ورودی با موفقیت حذف شد", "inboundDeleteSuccess": "ورودی با موفقیت حذف شد",
"inboundClientAddSuccess": "کلاینت(های) ورودی اضافه شدند", "inboundClientAddSuccess": "کلاینت(های) ورودی اضافه شدند",
"inboundClientDeleteSuccess": "کلاینت ورودی حذف شد", "inboundClientDeleteSuccess": "کلاینت ورودی حذف شد",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Tindakan ini menghapus inbound beserta semua kliennya. Tidak dapat dibatalkan.", "deleteConfirmContent": "Tindakan ini menghapus inbound beserta semua kliennya. Tidak dapat dibatalkan.",
"resetConfirmTitle": "Reset trafik \"{remark}\"?", "resetConfirmTitle": "Reset trafik \"{remark}\"?",
"resetConfirmContent": "Mengatur ulang counter unggah/unduh ke 0 untuk inbound ini.", "resetConfirmContent": "Mengatur ulang counter unggah/unduh ke 0 untuk inbound ini.",
"selectedCount": "{count} dipilih",
"selectAll": "Pilih semua",
"bulkDeleteConfirmTitle": "Hapus {count} inbound?",
"bulkDeleteConfirmContent": "Tindakan ini menghapus inbound yang dipilih beserta semua kliennya. Tidak dapat dibatalkan.",
"cloneConfirmTitle": "Klon inbound \"{remark}\"?", "cloneConfirmTitle": "Klon inbound \"{remark}\"?",
"cloneConfirmContent": "Membuat salinan dengan port baru dan daftar klien kosong.", "cloneConfirmContent": "Membuat salinan dengan port baru dan daftar klien kosong.",
"delAllClients": "Hapus Semua Klien", "delAllClients": "Hapus Semua Klien",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Inbound berhasil diperbarui", "inboundsUpdateSuccess": "Inbound berhasil diperbarui",
"inboundUpdateSuccess": "Inbound berhasil diperbarui", "inboundUpdateSuccess": "Inbound berhasil diperbarui",
"inboundCreateSuccess": "Inbound berhasil dibuat", "inboundCreateSuccess": "Inbound berhasil dibuat",
"bulkDeleted": "{count} inbound dihapus",
"bulkDeletedMixed": "{ok} dihapus, {failed} gagal",
"inboundDeleteSuccess": "Inbound berhasil dihapus", "inboundDeleteSuccess": "Inbound berhasil dihapus",
"inboundClientAddSuccess": "Klien inbound telah ditambahkan", "inboundClientAddSuccess": "Klien inbound telah ditambahkan",
"inboundClientDeleteSuccess": "Klien inbound telah dihapus", "inboundClientDeleteSuccess": "Klien inbound telah dihapus",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "インバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。", "deleteConfirmContent": "インバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。",
"resetConfirmTitle": "「{remark}」のトラフィックをリセットしますか?", "resetConfirmTitle": "「{remark}」のトラフィックをリセットしますか?",
"resetConfirmContent": "このインバウンドの送受信カウンタを 0 にリセットします。", "resetConfirmContent": "このインバウンドの送受信カウンタを 0 にリセットします。",
"selectedCount": "{count} 選択中",
"selectAll": "すべて選択",
"bulkDeleteConfirmTitle": "{count} 件のインバウンドを削除しますか?",
"bulkDeleteConfirmContent": "選択したインバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。",
"cloneConfirmTitle": "インバウンド「{remark}」を複製しますか?", "cloneConfirmTitle": "インバウンド「{remark}」を複製しますか?",
"cloneConfirmContent": "新しいポートと空のクライアント一覧でコピーを作成します。", "cloneConfirmContent": "新しいポートと空のクライアント一覧でコピーを作成します。",
"delAllClients": "すべてのクライアントを削除", "delAllClients": "すべてのクライアントを削除",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "インバウンドが正常に更新されました", "inboundsUpdateSuccess": "インバウンドが正常に更新されました",
"inboundUpdateSuccess": "インバウンドが正常に更新されました", "inboundUpdateSuccess": "インバウンドが正常に更新されました",
"inboundCreateSuccess": "インバウンドが正常に作成されました", "inboundCreateSuccess": "インバウンドが正常に作成されました",
"bulkDeleted": "{count} 件のインバウンドを削除しました",
"bulkDeletedMixed": "{ok} 件削除、{failed} 件失敗",
"inboundDeleteSuccess": "インバウンドが正常に削除されました", "inboundDeleteSuccess": "インバウンドが正常に削除されました",
"inboundClientAddSuccess": "インバウンドクライアントが追加されました", "inboundClientAddSuccess": "インバウンドクライアントが追加されました",
"inboundClientDeleteSuccess": "インバウンドクライアントが削除されました", "inboundClientDeleteSuccess": "インバウンドクライアントが削除されました",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Isto remove o inbound e todos os seus clientes. Não é possível desfazer.", "deleteConfirmContent": "Isto remove o inbound e todos os seus clientes. Não é possível desfazer.",
"resetConfirmTitle": "Redefinir o tráfego de \"{remark}\"?", "resetConfirmTitle": "Redefinir o tráfego de \"{remark}\"?",
"resetConfirmContent": "Zera os contadores de envio/recebimento para este inbound.", "resetConfirmContent": "Zera os contadores de envio/recebimento para este inbound.",
"selectedCount": "{count} selecionado(s)",
"selectAll": "Selecionar tudo",
"bulkDeleteConfirmTitle": "Excluir {count} inbounds?",
"bulkDeleteConfirmContent": "Isto remove os inbounds selecionados e todos os seus clientes. Não é possível desfazer.",
"cloneConfirmTitle": "Clonar o inbound \"{remark}\"?", "cloneConfirmTitle": "Clonar o inbound \"{remark}\"?",
"cloneConfirmContent": "Cria uma cópia com uma nova porta e lista de clientes vazia.", "cloneConfirmContent": "Cria uma cópia com uma nova porta e lista de clientes vazia.",
"delAllClients": "Excluir todos os clientes", "delAllClients": "Excluir todos os clientes",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Entradas atualizadas com sucesso", "inboundsUpdateSuccess": "Entradas atualizadas com sucesso",
"inboundUpdateSuccess": "Entrada atualizada com sucesso", "inboundUpdateSuccess": "Entrada atualizada com sucesso",
"inboundCreateSuccess": "Entrada criada com sucesso", "inboundCreateSuccess": "Entrada criada com sucesso",
"bulkDeleted": "{count} inbounds excluídos",
"bulkDeletedMixed": "{ok} excluídos, {failed} com falha",
"inboundDeleteSuccess": "Entrada excluída com sucesso", "inboundDeleteSuccess": "Entrada excluída com sucesso",
"inboundClientAddSuccess": "Cliente(s) de entrada adicionado(s)", "inboundClientAddSuccess": "Cliente(s) de entrada adicionado(s)",
"inboundClientDeleteSuccess": "Cliente de entrada excluído", "inboundClientDeleteSuccess": "Cliente de entrada excluído",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Подключение и все его клиенты будут удалены. Это действие нельзя отменить.", "deleteConfirmContent": "Подключение и все его клиенты будут удалены. Это действие нельзя отменить.",
"resetConfirmTitle": "Сбросить трафик \"{remark}\"?", "resetConfirmTitle": "Сбросить трафик \"{remark}\"?",
"resetConfirmContent": "Сбрасывает счётчики отправки/получения этого подключения до 0.", "resetConfirmContent": "Сбрасывает счётчики отправки/получения этого подключения до 0.",
"selectedCount": "{count} выбрано",
"selectAll": "Выбрать всё",
"bulkDeleteConfirmTitle": "Удалить {count} подключений?",
"bulkDeleteConfirmContent": "Выбранные подключения и все их клиенты будут удалены. Это действие нельзя отменить.",
"cloneConfirmTitle": "Клонировать подключение \"{remark}\"?", "cloneConfirmTitle": "Клонировать подключение \"{remark}\"?",
"cloneConfirmContent": "Создаёт копию с новым портом и пустым списком клиентов.", "cloneConfirmContent": "Создаёт копию с новым портом и пустым списком клиентов.",
"delAllClients": "Удалить всех клиентов", "delAllClients": "Удалить всех клиентов",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Подключения успешно обновлены", "inboundsUpdateSuccess": "Подключения успешно обновлены",
"inboundUpdateSuccess": "Подключение успешно обновлено", "inboundUpdateSuccess": "Подключение успешно обновлено",
"inboundCreateSuccess": "Подключение успешно создано", "inboundCreateSuccess": "Подключение успешно создано",
"bulkDeleted": "Удалено подключений: {count}",
"bulkDeletedMixed": "Удалено: {ok}, не удалось: {failed}",
"inboundDeleteSuccess": "Подключение успешно удалено", "inboundDeleteSuccess": "Подключение успешно удалено",
"inboundClientAddSuccess": "Клиент(ы) подключения добавлен(ы)", "inboundClientAddSuccess": "Клиент(ы) подключения добавлен(ы)",
"inboundClientDeleteSuccess": "Клиент подключения удалён", "inboundClientDeleteSuccess": "Клиент подключения удалён",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Bu işlem inbound'u ve tüm istemcilerini siler. Geri alınamaz.", "deleteConfirmContent": "Bu işlem inbound'u ve tüm istemcilerini siler. Geri alınamaz.",
"resetConfirmTitle": "\"{remark}\" trafiği sıfırlansın mı?", "resetConfirmTitle": "\"{remark}\" trafiği sıfırlansın mı?",
"resetConfirmContent": "Bu inbound için gönderme/alma sayaçlarını 0'a sıfırlar.", "resetConfirmContent": "Bu inbound için gönderme/alma sayaçlarını 0'a sıfırlar.",
"selectedCount": "{count} seçildi",
"selectAll": "Tümünü seç",
"bulkDeleteConfirmTitle": "{count} inbound silinsin mi?",
"bulkDeleteConfirmContent": "Bu işlem seçili inbound'ları ve tüm istemcilerini siler. Geri alınamaz.",
"cloneConfirmTitle": "\"{remark}\" inbound klonlansın mı?", "cloneConfirmTitle": "\"{remark}\" inbound klonlansın mı?",
"cloneConfirmContent": "Yeni bir port ve boş istemci listesiyle bir kopya oluşturur.", "cloneConfirmContent": "Yeni bir port ve boş istemci listesiyle bir kopya oluşturur.",
"delAllClients": "Tüm istemcileri sil", "delAllClients": "Tüm istemcileri sil",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Gelen bağlantılar başarıyla güncellendi", "inboundsUpdateSuccess": "Gelen bağlantılar başarıyla güncellendi",
"inboundUpdateSuccess": "Gelen bağlantı başarıyla güncellendi", "inboundUpdateSuccess": "Gelen bağlantı başarıyla güncellendi",
"inboundCreateSuccess": "Gelen bağlantı başarıyla oluşturuldu", "inboundCreateSuccess": "Gelen bağlantı başarıyla oluşturuldu",
"bulkDeleted": "{count} inbound silindi",
"bulkDeletedMixed": "{ok} silindi, {failed} başarısız",
"inboundDeleteSuccess": "Gelen bağlantı başarıyla silindi", "inboundDeleteSuccess": "Gelen bağlantı başarıyla silindi",
"inboundClientAddSuccess": "Gelen bağlantı istemci(leri) eklendi", "inboundClientAddSuccess": "Gelen bağlantı istemci(leri) eklendi",
"inboundClientDeleteSuccess": "Gelen bağlantı istemcisi silindi", "inboundClientDeleteSuccess": "Gelen bağlantı istemcisi silindi",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Це видалить вхідні та всіх його клієнтів. Цю дію неможливо скасувати.", "deleteConfirmContent": "Це видалить вхідні та всіх його клієнтів. Цю дію неможливо скасувати.",
"resetConfirmTitle": "Скинути трафік \"{remark}\"?", "resetConfirmTitle": "Скинути трафік \"{remark}\"?",
"resetConfirmContent": "Скидає лічильники відправки/отримання цього вхідного до 0.", "resetConfirmContent": "Скидає лічильники відправки/отримання цього вхідного до 0.",
"selectedCount": "Обрано {count}",
"selectAll": "Вибрати все",
"bulkDeleteConfirmTitle": "Видалити {count} вхідних підключень?",
"bulkDeleteConfirmContent": "Будуть видалені вибрані вхідні підключення та всі їхні клієнти. Цю дію неможливо скасувати.",
"cloneConfirmTitle": "Клонувати вхідні \"{remark}\"?", "cloneConfirmTitle": "Клонувати вхідні \"{remark}\"?",
"cloneConfirmContent": "Створює копію з новим портом і порожнім списком клієнтів.", "cloneConfirmContent": "Створює копію з новим портом і порожнім списком клієнтів.",
"delAllClients": "Видалити всіх клієнтів", "delAllClients": "Видалити всіх клієнтів",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Вхідні підключення успішно оновлено", "inboundsUpdateSuccess": "Вхідні підключення успішно оновлено",
"inboundUpdateSuccess": "Вхідне підключення успішно оновлено", "inboundUpdateSuccess": "Вхідне підключення успішно оновлено",
"inboundCreateSuccess": "Вхідне підключення успішно створено", "inboundCreateSuccess": "Вхідне підключення успішно створено",
"bulkDeleted": "Видалено підключень: {count}",
"bulkDeletedMixed": "Видалено: {ok}, не вдалось: {failed}",
"inboundDeleteSuccess": "Вхідне підключення успішно видалено", "inboundDeleteSuccess": "Вхідне підключення успішно видалено",
"inboundClientAddSuccess": "Клієнт(и) вхідного підключення додано", "inboundClientAddSuccess": "Клієнт(и) вхідного підключення додано",
"inboundClientDeleteSuccess": "Клієнта вхідного підключення видалено", "inboundClientDeleteSuccess": "Клієнта вхідного підключення видалено",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "Hành động này xóa inbound và toàn bộ khách hàng của nó. Không thể hoàn tác.", "deleteConfirmContent": "Hành động này xóa inbound và toàn bộ khách hàng của nó. Không thể hoàn tác.",
"resetConfirmTitle": "Đặt lại lưu lượng của \"{remark}\"?", "resetConfirmTitle": "Đặt lại lưu lượng của \"{remark}\"?",
"resetConfirmContent": "Đặt lại bộ đếm lên/xuống về 0 cho inbound này.", "resetConfirmContent": "Đặt lại bộ đếm lên/xuống về 0 cho inbound này.",
"selectedCount": "Đã chọn {count}",
"selectAll": "Chọn tất cả",
"bulkDeleteConfirmTitle": "Xóa {count} inbound?",
"bulkDeleteConfirmContent": "Hành động này xóa các inbound đã chọn và toàn bộ khách hàng của chúng. Không thể hoàn tác.",
"cloneConfirmTitle": "Sao chép inbound \"{remark}\"?", "cloneConfirmTitle": "Sao chép inbound \"{remark}\"?",
"cloneConfirmContent": "Tạo bản sao với cổng mới và danh sách khách hàng trống.", "cloneConfirmContent": "Tạo bản sao với cổng mới và danh sách khách hàng trống.",
"delAllClients": "Xóa tất cả khách hàng", "delAllClients": "Xóa tất cả khách hàng",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "Đã cập nhật thành công các kết nối inbound", "inboundsUpdateSuccess": "Đã cập nhật thành công các kết nối inbound",
"inboundUpdateSuccess": "Đã cập nhật thành công kết nối inbound", "inboundUpdateSuccess": "Đã cập nhật thành công kết nối inbound",
"inboundCreateSuccess": "Đã tạo thành công kết nối inbound", "inboundCreateSuccess": "Đã tạo thành công kết nối inbound",
"bulkDeleted": "Đã xóa {count} inbound",
"bulkDeletedMixed": "Đã xóa {ok}, thất bại {failed}",
"inboundDeleteSuccess": "Đã xóa thành công kết nối inbound", "inboundDeleteSuccess": "Đã xóa thành công kết nối inbound",
"inboundClientAddSuccess": "Đã thêm client inbound", "inboundClientAddSuccess": "Đã thêm client inbound",
"inboundClientDeleteSuccess": "Đã xóa client inbound", "inboundClientDeleteSuccess": "Đã xóa client inbound",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "将删除此入站及其所有客户端。该操作不可撤销。", "deleteConfirmContent": "将删除此入站及其所有客户端。该操作不可撤销。",
"resetConfirmTitle": "重置 \"{remark}\" 的流量?", "resetConfirmTitle": "重置 \"{remark}\" 的流量?",
"resetConfirmContent": "将此入站的上/下行计数器清零。", "resetConfirmContent": "将此入站的上/下行计数器清零。",
"selectedCount": "已选 {count} 项",
"selectAll": "全选",
"bulkDeleteConfirmTitle": "删除 {count} 个入站?",
"bulkDeleteConfirmContent": "将删除所选入站及其所有客户端。该操作不可撤销。",
"cloneConfirmTitle": "克隆入站 \"{remark}\"", "cloneConfirmTitle": "克隆入站 \"{remark}\"",
"cloneConfirmContent": "使用新端口和空客户端列表创建副本。", "cloneConfirmContent": "使用新端口和空客户端列表创建副本。",
"delAllClients": "删除所有客户端", "delAllClients": "删除所有客户端",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "入站连接已成功更新", "inboundsUpdateSuccess": "入站连接已成功更新",
"inboundUpdateSuccess": "入站连接已成功更新", "inboundUpdateSuccess": "入站连接已成功更新",
"inboundCreateSuccess": "入站连接已成功创建", "inboundCreateSuccess": "入站连接已成功创建",
"bulkDeleted": "已删除 {count} 个入站",
"bulkDeletedMixed": "已删除 {ok} 个,失败 {failed} 个",
"inboundDeleteSuccess": "入站连接已成功删除", "inboundDeleteSuccess": "入站连接已成功删除",
"inboundClientAddSuccess": "已添加入站客户端", "inboundClientAddSuccess": "已添加入站客户端",
"inboundClientDeleteSuccess": "入站客户端已删除", "inboundClientDeleteSuccess": "入站客户端已删除",

View file

@ -296,6 +296,10 @@
"deleteConfirmContent": "將刪除此入站及其所有客戶端。此操作無法復原。", "deleteConfirmContent": "將刪除此入站及其所有客戶端。此操作無法復原。",
"resetConfirmTitle": "重置「{remark}」的流量?", "resetConfirmTitle": "重置「{remark}」的流量?",
"resetConfirmContent": "將此入站的上/下行計數器歸零。", "resetConfirmContent": "將此入站的上/下行計數器歸零。",
"selectedCount": "已選 {count} 項",
"selectAll": "全選",
"bulkDeleteConfirmTitle": "刪除 {count} 個入站?",
"bulkDeleteConfirmContent": "將刪除所選入站及其所有客戶端。此操作無法復原。",
"cloneConfirmTitle": "複製入站「{remark}」?", "cloneConfirmTitle": "複製入站「{remark}」?",
"cloneConfirmContent": "使用新連接埠和空客戶端清單建立副本。", "cloneConfirmContent": "使用新連接埠和空客戶端清單建立副本。",
"delAllClients": "刪除所有客戶端", "delAllClients": "刪除所有客戶端",
@ -421,6 +425,8 @@
"inboundsUpdateSuccess": "入站連接已成功更新", "inboundsUpdateSuccess": "入站連接已成功更新",
"inboundUpdateSuccess": "入站連接已成功更新", "inboundUpdateSuccess": "入站連接已成功更新",
"inboundCreateSuccess": "入站連接已成功建立", "inboundCreateSuccess": "入站連接已成功建立",
"bulkDeleted": "已刪除 {count} 個入站",
"bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個",
"inboundDeleteSuccess": "入站連接已成功刪除", "inboundDeleteSuccess": "入站連接已成功刪除",
"inboundClientAddSuccess": "已新增入站客戶端", "inboundClientAddSuccess": "已新增入站客戶端",
"inboundClientDeleteSuccess": "入站客戶端已刪除", "inboundClientDeleteSuccess": "入站客戶端已刪除",