diff options
| author | lolka1333 <xtrafcyz@gmail.com> | 2026-01-05 07:50:40 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-05 07:50:40 +0300 |
| commit | 4800f8fb706a092a38255ee70904227238b2a6f6 (patch) | |
| tree | 97253dc029dfd139c74e688cc28643fc22c4a12b /web | |
| parent | a9770e1da2453269a6337f0e8ab469c44ef08af5 (diff) | |
feat: Real-time Outbound Traffic, UI Improvements & Fix (#3629)
* Refactor HTML and JavaScript for improved UI and functionality
- Cleaned up JavaScript methods in subscription.js for better readability.
- Updated inbounds.html to clarify traffic update handling and removed unnecessary comments.
- Enhanced xray.html by correcting casing in routingDomainStrategies.
- Added mobile touch scrolling styles in page.html for better tab navigation on small screens.
- Streamlined vless.html by removing redundant line breaks and improving form layout.
- Refined subscription subpage.html for better structure and user experience.
- Adjusted outbounds.html to improve button visibility and functionality.
- Updated xray_traffic_job.go to ensure accurate traffic updates and real-time UI refresh.
* Refactor client traffic handling in InboundService
- Updated addClientTraffic method to initialize onlineClients as an empty slice instead of nil.
- Improved clarity and consistency in handling empty onlineUsers scenario.
* Add WebSocket support for outbounds traffic updates
- Implemented WebSocket connection in xray.html to handle real-time updates for outbounds traffic.
- Enhanced xray_traffic_job.go to retrieve and broadcast outbounds traffic updates.
- Introduced MessageTypeOutbounds in hub.go for managing outbounds messages.
- Added BroadcastOutbounds function in notifier.go to facilitate broadcasting outbounds updates to connected clients.
---------
Co-authored-by: lolka1333 <test123@gmail.com>
Diffstat (limited to 'web')
| -rw-r--r-- | web/assets/js/subscription.js | 10 | ||||
| -rw-r--r-- | web/html/common/page.html | 34 | ||||
| -rw-r--r-- | web/html/form/protocol/vless.html | 211 | ||||
| -rw-r--r-- | web/html/inbounds.html | 23 | ||||
| -rw-r--r-- | web/html/settings/panel/subscription/subpage.html | 167 | ||||
| -rw-r--r-- | web/html/settings/xray/outbounds.html | 4 | ||||
| -rw-r--r-- | web/html/xray.html | 13 | ||||
| -rw-r--r-- | web/job/xray_traffic_job.go | 24 | ||||
| -rw-r--r-- | web/service/inbound.go | 4 | ||||
| -rw-r--r-- | web/websocket/hub.go | 1 | ||||
| -rw-r--r-- | web/websocket/notifier.go | 8 |
11 files changed, 260 insertions, 239 deletions
diff --git a/web/assets/js/subscription.js b/web/assets/js/subscription.js index c7627837..b79d361c 100644 --- a/web/assets/js/subscription.js +++ b/web/assets/js/subscription.js @@ -138,14 +138,14 @@ return `streisand://import/${encodeURIComponent(this.app.subUrl)}`; }, v2raytunUrl() { - return this.app.subUrl; + return this.app.subUrl; }, npvtunUrl() { - return this.app.subUrl; + return this.app.subUrl; }, - happUrl() { - return `happ://add/${encodeURIComponent(this.app.subUrl)}`; - } + happUrl() { + return `happ://add/${encodeURIComponent(this.app.subUrl)}`; + } }, methods: { renderLink, diff --git a/web/html/common/page.html b/web/html/common/page.html index 0af63afb..058682d5 100644 --- a/web/html/common/page.html +++ b/web/html/common/page.html @@ -24,6 +24,40 @@ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } + + /* mobile touch scrolling for tabs */ + @media (max-width: 576px) { + .ant-tabs-nav-container { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + overscroll-behavior-x: contain; + white-space: nowrap; + max-width: 100%; + padding: 0 !important; /* Remove padding for arrows */ + } + .ant-tabs-nav-wrap { + overflow: visible !important; + padding: 0 !important; + } + .ant-tabs-nav-scroll { + overflow: visible !important; + box-shadow: none !important; + } + .ant-tabs-nav { + display: flex !important; + transform: none !important; /* Disable JS transform */ + width: auto !important; + margin: 0 !important; + } + .ant-tabs-tab-prev, + .ant-tabs-tab-next { + display: none !important; /* Hide arrows */ + } + .ant-tabs-nav-container::-webkit-scrollbar { + display: none; + } + } </style> <title>{{ .host }} – {{ i18n .title}}</title> {{ end }} diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html index bdf75be3..fc9c3852 100644 --- a/web/html/form/protocol/vless.html +++ b/web/html/form/protocol/vless.html @@ -1,6 +1,5 @@ {{define "form/vless"}} -<a-collapse activeKey="0" - v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> +<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> {{template "form/client"}} </a-collapse-panel> @@ -22,115 +21,103 @@ </a-collapse-panel> </a-collapse> <template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality"> - <a-form :colon="false" :label-col="{ md: {span:8} }" - :wrapper-col="{ md: {span:14} }"> - <a-form-item label="Authentication"> - <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" - :dropdown-class-name="themeSwitcher.currentTheme"> - <a-select-option value="X25519, not Post-Quantum">X25519 (not - Post-Quantum)</a-select-option> - <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 - (Post-Quantum)</a-select-option> - </a-select> - </a-form-item> - <a-form-item label="decryption"> - <a-input v-model.trim="inbound.settings.decryption"></a-input> - </a-form-item> - <a-form-item label="encryption"> - <a-input v-model="inbound.settings.encryption"></a-input> - </a-form-item> - <a-form-item label=" "> - <a-space> - <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New - keys</a-button> - <a-button danger @click="clearVlessEnc">Clear</a-button> - </a-space> - </a-form-item> - </a-form> - <a-divider v-if="inbound.settings.selectedAuth" - :style="{ margin: '5px 0' }"></a-divider> -</template> -<template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> - <a-form :colon="false" :label-col="{ md: {span:8} }" - :wrapper-col="{ md: {span:14} }"> - <a-form-item label="Fallbacks"> - <a-button icon="plus" type="primary" size="small" - @click="inbound.settings.addFallback()"></a-button> - </a-form-item> - </a-form> + <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> + <a-form-item label="Authentication"> + <a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" + :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option value="X25519, not Post-Quantum">X25519 (not + Post-Quantum)</a-select-option> + <a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 + (Post-Quantum)</a-select-option> + </a-select> + </a-form-item> + <a-form-item label="decryption"> + <a-input v-model.trim="inbound.settings.decryption"></a-input> + </a-form-item> + <a-form-item label="encryption"> + <a-input v-model="inbound.settings.encryption"></a-input> + </a-form-item> + <a-form-item label=" "> + <a-space> + <a-button type="primary" icon="import" @click="getNewVlessEnc">Get New + keys</a-button> + <a-button danger @click="clearVlessEnc">Clear</a-button> + </a-space> + </a-form-item> + </a-form> + <a-divider :style="{ margin: '5px 0' }"></a-divider> + </template> + <template v-if="inbound.isTcp && !inbound.settings.selectedAuth"> + <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> + <a-form-item label="Fallbacks"> + <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> + </a-form-item> + </a-form> - <!-- vless fallbacks --> - <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" - :label-col="{ md: {span:8} }" - :wrapper-col="{ md: {span:14} }"> - <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon - type="delete" - @click="() => inbound.settings.delFallback(index)" - :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> - </a-divider> - <a-form-item label='SNI'> - <a-input v-model="fallback.name"></a-input> - </a-form-item> - <a-form-item label='ALPN'> - <a-input v-model="fallback.alpn"></a-input> - </a-form-item> - <a-form-item label='Path'> - <a-input v-model="fallback.path"></a-input> - </a-form-item> - <a-form-item label='Dest'> - <a-input v-model="fallback.dest"></a-input> - </a-form-item> - <a-form-item label='xVer'> - <a-input-number v-model.number="fallback.xver" :min="0" - :max="2"></a-input-number> - </a-form-item> - </a-form> - <a-divider :style="{ margin: '5px 0' }"></a-divider> -</template> -<template v-if="inbound.canEnableVisionSeed()"> - <a-form :colon="false" :label-col="{ md: {span:8} }" - :wrapper-col="{ md: {span:14} }"> - <a-form-item label="Vision Seed"> - <a-row :gutter="8"> - <a-col :span="6"> - <a-input-number - :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" - @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" - :style="{ width: '100%' }" - placeholder="900" addon-before="[0]"></a-input-number> - </a-col> - <a-col :span="6"> - <a-input-number - :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" - @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" - :style="{ width: '100%' }" - placeholder="500" addon-before="[1]"></a-input-number> - </a-col> - <a-col :span="6"> - <a-input-number - :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" - @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" - :style="{ width: '100%' }" - placeholder="900" addon-before="[2]"></a-input-number> - </a-col> - <a-col :span="6"> - <a-input-number - :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" - @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" - :style="{ width: '100%' }" - placeholder="256" addon-before="[3]"></a-input-number> - </a-col> - </a-row> - <a-space :size="8" :style="{ marginTop: '8px' }"> - <a-button type="primary" @click="setRandomTestseed"> - Rand - </a-button> - <a-button @click="resetTestseed"> - Reset - </a-button> - </a-space> - </a-form-item> - </a-form> - <a-divider :style="{ margin: '5px 0' }"></a-divider> -</template> + <!-- vless fallbacks --> + <a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }" + :wrapper-col="{ md: {span:14} }"> + <a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete" + @click="() => inbound.settings.delFallback(index)" + :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> + </a-divider> + <a-form-item label='SNI'> + <a-input v-model="fallback.name"></a-input> + </a-form-item> + <a-form-item label='ALPN'> + <a-input v-model="fallback.alpn"></a-input> + </a-form-item> + <a-form-item label='Path'> + <a-input v-model="fallback.path"></a-input> + </a-form-item> + <a-form-item label='Dest'> + <a-input v-model="fallback.dest"></a-input> + </a-form-item> + <a-form-item label='xVer'> + <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number> + </a-form-item> + </a-form> + <a-divider :style="{ margin: '5px 0' }"></a-divider> + </template> + <template v-if="inbound.canEnableVisionSeed()"> + <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> + <a-form-item label="Vision Seed"> + <a-row :gutter="8"> + <a-col :span="6"> + <a-input-number + :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" + @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" + placeholder="900" addon-before="[0]"></a-input-number> + </a-col> + <a-col :span="6"> + <a-input-number + :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" + @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" + placeholder="500" addon-before="[1]"></a-input-number> + </a-col> + <a-col :span="6"> + <a-input-number + :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" + @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" + placeholder="900" addon-before="[2]"></a-input-number> + </a-col> + <a-col :span="6"> + <a-input-number + :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" + @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" + placeholder="256" addon-before="[3]"></a-input-number> + </a-col> + </a-row> + <a-space :size="8" :style="{ marginTop: '8px' }"> + <a-button type="primary" @click="setRandomTestseed"> + Rand + </a-button> + <a-button @click="resetTestseed"> + Reset + </a-button> + </a-space> + </a-form-item> + </a-form> + <a-divider :style="{ margin: '5px 0' }"></a-divider> + </template> {{end}}
\ No newline at end of file diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 4e1149ae..eeffd98d 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -1608,24 +1608,9 @@ // Listen for traffic updates window.wsClient.on('traffic', (payload) => { - if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) { - // Update client traffic statistics - payload.clientTraffics.forEach(clientTraffic => { - const dbInbound = this.dbInbounds.find(ib => { - if (!ib) return false; - const clients = this.getInboundClients(ib); - return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email); - }); - if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) { - const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email); - if (stats) { - stats.up = clientTraffic.up || stats.up; - stats.down = clientTraffic.down || stats.down; - stats.total = clientTraffic.total || stats.total; - } - } - }); - } + // 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. // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { @@ -1645,8 +1630,6 @@ } }); - // Notifications disabled - white notifications are not needed - // Fallback to polling if WebSocket fails window.wsClient.on('error', () => { console.warn('WebSocket connection failed, falling back to polling'); diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index 0043d0d2..222352ff 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -20,28 +20,20 @@ </a-space> </template> <template #extra> - <a-popover - :overlay-class-name="themeSwitcher.currentTheme" - title='{{ i18n "menu.settings" }}' + <a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click"> <template #content> <a-space direction="vertical" :size="10"> <a-theme-switch-login></a-theme-switch-login> <span>{{ i18n "pages.settings.language" }}</span> - <a-select ref="selectLang" class="w-100" - v-model="lang" + <a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme"> - <a-select-option :value="l.value" - label="English" - v-for="l in LanguageManager.supportedLanguages" - :key="l.value"> - <span role="img" - :aria-label="l.name" - v-text="l.icon"></span> - <span - v-text="l.name"></span> + <a-select-option :value="l.value" label="English" + v-for="l in LanguageManager.supportedLanguages" :key="l.value"> + <span role="img" :aria-label="l.name" v-text="l.icon"></span> + <span v-text="l.name"></span> </a-select-option> </a-select> </a-space> @@ -53,42 +45,31 @@ <a-form layout="vertical"> <a-form-item> <a-space direction="vertical" align="center"> - <a-row type="flex" :gutter="[8,8]" - justify="center" style="width:100%"> - <a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24" - style="text-align:center;"> + <a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%"> + <a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24" style="text-align:center;"> <tr-qr-box class="qr-box"> - <a-tag color="purple" - class="qr-tag"> + <a-tag color="purple" class="qr-tag"> <span>{{ i18n "pages.settings.subSettings"}}</span> </a-tag> <tr-qr-bg class="qr-bg-sub"> - <tr-qr-bg-inner - class="qr-bg-sub-inner"> - <canvas id="qrcode" - class="qr-cv" - title='{{ i18n "copy" }}' + <tr-qr-bg-inner class="qr-bg-sub-inner"> + <canvas id="qrcode" class="qr-cv" title='{{ i18n "copy" }}' @click="copy(app.subUrl)"></canvas> </tr-qr-bg-inner> </tr-qr-bg> </tr-qr-box> </a-col> - <a-col v-if="app.subJsonUrl" :xs="24" :sm="12" - style="text-align:center;"> + <a-col v-if="app.subJsonUrl" :xs="24" :sm="12" style="text-align:center;"> <tr-qr-box class="qr-box"> - <a-tag color="purple" - class="qr-tag"> + <a-tag color="purple" class="qr-tag"> <span>{{ i18n "pages.settings.subSettings"}} Json</span> </a-tag> <tr-qr-bg class="qr-bg-sub"> - <tr-qr-bg-inner - class="qr-bg-sub-inner"> - <canvas id="qrcode-subjson" - class="qr-cv" - title='{{ i18n "copy" }}' + <tr-qr-bg-inner class="qr-bg-sub-inner"> + <canvas id="qrcode-subjson" class="qr-cv" title='{{ i18n "copy" }}' @click="copy(app.subJsonUrl)"></canvas> </tr-qr-bg-inner> </tr-qr-bg> @@ -100,45 +81,36 @@ <a-form-item> <a-descriptions bordered :column="1" size="small"> - <a-descriptions-item - label='{{ i18n "subscription.subId" }}'>[[ + <a-descriptions-item label='{{ i18n "subscription.subId" }}'>[[ app.sId ]]</a-descriptions-item> - <a-descriptions-item - label='{{ i18n "subscription.status" }}'> + <a-descriptions-item label='{{ i18n "subscription.status" }}'> <template v-if="isUnlimited"> <a-tag color="purple">{{ i18n "subscription.unlimited" }}</a-tag> </template> <template v-else> - <a-tag - :color="isActive ? 'green' : 'red'">[[ + <a-tag :color="isActive ? 'green' : 'red'">[[ isActive ? '{{ i18n "subscription.active" }}' : '{{ i18n "subscription.inactive" }}' ]]</a-tag> </template> </a-descriptions-item> - <a-descriptions-item - label='{{ i18n "subscription.downloaded" }}'>[[ + <a-descriptions-item label='{{ i18n "subscription.downloaded" }}'>[[ app.download ]]</a-descriptions-item> - <a-descriptions-item - label='{{ i18n "subscription.uploaded" }}'>[[ + <a-descriptions-item label='{{ i18n "subscription.uploaded" }}'>[[ app.upload ]]</a-descriptions-item> - <a-descriptions-item - label='{{ i18n "usage" }}'>[[ app.used + <a-descriptions-item label='{{ i18n "usage" }}'>[[ app.used ]]</a-descriptions-item> - <a-descriptions-item - label='{{ i18n "subscription.totalQuota" }}'>[[ + <a-descriptions-item label='{{ i18n "subscription.totalQuota" }}'>[[ app.total ]]</a-descriptions-item> - <a-descriptions-item v-if="app.totalByte > 0" - label='{{ i18n "remained" }}'>[[ + <a-descriptions-item v-if="app.totalByte > 0" label='{{ i18n "remained" }}'>[[ app.remained ]]</a-descriptions-item> - <a-descriptions-item - label='{{ i18n "lastOnline" }}'> + <a-descriptions-item label='{{ i18n "lastOnline" }}'> <template v-if="app.lastOnlineMs > 0"> [[ IntlUtil.formatDate(app.lastOnlineMs) ]] </template> @@ -146,8 +118,7 @@ <span>-</span> </template> </a-descriptions-item> - <a-descriptions-item - label='{{ i18n "subscription.expiry" }}'> + <a-descriptions-item label='{{ i18n "subscription.expiry" }}'> <template v-if="app.expireMs === 0"> {{ i18n "subscription.noExpiry" }} </template> @@ -160,32 +131,48 @@ </a-form> <br /> - <a-list bordered> - <a-list-item v-for="(link, idx) in links" :key="link"> - <div style="width:100%; text-align:center;"> - <a-button type="primary" :block="isMobile" - @click="copy(link)">[[ linkName(link, idx) - ]]</a-button> + <div v-for="(link, idx) in links" :key="link" + style="position: relative; margin-bottom: 20px; text-align: center;"> + <div class="qr-box" style="display: inline-block; width: 100%; max-width: 100%;"> + <a-tag color="purple" + style="margin-bottom: -10px; position: relative; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"> + <span>[[ linkName(link, idx) ]]</span> + </a-tag> + <div @click="copy(link)" style=" + cursor: pointer; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 25px 20px 15px 20px; + margin-top: -12px; + word-break: break-all; + color: #fff; + font-size: 13px; + line-height: 1.5; + text-align: left; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + transition: all 0.3s; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + " onmouseover="this.style.background='rgba(0, 0, 0, 0.3)'; this.style.borderColor='rgba(255, 255, 255, 0.2)'" + onmouseout="this.style.background='rgba(0, 0, 0, 0.2)'; this.style.borderColor='rgba(255, 255, 255, 0.1)'"> + [[ link ]] </div> - </a-list-item> - </a-list> + </div> + </div> + </div> <br /> <a-form layout="vertical"> <a-form-item> - <a-row type="flex" justify="center" :gutter="[8,8]" - style="width:100%"> - <a-col :xs="24" :sm="12" - style="text-align:center;"> + <a-row type="flex" justify="center" :gutter="[8,8]" style="width:100%"> + <a-col :xs="24" :sm="12" style="text-align:center;"> <!-- Android dropdown --> <a-dropdown :trigger="['click']"> <a-button icon="android" :block="isMobile" - :style="{ marginTop: isMobile ? '6px' : 0 }" - size="large" type="primary"> + :style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary"> Android <a-icon type="down" /> </a-button> - <a-menu slot="overlay" - :class="themeSwitcher.currentTheme"> + <a-menu slot="overlay" :class="themeSwitcher.currentTheme"> <a-menu-item key="android-v2box" @click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item> <a-menu-item key="android-v2rayng" @@ -194,39 +181,32 @@ @click="copy(app.subUrl)">Sing-box</a-menu-item> <a-menu-item key="android-v2raytun" @click="copy(app.subUrl)">V2RayTun</a-menu-item> - <a-menu-item key="android-npvtunnel" - @click="copy(app.subUrl)">NPV + <a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV Tunnel</a-menu-item> - <a-menu-item key="android-happ" - @click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item> + <a-menu-item key="android-happ" + @click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item> </a-menu> </a-dropdown> </a-col> - <a-col :xs="24" :sm="12" - style="text-align:center;"> + <a-col :xs="24" :sm="12" style="text-align:center;"> <!-- iOS dropdown --> <a-dropdown :trigger="['click']"> <a-button icon="apple" :block="isMobile" - :style="{ marginTop: isMobile ? '6px' : 0 }" - size="large" type="primary"> + :style="{ marginTop: isMobile ? '6px' : 0 }" size="large" type="primary"> iOS <a-icon type="down" /> </a-button> - <a-menu slot="overlay" - :class="themeSwitcher.currentTheme"> + <a-menu slot="overlay" :class="themeSwitcher.currentTheme"> <a-menu-item key="ios-shadowrocket" @click="open(shadowrocketUrl)">Shadowrocket</a-menu-item> - <a-menu-item key="ios-v2box" - @click="open(v2boxUrl)">V2Box</a-menu-item> + <a-menu-item key="ios-v2box" @click="open(v2boxUrl)">V2Box</a-menu-item> <a-menu-item key="ios-streisand" @click="open(streisandUrl)">Streisand</a-menu-item> <a-menu-item key="ios-v2raytun" @click="copy(v2raytunUrl)">V2RayTun</a-menu-item> - <a-menu-item key="ios-npvtunnel" - @click="copy(npvtunUrl)">NPV + <a-menu-item key="ios-npvtunnel" @click="copy(npvtunUrl)">NPV Tunnel </a-menu-item> - <a-menu-item key="ios-happ" - @click="open(happUrl)">Happ</a-menu-item> + <a-menu-item key="ios-happ" @click="open(happUrl)">Happ</a-menu-item> </a-menu> </a-dropdown> </a-col> @@ -240,17 +220,12 @@ </a-layout> <!-- Bootstrap data for external JS --> -<template id="subscription-data" data-sid="{{ .sId }}" - data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" - data-download="{{ .download }}" - data-upload="{{ .upload }}" data-used="{{ .used }}" - data-total="{{ .total }}" data-remained="{{ .remained }}" - data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}" - data-downloadbyte="{{ .downloadByte }}" - data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}" +<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" + data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}" + data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}" + data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}" data-datepicker="{{ .datepicker }}"></template> -<textarea id="subscription-links" - style="display:none">{{ range .result }}{{ . }} +<textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }} {{ end }}</textarea> {{template "component/aThemeSwitch" .}} diff --git a/web/html/settings/xray/outbounds.html b/web/html/settings/xray/outbounds.html
index a5211fc |
