inbounds.html 71 KB

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