{{ t('pages.inbounds.allTimeTraffic') }}
{{ SizeFormatter.sizeFormat(getAllTime(client.email)) }}
@@ -389,8 +483,28 @@ function rowKey(client) {
font-size: 13px;
}
+.bulk-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 16px;
+ background: rgba(22, 119, 255, 0.08);
+ border-bottom: 1px solid rgba(22, 119, 255, 0.18);
+}
+
+.bulk-count {
+ font-weight: 500;
+ font-size: 13px;
+}
+
+.is-selected {
+ background: rgba(22, 119, 255, 0.06);
+}
+
.client-row {
display: grid;
+ /* Default — no select column (single-client inbounds). The .has-select
+ * modifier below prepends the 40px checkbox column. */
grid-template-columns:
140px
/* actions */
@@ -404,6 +518,8 @@ function rowKey(client) {
/* traffic */
130px
/* all-time */
+ 130px
+ /* remained */
140px;
/* expiry */
gap: 12px;
@@ -412,6 +528,28 @@ function rowKey(client) {
border-top: 1px solid rgba(128, 128, 128, 0.12);
}
+.client-list.has-select .client-row {
+ grid-template-columns:
+ 40px
+ /* select */
+ 140px
+ /* actions */
+ 60px
+ /* enable */
+ 80px
+ /* online */
+ minmax(160px, 2fr)
+ /* client identity */
+ minmax(160px, 2fr)
+ /* traffic */
+ 130px
+ /* all-time */
+ 130px
+ /* remained */
+ 140px;
+ /* expiry */
+}
+
.client-row:last-child {
border-bottom: 1px solid rgba(128, 128, 128, 0.12);
}
@@ -432,10 +570,12 @@ function rowKey(client) {
/* allow grid children to shrink instead of overflowing */
}
+.cell-select,
.cell-actions,
.cell-enable,
.cell-online,
-.cell-alltime {
+.cell-alltime,
+.cell-remained {
text-align: center;
display: inline-flex;
align-items: center;
diff --git a/frontend/src/pages/inbounds/InboundInfoModal.vue b/frontend/src/pages/inbounds/InboundInfoModal.vue
index 969e3033..288d8980 100644
--- a/frontend/src/pages/inbounds/InboundInfoModal.vue
+++ b/frontend/src/pages/inbounds/InboundInfoModal.vue
@@ -387,6 +387,9 @@ const showSubscriptionTab = computed(
{{
getRemainingStats() }}
+
+
+
|
{{
diff --git a/frontend/src/pages/inbounds/InboundList.vue b/frontend/src/pages/inbounds/InboundList.vue
index d3568b62..6841ed3a 100644
--- a/frontend/src/pages/inbounds/InboundList.vue
+++ b/frontend/src/pages/inbounds/InboundList.vue
@@ -62,6 +62,7 @@ const emit = defineEmits([
'info-client',
'reset-traffic-client',
'delete-client',
+ 'delete-clients',
'toggle-enable-client',
]);
@@ -404,6 +405,7 @@ function showQrCodeMenu(dbInbound) {
@info-client="(p) => emit('info-client', p)"
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
@delete-client="(p) => emit('delete-client', p)"
+ @delete-clients="(p) => emit('delete-clients', p)"
@toggle-enable-client="(p) => emit('toggle-enable-client', p)" />
@@ -423,6 +425,7 @@ function showQrCodeMenu(dbInbound) {
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-client', p)"
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
@delete-client="(p) => emit('delete-client', p)"
+ @delete-clients="(p) => emit('delete-clients', p)"
@toggle-enable-client="(p) => emit('toggle-enable-client', p)" />
@@ -523,27 +526,35 @@ function showQrCodeMenu(dbInbound) {
{{ clientCount[record.id].clients }}
- {{ email }}
+
{{ clientCount[record.id].deactive.length }}
- {{ email }}
+
{{ clientCount[record.id].depleted.length
}}
- {{ email }}
+
{{ clientCount[record.id].expiring.length
}}
- {{ email }}
+
{{ clientCount[record.id].online.length }}
diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue
index a46ed98d..5a4915f0 100644
--- a/frontend/src/pages/inbounds/InboundsPage.vue
+++ b/frontend/src/pages/inbounds/InboundsPage.vue
@@ -322,6 +322,14 @@ async function onDeleteClient({ dbInbound, client }) {
if (msg?.success) await refresh();
}
+async function onDeleteClients({ dbInbound, clients }) {
+ for (const client of clients) {
+ const clientId = getClientId(dbInbound.protocol, client);
+ await HttpUtil.post(`/panel/api/inbounds/${dbInbound.id}/delClient/${clientId}`);
+ }
+ await refresh();
+}
+
async function onToggleEnableClient({ dbInbound, client, next }) {
// Mirror legacy: clone the parsed inbound, flip enable on the matching
// client, and post the whole client back through updateClient. This
@@ -593,9 +601,38 @@ function onRowAction({ key, dbInbound }) {
{{ totals.clients }}
- {{ totals.deactive.length }}
- {{ totals.depleted.length }}
- {{ totals.expiring.length }}
+
+
+
+
+ {{ totals.deactive.length }}
+
+
+
+
+
+ {{ totals.depleted.length }}
+
+
+
+
+
+ {{ totals.expiring.length }}
+
+
+
+
+
+ {{ totals.online.length }}
+
@@ -613,7 +650,7 @@ function onRowAction({ key, dbInbound }) {
@add-inbound="onAddInbound" @general-action="onGeneralAction" @row-action="onRowAction"
@edit-client="onEditClient" @qrcode-client="onQrcodeClient" @info-client="onInfoClient"
@reset-traffic-client="onResetTrafficClient" @delete-client="onDeleteClient"
- @toggle-enable-client="onToggleEnableClient" />
+ @delete-clients="onDeleteClients" @toggle-enable-client="onToggleEnableClient" />
@@ -692,3 +729,20 @@ function onRowAction({ key, dbInbound }) {
}
}
+
+
diff --git a/frontend/src/pages/inbounds/useInbounds.js b/frontend/src/pages/inbounds/useInbounds.js
index c89e1ad6..4f389a02 100644
--- a/frontend/src/pages/inbounds/useInbounds.js
+++ b/frontend/src/pages/inbounds/useInbounds.js
@@ -287,6 +287,7 @@ export function useInbounds() {
const deactive = [];
const depleted = [];
const expiring = [];
+ const online = [];
for (const ib of dbInbounds.value) {
up += ib.up || 0;
down += ib.down || 0;
@@ -297,9 +298,10 @@ export function useInbounds() {
deactive.push(...c.deactive);
depleted.push(...c.depleted);
expiring.push(...c.expiring);
+ online.push(...c.online);
}
}
- return { up, down, allTime, clients, deactive, depleted, expiring };
+ return { up, down, allTime, clients, deactive, depleted, expiring, online };
});
// ObjectUtil reference is wired at module load — keeping a no-op import
|