inbounds.html 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. {{template "head" .}}
  4. <style>
  5. .ant-table:not(.ant-table-expanded-row .ant-table) {
  6. outline: 1px solid #f0f0f0;
  7. outline-offset: -1px;
  8. border-radius: 1rem;
  9. overflow-x: hidden;
  10. }
  11. .dark .ant-table:not(.ant-table-expanded-row .ant-table) {
  12. outline-color: var(--dark-color-table-ring);
  13. }
  14. .ant-table .ant-table-content .ant-table-scroll .ant-table-body {
  15. overflow-y: hidden;
  16. }
  17. .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
  18. margin:-10px 22px !important;
  19. }
  20. .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
  21. border-bottom-left-radius: 1rem;
  22. border-bottom-right-radius: 1rem;
  23. }
  24. .ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
  25. border-bottom-color: transparent;
  26. }
  27. .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
  28. border-bottom-left-radius: 6px;
  29. }
  30. .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
  31. border-bottom-right-radius: 6px;
  32. }
  33. @media (min-width: 769px) {
  34. .ant-layout-content {
  35. margin: 24px 16px;
  36. }
  37. }
  38. @media (max-width: 768px) {
  39. .ant-card-body {
  40. padding: .5rem;
  41. }
  42. .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
  43. margin:-10px 2px !important;
  44. }
  45. }
  46. .dark .ant-switch-small:not(.ant-switch-checked) {
  47. background-color: var(--dark-color-surface-100) !important;
  48. }
  49. .ant-custom-popover-title {
  50. display: flex;
  51. align-items: center;
  52. gap: 10px;
  53. margin: 5px 0;
  54. }
  55. .ant-col-sm-24 {
  56. margin: 0.5rem -2rem 0.5rem 2rem;
  57. }
  58. tr.hideExpandIcon .ant-table-row-expand-icon {
  59. display: none;
  60. }
  61. .infinite-tag {
  62. padding: 0 5px;
  63. border-radius: 2rem;
  64. min-width: 50px;
  65. min-height: 22px;
  66. }
  67. .infinite-bar .ant-progress-inner .ant-progress-bg {
  68. background-color: #F2EAF1;
  69. border: #D5BED2 solid 1px;
  70. }
  71. .dark .infinite-bar .ant-progress-inner .ant-progress-bg {
  72. background-color: #7a316f !important;
  73. border: #7a316f solid 1px;
  74. }
  75. .ant-collapse {
  76. margin: 5px 0;
  77. }
  78. .info-large-tag {
  79. max-width: 200px;
  80. overflow: hidden;
  81. }
  82. .online-animation .ant-badge-status-dot {
  83. animation: onlineAnimation 1.2s linear infinite;
  84. }
  85. @keyframes onlineAnimation {
  86. 0%,
  87. 50%,
  88. 100% {
  89. transform: scale(1);
  90. opacity: 1;
  91. }
  92. 10% {
  93. transform: scale(1.5);
  94. opacity: .2;
  95. }
  96. }
  97. .tr-table-box {
  98. display: flex;
  99. gap: 4px;
  100. justify-content: center;
  101. align-items: center;
  102. }
  103. .tr-table-rt {
  104. flex-basis: 70px;
  105. min-width: 70px;
  106. text-align: end;
  107. }
  108. .tr-table-lt {
  109. flex-basis: 70px;
  110. min-width: 70px;
  111. text-align: start;
  112. }
  113. .tr-table-bar {
  114. flex-basis: 160px;
  115. min-width: 60px;
  116. }
  117. .tr-infinity-ch {
  118. font-size: 14pt;
  119. max-height: 24px;
  120. display: inline-flex;
  121. align-items: center;
  122. }
  123. .ant-table-expanded-row .ant-table .ant-table-body {
  124. overflow-x: hidden;
  125. }
  126. .ant-table-expanded-row .ant-table-tbody>tr>td {
  127. padding: 10px 2px;
  128. }
  129. .ant-table-expanded-row .ant-table-thead>tr>th {
  130. padding: 12px 2px;
  131. }
  132. </style>
  133. <body>
  134. <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
  135. <a-sidebar></a-sidebar>
  136. <a-layout id="content-layout">
  137. <a-layout-content>
  138. <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
  139. <transition name="list" appear>
  140. <a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
  141. message='{{ i18n "secAlertTitle" }}'
  142. color="red"
  143. description='{{ i18n "secAlertSsl" }}'
  144. show-icon closable>
  145. </a-alert>
  146. </transition>
  147. <transition name="list" appear>
  148. <a-card size="small" :style="{ padding: '16px' }" hoverable>
  149. <a-row>
  150. <a-col :sm="12" :md="6">
  151. <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
  152. <template #prefix>
  153. <a-icon type="swap"></a-icon>
  154. </template>
  155. </a-custom-statistic>
  156. </a-col>
  157. <a-col :sm="12" :md="6">
  158. <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
  159. <template #prefix>
  160. <a-icon type="pie-chart"></a-icon>
  161. </template>
  162. </a-custom-statistic>
  163. </a-col>
  164. <a-col :sm="12" :md="6">
  165. <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
  166. <template #prefix>
  167. <a-icon type="bars"></a-icon>
  168. </template>
  169. </a-custom-statistic>
  170. </a-col>
  171. <a-col :sm="12" :md="6">
  172. <a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
  173. <template #prefix>
  174. <a-space direction="horizontal">
  175. <a-icon type="team"></a-icon>
  176. <div>
  177. <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
  178. <a-tag color="green">[[ total.clients ]]</a-tag>
  179. <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
  180. <template slot="content">
  181. <div v-for="clientEmail in total.deactive"><span>[[ clientEmail ]]</span></div>
  182. </template>
  183. <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
  184. </a-popover>
  185. <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
  186. <template slot="content">
  187. <div v-for="clientEmail in total.depleted"><span>[[ clientEmail ]]</span></div>
  188. </template>
  189. <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
  190. </a-popover>
  191. <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
  192. <template slot="content">
  193. <div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div>
  194. </template>
  195. <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
  196. </a-popover>
  197. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  198. <template slot="content">
  199. <div v-for="clientEmail in onlineClients"><span>[[ clientEmail ]]</span></div>
  200. </template>
  201. <a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
  202. </a-popover>
  203. </div>
  204. </a-space>
  205. </template>
  206. </a-custom-statistic>
  207. </a-col>
  208. </a-row>
  209. </a-card>
  210. </transition>
  211. <transition name="list" appear>
  212. <a-card hoverable>
  213. <template #title>
  214. <a-space direction="horizontal">
  215. <a-button type="primary" icon="plus" @click="openAddInbound">
  216. <template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
  217. </a-button>
  218. <a-dropdown :trigger="['click']">
  219. <a-button type="primary" icon="menu">
  220. <template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
  221. </a-button>
  222. <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
  223. <a-menu-item key="import">
  224. <a-icon type="import"></a-icon>
  225. {{ i18n "pages.inbounds.importInbound" }}
  226. </a-menu-item>
  227. <a-menu-item key="export">
  228. <a-icon type="export"></a-icon>
  229. {{ i18n "pages.inbounds.export" }}
  230. </a-menu-item>
  231. <a-menu-item key="subs" v-if="subSettings.enable">
  232. <a-icon type="export"></a-icon>
  233. {{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
  234. </a-menu-item>
  235. <a-menu-item key="resetInbounds">
  236. <a-icon type="reload"></a-icon>
  237. {{ i18n "pages.inbounds.resetAllTraffic" }}
  238. </a-menu-item>
  239. <a-menu-item key="resetClients">
  240. <a-icon type="file-done"></a-icon>
  241. {{ i18n "pages.inbounds.resetAllClientTraffics" }}
  242. </a-menu-item>
  243. <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
  244. <a-icon type="rest"></a-icon>
  245. {{ i18n "pages.inbounds.delDepletedClients" }}
  246. </a-menu-item>
  247. </a-menu>
  248. </a-dropdown>
  249. </a-space>
  250. </template>
  251. <template #extra>
  252. <a-button-group>
  253. <a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
  254. <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
  255. <template #title>
  256. <div class="ant-custom-popover-title">
  257. <a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
  258. <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
  259. </div>
  260. </template>
  261. <template #content>
  262. <a-space direction="vertical">
  263. <span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span>
  264. <a-select v-model="refreshInterval"
  265. :disabled="!isRefreshEnabled"
  266. :style="{ width: '100%' }"
  267. @change="changeRefreshInterval"
  268. :dropdown-class-name="themeSwitcher.currentTheme">
  269. <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
  270. </a-select>
  271. </a-space>
  272. </template>
  273. <a-button icon="down"></a-button>
  274. </a-popover>
  275. </a-button-group>
  276. </template>
  277. <a-space direction="vertical">
  278. <div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
  279. <a-switch v-model="enableFilter"
  280. :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
  281. @change="toggleFilter">
  282. <a-icon slot="checkedChildren" type="search"></a-icon>
  283. <a-icon slot="unCheckedChildren" type="filter"></a-icon>
  284. </a-switch>
  285. <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
  286. <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
  287. <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
  288. <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
  289. <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
  290. <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
  291. <a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
  292. </a-radio-group>
  293. </div>
  294. <a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
  295. :data-source="searchedInbounds"
  296. :scroll="isMobile ? {} : { x: 1000 }"
  297. :pagination=pagination(searchedInbounds)
  298. :expand-icon-as-cell="false"
  299. :expand-row-by-click="false"
  300. :expand-icon-column-index="0"
  301. :indent-size="0"
  302. :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
  303. :style="{ marginTop: '10px' }"
  304. :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}`, emptyText: `{{ i18n "noData" }}` }'>
  305. <template slot="action" slot-scope="text, dbInbound">
  306. <a-dropdown :trigger="['click']">
  307. <a-icon @click="e => e.preventDefault()" type="more" :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
  308. <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
  309. <a-menu-item key="edit">
  310. <a-icon type="edit"></a-icon>
  311. {{ i18n "edit" }}
  312. </a-menu-item>
  313. <a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
  314. <a-icon type="qrcode"></a-icon>
  315. {{ i18n "qrCode" }}
  316. </a-menu-item>
  317. <template v-if="dbInbound.isMultiUser()">
  318. <a-menu-item key="addClient">
  319. <a-icon type="user-add"></a-icon>
  320. {{ i18n "pages.client.add"}}
  321. </a-menu-item>
  322. <a-menu-item key="addBulkClient">
  323. <a-icon type="usergroup-add"></a-icon>
  324. {{ i18n "pages.client.bulk"}}
  325. </a-menu-item>
  326. <a-menu-item key="resetClients">
  327. <a-icon type="file-done"></a-icon>
  328. {{ i18n "pages.inbounds.resetInboundClientTraffics"}}
  329. </a-menu-item>
  330. <a-menu-item key="export">
  331. <a-icon type="export"></a-icon>
  332. {{ i18n "pages.inbounds.export"}}
  333. </a-menu-item>
  334. <a-menu-item key="subs" v-if="subSettings.enable">
  335. <a-icon type="export"></a-icon>
  336. {{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
  337. </a-menu-item>
  338. <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
  339. <a-icon type="rest"></a-icon>
  340. {{ i18n "pages.inbounds.delDepletedClients" }}
  341. </a-menu-item>
  342. </template>
  343. <template v-else>
  344. <a-menu-item key="showInfo">
  345. <a-icon type="info-circle"></a-icon>
  346. {{ i18n "info"}}
  347. </a-menu-item>
  348. </template>
  349. <a-menu-item key="clipboard">
  350. <a-icon type="copy"></a-icon>
  351. {{ i18n "pages.inbounds.exportInbound" }}
  352. </a-menu-item>
  353. <a-menu-item key="resetTraffic">
  354. <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
  355. </a-menu-item>
  356. <a-menu-item key="clone">
  357. <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
  358. </a-menu-item>
  359. <a-menu-item key="delete">
  360. <span :style="{ color: '#FF4D4F' }">
  361. <a-icon type="delete"></a-icon> {{ i18n "delete"}}
  362. </span>
  363. </a-menu-item>
  364. <a-menu-item v-if="isMobile">
  365. <a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
  366. {{ i18n "pages.inbounds.enable" }}
  367. </a-menu-item>
  368. </a-menu>
  369. </a-dropdown>
  370. </template>
  371. <template slot="protocol" slot-scope="text, dbInbound">
  372. <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
  373. <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
  374. <a-tag :style="{ margin: '0' }" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
  375. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
  376. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
  377. </template>
  378. </template>
  379. <template slot="clients" slot-scope="text, dbInbound">
  380. <template v-if="clientCount[dbInbound.id]">
  381. <a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
  382. <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
  383. <template slot="content">
  384. <div v-for="clientEmail in clientCount[dbInbound.id].deactive"><span>[[ clientEmail ]]</span></div>
  385. </template>
  386. <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
  387. </a-popover>
  388. <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
  389. <template slot="content">
  390. <div v-for="clientEmail in clientCount[dbInbound.id].depleted"><span>[[ clientEmail ]]</span></div>
  391. </template>
  392. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
  393. </a-popover>
  394. <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
  395. <template slot="content">
  396. <div v-for="clientEmail in clientCount[dbInbound.id].expiring"><span>[[ clientEmail ]]</span></div>
  397. </template>
  398. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
  399. </a-popover>
  400. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  401. <template slot="content">
  402. <div v-for="clientEmail in clientCount[dbInbound.id].online"><span>[[ clientEmail ]]</span></div>
  403. </template>
  404. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
  405. </a-popover>
  406. </template>
  407. </template>
  408. <template slot="traffic" slot-scope="text, dbInbound">
  409. <a-popover :overlay-class-name="themeSwitcher.currentTheme">
  410. <template slot="content">
  411. <table cellpadding="2" width="100%">
  412. <tr>
  413. <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
  414. <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
  415. </tr>
  416. <tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
  417. <td>{{ i18n "remained" }}</td>
  418. <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
  419. </tr>
  420. </table>
  421. </template>
  422. <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
  423. [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
  424. <template v-if="dbInbound.total > 0">
  425. [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
  426. </template>
  427. <template v-else>
  428. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  429. <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
  430. </svg>
  431. </template>
  432. </a-tag>
  433. </a-popover>
  434. </template>
  435. <template slot="enable" slot-scope="text, dbInbound">
  436. <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
  437. </template>
  438. <template slot="expiryTime" slot-scope="text, dbInbound">
  439. <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
  440. <template slot="content" v-if="app.datepicker === 'gregorian'">
  441. [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
  442. </template>
  443. <template v-else slot="content">
  444. [[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
  445. </template>
  446. <a-tag :style="{ minWidth: '50px' }" :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
  447. [[ remainedDays(dbInbound._expiryTime) ]]
  448. </a-tag>
  449. </a-popover>
  450. <a-tag v-else color="purple" class="infinite-tag">
  451. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  452. <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
  453. </svg>
  454. </a-tag>
  455. </template>
  456. <template slot="info" slot-scope="text, dbInbound">
  457. <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
  458. <template slot="content">
  459. <table cellpadding="2">
  460. <tr>
  461. <td>{{ i18n "pages.inbounds.protocol" }}</td>
  462. <td>
  463. <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag>
  464. <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
  465. <a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
  466. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
  467. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
  468. </template>
  469. </td>
  470. </tr>
  471. <tr>
  472. <td>{{ i18n "pages.inbounds.port" }}</td>
  473. <td><a-tag>[[ dbInbound.port ]]</a-tag></td>
  474. </tr>
  475. <tr v-if="clientCount[dbInbound.id]">
  476. <td>{{ i18n "clients" }}</td>
  477. <td>
  478. <a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
  479. <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
  480. <template slot="content">
  481. <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
  482. </template>
  483. <a-tag :style="{ margin: '0', padding: '0 2px' }" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
  484. </a-popover>
  485. <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
  486. <template slot="content">
  487. <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
  488. </template>
  489. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
  490. </a-popover>
  491. <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
  492. <template slot="content">
  493. <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
  494. </template>
  495. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
  496. </a-popover>
  497. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  498. <template slot="content">
  499. <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
  500. </template>
  501. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
  502. </a-popover>
  503. </td>
  504. </tr>
  505. <tr>
  506. <td>{{ i18n "pages.inbounds.traffic" }}</td>
  507. <td>
  508. <a-popover :overlay-class-name="themeSwitcher.currentTheme">
  509. <template slot="content">
  510. <table cellpadding="2" width="100%">
  511. <tr>
  512. <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td>
  513. <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td>
  514. </tr>
  515. <tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
  516. <td>{{ i18n "remained" }}</td>
  517. <td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
  518. </tr>
  519. </table>
  520. </template>
  521. <a-tag :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
  522. [[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] /
  523. <template v-if="dbInbound.total > 0">
  524. [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
  525. </template>
  526. <template v-else>
  527. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  528. <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
  529. </svg>
  530. </template>
  531. </a-tag>
  532. </a-popover>
  533. </td>
  534. </tr>
  535. <tr>
  536. <td>{{ i18n "pages.inbounds.expireDate" }}</td>
  537. <td>
  538. <a-tag :style="{ minWidth: '50px', textAlign: 'center' }" v-if="dbInbound.expiryTime > 0"
  539. :color="dbInbound.isExpiry? 'red': 'blue'">
  540. <template v-if="app.datepicker === 'gregorian'">
  541. [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
  542. </template>
  543. <template v-else>
  544. [[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
  545. </template>
  546. </a-tag>
  547. <a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
  548. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  549. <path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
  550. </svg>
  551. </a-tag>
  552. </td>
  553. </tr>
  554. </table>
  555. </template>
  556. <a-badge>
  557. <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
  558. <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
  559. <a-icon type="info"></a-icon>
  560. </a-button>
  561. </a-badge>
  562. </a-popover>
  563. </template>
  564. <template slot="expandedRowRender" slot-scope="record">
  565. <a-table
  566. :row-key="client => client.id"
  567. :columns="isMobile ? innerMobileColumns : innerColumns"
  568. :data-source="getInboundClients(record)"
  569. :pagination=pagination(getInboundClients(record))
  570. :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
  571. {{template "component/aClientTable"}}
  572. </a-table>
  573. </template>
  574. </a-table>
  575. </a-space>
  576. </a-card>
  577. </transition>
  578. </a-spin>
  579. </a-layout-content>
  580. </a-layout>
  581. </a-layout>
  582. {{template "js" .}}
  583. <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
  584. <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
  585. <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
  586. <script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
  587. {{template "component/aSidebar" .}}
  588. {{template "component/aThemeSwitch" .}}
  589. {{template "component/aCustomStatistic" .}}
  590. {{template "component/aPersianDatepicker" .}}
  591. <script>
  592. const columns = [{
  593. title: "ID",
  594. align: 'right',
  595. dataIndex: "id",
  596. width: 30,
  597. responsive: ["xs"],
  598. }, {
  599. title: '{{ i18n "pages.inbounds.operate" }}',
  600. align: 'center',
  601. width: 30,
  602. scopedSlots: { customRender: 'action' },
  603. }, {
  604. title: '{{ i18n "pages.inbounds.enable" }}',
  605. align: 'center',
  606. width: 30,
  607. scopedSlots: { customRender: 'enable' },
  608. }, {
  609. title: '{{ i18n "pages.inbounds.remark" }}',
  610. align: 'center',
  611. width: 60,
  612. dataIndex: "remark",
  613. }, {
  614. title: '{{ i18n "pages.inbounds.port" }}',
  615. align: 'center',
  616. dataIndex: "port",
  617. width: 40,
  618. }, {
  619. title: '{{ i18n "pages.inbounds.protocol" }}',
  620. align: 'left',
  621. width: 70,
  622. scopedSlots: { customRender: 'protocol' },
  623. }, {
  624. title: '{{ i18n "clients" }}',
  625. align: 'left',
  626. width: 50,
  627. scopedSlots: { customRender: 'clients' },
  628. }, {
  629. title: '{{ i18n "pages.inbounds.traffic" }}',
  630. align: 'center',
  631. width: 60,
  632. scopedSlots: { customRender: 'traffic' },
  633. }, {
  634. title: '{{ i18n "pages.inbounds.expireDate" }}',
  635. align: 'center',
  636. width: 40,
  637. scopedSlots: { customRender: 'expiryTime' },
  638. }];
  639. const mobileColumns = [{
  640. title: "ID",
  641. align: 'right',
  642. dataIndex: "id",
  643. width: 10,
  644. responsive: ["s"],
  645. }, {
  646. title: '{{ i18n "pages.inbounds.operate" }}',
  647. align: 'center',
  648. width: 25,
  649. scopedSlots: { customRender: 'action' },
  650. }, {
  651. title: '{{ i18n "pages.inbounds.remark" }}',
  652. align: 'left',
  653. width: 70,
  654. dataIndex: "remark",
  655. }, {
  656. title: '{{ i18n "pages.inbounds.info" }}',
  657. align: 'center',
  658. width: 10,
  659. scopedSlots: { customRender: 'info' },
  660. }];
  661. const innerColumns = [
  662. { title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
  663. { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
  664. { title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
  665. { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
  666. { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
  667. { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
  668. ];
  669. const innerMobileColumns = [
  670. { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
  671. { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
  672. { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
  673. ];
  674. const app = new Vue({
  675. delimiters: ['[[', ']]'],
  676. el: '#app',
  677. mixins: [MediaQueryMixin],
  678. data: {
  679. themeSwitcher,
  680. persianDatepicker,
  681. spinning: false,
  682. inbounds: [],
  683. dbInbounds: [],
  684. searchKey: '',
  685. enableFilter: false,
  686. filterBy: '',
  687. searchedInbounds: [],
  688. expireDiff: 0,
  689. trafficDiff: 0,
  690. defaultCert: '',
  691. defaultKey: '',
  692. clientCount: [],
  693. onlineClients: [],
  694. isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
  695. refreshing: false,
  696. refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
  697. subSettings: {
  698. enable : false,
  699. subTitle : '',
  700. subURI : '',
  701. subJsonURI : '',
  702. },
  703. remarkModel: '-ieo',
  704. datepicker: 'gregorian',
  705. tgBotEnable: false,
  706. showAlert: false,
  707. ipLimitEnable: false,
  708. pageSize: 50,
  709. },
  710. methods: {
  711. loading(spinning = true) {
  712. this.spinning = spinning;
  713. },
  714. async getDBInbounds() {
  715. this.refreshing = true;
  716. const msg = await HttpUtil.post('/panel/inbound/list');
  717. if (!msg.success) {
  718. this.refreshing = false;
  719. return;
  720. }
  721. await this.getOnlineUsers();
  722. this.setInbounds(msg.obj);
  723. setTimeout(() => {
  724. this.refreshing = false;
  725. }, 500);
  726. },
  727. async getOnlineUsers() {
  728. const msg = await HttpUtil.post('/panel/inbound/onlines');
  729. if (!msg.success) {
  730. return;
  731. }
  732. this.onlineClients = msg.obj != null ? msg.obj : [];
  733. },
  734. async getDefaultSettings() {
  735. const msg = await HttpUtil.post('/panel/setting/defaultSettings');
  736. if (!msg.success) {
  737. return;
  738. }
  739. with(msg.obj){
  740. this.expireDiff = expireDiff * 86400000;
  741. this.trafficDiff = trafficDiff * 1073741824;
  742. this.defaultCert = defaultCert;
  743. this.defaultKey = defaultKey;
  744. this.tgBotEnable = tgBotEnable;
  745. this.subSettings = {
  746. enable : subEnable,
  747. subTitle : subTitle,
  748. subURI: subURI,
  749. subJsonURI: subJsonURI
  750. };
  751. this.pageSize = pageSize;
  752. this.remarkModel = remarkModel;
  753. this.datepicker = datepicker;
  754. this.ipLimitEnable = ipLimitEnable;
  755. }
  756. },
  757. setInbounds(dbInbounds) {
  758. this.inbounds.splice(0);
  759. this.dbInbounds.splice(0);
  760. this.clientCount.splice(0);
  761. for (const inbound of dbInbounds) {
  762. const dbInbound = new DBInbound(inbound);
  763. to_inbound = dbInbound.toInbound()
  764. this.inbounds.push(to_inbound);
  765. this.dbInbounds.push(dbInbound);
  766. if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
  767. if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
  768. continue;
  769. }
  770. this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
  771. }
  772. }
  773. if(this.enableFilter){
  774. this.filterInbounds();
  775. } else {
  776. this.searchInbounds(this.searchKey);
  777. }
  778. },
  779. getClientCounts(dbInbound, inbound) {
  780. let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
  781. clients = inbound.clients;
  782. clientStats = dbInbound.clientStats
  783. now = new Date().getTime()
  784. if (clients) {
  785. clientCount = clients.length;
  786. if (dbInbound.enable) {
  787. clients.forEach(client => {
  788. if (client.enable) {
  789. active.push(client.email);
  790. if (this.isClientOnline(client.email)) online.push(client.email);
  791. } else {
  792. deactive.push(client.email);
  793. }
  794. });
  795. clientStats.forEach(client => {
  796. if (!client.enable) {
  797. depleted.push(client.email);
  798. } else {
  799. if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
  800. (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
  801. }
  802. });
  803. } else {
  804. clients.forEach(client => {
  805. deactive.push(client.email);
  806. });
  807. }
  808. }
  809. return {
  810. clients: clientCount,
  811. active: active,
  812. deactive: deactive,
  813. depleted: depleted,
  814. expiring: expiring,
  815. online: online,
  816. };
  817. },
  818. searchInbounds(key) {
  819. if (ObjectUtil.isEmpty(key)) {
  820. this.searchedInbounds = this.dbInbounds.slice();
  821. } else {
  822. this.searchedInbounds.splice(0, this.searchedInbounds.length);
  823. this.dbInbounds.forEach(inbound => {
  824. if (ObjectUtil.deepSearch(inbound, key)) {
  825. const newInbound = new DBInbound(inbound);
  826. const inboundSettings = JSON.parse(inbound.settings);
  827. if (inboundSettings.hasOwnProperty('clients')) {
  828. const searchedSettings = { "clients": [] };
  829. inboundSettings.clients.forEach(client => {
  830. if (ObjectUtil.deepSearch(client, key)) {
  831. searchedSettings.clients.push(client);
  832. }
  833. });
  834. newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
  835. }
  836. this.searchedInbounds.push(newInbound);
  837. }
  838. });
  839. }
  840. },
  841. filterInbounds() {
  842. if (ObjectUtil.isEmpty(this.filterBy)) {
  843. this.searchedInbounds = this.dbInbounds.slice();
  844. } else {
  845. this.searchedInbounds.splice(0, this.searchedInbounds.length);
  846. this.dbInbounds.forEach(inbound => {
  847. const newInbound = new DBInbound(inbound);
  848. const inboundSettings = JSON.parse(inbound.settings);
  849. if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
  850. const list = this.clientCount[inbound.id][this.filterBy];
  851. if (list.length > 0) {
  852. const filteredSettings = { "clients": [] };
  853. inboundSettings.clients.forEach(client => {
  854. if (list.includes(client.email)) {
  855. filteredSettings.clients.push(client);
  856. }
  857. });
  858. newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
  859. this.searchedInbounds.push(newInbound);
  860. }
  861. }
  862. });
  863. }
  864. },
  865. toggleFilter(){
  866. if(this.enableFilter) {
  867. this.searchKey = '';
  868. } else {
  869. this.filterBy = '';
  870. this.searchedInbounds = this.dbInbounds.slice();
  871. }
  872. },
  873. generalActions(action) {
  874. switch (action.key) {
  875. case "import":
  876. this.importInbound();
  877. break;
  878. case "export":
  879. this.exportAllLinks();
  880. break;
  881. case "subs":
  882. this.exportAllSubs();
  883. break;
  884. case "resetInbounds":
  885. this.resetAllTraffic();
  886. break;
  887. case "resetClients":
  888. this.resetAllClientTraffics(-1);
  889. break;
  890. case "delDepletedClients":
  891. this.delDepletedClients(-1)
  892. break;
  893. }
  894. },
  895. clickAction(action, dbInbound) {
  896. switch (action.key) {
  897. case "qrcode":
  898. this.showQrcode(dbInbound.id);
  899. break;
  900. case "showInfo":
  901. this.showInfo(dbInbound.id);
  902. break;
  903. case "edit":
  904. this.openEditInbound(dbInbound.id);
  905. break;
  906. case "addClient":
  907. this.openAddClient(dbInbound.id)
  908. break;
  909. case "addBulkClient":
  910. this.openAddBulkClient(dbInbound.id)
  911. break;
  912. case "export":
  913. this.inboundLinks(dbInbound.id);
  914. break;
  915. case "subs":
  916. this.exportSubs(dbInbound.id);
  917. break;
  918. case "clipboard":
  919. this.copy(dbInbound.id);
  920. break;
  921. case "resetTraffic":
  922. this.resetTraffic(dbInbound.id);
  923. break;
  924. case "resetClients":
  925. this.resetAllClientTraffics(dbInbound.id);
  926. break;
  927. case "clone":
  928. this.openCloneInbound(dbInbound);
  929. break;
  930. case "delete":
  931. this.delInbound(dbInbound.id);
  932. break;
  933. case "delDepletedClients":
  934. this.delDepletedClients(dbInbound.id)
  935. break;
  936. }
  937. },
  938. openCloneInbound(dbInbound) {
  939. this.$confirm({
  940. title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
  941. content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
  942. okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
  943. class: themeSwitcher.currentTheme,
  944. cancelText: '{{ i18n "cancel" }}',
  945. onOk: () => {
  946. const baseInbound = dbInbound.toInbound();
  947. dbInbound.up = 0;
  948. dbInbound.down = 0;
  949. this.cloneInbound(baseInbound, dbInbound);
  950. },
  951. });
  952. },
  953. async cloneInbound(baseInbound, dbInbound) {
  954. const data = {
  955. up: dbInbound.up,
  956. down: dbInbound.down,
  957. total: dbInbound.total,
  958. remark: dbInbound.remark + " - Cloned",
  959. enable: dbInbound.enable,
  960. expiryTime: dbInbound.expiryTime,
  961. listen: '',
  962. port: RandomUtil.randomInteger(10000, 60000),
  963. protocol: baseInbound.protocol,
  964. settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
  965. streamSettings: baseInbound.stream.toString(),
  966. sniffing: baseInbound.sniffing.toString(),
  967. allocate: baseInbound.allocate.toString(),
  968. };
  969. await this.submit('/panel/inbound/add', data, inModal);
  970. },
  971. openAddInbound() {
  972. inModal.show({
  973. title: '{{ i18n "pages.inbounds.addInbound"}}',
  974. okText: '{{ i18n "create"}}',
  975. cancelText: '{{ i18n "close" }}',
  976. confirm: async (inbound, dbInbound) => {
  977. await this.addInbound(inbound, dbInbound, inModal);
  978. },
  979. isEdit: false
  980. });
  981. },
  982. openEditInbound(dbInboundId) {
  983. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  984. const inbound = dbInbound.toInbound();
  985. inModal.show({
  986. title: '{{ i18n "pages.inbounds.modifyInbound"}}',
  987. okText: '{{ i18n "update"}}',
  988. cancelText: '{{ i18n "close" }}',
  989. inbound: inbound,
  990. dbInbound: dbInbound,
  991. confirm: async (inbound, dbInbound) => {
  992. await this.updateInbound(inbound, dbInbound);
  993. },
  994. isEdit: true
  995. });
  996. },
  997. async addInbound(inbound, dbInbound) {
  998. const data = {
  999. up: dbInbound.up,
  1000. down: dbInbound.down,
  1001. total: dbInbound.total,
  1002. remark: dbInbound.remark,
  1003. enable: dbInbound.enable,
  1004. expiryTime: dbInbound.expiryTime,
  1005. listen: inbound.listen,
  1006. port: inbound.port,
  1007. protocol: inbound.protocol,
  1008. settings: inbound.settings.toString(),
  1009. };
  1010. if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
  1011. data.sniffing = inbound.sniffing.toString();
  1012. data.allocate = inbound.allocate.toString();
  1013. await this.submit('/panel/inbound/add', data, inModal);
  1014. },
  1015. async updateInbound(inbound, dbInbound) {
  1016. const data = {
  1017. up: dbInbound.up,
  1018. down: dbInbound.down,
  1019. total: dbInbound.total,
  1020. remark: dbInbound.remark,
  1021. enable: dbInbound.enable,
  1022. expiryTime: dbInbound.expiryTime,
  1023. listen: inbound.listen,
  1024. port: inbound.port,
  1025. protocol: inbound.protocol,
  1026. settings: inbound.settings.toString(),
  1027. };
  1028. if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
  1029. data.sniffing = inbound.sniffing.toString();
  1030. data.allocate = inbound.allocate.toString();
  1031. await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
  1032. },
  1033. openAddClient(dbInboundId) {
  1034. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1035. clientModal.show({
  1036. title: '{{ i18n "pages.client.add"}}',
  1037. okText: '{{ i18n "pages.client.submitAdd"}}',
  1038. dbInbound: dbInbound,
  1039. confirm: async (clients, dbInboundId) => {
  1040. await this.addClient(clients, dbInboundId, clientModal);
  1041. },
  1042. isEdit: false
  1043. });
  1044. },
  1045. openAddBulkClient(dbInboundId) {
  1046. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1047. clientsBulkModal.show({
  1048. title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
  1049. okText: '{{ i18n "pages.client.bulk"}}',
  1050. dbInbound: dbInbound,
  1051. confirm: async (clients, dbInboundId) => {
  1052. await this.addClient(clients, dbInboundId, clientsBulkModal);
  1053. },
  1054. });
  1055. },
  1056. openEditClient(dbInboundId, client) {
  1057. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1058. clients = this.getInboundClients(dbInbound);
  1059. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1060. clientModal.show({
  1061. title: '{{ i18n "pages.client.edit"}}',
  1062. okText: '{{ i18n "pages.client.submitEdit"}}',
  1063. dbInbound: dbInbound,
  1064. index: index,
  1065. confirm: async (client, dbInboundId, clientId) => {
  1066. clientModal.loading();
  1067. await this.updateClient(client, dbInboundId, clientId);
  1068. clientModal.close();
  1069. },
  1070. isEdit: true
  1071. });
  1072. },
  1073. findIndexOfClient(protocol, clients, client) {
  1074. switch (protocol) {
  1075. case Protocols.TROJAN:
  1076. case Protocols.SHADOWSOCKS:
  1077. return clients.findIndex(item => item.password === client.password && item.email === client.email);
  1078. default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
  1079. }
  1080. },
  1081. async addClient(clients, dbInboundId, modal) {
  1082. const data = {
  1083. id: dbInboundId,
  1084. settings: '{"clients": [' + clients.toString() + ']}',
  1085. };
  1086. await this.submit(`/panel/inbound/addClient`, data, modal);
  1087. },
  1088. async updateClient(client, dbInboundId, clientId) {
  1089. const data = {
  1090. id: dbInboundId,
  1091. settings: '{"clients": [' + client.toString() + ']}',
  1092. };
  1093. await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
  1094. },
  1095. resetTraffic(dbInboundId) {
  1096. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1097. this.$confirm({
  1098. title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
  1099. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  1100. class: themeSwitcher.currentTheme,
  1101. okText: '{{ i18n "reset"}}',
  1102. cancelText: '{{ i18n "cancel"}}',
  1103. onOk: () => {
  1104. const inbound = dbInbound.toInbound();
  1105. dbInbound.up = 0;
  1106. dbInbound.down = 0;
  1107. this.updateInbound(inbound, dbInbound);
  1108. },
  1109. });
  1110. },
  1111. delInbound(dbInboundId) {
  1112. this.$confirm({
  1113. title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
  1114. content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
  1115. class: themeSwitcher.currentTheme,
  1116. okText: '{{ i18n "delete"}}',
  1117. cancelText: '{{ i18n "cancel"}}',
  1118. onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
  1119. });
  1120. },
  1121. delClient(dbInboundId, client,confirmation = true) {
  1122. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1123. clientId = this.getClientId(dbInbound.protocol, client);
  1124. if (confirmation){
  1125. this.$confirm({
  1126. title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
  1127. content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
  1128. class: themeSwitcher.currentTheme,
  1129. okText: '{{ i18n "delete"}}',
  1130. cancelText: '{{ i18n "cancel"}}',
  1131. onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
  1132. });
  1133. } else {
  1134. this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
  1135. }
  1136. },
  1137. getSubGroupClients(dbInbounds, currentClient) {
  1138. const response = {
  1139. inbounds: [],
  1140. clients: [],
  1141. editIds: []
  1142. }
  1143. if (dbInbounds && dbInbounds.length > 0 && currentClient) {
  1144. dbInbounds.forEach((dbInboundItem) => {
  1145. const dbInbound = new DBInbound(dbInboundItem);
  1146. if (dbInbound) {
  1147. const inbound = dbInbound.toInbound();
  1148. if (inbound) {
  1149. const clients = inbound.clients;
  1150. if (clients.length > 0) {
  1151. clients.forEach((client) => {
  1152. if (client['subId'] === currentClient['subId']) {
  1153. client['inboundId'] = dbInboundItem.id
  1154. client['clientId'] = this.getClientId(dbInbound.protocol, client)
  1155. response.inbounds.push(dbInboundItem.id)
  1156. response.clients.push(client)
  1157. response.editIds.push(client['clientId'])
  1158. }
  1159. })
  1160. }
  1161. }
  1162. }
  1163. })
  1164. }
  1165. return response;
  1166. },
  1167. getClientId(protocol, client) {
  1168. switch (protocol) {
  1169. case Protocols.TROJAN: return client.password;
  1170. case Protocols.SHADOWSOCKS: return client.email;
  1171. default: return client.id;
  1172. }
  1173. },
  1174. checkFallback(dbInbound) {
  1175. newDbInbound = new DBInbound(dbInbound);
  1176. if (dbInbound.listen.startsWith("@")){
  1177. rootInbound = this.inbounds.find((i) =>
  1178. i.isTcp &&
  1179. ['trojan','vless'].includes(i.protocol) &&
  1180. i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
  1181. );
  1182. if (rootInbound) {
  1183. newDbInbound.listen = rootInbound.listen;
  1184. newDbInbound.port = rootInbound.port;
  1185. newInbound = newDbInbound.toInbound();
  1186. newInbound.stream.security = rootInbound.stream.security;
  1187. newInbound.stream.tls = rootInbound.stream.tls;
  1188. newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
  1189. newDbInbound.streamSettings = newInbound.stream.toString();
  1190. }
  1191. }
  1192. return newDbInbound;
  1193. },
  1194. showQrcode(dbInboundId, client) {
  1195. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1196. newDbInbound = this.checkFallback(dbInbound);
  1197. qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
  1198. },
  1199. showInfo(dbInboundId, client) {
  1200. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1201. index=0;
  1202. if (dbInbound.isMultiUser()){
  1203. inbound = dbInbound.toInbound();
  1204. clients = inbound.clients;
  1205. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1206. }
  1207. newDbInbound = this.checkFallback(dbInbound);
  1208. infoModal.show(newDbInbound, index);
  1209. },
  1210. switchEnable(dbInboundId,state) {
  1211. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1212. dbInbound.enable = state;
  1213. this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
  1214. },
  1215. async switchEnableClient(dbInboundId, client) {
  1216. this.loading()
  1217. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1218. inbound = dbInbound.toInbound();
  1219. clients = inbound.clients;
  1220. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1221. clients[index].enable = !clients[index].enable;
  1222. clientId = this.getClientId(dbInbound.protocol, clients[index]);
  1223. await this.updateClient(clients[index], dbInboundId, clientId);
  1224. this.loading(false);
  1225. },
  1226. async submit(url, data, modal) {
  1227. const msg = await HttpUtil.postWithModal(url, data, modal);
  1228. if (msg.success) {
  1229. await this.getDBInbounds();
  1230. }
  1231. },
  1232. getInboundClients(dbInbound) {
  1233. return dbInbound.toInbound().clients;
  1234. },
  1235. resetClientTraffic(client, dbInboundId, confirmation = true) {
  1236. if (confirmation){
  1237. this.$confirm({
  1238. title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
  1239. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  1240. class: themeSwitcher.currentTheme,
  1241. okText: '{{ i18n "reset"}}',
  1242. cancelText: '{{ i18n "cancel"}}',
  1243. onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
  1244. })
  1245. } else {
  1246. this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
  1247. }
  1248. },
  1249. resetAllTraffic() {
  1250. this.$confirm({
  1251. title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
  1252. content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
  1253. class: themeSwitcher.currentTheme,
  1254. okText: '{{ i18n "reset"}}',
  1255. cancelText: '{{ i18n "cancel"}}',
  1256. onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
  1257. });
  1258. },
  1259. resetAllClientTraffics(dbInboundId) {
  1260. this.$confirm({
  1261. title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
  1262. content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
  1263. class: themeSwitcher.currentTheme,
  1264. okText: '{{ i18n "reset"}}',
  1265. cancelText: '{{ i18n "cancel"}}',
  1266. onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
  1267. })
  1268. },
  1269. delDepletedClients(dbInboundId) {
  1270. this.$confirm({
  1271. title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
  1272. content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
  1273. class: themeSwitcher.currentTheme,
  1274. okText: '{{ i18n "delete"}}',
  1275. cancelText: '{{ i18n "cancel"}}',
  1276. onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
  1277. })
  1278. },
  1279. isExpiry(dbInbound, index) {
  1280. return dbInbound.toInbound().isExpiry(index);
  1281. },
  1282. getUpStats(dbInbound, email) {
  1283. if (email.length == 0) return 0;
  1284. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1285. return clientStats ? clientStats.up : 0;
  1286. },
  1287. getDownStats(dbInbound, email) {
  1288. if (email.length == 0) return 0;
  1289. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1290. return clientStats ? clientStats.down : 0;
  1291. },
  1292. getSumStats(dbInbound, email) {
  1293. if (email.length == 0) return 0;
  1294. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1295. return clientStats ? clientStats.up + clientStats.down : 0;
  1296. },
  1297. getRemStats(dbInbound, email) {
  1298. if (email.length == 0) return 0;
  1299. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1300. if (!clientStats) return 0;
  1301. remained = clientStats.total - (clientStats.up + clientStats.down);
  1302. return remained>0 ? remained : 0;
  1303. },
  1304. clientStatsColor(dbInbound, email) {
  1305. if (email.length == 0) return ColorUtils.clientUsageColor();
  1306. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1307. return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
  1308. },
  1309. statsProgress(dbInbound, email) {
  1310. if (email.length == 0) return 100;
  1311. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1312. if (!clientStats) return 0;
  1313. if (clientStats.total == 0) return 100;
  1314. return 100*(clientStats.down + clientStats.up)/clientStats.total;
  1315. },
  1316. expireProgress(expTime, reset) {
  1317. now = new Date().getTime();
  1318. remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
  1319. resetSeconds = reset * 86400;
  1320. if (remainedSeconds >= resetSeconds) return 0;
  1321. return 100*(1-(remainedSeconds/resetSeconds));
  1322. },
  1323. remainedDays(expTime){
  1324. if (expTime == 0) return null;
  1325. if (expTime < 0) return TimeFormatter.formatSecond(expTime/-1000);
  1326. now = new Date().getTime();
  1327. if (expTime < now) return '{{ i18n "depleted" }}';
  1328. return TimeFormatter.formatSecond((expTime-now)/1000);
  1329. },
  1330. statsExpColor(dbInbound, email){
  1331. if (email.length == 0) return '#7a316f';
  1332. clientStats = dbInbound.clientStats.find(stats => stats.email === email);
  1333. if (!clientStats) return '#7a316f';
  1334. statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
  1335. expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
  1336. switch (true) {
  1337. case statsColor == "red" || expColor == "red":
  1338. return "#cf3c3c"; // Red
  1339. case statsColor == "orange" || expColor == "orange":
  1340. return "#f37b24"; // Orange
  1341. case statsColor == "green" || expColor == "green":
  1342. return "#008771"; // Green
  1343. default:
  1344. return "#7a316f"; // purple
  1345. }
  1346. },
  1347. isClientEnabled(dbInbound, email) {
  1348. clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
  1349. return clientStats ? clientStats['enable'] : true;
  1350. },
  1351. isClientOnline(email) {
  1352. return this.onlineClients.includes(email);
  1353. },
  1354. isRemovable(dbInboundId) {
  1355. return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
  1356. },
  1357. inboundLinks(dbInboundId) {
  1358. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1359. newDbInbound = this.checkFallback(dbInbound);
  1360. txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
  1361. },
  1362. exportSubs(dbInboundId) {
  1363. const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1364. const clients = this.getInboundClients(dbInbound);
  1365. let subLinks = []
  1366. if (clients != null){
  1367. clients.forEach(c => {
  1368. if (c.subId && c.subId.length>0){
  1369. subLinks.push(this.subSettings.subURI + c.subId)
  1370. }
  1371. })
  1372. }
  1373. txtModal.show(
  1374. '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
  1375. [...new Set(subLinks)].join('\n'),
  1376. dbInbound.remark + "-Subs");
  1377. },
  1378. importInbound() {
  1379. promptModal.open({
  1380. title: '{{ i18n "pages.inbounds.importInbound" }}',
  1381. type: 'textarea',
  1382. value: '',
  1383. okText: '{{ i18n "pages.inbounds.import" }}',
  1384. confirm: async (dbInboundText) => {
  1385. await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
  1386. },
  1387. });
  1388. },
  1389. exportAllSubs() {
  1390. let subLinks = []
  1391. for (const dbInbound of this.dbInbounds) {
  1392. const clients = this.getInboundClients(dbInbound);
  1393. if (clients != null){
  1394. clients.forEach(c => {
  1395. if (c.subId && c.subId.length>0){
  1396. subLinks.push(this.subSettings.subURI + c.subId)
  1397. }
  1398. })
  1399. }
  1400. }
  1401. txtModal.show(
  1402. '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
  1403. [...new Set(subLinks)].join('\r\n'),
  1404. 'All-Inbounds-Subs');
  1405. },
  1406. exportAllLinks() {
  1407. let copyText = [];
  1408. for (const dbInbound of this.dbInbounds) {
  1409. copyText.push(dbInbound.genInboundLinks(this.remarkModel));
  1410. }
  1411. txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
  1412. },
  1413. copy(dbInboundId) {
  1414. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1415. txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
  1416. },
  1417. async startDataRefreshLoop() {
  1418. while (this.isRefreshEnabled) {
  1419. try {
  1420. await this.getDBInbounds();
  1421. } catch (e) {
  1422. console.error(e);
  1423. }
  1424. await PromiseUtil.sleep(this.refreshInterval);
  1425. }
  1426. },
  1427. toggleRefresh() {
  1428. localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
  1429. if (this.isRefreshEnabled) {
  1430. this.startDataRefreshLoop();
  1431. }
  1432. },
  1433. changeRefreshInterval() {
  1434. localStorage.setItem("refreshInterval", this.refreshInterval);
  1435. },
  1436. async manualRefresh() {
  1437. if (!this.refreshing) {
  1438. this.spinning = true;
  1439. await this.getDBInbounds();
  1440. this.spinning = false;
  1441. }
  1442. },
  1443. pagination(obj){
  1444. if (this.pageSize > 0 && obj.length>this.pageSize) {
  1445. // Set page options based on object size
  1446. sizeOptions = [];
  1447. for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
  1448. sizeOptions.push(i.toString());
  1449. }
  1450. // Add option to see all in one page
  1451. sizeOptions.push(i.toString());
  1452. p = {
  1453. showSizeChanger: true,
  1454. size: 'small',
  1455. position: 'bottom',
  1456. pageSize: this.pageSize,
  1457. pageSizeOptions: sizeOptions
  1458. };
  1459. return p;
  1460. }
  1461. return false
  1462. }
  1463. },
  1464. watch: {
  1465. searchKey: Utils.debounce(function (newVal) {
  1466. this.searchInbounds(newVal);
  1467. }, 500)
  1468. },
  1469. mounted() {
  1470. if (window.location.protocol !== "https:") {
  1471. this.showAlert = true;
  1472. }
  1473. this.loading();
  1474. this.getDefaultSettings();
  1475. if (this.isRefreshEnabled) {
  1476. this.startDataRefreshLoop();
  1477. }
  1478. else {
  1479. this.getDBInbounds();
  1480. }
  1481. this.loading(false);
  1482. },
  1483. computed: {
  1484. total() {
  1485. let down = 0, up = 0;
  1486. let clients = 0, deactive = [], depleted = [], expiring = [];
  1487. this.dbInbounds.forEach(dbInbound => {
  1488. down += dbInbound.down;
  1489. up += dbInbound.up;
  1490. if (this.clientCount[dbInbound.id]) {
  1491. clients += this.clientCount[dbInbound.id].clients;
  1492. deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
  1493. depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
  1494. expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
  1495. }
  1496. });
  1497. return {
  1498. down: down,
  1499. up: up,
  1500. clients: clients,
  1501. deactive: deactive,
  1502. depleted: depleted,
  1503. expiring: expiring,
  1504. };
  1505. }
  1506. },
  1507. });
  1508. </script>
  1509. {{template "modals/inboundModal"}}
  1510. {{template "modals/promptModal"}}
  1511. {{template "modals/qrcodeModal"}}
  1512. {{template "modals/textModal"}}
  1513. {{template "modals/inboundInfoModal"}}
  1514. {{template "modals/clientsModal"}}
  1515. {{template "modals/clientsBulkModal"}}
  1516. </body>
  1517. </html>