inbounds.html 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. {{template "head" .}}
  4. <style>
  5. @media (min-width: 769px) {
  6. .ant-layout-content {
  7. margin: 24px 16px;
  8. }
  9. }
  10. .ant-col-sm-24 {
  11. margin-top: 10px;
  12. }
  13. </style>
  14. <body>
  15. <a-layout id="app" v-cloak>
  16. {{ template "commonSider" . }}
  17. <a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
  18. <a-layout-content>
  19. <a-spin :spinning="spinning" :delay="500" tip="loading">
  20. <transition name="list" appear>
  21. <a-tag v-if="false" color="red" style="margin-bottom: 10px">
  22. Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
  23. </a-tag>
  24. </transition>
  25. <transition name="list" appear>
  26. <a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
  27. <a-row>
  28. <a-col :xs="24" :sm="24" :lg="12">
  29. {{ i18n "pages.inbounds.totalDownUp" }}:
  30. <a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
  31. </a-col>
  32. <a-col :xs="24" :sm="24" :lg="12">
  33. {{ i18n "pages.inbounds.totalUsage" }}:
  34. <a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
  35. </a-col>
  36. <a-col :xs="24" :sm="24" :lg="12">
  37. {{ i18n "pages.inbounds.inboundCount" }}:
  38. <a-tag color="green">[[ dbInbounds.length ]]</a-tag>
  39. </a-col>
  40. <a-col :xs="24" :sm="24" :lg="12">
  41. {{ i18n "clients" }}:
  42. <a-tag color="green">[[ total.clients ]]</a-tag>
  43. <a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  44. <template slot="content">
  45. <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
  46. </template>
  47. <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
  48. </a-popover>
  49. <a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  50. <template slot="content">
  51. <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
  52. </template>
  53. <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
  54. </a-popover>
  55. <a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  56. <template slot="content">
  57. <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
  58. </template>
  59. <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
  60. </a-popover>
  61. </a-col>
  62. </a-row>
  63. </a-card>
  64. </transition>
  65. <transition name="list" appear>
  66. <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
  67. <div slot="title">
  68. <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
  69. <a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
  70. <a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
  71. </div>
  72. <a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
  73. <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
  74. :data-source="searchedInbounds"
  75. :loading="spinning" :scroll="{ x: 1300 }"
  76. :pagination="false"
  77. style="margin-top: 20px"
  78. @change="() => getDBInbounds()">
  79. <template slot="action" slot-scope="text, dbInbound">
  80. <a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
  81. <a-dropdown :trigger="['click']">
  82. <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
  83. <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
  84. <a-menu-item v-if="dbInbound.isSS" key="qrcode">
  85. <a-icon type="qrcode"></a-icon>
  86. {{ i18n "qrCode" }}
  87. </a-menu-item>
  88. <a-menu-item key="edit">
  89. <a-icon type="edit"></a-icon>
  90. {{ i18n "edit" }}
  91. </a-menu-item>
  92. <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
  93. <a-menu-item key="addClient">
  94. <a-icon type="user-add"></a-icon>
  95. {{ i18n "pages.client.add"}}
  96. </a-menu-item>
  97. <a-menu-item key="addBulkClient">
  98. <a-icon type="usergroup-add"></a-icon>
  99. {{ i18n "pages.client.bulk"}}
  100. </a-menu-item>
  101. <a-menu-item key="resetClients">
  102. <a-icon type="file-done"></a-icon>
  103. {{ i18n "pages.inbounds.resetAllClientTraffics"}}
  104. </a-menu-item>
  105. <a-menu-item key="export">
  106. <a-icon type="export"></a-icon>
  107. {{ i18n "pages.inbounds.export"}}
  108. </a-menu-item>
  109. </template>
  110. <template v-else>
  111. <a-menu-item key="showInfo">
  112. <a-icon type="info-circle"></a-icon>
  113. {{ i18n "info"}}
  114. </a-menu-item>
  115. </template>
  116. <a-menu-item key="resetTraffic">
  117. <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
  118. </a-menu-item>
  119. <a-menu-item key="clone">
  120. <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}}
  121. </a-menu-item>
  122. <a-menu-item key="delete">
  123. <span style="color: #FF4D4F">
  124. <a-icon type="delete"></a-icon> {{ i18n "delete"}}
  125. </span>
  126. </a-menu-item>
  127. </a-menu>
  128. </a-dropdown>
  129. </template>
  130. <template slot="protocol" slot-scope="text, dbInbound">
  131. <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
  132. <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
  133. <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
  134. <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
  135. <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXTLS" color="cyan">XTLS</a-tag>
  136. </template>
  137. </template>
  138. <template slot="clients" slot-scope="text, dbInbound">
  139. <template v-if="clientCount[dbInbound.id]">
  140. <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
  141. <a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  142. <template slot="content">
  143. <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
  144. </template>
  145. <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
  146. </a-popover>
  147. <a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  148. <template slot="content">
  149. <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
  150. </template>
  151. <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
  152. </a-popover>
  153. <a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
  154. <template slot="content">
  155. <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
  156. </template>
  157. <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
  158. </a-popover>
  159. </template>
  160. </template>
  161. <template slot="traffic" slot-scope="text, dbInbound">
  162. <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
  163. <template v-if="dbInbound.total > 0">
  164. <a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
  165. <a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
  166. </template>
  167. <a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
  168. </template>
  169. <template slot="enable" slot-scope="text, dbInbound">
  170. <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
  171. </template>
  172. <template slot="expiryTime" slot-scope="text, dbInbound">
  173. <template v-if="dbInbound.expiryTime > 0">
  174. <a-tag v-if="dbInbound.isExpiry" color="red">
  175. [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
  176. </a-tag>
  177. <a-tag v-else color="blue">
  178. [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
  179. </a-tag>
  180. </template>
  181. <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
  182. </template>
  183. <template slot="expandedRowRender" slot-scope="record">
  184. <a-table
  185. v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
  186. :row-key="client => client.id"
  187. :columns="innerColumns"
  188. :data-source="getInboundClients(record)"
  189. :pagination="false"
  190. >
  191. {{template "client_table"}}
  192. </a-table>
  193. <a-table
  194. v-else-if="record.protocol === Protocols.TROJAN"
  195. :row-key="client => client.id"
  196. :columns="innerTrojanColumns"
  197. :data-source="getInboundClients(record)"
  198. :pagination="false"
  199. >
  200. {{template "client_table"}}
  201. </a-table>
  202. </template>
  203. </a-table>
  204. </a-card>
  205. </transition>
  206. </a-spin>
  207. </a-layout-content>
  208. </a-layout>
  209. </a-layout>
  210. {{template "js" .}}
  211. <script>
  212. const columns = [{
  213. title: '{{ i18n "pages.inbounds.operate" }}',
  214. align: 'center',
  215. width: 60,
  216. scopedSlots: { customRender: 'action' },
  217. }, {
  218. title: '{{ i18n "pages.inbounds.enable" }}',
  219. align: 'center',
  220. width: 40,
  221. scopedSlots: { customRender: 'enable' },
  222. }, {
  223. title: "ID",
  224. align: 'center',
  225. dataIndex: "id",
  226. width: 30,
  227. }, {
  228. title: '{{ i18n "pages.inbounds.remark" }}',
  229. align: 'center',
  230. width: 80,
  231. dataIndex: "remark",
  232. }, {
  233. title: '{{ i18n "pages.inbounds.port" }}',
  234. align: 'center',
  235. dataIndex: "port",
  236. width: 40,
  237. }, {
  238. title: '{{ i18n "pages.inbounds.protocol" }}',
  239. align: 'left',
  240. width: 70,
  241. scopedSlots: { customRender: 'protocol' },
  242. }, {
  243. title: '{{ i18n "clients" }}',
  244. align: 'left',
  245. width: 50,
  246. scopedSlots: { customRender: 'clients' },
  247. }, {
  248. title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
  249. align: 'center',
  250. width: 120,
  251. scopedSlots: { customRender: 'traffic' },
  252. }, {
  253. title: '{{ i18n "pages.inbounds.expireDate" }}',
  254. align: 'center',
  255. width: 80,
  256. scopedSlots: { customRender: 'expiryTime' },
  257. }];
  258. const innerColumns = [
  259. { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
  260. { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
  261. { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
  262. { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
  263. { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
  264. { title: 'UID', width: 120, dataIndex: "id" },
  265. ];
  266. const innerTrojanColumns = [
  267. { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
  268. { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
  269. { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
  270. { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
  271. { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
  272. { title: 'Password', width: 100, dataIndex: "password" },
  273. ];
  274. const app = new Vue({
  275. delimiters: ['[[', ']]'],
  276. el: '#app',
  277. data: {
  278. siderDrawer,
  279. spinning: false,
  280. inbounds: [],
  281. dbInbounds: [],
  282. searchKey: '',
  283. searchedInbounds: [],
  284. expireDiff: 0,
  285. trafficDiff: 0,
  286. defaultCert: '',
  287. defaultKey: '',
  288. clientCount: {},
  289. },
  290. methods: {
  291. loading(spinning=true) {
  292. this.spinning = spinning;
  293. },
  294. async getDBInbounds() {
  295. this.loading();
  296. const msg = await HttpUtil.post('/xui/inbound/list');
  297. this.loading(false);
  298. if (!msg.success) {
  299. return;
  300. }
  301. this.setInbounds(msg.obj);
  302. this.searchKey = '';
  303. },
  304. async getDefaultSettings() {
  305. this.loading();
  306. const msg = await HttpUtil.post('/xui/setting/defaultSettings');
  307. this.loading(false);
  308. if (!msg.success) {
  309. return;
  310. }
  311. this.expireDiff = msg.obj.expireDiff * 86400000;
  312. this.trafficDiff = msg.obj.trafficDiff * 1073741824;
  313. this.defaultCert = msg.obj.defaultCert;
  314. this.defaultKey = msg.obj.defaultKey;
  315. },
  316. setInbounds(dbInbounds) {
  317. this.inbounds.splice(0);
  318. this.dbInbounds.splice(0);
  319. this.searchedInbounds.splice(0);
  320. for (const inbound of dbInbounds) {
  321. const dbInbound = new DBInbound(inbound);
  322. to_inbound = dbInbound.toInbound()
  323. this.inbounds.push(to_inbound);
  324. this.dbInbounds.push(dbInbound);
  325. this.searchedInbounds.push(dbInbound);
  326. if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
  327. this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
  328. }
  329. }
  330. },
  331. getClientCounts(dbInbound,inbound){
  332. let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
  333. clients = this.getClients(dbInbound.protocol, inbound.settings);
  334. clientStats = dbInbound.clientStats
  335. now = new Date().getTime()
  336. if(clients){
  337. clientCount = clients.length;
  338. if(dbInbound.enable){
  339. clients.forEach(client => {
  340. client.enable ? active.push(client.email) : deactive.push(client.email);
  341. });
  342. clientStats.forEach(client => {
  343. if(!client.enable) {
  344. depleted.push(client.email);
  345. } else {
  346. if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) ||
  347. (client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email);
  348. }
  349. });
  350. } else {
  351. clients.forEach(client => {
  352. deactive.push(client.email);
  353. });
  354. }
  355. }
  356. return {
  357. clients: clientCount,
  358. active: active,
  359. deactive: deactive,
  360. depleted: depleted,
  361. expiring: expiring,
  362. };
  363. },
  364. searchInbounds(key) {
  365. if (ObjectUtil.isEmpty(key)) {
  366. this.searchedInbounds = this.dbInbounds.slice();
  367. } else {
  368. this.searchedInbounds.splice(0, this.searchedInbounds.length);
  369. this.dbInbounds.forEach(inbound => {
  370. if (ObjectUtil.deepSearch(inbound, key)) {
  371. const newInbound = new DBInbound(inbound);
  372. const inboundSettings = JSON.parse(inbound.settings);
  373. if (inboundSettings.hasOwnProperty('clients')){
  374. const searchedSettings = { "clients": [] };
  375. inboundSettings.clients.forEach(client => {
  376. if (ObjectUtil.deepSearch(client, key)){
  377. searchedSettings.clients.push(client);
  378. }
  379. });
  380. newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
  381. }
  382. this.searchedInbounds.push(newInbound);
  383. }
  384. });
  385. }
  386. },
  387. clickAction(action, dbInbound) {
  388. switch (action.key) {
  389. case "qrcode":
  390. this.showQrcode(dbInbound);
  391. break;
  392. case "showInfo":
  393. this.showInfo(dbInbound);
  394. break;
  395. case "edit":
  396. this.openEditInbound(dbInbound.id);
  397. break;
  398. case "addClient":
  399. this.openAddClient(dbInbound.id)
  400. break;
  401. case "addBulkClient":
  402. this.openAddBulkClient(dbInbound.id)
  403. break;
  404. case "export":
  405. this.inboundLinks(dbInbound.id);
  406. break;
  407. case "resetTraffic":
  408. this.resetTraffic(dbInbound.id);
  409. break;
  410. case "resetClients":
  411. this.resetAllClientTraffics(dbInbound.id);
  412. break;
  413. case "clone":
  414. this.openCloneInbound(dbInbound);
  415. break;
  416. case "delete":
  417. this.delInbound(dbInbound.id);
  418. break;
  419. }
  420. },
  421. openCloneInbound(dbInbound) {
  422. this.$confirm({
  423. title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
  424. content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
  425. okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
  426. cancelText: '{{ i18n "cancel" }}',
  427. onOk: () => {
  428. const baseInbound = dbInbound.toInbound();
  429. dbInbound.up = 0;
  430. dbInbound.down = 0;
  431. this.cloneInbound(baseInbound, dbInbound);
  432. },
  433. });
  434. },
  435. async cloneInbound(baseInbound, dbInbound) {
  436. const inbound = new Inbound();
  437. const data = {
  438. up: dbInbound.up,
  439. down: dbInbound.down,
  440. total: dbInbound.total,
  441. remark: dbInbound.remark + " - Cloned",
  442. enable: dbInbound.enable,
  443. expiryTime: dbInbound.expiryTime,
  444. listen: inbound.listen,
  445. port: inbound.port,
  446. protocol: baseInbound.protocol,
  447. settings: inbound.settings.toString(),
  448. streamSettings: baseInbound.stream.toString(),
  449. sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
  450. };
  451. await this.submit('/xui/inbound/add', data, inModal);
  452. },
  453. openAddInbound() {
  454. inModal.show({
  455. title: '{{ i18n "pages.inbounds.addInbound"}}',
  456. okText: '{{ i18n "pages.inbounds.addTo"}}',
  457. cancelText: '{{ i18n "close" }}',
  458. confirm: async (inbound, dbInbound) => {
  459. inModal.loading();
  460. await this.addInbound(inbound, dbInbound);
  461. inModal.close();
  462. },
  463. isEdit: false
  464. });
  465. },
  466. openEditInbound(dbInboundId) {
  467. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  468. const inbound = dbInbound.toInbound();
  469. inModal.show({
  470. title: '{{ i18n "pages.inbounds.modifyInbound"}}',
  471. okText: '{{ i18n "pages.inbounds.revise"}}',
  472. cancelText: '{{ i18n "close" }}',
  473. inbound: inbound,
  474. dbInbound: dbInbound,
  475. confirm: async (inbound, dbInbound) => {
  476. inModal.loading();
  477. await this.updateInbound(inbound, dbInbound);
  478. inModal.close();
  479. },
  480. isEdit: true
  481. });
  482. },
  483. async addInbound(inbound, dbInbound) {
  484. const data = {
  485. up: dbInbound.up,
  486. down: dbInbound.down,
  487. total: dbInbound.total,
  488. remark: dbInbound.remark,
  489. enable: dbInbound.enable,
  490. expiryTime: dbInbound.expiryTime,
  491. listen: inbound.listen,
  492. port: inbound.port,
  493. protocol: inbound.protocol,
  494. settings: inbound.settings.toString(),
  495. };
  496. if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
  497. if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
  498. await this.submit('/xui/inbound/add', data, inModal);
  499. },
  500. async updateInbound(inbound, dbInbound) {
  501. const data = {
  502. up: dbInbound.up,
  503. down: dbInbound.down,
  504. total: dbInbound.total,
  505. remark: dbInbound.remark,
  506. enable: dbInbound.enable,
  507. expiryTime: dbInbound.expiryTime,
  508. listen: inbound.listen,
  509. port: inbound.port,
  510. protocol: inbound.protocol,
  511. settings: inbound.settings.toString(),
  512. };
  513. if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
  514. if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
  515. await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
  516. },
  517. openAddClient(dbInboundId) {
  518. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  519. clientModal.show({
  520. title: '{{ i18n "pages.client.add"}}',
  521. okText: '{{ i18n "pages.client.submitAdd"}}',
  522. dbInbound: dbInbound,
  523. confirm: async (inbound, dbInbound, index) => {
  524. clientModal.loading();
  525. await this.addClient(inbound, dbInbound);
  526. clientModal.close();
  527. },
  528. isEdit: false
  529. });
  530. },
  531. openAddBulkClient(dbInboundId) {
  532. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  533. clientsBulkModal.show({
  534. title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
  535. okText: '{{ i18n "pages.client.bulk"}}',
  536. dbInbound: dbInbound,
  537. confirm: async (inbound, dbInbound) => {
  538. clientsBulkModal.loading();
  539. await this.addClient(inbound, dbInbound);
  540. clientsBulkModal.close();
  541. },
  542. });
  543. },
  544. openEditClient(dbInboundId, client) {
  545. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  546. clients = this.getInboundClients(dbInbound);
  547. index = this.findIndexOfClient(clients, client);
  548. clientModal.show({
  549. title: '{{ i18n "pages.client.edit"}}',
  550. okText: '{{ i18n "pages.client.submitEdit"}}',
  551. dbInbound: dbInbound,
  552. index: index,
  553. confirm: async (inbound, dbInbound, index) => {
  554. clientModal.loading();
  555. await this.updateClient(inbound, dbInbound, index);
  556. clientModal.close();
  557. },
  558. isEdit: true
  559. });
  560. },
  561. findIndexOfClient(clients,client) {
  562. firstKey = Object.keys(client)[0];
  563. return clients.findIndex(c => c[firstKey] === client[firstKey]);
  564. },
  565. async addClient(inbound, dbInbound) {
  566. const data = {
  567. id: dbInbound.id,
  568. settings: inbound.settings.toString(),
  569. };
  570. await this.submit('/xui/inbound/addClient/', data);
  571. },
  572. async updateClient(inbound, dbInbound, index) {
  573. const data = {
  574. id: dbInbound.id,
  575. settings: inbound.settings.toString(),
  576. };
  577. await this.submit(`/xui/inbound/updateClient/${index}`, data);
  578. },
  579. resetTraffic(dbInboundId) {
  580. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  581. this.$confirm({
  582. title: '{{ i18n "pages.inbounds.resetTraffic"}}',
  583. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  584. class: siderDrawer.isDarkTheme ? darkClass : '',
  585. okText: '{{ i18n "reset"}}',
  586. cancelText: '{{ i18n "cancel"}}',
  587. onOk: () => {
  588. const inbound = dbInbound.toInbound();
  589. dbInbound.up = 0;
  590. dbInbound.down = 0;
  591. this.updateInbound(inbound, dbInbound);
  592. },
  593. });
  594. },
  595. delInbound(dbInboundId) {
  596. this.$confirm({
  597. title: '{{ i18n "pages.inbounds.deleteInbound"}}',
  598. content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
  599. class: siderDrawer.isDarkTheme ? darkClass : '',
  600. okText: '{{ i18n "delete"}}',
  601. cancelText: '{{ i18n "cancel"}}',
  602. onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
  603. });
  604. },
  605. delClient(dbInboundId,client) {
  606. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  607. newDbInbound = new DBInbound(dbInbound);
  608. inbound = newDbInbound.toInbound();
  609. clients = this.getClients(dbInbound.protocol, inbound.settings);
  610. index = this.findIndexOfClient(clients, client);
  611. clients.splice(index, 1);
  612. const data = {
  613. id: dbInboundId,
  614. settings: inbound.settings.toString(),
  615. };
  616. this.$confirm({
  617. title: '{{ i18n "pages.inbounds.deleteInbound"}}',
  618. content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
  619. class: siderDrawer.isDarkTheme ? darkClass : '',
  620. okText: '{{ i18n "delete"}}',
  621. cancelText: '{{ i18n "cancel"}}',
  622. onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
  623. });
  624. },
  625. getClients(protocol, clientSettings) {
  626. switch(protocol){
  627. case Protocols.VMESS: return clientSettings.vmesses;
  628. case Protocols.VLESS: return clientSettings.vlesses;
  629. case Protocols.TROJAN: return clientSettings.trojans;
  630. default: return null;
  631. }
  632. },
  633. showQrcode(dbInbound, clientIndex) {
  634. const link = dbInbound.genLink(clientIndex);
  635. qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
  636. },
  637. showInfo(dbInbound, index) {
  638. infoModal.show(dbInbound, index);
  639. },
  640. switchEnable(dbInboundId) {
  641. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  642. this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
  643. },
  644. async switchEnableClient(dbInboundId, client) {
  645. this.loading()
  646. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  647. inbound = dbInbound.toInbound();
  648. clients = this.getClients(dbInbound.protocol, inbound.settings);
  649. index = this.findIndexOfClient(clients, client);
  650. clients[index].enable = ! clients[index].enable
  651. await this.updateClient(inbound, dbInbound, index);
  652. this.loading(false);
  653. },
  654. async submit(url, data) {
  655. const msg = await HttpUtil.postWithModal(url, data);
  656. if (msg.success) {
  657. await this.getDBInbounds();
  658. }
  659. },
  660. getInboundClients(dbInbound) {
  661. if(dbInbound.protocol == Protocols.VLESS) {
  662. return dbInbound.toInbound().settings.vlesses
  663. } else if(dbInbound.protocol == Protocols.VMESS) {
  664. return dbInbound.toInbound().settings.vmesses
  665. } else if(dbInbound.protocol == Protocols.TROJAN) {
  666. return dbInbound.toInbound().settings.trojans
  667. }
  668. },
  669. resetClientTraffic(client,dbInboundId) {
  670. this.$confirm({
  671. title: '{{ i18n "pages.inbounds.resetTraffic"}}',
  672. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  673. class: siderDrawer.isDarkTheme ? darkClass : '',
  674. okText: '{{ i18n "reset"}}',
  675. cancelText: '{{ i18n "cancel"}}',
  676. onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
  677. })
  678. },
  679. resetAllTraffic() {
  680. this.$confirm({
  681. title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
  682. content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
  683. class: siderDrawer.isDarkTheme ? darkClass : '',
  684. okText: '{{ i18n "reset"}}',
  685. cancelText: '{{ i18n "cancel"}}',
  686. onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
  687. });
  688. },
  689. resetAllClientTraffics(dbInboundId) {
  690. this.$confirm({
  691. title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
  692. content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
  693. class: siderDrawer.isDarkTheme ? darkClass : '',
  694. okText: '{{ i18n "reset"}}',
  695. cancelText: '{{ i18n "cancel"}}',
  696. onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
  697. })
  698. },
  699. isExpiry(dbInbound, index) {
  700. return dbInbound.toInbound().isExpiry(index)
  701. },
  702. getUpStats(dbInbound, email) {
  703. if(email.length == 0) return 0
  704. clientStats = dbInbound.clientStats.find(stats => stats.email === email)
  705. return clientStats ? clientStats.up : 0
  706. },
  707. getDownStats(dbInbound, email) {
  708. if(email.length == 0) return 0
  709. clientStats = dbInbound.clientStats.find(stats => stats.email === email)
  710. return clientStats ? clientStats.down : 0
  711. },
  712. isTrafficExhausted(dbInbound, email) {
  713. if(email.length == 0) return false
  714. clientStats = dbInbound.clientStats.find(stats => stats.email === email)
  715. return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
  716. },
  717. isClientEnabled(dbInbound, email) {
  718. clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
  719. return clientStats ? clientStats['enable'] : true
  720. },
  721. isRemovable(dbInbound_id){
  722. return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
  723. },
  724. inboundLinks(dbInboundId) {
  725. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  726. txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
  727. },
  728. exportAllLinks() {
  729. let copyText = '';
  730. for (const dbInbound of this.dbInbounds) {
  731. copyText += dbInbound.genInboundLinks
  732. }
  733. txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
  734. },
  735. },
  736. watch: {
  737. searchKey: debounce(function (newVal) {
  738. this.searchInbounds(newVal);
  739. }, 500)
  740. },
  741. mounted() {
  742. this.getDefaultSettings();
  743. this.getDBInbounds();
  744. },
  745. computed: {
  746. total() {
  747. let down = 0, up = 0;
  748. let clients = 0, deactive = [], depleted = [], expiring = [];
  749. this.dbInbounds.forEach(dbInbound => {
  750. down += dbInbound.down;
  751. up += dbInbound.up;
  752. if (this.clientCount[dbInbound.id]) {
  753. clients += this.clientCount[dbInbound.id].clients;
  754. deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
  755. depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
  756. expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
  757. }
  758. });
  759. return {
  760. down: down,
  761. up: up,
  762. clients: clients,
  763. deactive: deactive,
  764. depleted: depleted,
  765. expiring: expiring,
  766. };
  767. }
  768. },
  769. });
  770. </script>
  771. {{template "inboundModal"}}
  772. {{template "promptModal"}}
  773. {{template "qrcodeModal"}}
  774. {{template "textModal"}}
  775. {{template "inboundInfoModal"}}
  776. {{template "clientsModal"}}
  777. {{template "clientsBulkModal"}}
  778. </body>
  779. </html>