diff --git a/frontend/src/pages/clients/ClientsPage.vue b/frontend/src/pages/clients/ClientsPage.vue
index 1678b695..75a8189d 100644
--- a/frontend/src/pages/clients/ClientsPage.vue
+++ b/frontend/src/pages/clients/ClientsPage.vue
@@ -10,8 +10,6 @@ import {
InfoCircleOutlined,
QrcodeOutlined,
RetweetOutlined,
- ControlOutlined,
- DownOutlined,
MoreOutlined,
UsergroupAddOutlined,
} from '@ant-design/icons-vue';
@@ -218,15 +216,14 @@ function onShowQr(row) {
function onResetAllTraffics() {
Modal.confirm({
- title: t('pages.clients.resetAllTrafficsTitle') || 'Reset all client traffic?',
- content: t('pages.clients.resetAllTrafficsContent')
- || 'Every client’s up/down counter drops to zero. Quotas and expiry are not affected.',
+ title: t('pages.clients.resetAllTrafficsTitle'),
+ content: t('pages.clients.resetAllTrafficsContent'),
okText: t('reset') || 'Reset',
okType: 'danger',
cancelText: t('cancel'),
onOk: async () => {
const msg = await resetAllTraffics();
- if (msg?.success) message.success(t('pages.clients.toasts.allTrafficsReset') || 'All client traffic reset');
+ if (msg?.success) message.success(t('pages.clients.toasts.allTrafficsReset'));
},
});
}
@@ -340,23 +337,12 @@ const columns = computed(() => [
{{ t('pages.clients.deleteSelected', { count: selectedRowKeys.length })
|| `Delete (${selectedRowKeys.length})` }}
-
-
-
- {{ t('pages.clients.general') }}
-
-
-
-
-
-
-
- {{ t('pages.clients.resetAllTraffics') }}
-
-
-
+
+
+
-
+ {{ t('pages.clients.resetAllTraffics') }}
+
@@ -462,7 +448,8 @@ const columns = computed(() => [
toggleSelect(row.id, e.target.checked)" />
-
+
{{ row.email }}
diff --git a/frontend/src/pages/inbounds/ClientBulkModal.vue b/frontend/src/pages/inbounds/ClientBulkModal.vue
deleted file mode 100644
index 6e76642b..00000000
--- a/frontend/src/pages/inbounds/ClientBulkModal.vue
+++ /dev/null
@@ -1,280 +0,0 @@
-
-
-
-
-
-
-
- Random
- Random + Prefix
- Random + Prefix + Num
- Random + Prefix + Num + Postfix
- Prefix + Num + Postfix
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ key }}
-
-
-
-
-
- {{ t('none') }}
- {{ key }}
-
-
-
-
-
- {{ t('subscription.title') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.totalFlow') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.expireDate')
- }}
-
-
-
-
-
-
- {{ t('pages.clients.renew') }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/inbounds/ClientFormModal.vue b/frontend/src/pages/inbounds/ClientFormModal.vue
deleted file mode 100644
index 7a3d4bcd..00000000
--- a/frontend/src/pages/inbounds/ClientFormModal.vue
+++ /dev/null
@@ -1,394 +0,0 @@
-
-
-
-
-
- {{ t('depleted') }}
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.email') }}
-
-
-
-
-
-
-
- {{ t('password') }}
-
-
-
-
-
-
-
- {{ t('password') }}
-
-
-
-
-
-
-
- ID
-
-
-
-
-
-
-
-
- {{ key }}
-
-
-
-
-
-
- {{ t('subscription.title') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.IPLimitlogclear') }}
-
-
-
-
-
- {{ t('none') }}
-
- {{ key }}
-
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.totalFlow') }}
-
-
-
-
-
-
- {{ SizeFormatter.sizeFormat(clientStats.up) }} /
- {{ SizeFormatter.sizeFormat(clientStats.down) }}
- ({{ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) }})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('pages.inbounds.expireDate')
- }}
-
-
- {{ t('depleted') }}
-
-
-
-
- {{ t('pages.clients.renew') }}
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/inbounds/ClientRowTable.vue b/frontend/src/pages/inbounds/ClientRowTable.vue
deleted file mode 100644
index 5fdd2067..00000000
--- a/frontend/src/pages/inbounds/ClientRowTable.vue
+++ /dev/null
@@ -1,818 +0,0 @@
-
-
-
-
-
-
{{ selectedCount }} selected
-
{{ t('cancel') }}
-
- {{ t('delete') }}
-
-
-
-
-
-
-
-
-
-
toggleSelect(rowKey(client), e.target.checked)" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
emit('toggle-enable-client', { dbInbound, client, next })" />
-
-
-
-
- {{ t('lastOnline') }}: {{ lastOnlineLabel(client.email) }}
- {{ t('online') }}
- {{ t('offline') }}
-
-
-
-
-
-
- {{ t('depleted') }}
- {{ t('disabled') }}
- {{ t('online') }}
- {{ t('offline') }}
-
-
-
-
-
- {{ client.email }}
-
-
-
-
-
-
-
-
-
-
-
- | ↑ {{ SizeFormatter.sizeFormat(getUp(client.email)) }} |
- ↓ {{ SizeFormatter.sizeFormat(getDown(client.email)) }} |
-
-
- | {{ t('remained') }} |
- {{ SizeFormatter.sizeFormat(getRem(client.email)) }} |
-
-
-
-
-
-
{{ SizeFormatter.sizeFormat(getSum(client.email)) }}
-
-
-
-
-
- {{ totalGbDisplay(client) }}
-
-
-
-
-
-
-
-
-
-
- {{ SizeFormatter.sizeFormat(getRem(client.email)) }}
-
-
-
-
-
-
-
- {{ t('pages.clients.delayedStart') }}
- {{ IntlUtil.formatDate(client.expiryTime, datepicker) }}
-
-
-
{{ IntlUtil.formatRelativeTime(client.expiryTime) }}
-
-
{{ client.reset }}d
-
-
-
-
-
- {{ t('pages.clients.delayedStart') }}
- {{ IntlUtil.formatDate(client.expiryTime) }}
-
-
- {{ IntlUtil.formatRelativeTime(client.expiryTime) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
toggleSelect(rowKey(client), e.target.checked)" />
-
-
- {{ t('depleted') }}
- {{ t('disabled') }}
- {{ t('online') }}
- {{ t('offline') }}
-
-
-
-
- {{ client.email }}
-
-
-
-
-
-
emit('toggle-enable-client', { dbInbound, client, next })" />
-
-
-
-
-
- {{ t('qrCode') }}
-
-
- {{ t('edit') }}
-
-
- {{ t('info') }}
-
-
- {{ t('pages.inbounds.resetTraffic') }}
-
-
- {{ t('delete') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/pages/inbounds/CopyClientsModal.vue b/frontend/src/pages/inbounds/CopyClientsModal.vue
deleted file mode 100644
index f3df97b5..00000000
--- a/frontend/src/pages/inbounds/CopyClientsModal.vue
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
-
-
-
{{ t('pages.clients.copySource') }}
-
-
- {{ item.label }}
-
-
-
-
-
-
- {{ t('pages.clients.selectAll') }}
- {{ t('pages.clients.clearAll') }}
-
-
-
-
-
-
{{ t('pages.clients.copyFlowLabel') }}
-
- {{ t('none') }}
- {{ key }}
-
-
- {{ t('pages.clients.copyFlowHint') }}
-
-
-
-
-
diff --git a/frontend/src/pages/inbounds/InboundList.vue b/frontend/src/pages/inbounds/InboundList.vue
index 0a50ab9d..00352c26 100644
--- a/frontend/src/pages/inbounds/InboundList.vue
+++ b/frontend/src/pages/inbounds/InboundList.vue
@@ -9,8 +9,6 @@ import {
MoreOutlined,
EditOutlined,
QrcodeOutlined,
- UserAddOutlined,
- UsergroupAddOutlined,
CopyOutlined,
FileDoneOutlined,
ExportOutlined,
@@ -21,14 +19,12 @@ import {
BlockOutlined,
DeleteOutlined,
InfoCircleOutlined,
- RightOutlined,
} from '@ant-design/icons-vue';
import { HttpUtil, ObjectUtil, SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
import { DBInbound } from '@/models/dbinbound.js';
import { Inbound } from '@/models/inbound.js';
import InfinityIcon from '@/components/InfinityIcon.vue';
-import ClientRowTable from './ClientRowTable.vue';
import { useDatepicker } from '@/composables/useDatepicker.js';
const { datepicker } = useDatepicker();
@@ -58,14 +54,6 @@ const emit = defineEmits([
'add-inbound',
'general-action',
'row-action',
- // Per-client events surfaced from the expand-row table.
- 'edit-client',
- 'qrcode-client',
- 'info-client',
- 'reset-traffic-client',
- 'delete-client',
- 'delete-clients',
- 'toggle-enable-client',
]);
// ============ Toolbar / search & filter =============================
@@ -249,19 +237,6 @@ const desktopColumns = computed(() => {
});
const columns = computed(() => desktopColumns.value);
-// Mobile expansion state — replaces a-table's expandable() since the
-// mobile branch renders a hand-rolled card list rather than a table.
-const expandedIds = ref(new Set());
-function toggleExpanded(id) {
- const next = new Set(expandedIds.value);
- if (next.has(id)) next.delete(id);
- else next.add(id);
- expandedIds.value = next;
-}
-function isExpanded(id) {
- return expandedIds.value.has(id);
-}
-
const statsRecord = ref(null);
function openStats(record) {
statsRecord.value = record;
@@ -395,10 +370,8 @@ function showQrCodeMenu(dbInbound) {
—
-
-
-
+
+
#{{ record.id }}
{{ record.remark }}
@@ -417,15 +390,6 @@ function showQrCodeMenu(dbInbound) {
{{ t('qrCode') }}
-
- {{ t('pages.clients.add') }}
-
-
- {{ t('pages.clients.bulk') }}
-
-
- {{ t('pages.clients.copyFromInbound') }}
-
{{ t('pages.inbounds.resetInboundClientTraffics') }}
@@ -461,18 +425,6 @@ function showQrCodeMenu(dbInbound) {
-
-
-
- emit('edit-client', p)"
- @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)" />
-
@@ -542,21 +494,7 @@ function showQrCodeMenu(dbInbound) {
-
-
- emit('edit-client', p)" @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)" />
-
-
+ @change="onTableChange">
@@ -579,15 +517,6 @@ function showQrCodeMenu(dbInbound) {
{{ t('qrCode') }}
-
- {{ t('pages.clients.add') }}
-
-
- {{ t('pages.clients.bulk') }}
-
-
- {{ t('pages.clients.copyFromInbound') }}
-
{{ t('pages.inbounds.resetInboundClientTraffics') }}
@@ -789,23 +718,6 @@ function showQrCodeMenu(dbInbound) {
color: #ff4d4f;
}
-/* Hide the expand chevron on rows whose inbound has no client list
- * (HTTP/Mixed/Tunnel/WireGuard single-config). */
-:deep(.hide-expand-icon .ant-table-row-expand-icon) {
- visibility: hidden;
-}
-
-/* Push the expand chevron away from the table's left edge so it has
- * a little breathing room instead of being flush against the corner. */
-:deep(.ant-table-tbody .ant-table-cell-with-append) {
- padding-left: 12px;
-}
-
-:deep(.ant-table-row-expand-icon) {
- margin-inline-end: 10px;
- margin-inline-start: 4px;
-}
-
/* Round the table's outer corners — AD-Vue gives .ant-table the radius
* token, but the inner header strip and footer touch the edges, so clip
* them here. */
@@ -890,17 +802,6 @@ function showQrCodeMenu(dbInbound) {
flex-shrink: 0;
}
-.card-expand {
- font-size: 12px;
- opacity: 0.6;
- transition: transform 150ms ease;
- flex-shrink: 0;
-}
-
-.card-expand.is-expanded {
- transform: rotate(90deg);
-}
-
.card-stats {
display: flex;
flex-direction: column;
@@ -927,11 +828,6 @@ function showQrCodeMenu(dbInbound) {
margin: 0;
}
-.card-clients {
- margin-top: 4px;
- padding-top: 8px;
- border-top: 1px solid rgba(128, 128, 128, 0.15);
-}
.card-empty {
text-align: center;
diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue
index e2a7cf7e..efd0ec9c 100644
--- a/frontend/src/pages/inbounds/InboundsPage.vue
+++ b/frontend/src/pages/inbounds/InboundsPage.vue
@@ -18,9 +18,6 @@ import CustomStatistic from '@/components/CustomStatistic.vue';
import { useNodeList } from '@/composables/useNodeList.js';
import InboundList from './InboundList.vue';
import InboundFormModal from './InboundFormModal.vue';
-import ClientFormModal from './ClientFormModal.vue';
-import ClientBulkModal from './ClientBulkModal.vue';
-import CopyClientsModal from './CopyClientsModal.vue';
import InboundInfoModal from './InboundInfoModal.vue';
import QrCodeModal from './QrCodeModal.vue';
import TextModal from '@/components/TextModal.vue';
@@ -81,17 +78,6 @@ const formOpen = ref(false);
const formMode = ref('add');
const formDbInbound = ref(null);
-// === Client modal (single + bulk) =====================================
-const clientOpen = ref(false);
-const clientMode = ref('add');
-const clientDbInbound = ref(null);
-const clientIndex = ref(null);
-
-const bulkOpen = ref(false);
-const bulkDbInbound = ref(null);
-const copyOpen = ref(false);
-const copyDbInbound = ref(null);
-
// === Info / QR-code modals ===========================================
const infoOpen = ref(false);
const infoDbInbound = ref(null);
@@ -283,73 +269,6 @@ function findClientIndex(dbInbound, client) {
return idx >= 0 ? idx : 0;
}
-function getClientId(protocol, client) {
- switch (protocol) {
- case 'trojan': return client.password;
- case 'shadowsocks': return client.email;
- case 'hysteria': return client.auth;
- default: return client.id;
- }
-}
-
-// === Per-client handlers (called from the expand-row table) =========
-function onEditClient({ dbInbound, client }) {
- clientMode.value = 'edit';
- clientDbInbound.value = dbInbound;
- clientIndex.value = findClientIndex(dbInbound, client);
- clientOpen.value = true;
-}
-
-function onQrcodeClient({ dbInbound, client }) {
- qrDbInbound.value = checkFallback(dbInbound);
- qrClient.value = client || null;
- qrOpen.value = true;
-}
-
-function onInfoClient({ dbInbound, client }) {
- infoDbInbound.value = checkFallback(dbInbound);
- infoClientIndex.value = findClientIndex(dbInbound, client);
- infoOpen.value = true;
-}
-
-async function onResetTrafficClient({ dbInbound, client }) {
- const msg = await HttpUtil.post(
- `/panel/api/inbounds/${dbInbound.id}/resetClientTraffic/${client.email}`,
- );
- if (msg?.success) await refresh();
-}
-
-async function onDeleteClient({ dbInbound, client }) {
- const clientId = getClientId(dbInbound.protocol, client);
- const msg = await HttpUtil.post(`/panel/api/inbounds/${dbInbound.id}/delClient/${clientId}`);
- 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
- // keeps the wire shape identical to the modal save path.
- const inbound = dbInbound.toInbound();
- const clients = inbound?.clients || [];
- const idx = findClientIndex(dbInbound, client);
- if (idx < 0 || !clients[idx]) return;
- clients[idx].enable = next;
- const clientId = getClientId(dbInbound.protocol, clients[idx]);
- const msg = await HttpUtil.post(`/panel/api/inbounds/updateClient/${clientId}`, {
- id: dbInbound.id,
- settings: `{"clients": [${clients[idx].toString()}]}`,
- });
- if (msg?.success) await refresh();
-}
-
function onAddInbound() {
formMode.value = 'add';
formDbInbound.value = null;
@@ -362,18 +281,6 @@ function openEdit(dbInbound) {
formOpen.value = true;
}
-function openAddClient(dbInbound) {
- clientMode.value = 'add';
- clientDbInbound.value = dbInbound;
- clientIndex.value = null;
- clientOpen.value = true;
-}
-
-function openAddBulkClient(dbInbound) {
- bulkDbInbound.value = dbInbound;
- bulkOpen.value = true;
-}
-
// Per-row destructive actions go through Modal.confirm (matches legacy).
function confirmDelete(dbInbound) {
Modal.confirm({
@@ -492,12 +399,6 @@ function onRowAction({ key, dbInbound }) {
case 'edit':
openEdit(dbInbound);
break;
- case 'addClient':
- openAddClient(dbInbound);
- break;
- case 'addBulkClient':
- openAddBulkClient(dbInbound);
- break;
case 'showInfo':
infoDbInbound.value = checkFallback(dbInbound);
infoClientIndex.value = findClientIndex(dbInbound, null);
@@ -517,10 +418,6 @@ function onRowAction({ key, dbInbound }) {
case 'clipboard':
exportInboundClipboard(dbInbound);
break;
- case 'copyClients':
- copyDbInbound.value = dbInbound;
- copyOpen.value = true;
- break;
case 'delete':
confirmDelete(dbInbound);
break;
@@ -642,10 +539,7 @@ function onRowAction({ key, dbInbound }) {
:sub-enable="subSettings.enable" :nodes-by-id="nodesById" :has-active-node="hasActiveNode"
:stats-version="statsVersion"
@refresh="refresh"
- @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"
- @delete-clients="onDeleteClients" @toggle-enable-client="onToggleEnableClient" />
+ @add-inbound="onAddInbound" @general-action="onGeneralAction" @row-action="onRowAction" />
@@ -654,13 +548,6 @@ function onRowAction({ key, dbInbound }) {
-
-
-