diff options
Diffstat (limited to 'web/html/inbounds.html')
| -rw-r--r-- | web/html/inbounds.html | 152 |
1 files changed, 115 insertions, 37 deletions
diff --git a/web/html/inbounds.html b/web/html/inbounds.html index b945da90..231fc0c0 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -6,7 +6,7 @@ <a-sidebar></a-sidebar> <a-layout id="content-layout"> <a-layout-content> - <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> + <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large"> <transition name="list" appear> <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> @@ -14,10 +14,7 @@ </transition> <transition name="list" appear> <a-row v-if="!loadingStates.fetched"> - <a-card - :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> - <a-spin tip='{{ i18n "loading" }}'></a-spin> - </a-card> + <div :style="{ minHeight: 'calc(100vh - 120px)' }"></div> </a-row> <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else> <a-col> @@ -1101,7 +1098,10 @@ } data.sniffing = inbound.sniffing.toString(); - await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal); + const formData = new FormData(); + Object.keys(data).forEach(key => formData.append(key, data[key])); + + await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, formData, inModal); }, openAddClient(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); @@ -1291,9 +1291,36 @@ infoModal.show(newDbInbound, index); }, switchEnable(dbInboundId, state) { - dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); + let dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); + if (!dbInbound) return; dbInbound.enable = state; - this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound); + let inbound = dbInbound.toInbound(); + const data = { + up: dbInbound.up, + down: dbInbound.down, + total: dbInbound.total, + remark: dbInbound.remark, + enable: dbInbound.enable, + expiryTime: dbInbound.expiryTime, + trafficReset: dbInbound.trafficReset, + lastTrafficResetTime: dbInbound.lastTrafficResetTime, + + listen: inbound.listen, + port: inbound.port, + protocol: inbound.protocol, + settings: inbound.settings.toString(), + }; + if (inbound.canEnableStream()) { + data.streamSettings = inbound.stream.toString(); + } else if (inbound.stream?.sockopt) { + data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2); + } + data.sniffing = inbound.sniffing.toString(); + + const formData = new FormData(); + Object.keys(data).forEach(key => formData.append(key, data[key])); + + this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData); }, async switchEnableClient(dbInboundId, client) { this.loading() @@ -1367,42 +1394,54 @@ isExpiry(dbInbound, index) { return dbInbound.toInbound().isExpiry(index); }, + getClientStats(dbInbound, email) { + if (!dbInbound) return null; + if (!dbInbound._clientStatsMap) { + dbInbound._clientStatsMap = new Map(); + if (dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) { + for (const stats of dbInbound.clientStats) { + dbInbound._clientStatsMap.set(stats.email, stats); + } + } + } + return dbInbound._clientStatsMap.get(email); + }, getUpStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up : 0; }, getDownStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.down : 0; }, getSumStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up + clientStats.down : 0; }, getAllTimeClient(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; return clientStats.allTime || (clientStats.up + clientStats.down); }, getRemStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; - remained = clientStats.total - (clientStats.up + clientStats.down); + let remained = clientStats.total - (clientStats.up + clientStats.down); return remained > 0 ? remained : 0; }, clientStatsColor(dbInbound, email) { - if (email.length == 0) return ColorUtils.clientUsageColor(); - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return ColorUtils.clientUsageColor(); + let clientStats = this.getClientStats(dbInbound, email); return ColorUtils.clientUsageColor(clientStats, app.trafficDiff) }, statsProgress(dbInbound, email) { - if (email.length == 0) return 100; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 100; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; if (clientStats.total == 0) return 100; return 100 * (clientStats.down + clientStats.up) / clientStats.total; @@ -1415,11 +1454,11 @@ return 100 * (1 - (remainedSeconds / resetSeconds)); }, statsExpColor(dbInbound, email) { - if (email.length == 0) return '#7a316f'; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return '#7a316f'; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return '#7a316f'; - statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); - expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); + let statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); + let expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); switch (true) { case statsColor == "red" || expColor == "red": return "#cf3c3c"; // Red @@ -1432,12 +1471,12 @@ } }, isClientEnabled(dbInbound, email) { - clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null; + let clientStats = dbInbound ? this.getClientStats(dbInbound, email) : null; return clientStats ? clientStats['enable'] : true; }, isClientDepleted(dbInbound, email) { - if (!email || !dbInbound || !dbInbound.clientStats) return false; - const stats = dbInbound.clientStats.find(s => s.email === email); + if (!email || !dbInbound) return false; + const stats = this.getClientStats(dbInbound, email); if (!stats) return false; const total = stats.total ?? 0; const used = (stats.up ?? 0) + (stats.down ?? 0); @@ -1557,12 +1596,18 @@ pagination(obj) { if (this.pageSize > 0 && obj.length > this.pageSize) { // Set page options based on object size - sizeOptions = []; - for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) { - sizeOptions.push(i.toString()); + let sizeOptions = [this.pageSize.toString()]; + const increments = [2, 5, 10, 20]; + for (const m of increments) { + const val = this.pageSize * m; + if (val < obj.length && val <= 1000) { + sizeOptions.push(val.toString()); + } } // Add option to see all in one page - sizeOptions.push(i.toString()); + if (!sizeOptions.includes(obj.length.toString())) { + sizeOptions.push(obj.length.toString()); + } p = { showSizeChanger: true, @@ -1605,11 +1650,25 @@ } }); + // Listen for invalidate signals (sent when payload is too large for WebSocket) + // The server sends a lightweight notification and we re-fetch via REST API + let invalidateTimer = null; + window.wsClient.on('invalidate', (payload) => { + if (payload && (payload.type === 'inbounds' || payload.type === 'traffic')) { + // Debounce to avoid flooding the REST API with multiple invalidate signals + if (invalidateTimer) clearTimeout(invalidateTimer); + invalidateTimer = setTimeout(() => { + invalidateTimer = null; + this.getDBInbounds(); + }, 1000); + } + }); + // Listen for traffic updates window.wsClient.on('traffic', (payload) => { // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event // because clientTraffics contains delta/incremental values, not total accumulated values. - // Total traffic is updated via the 'inbounds' event which contains accumulated values from database. + // Total traffic is updated via the 'inbounds' WebSocket event (or 'invalidate' fallback for large panels). // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { @@ -1627,22 +1686,27 @@ this.onlineClients = nextOnlineClients; if (onlineChanged) { // Recalculate client counts to update online status + // Use $set for Vue 2 reactivity — direct array index assignment is not reactive this.dbInbounds.forEach(dbInbound => { const inbound = this.inbounds.find(ib => ib.id === dbInbound.id); if (inbound && this.clientCount[dbInbound.id]) { - this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound); + this.$set(this.clientCount, dbInbound.id, this.getClientCounts(dbInbound, inbound)); } }); + // Always trigger UI refresh — not just when filter is enabled if (this.enableFilter) { this.filterInbounds(); + } else { + this.searchInbounds(this.searchKey); } } } // Update last online map in real-time + // Replace entirely (server sends the full map) to avoid unbounded growth from deleted clients if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') { - this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap }; + this.lastOnlineMap = payload.lastOnlineMap; } }); @@ -1697,4 +1761,18 @@ }, }); </script> +<style> + #content-layout > .ant-layout-content > .ant-spin-nested-loading > div > .ant-spin { + position: fixed !important; + top: 50vh !important; + left: calc(50vw + 100px) !important; + transform: translate(-50%, -50%); + z-index: 99999 !important; + } + @media (max-width: 768px) { + #content-layout > .ant-layout-content > .ant-spin-nested-loading > div > .ant-spin { + left: 50vw !important; + } + } +</style> {{ template "page/body_end" .}}
\ No newline at end of file |
