From cf50952921c1b4ccd07f3b64684d0807e35a454e Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 31 May 2026 00:29:24 +0200 Subject: [PATCH] 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. --- frontend/public/openapi.json | 59 +++++++++++++++++ frontend/src/pages/api-docs/endpoints.ts | 7 ++ frontend/src/pages/inbounds/InboundsPage.tsx | 31 +++++++++ .../src/pages/inbounds/list/InboundList.css | 20 ++++++ .../src/pages/inbounds/list/InboundList.tsx | 65 +++++++++++++++++-- frontend/src/pages/inbounds/list/types.ts | 1 + web/controller/inbound.go | 27 ++++++++ web/service/inbound.go | 31 +++++++++ web/translation/ar-EG.json | 6 ++ web/translation/en-US.json | 6 ++ web/translation/es-ES.json | 6 ++ web/translation/fa-IR.json | 6 ++ web/translation/id-ID.json | 6 ++ web/translation/ja-JP.json | 6 ++ web/translation/pt-BR.json | 6 ++ web/translation/ru-RU.json | 6 ++ web/translation/tr-TR.json | 6 ++ web/translation/uk-UA.json | 6 ++ web/translation/vi-VN.json | 6 ++ web/translation/zh-CN.json | 6 ++ web/translation/zh-TW.json | 6 ++ 21 files changed, 315 insertions(+), 4 deletions(-) diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 29aab8f3..745b9e5c 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -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}": { "post": { "tags": [ diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 77361d22..602f955c 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -149,6 +149,13 @@ export const sections: readonly Section[] = [ { 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', path: '/panel/api/inbounds/update/:id', diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx index eca49ac7..a63a4d05 100644 --- a/frontend/src/pages/inbounds/InboundsPage.tsx +++ b/frontend/src/pages/inbounds/InboundsPage.tsx @@ -357,6 +357,36 @@ export default function InboundsPage() { }); }, [modal, refresh, t]); + const confirmBulkDelete = useCallback((ids: number[]) => new Promise((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) => { modal.confirm({ title: t('pages.inbounds.resetConfirmTitle', { remark: dbInbound.remark }), @@ -567,6 +597,7 @@ export default function InboundsPage() { onAddInbound={onAddInbound} onGeneralAction={onGeneralAction} onRowAction={({ key, dbInbound }) => onRowAction({ key, dbInbound: dbInbound as unknown as DBInbound })} + onBulkDelete={confirmBulkDelete} /> diff --git a/frontend/src/pages/inbounds/list/InboundList.css b/frontend/src/pages/inbounds/list/InboundList.css index dfd68c78..16f2db9a 100644 --- a/frontend/src/pages/inbounds/list/InboundList.css +++ b/frontend/src/pages/inbounds/list/InboundList.css @@ -75,6 +75,26 @@ 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 { display: flex; align-items: center; diff --git a/frontend/src/pages/inbounds/list/InboundList.tsx b/frontend/src/pages/inbounds/list/InboundList.tsx index 484587db..5dfb51fd 100644 --- a/frontend/src/pages/inbounds/list/InboundList.tsx +++ b/frontend/src/pages/inbounds/list/InboundList.tsx @@ -1,12 +1,14 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState, type Key } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Card, + Checkbox, Dropdown, Space, Switch, Table, + Tag, Tooltip, type MenuProps, } from 'antd'; @@ -18,6 +20,7 @@ import { ImportOutlined, ReloadOutlined, InfoCircleOutlined, + DeleteOutlined, } from '@ant-design/icons'; import { HttpUtil } from '@/utils'; @@ -43,11 +46,13 @@ export default function InboundList({ onAddInbound, onGeneralAction, onRowAction, + onBulkDelete, }: InboundListProps) { const { t } = useTranslation(); const [sortKey, setSortKey] = useState(null); const [sortOrder, setSortOrder] = useState(null); const [statsRecord, setStatsRecord] = useState(null); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => { const previous = dbInbound.enable; @@ -75,6 +80,26 @@ export default function InboundList({ [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({ hasAnyRemark, hasActiveNode, @@ -119,6 +144,16 @@ export default function InboundList({ {!isMobile && t('pages.inbounds.generalActions')} + {selectedRowKeys.length > 0 && ( + <> + setSelectedRowKeys([])} style={{ marginInlineEnd: 0 }}> + {t('pages.inbounds.selectedCount', { count: selectedRowKeys.length })} + + + + )} )} > @@ -131,9 +166,26 @@ export default function InboundList({
{t('noData')}
) : ( - sortedInbounds.map((record) => ( -
+ <> +
+ selectAll(e.target.checked)} + > + {t('pages.inbounds.selectAll')} + + {selectedRowKeys.length > 0 && ( + {selectedRowKeys.length} + )} +
+ {sortedInbounds.map((record) => ( +
+ toggleSelect(record.id, e.target.checked)} + /> #{record.id} {record.remark}
e.stopPropagation()}> @@ -158,7 +210,8 @@ export default function InboundList({
- )) + ))} + )}
) : ( @@ -166,6 +219,10 @@ export default function InboundList({ columns={columns} dataSource={sortedInbounds} rowKey={(r) => r.id} + rowSelection={{ + selectedRowKeys, + onChange: (keys: Key[]) => setSelectedRowKeys(keys as number[]), + }} pagination={paginationFor(sortedInbounds)} scroll={{ x: 1000 }} style={{ marginTop: 10 }} diff --git a/frontend/src/pages/inbounds/list/types.ts b/frontend/src/pages/inbounds/list/types.ts index cb093ecc..1f6230e9 100644 --- a/frontend/src/pages/inbounds/list/types.ts +++ b/frontend/src/pages/inbounds/list/types.ts @@ -72,6 +72,7 @@ export interface InboundListProps { onAddInbound: () => void; onGeneralAction: (key: GeneralAction) => void; onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void; + onBulkDelete: (ids: number[]) => Promise; } export type SortKey = diff --git a/web/controller/inbound.go b/web/controller/inbound.go index aa5f77a6..10e334e3 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -69,6 +69,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/add", a.addInbound) g.POST("/del/:id", a.delInbound) + g.POST("/bulkDel", a.bulkDelInbounds) g.POST("/update/:id", a.updateInbound) g.POST("/setEnable/:id", a.setInboundEnable) g.POST("/:id/resetTraffic", a.resetInboundTraffic) @@ -179,6 +180,32 @@ func (a *InboundController) delInbound(c *gin.Context) { 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. func (a *InboundController) updateInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) diff --git a/web/service/inbound.go b/web/service/inbound.go index 039b30a8..972aef45 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -617,6 +617,37 @@ func (s *InboundService) DelInbound(id int) (bool, 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) { db := database.GetDB() inbound := &model.Inbound{} diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 3ca3f842..7088c519 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "سيؤدي هذا إلى إزالة الإدخال وجميع عملائه. لا يمكن التراجع.", "resetConfirmTitle": "إعادة تعيين ترافيك \"{remark}\"؟", "resetConfirmContent": "يعيد عدادات الإرسال/الاستقبال لهذا الإدخال إلى 0.", + "selectedCount": "{count} محدد", + "selectAll": "تحديد الكل", + "bulkDeleteConfirmTitle": "حذف {count} إدخال؟", + "bulkDeleteConfirmContent": "سيؤدي هذا إلى إزالة الإدخالات المحددة وجميع عملائها. لا يمكن التراجع.", "cloneConfirmTitle": "نسخ الإدخال \"{remark}\"؟", "cloneConfirmContent": "ينشئ نسخة بمنفذ جديد وقائمة عملاء فارغة.", "delAllClients": "حذف جميع العملاء", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "تم تحديث الواردات بنجاح", "inboundUpdateSuccess": "تم تحديث الوارد بنجاح", "inboundCreateSuccess": "تم إنشاء الوارد بنجاح", + "bulkDeleted": "تم حذف {count} إدخال", + "bulkDeletedMixed": "تم حذف {ok}, وفشل {failed}", "inboundDeleteSuccess": "تم حذف الوارد بنجاح", "inboundClientAddSuccess": "تمت إضافة عميل(عملاء) وارد", "inboundClientDeleteSuccess": "تم حذف عميل وارد", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 4e299ea5..38909d6d 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "This removes the inbound and all its clients. This cannot be undone.", "resetConfirmTitle": "Reset traffic for \"{remark}\"?", "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}\"?", "cloneConfirmContent": "Creates a copy with a new port and an empty client list.", "delAllClients": "Delete All Clients", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Inbounds have been successfully updated.", "inboundUpdateSuccess": "Inbound has been successfully updated.", "inboundCreateSuccess": "Inbound has been successfully created.", + "bulkDeleted": "{count} inbounds deleted", + "bulkDeletedMixed": "{ok} deleted, {failed} failed", "inboundDeleteSuccess": "Inbound has been successfully deleted.", "inboundClientAddSuccess": "Inbound client(s) have been added.", "inboundClientDeleteSuccess": "Inbound client has been deleted.", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index 88bd86f8..c2f33391 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Esto elimina el inbound y todos sus clientes. No se puede deshacer.", "resetConfirmTitle": "¿Restablecer el tráfico de \"{remark}\"?", "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}\"?", "cloneConfirmContent": "Crea una copia con un puerto nuevo y una lista de clientes vacía.", "delAllClients": "Eliminar todos los clientes", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Entradas actualizadas correctamente", "inboundUpdateSuccess": "Entrada actualizada correctamente", "inboundCreateSuccess": "Entrada creada correctamente", + "bulkDeleted": "{count} inbounds eliminados", + "bulkDeletedMixed": "{ok} eliminados, {failed} fallidos", "inboundDeleteSuccess": "Entrada eliminada correctamente", "inboundClientAddSuccess": "Cliente(s) de entrada añadido(s)", "inboundClientDeleteSuccess": "Cliente de entrada eliminado", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index f7477982..5c623388 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "این اینباند و تمام کلاینت‌های آن حذف می‌شود. این عمل غیرقابل بازگشت است.", "resetConfirmTitle": "ترافیک اینباند «{remark}» صفر شود؟", "resetConfirmContent": "شمارنده‌های ارسال/دریافت این اینباند به صفر برمی‌گردد.", + "selectedCount": "{count} انتخاب‌شده", + "selectAll": "انتخاب همه", + "bulkDeleteConfirmTitle": "حذف {count} اینباند؟", + "bulkDeleteConfirmContent": "اینباندهای انتخاب‌شده و تمام کلاینت‌های آن‌ها حذف می‌شوند. این عمل غیرقابل بازگشت است.", "cloneConfirmTitle": "اینباند «{remark}» کپی شود؟", "cloneConfirmContent": "یک نسخه با پورت جدید و لیست کلاینت خالی ساخته می‌شود.", "delAllClients": "حذف همه کلاینت‌ها", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "ورودی‌ها با موفقیت به‌روزرسانی شدند", "inboundUpdateSuccess": "ورودی با موفقیت به‌روزرسانی شد", "inboundCreateSuccess": "ورودی با موفقیت ایجاد شد", + "bulkDeleted": "{count} اینباند حذف شد", + "bulkDeletedMixed": "{ok} حذف، {failed} ناموفق", "inboundDeleteSuccess": "ورودی با موفقیت حذف شد", "inboundClientAddSuccess": "کلاینت(های) ورودی اضافه شدند", "inboundClientDeleteSuccess": "کلاینت ورودی حذف شد", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index eebd74f6..f9f554eb 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Tindakan ini menghapus inbound beserta semua kliennya. Tidak dapat dibatalkan.", "resetConfirmTitle": "Reset trafik \"{remark}\"?", "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}\"?", "cloneConfirmContent": "Membuat salinan dengan port baru dan daftar klien kosong.", "delAllClients": "Hapus Semua Klien", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Inbound berhasil diperbarui", "inboundUpdateSuccess": "Inbound berhasil diperbarui", "inboundCreateSuccess": "Inbound berhasil dibuat", + "bulkDeleted": "{count} inbound dihapus", + "bulkDeletedMixed": "{ok} dihapus, {failed} gagal", "inboundDeleteSuccess": "Inbound berhasil dihapus", "inboundClientAddSuccess": "Klien inbound telah ditambahkan", "inboundClientDeleteSuccess": "Klien inbound telah dihapus", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 541c6640..19a5842e 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "インバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。", "resetConfirmTitle": "「{remark}」のトラフィックをリセットしますか?", "resetConfirmContent": "このインバウンドの送受信カウンタを 0 にリセットします。", + "selectedCount": "{count} 選択中", + "selectAll": "すべて選択", + "bulkDeleteConfirmTitle": "{count} 件のインバウンドを削除しますか?", + "bulkDeleteConfirmContent": "選択したインバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。", "cloneConfirmTitle": "インバウンド「{remark}」を複製しますか?", "cloneConfirmContent": "新しいポートと空のクライアント一覧でコピーを作成します。", "delAllClients": "すべてのクライアントを削除", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "インバウンドが正常に更新されました", "inboundUpdateSuccess": "インバウンドが正常に更新されました", "inboundCreateSuccess": "インバウンドが正常に作成されました", + "bulkDeleted": "{count} 件のインバウンドを削除しました", + "bulkDeletedMixed": "{ok} 件削除、{failed} 件失敗", "inboundDeleteSuccess": "インバウンドが正常に削除されました", "inboundClientAddSuccess": "インバウンドクライアントが追加されました", "inboundClientDeleteSuccess": "インバウンドクライアントが削除されました", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 799d60d6..5f00062f 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Isto remove o inbound e todos os seus clientes. Não é possível desfazer.", "resetConfirmTitle": "Redefinir o tráfego de \"{remark}\"?", "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}\"?", "cloneConfirmContent": "Cria uma cópia com uma nova porta e lista de clientes vazia.", "delAllClients": "Excluir todos os clientes", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Entradas atualizadas com sucesso", "inboundUpdateSuccess": "Entrada atualizada 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", "inboundClientAddSuccess": "Cliente(s) de entrada adicionado(s)", "inboundClientDeleteSuccess": "Cliente de entrada excluído", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index 27aff996..8b396060 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Подключение и все его клиенты будут удалены. Это действие нельзя отменить.", "resetConfirmTitle": "Сбросить трафик \"{remark}\"?", "resetConfirmContent": "Сбрасывает счётчики отправки/получения этого подключения до 0.", + "selectedCount": "{count} выбрано", + "selectAll": "Выбрать всё", + "bulkDeleteConfirmTitle": "Удалить {count} подключений?", + "bulkDeleteConfirmContent": "Выбранные подключения и все их клиенты будут удалены. Это действие нельзя отменить.", "cloneConfirmTitle": "Клонировать подключение \"{remark}\"?", "cloneConfirmContent": "Создаёт копию с новым портом и пустым списком клиентов.", "delAllClients": "Удалить всех клиентов", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Подключения успешно обновлены", "inboundUpdateSuccess": "Подключение успешно обновлено", "inboundCreateSuccess": "Подключение успешно создано", + "bulkDeleted": "Удалено подключений: {count}", + "bulkDeletedMixed": "Удалено: {ok}, не удалось: {failed}", "inboundDeleteSuccess": "Подключение успешно удалено", "inboundClientAddSuccess": "Клиент(ы) подключения добавлен(ы)", "inboundClientDeleteSuccess": "Клиент подключения удалён", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 4e6c23a5..606b9d0c 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Bu işlem inbound'u ve tüm istemcilerini siler. Geri alınamaz.", "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.", + "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ı?", "cloneConfirmContent": "Yeni bir port ve boş istemci listesiyle bir kopya oluşturur.", "delAllClients": "Tüm istemcileri sil", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Gelen bağlantılar başarıyla güncellendi", "inboundUpdateSuccess": "Gelen bağlantı başarıyla güncellendi", "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", "inboundClientAddSuccess": "Gelen bağlantı istemci(leri) eklendi", "inboundClientDeleteSuccess": "Gelen bağlantı istemcisi silindi", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 0852b478..47f9471e 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "Це видалить вхідні та всіх його клієнтів. Цю дію неможливо скасувати.", "resetConfirmTitle": "Скинути трафік \"{remark}\"?", "resetConfirmContent": "Скидає лічильники відправки/отримання цього вхідного до 0.", + "selectedCount": "Обрано {count}", + "selectAll": "Вибрати все", + "bulkDeleteConfirmTitle": "Видалити {count} вхідних підключень?", + "bulkDeleteConfirmContent": "Будуть видалені вибрані вхідні підключення та всі їхні клієнти. Цю дію неможливо скасувати.", "cloneConfirmTitle": "Клонувати вхідні \"{remark}\"?", "cloneConfirmContent": "Створює копію з новим портом і порожнім списком клієнтів.", "delAllClients": "Видалити всіх клієнтів", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "Вхідні підключення успішно оновлено", "inboundUpdateSuccess": "Вхідне підключення успішно оновлено", "inboundCreateSuccess": "Вхідне підключення успішно створено", + "bulkDeleted": "Видалено підключень: {count}", + "bulkDeletedMixed": "Видалено: {ok}, не вдалось: {failed}", "inboundDeleteSuccess": "Вхідне підключення успішно видалено", "inboundClientAddSuccess": "Клієнт(и) вхідного підключення додано", "inboundClientDeleteSuccess": "Клієнта вхідного підключення видалено", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index e0926290..9aac1b64 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -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.", "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.", + "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}\"?", "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", @@ -421,6 +425,8 @@ "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", "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", "inboundClientAddSuccess": "Đã thêm client inbound", "inboundClientDeleteSuccess": "Đã xóa client inbound", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 9c603ba8..1a72d067 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "将删除此入站及其所有客户端。该操作不可撤销。", "resetConfirmTitle": "重置 \"{remark}\" 的流量?", "resetConfirmContent": "将此入站的上/下行计数器清零。", + "selectedCount": "已选 {count} 项", + "selectAll": "全选", + "bulkDeleteConfirmTitle": "删除 {count} 个入站?", + "bulkDeleteConfirmContent": "将删除所选入站及其所有客户端。该操作不可撤销。", "cloneConfirmTitle": "克隆入站 \"{remark}\"?", "cloneConfirmContent": "使用新端口和空客户端列表创建副本。", "delAllClients": "删除所有客户端", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "入站连接已成功更新", "inboundUpdateSuccess": "入站连接已成功更新", "inboundCreateSuccess": "入站连接已成功创建", + "bulkDeleted": "已删除 {count} 个入站", + "bulkDeletedMixed": "已删除 {ok} 个,失败 {failed} 个", "inboundDeleteSuccess": "入站连接已成功删除", "inboundClientAddSuccess": "已添加入站客户端", "inboundClientDeleteSuccess": "入站客户端已删除", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index e9e0a2e8..593f6378 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -296,6 +296,10 @@ "deleteConfirmContent": "將刪除此入站及其所有客戶端。此操作無法復原。", "resetConfirmTitle": "重置「{remark}」的流量?", "resetConfirmContent": "將此入站的上/下行計數器歸零。", + "selectedCount": "已選 {count} 項", + "selectAll": "全選", + "bulkDeleteConfirmTitle": "刪除 {count} 個入站?", + "bulkDeleteConfirmContent": "將刪除所選入站及其所有客戶端。此操作無法復原。", "cloneConfirmTitle": "複製入站「{remark}」?", "cloneConfirmContent": "使用新連接埠和空客戶端清單建立副本。", "delAllClients": "刪除所有客戶端", @@ -421,6 +425,8 @@ "inboundsUpdateSuccess": "入站連接已成功更新", "inboundUpdateSuccess": "入站連接已成功更新", "inboundCreateSuccess": "入站連接已成功建立", + "bulkDeleted": "已刪除 {count} 個入站", + "bulkDeletedMixed": "已刪除 {ok} 個,失敗 {failed} 個", "inboundDeleteSuccess": "入站連接已成功刪除", "inboundClientAddSuccess": "已新增入站客戶端", "inboundClientDeleteSuccess": "入站客戶端已刪除",