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
21 changed files with 315 additions and 4 deletions
+27
View File
@@ -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"))
+31
View File
@@ -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{}
+6
View File
@@ -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": "تم حذف عميل وارد",
+6
View File
@@ -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.",
+6
View File
@@ -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",
+6
View File
@@ -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": "کلاینت ورودی حذف شد",
+6
View File
@@ -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",
+6
View File
@@ -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": "インバウンドクライアントが削除されました",
+6
View File
@@ -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",
+6
View File
@@ -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": "Клиент подключения удалён",
+6
View File
@@ -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",
+6
View File
@@ -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": "Клієнта вхідного підключення видалено",
+6
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.",
"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",
+6
View File
@@ -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": "入站客户端已删除",
+6
View File
@@ -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": "入站客戶端已刪除",