diff --git a/frontend/src/pages/clients/ClientsPage.vue b/frontend/src/pages/clients/ClientsPage.vue index a03efa83..b506a0d3 100644 --- a/frontend/src/pages/clients/ClientsPage.vue +++ b/frontend/src/pages/clients/ClientsPage.vue @@ -15,11 +15,14 @@ import { UsergroupAddOutlined, SearchOutlined, FilterOutlined, + TeamOutlined, } from '@ant-design/icons-vue'; import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js'; import { useMediaQuery } from '@/composables/useMediaQuery.js'; +import { useWebSocket } from '@/composables/useWebSocket.js'; import AppSidebar from '@/components/AppSidebar.vue'; +import CustomStatistic from '@/components/CustomStatistic.vue'; import { ObjectUtil, SizeFormatter, IntlUtil } from '@/utils'; import { useClients } from './useClients.js'; import ClientFormModal from './ClientFormModal.vue'; @@ -48,8 +51,17 @@ const { resetAllTraffics, delDepleted, setEnable, + applyTrafficEvent, + applyClientStatsEvent, + applyInvalidate, } = useClients(); +useWebSocket({ + traffic: applyTrafficEvent, + client_stats: applyClientStatsEvent, + invalidate: applyInvalidate, +}); + const togglingId = ref(null); async function onToggleEnable(row, next) { @@ -214,7 +226,6 @@ function inboundLabel(id) { function clientBucket(row) { if (!row) return null; - if (!row.enable) return 'deactive'; const traffic = row.traffic || {}; const used = (traffic.up || 0) + (traffic.down || 0); const total = row.totalGB || 0; @@ -222,12 +233,23 @@ function clientBucket(row) { const expired = row.expiryTime > 0 && row.expiryTime <= now; const exhausted = total > 0 && used >= total; if (expired || exhausted) return 'depleted'; + if (!row.enable) return 'deactive'; const nearExpiry = row.expiryTime > 0 && row.expiryTime - now < (expireDiff.value || 0); const nearLimit = total > 0 && total - used < (trafficDiff.value || 0); if (nearExpiry || nearLimit) return 'expiring'; return 'active'; } +function bucketTagColor(bucket) { + switch (bucket) { + case 'depleted': return 'red'; + case 'expiring': return 'orange'; + case 'deactive': return 'default'; + case 'active': return 'green'; + default: return 'default'; + } +} + function clientMatchesProtocol(row, protocol) { if (!protocol) return true; const ids = Array.isArray(row.inboundIds) ? row.inboundIds : []; @@ -255,6 +277,24 @@ const filteredClients = computed(() => { return rows; }); +const summary = computed(() => { + const rows = clients.value || []; + const deactive = []; + const depleted = []; + const expiring = []; + const online = []; + let active = 0; + for (const row of rows) { + const bucket = clientBucket(row); + if (bucket === 'deactive') deactive.push(row.email); + else if (bucket === 'depleted') depleted.push(row.email); + else if (bucket === 'expiring') expiring.push(row.email); + else if (bucket === 'active') active++; + if (row.enable && isOnline(row.email)) online.push(row.email); + } + return { total: rows.length, active, deactive, depleted, expiring, online }; +}); + function onAdd() { formMode.value = 'add'; editingClient.value = null; @@ -412,6 +452,83 @@ const columns = computed(() => [
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +