1
0

inbounds.html 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190
  1. {{ template "page/head_start" .}}
  2. {{ template "page/head_end" .}}
  3. {{ template "page/body_start" .}}
  4. <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'">
  5. <a-sidebar></a-sidebar>
  6. <a-layout id="content-layout">
  7. <a-layout-content>
  8. <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
  9. <transition name="list" appear>
  10. <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
  11. message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
  12. </a-alert>
  13. </transition>
  14. <transition name="list" appear>
  15. <a-row v-if="!loadingStates.fetched">
  16. <div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
  17. </a-row>
  18. <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
  19. <a-col>
  20. <a-card size="small" :style="{ padding: '16px' }" hoverable>
  21. <a-row>
  22. <a-col :sm="12" :md="5">
  23. <a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}'
  24. :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
  25. <template #prefix>
  26. <a-icon type="swap"></a-icon>
  27. </template>
  28. </a-custom-statistic>
  29. </a-col>
  30. <a-col :sm="12" :md="5">
  31. <a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}'
  32. :value="SizeFormatter.sizeFormat(total.up + total.down)"
  33. :style="{ marginTop: isMobile ? '10px' : 0 }">
  34. <template #prefix>
  35. <a-icon type="pie-chart"></a-icon>
  36. </template>
  37. </a-custom-statistic>
  38. </a-col>
  39. <a-col :sm="12" :md="5">
  40. <a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}'
  41. :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
  42. <template #prefix>
  43. <a-icon type="history"></a-icon>
  44. </template>
  45. </a-custom-statistic>
  46. </a-col>
  47. <a-col :sm="12" :md="5">
  48. <a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length"
  49. :style="{ marginTop: isMobile ? '10px' : 0 }">
  50. <template #prefix>
  51. <a-icon type="bars"></a-icon>
  52. </template>
  53. </a-custom-statistic>
  54. </a-col>
  55. <a-col :sm="12" :md="4">
  56. <a-custom-statistic title='{{ i18n "clients" }}' value=" "
  57. :style="{ marginTop: isMobile ? '10px' : 0 }">
  58. <template #prefix>
  59. <a-space direction="horizontal">
  60. <a-icon type="team"></a-icon>
  61. <div>
  62. <a-back-top :target="() => document.getElementById('content-layout')"
  63. visibility-height="200"></a-back-top>
  64. <a-tag color="green">[[ total.clients ]]</a-tag>
  65. <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
  66. <template slot="content">
  67. <div v-for="clientEmail in total.deactive"><span>[[
  68. clientEmail ]]</span></div>
  69. </template>
  70. <a-tag v-if="total.deactive.length">[[
  71. total.deactive.length ]]</a-tag>
  72. </a-popover>
  73. <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
  74. <template slot="content">
  75. <div v-for="clientEmail in total.depleted"><span>[[
  76. clientEmail ]]</span></div>
  77. </template>
  78. <a-tag color="red" v-if="total.depleted.length">[[
  79. total.depleted.length ]]</a-tag>
  80. </a-popover>
  81. <a-popover title='{{ i18n "depletingSoon" }}'
  82. :overlay-class-name="themeSwitcher.currentTheme">
  83. <template slot="content">
  84. <div v-for="clientEmail in total.expiring"><span>[[
  85. clientEmail ]]</span></div>
  86. </template>
  87. <a-tag color="orange" v-if="total.expiring.length">[[
  88. total.expiring.length ]]</a-tag>
  89. </a-popover>
  90. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  91. <template slot="content">
  92. <div v-for="clientEmail in onlineClients"><span>[[
  93. clientEmail ]]</span></div>
  94. </template>
  95. <a-tag color="blue" v-if="onlineClients.length">[[
  96. onlineClients.length ]]</a-tag>
  97. </a-popover>
  98. </div>
  99. </a-space>
  100. </template>
  101. </a-custom-statistic>
  102. </a-col>
  103. </a-row>
  104. </a-card>
  105. </a-col>
  106. <a-col>
  107. <a-card hoverable>
  108. <template #title>
  109. <a-space direction="horizontal">
  110. <a-button type="primary" icon="plus" @click="openAddInbound">
  111. <template v-if="!isMobile">{{ i18n
  112. "pages.inbounds.addInbound" }}</template>
  113. </a-button>
  114. <a-dropdown :trigger="['click']">
  115. <a-button type="primary" icon="menu">
  116. <template v-if="!isMobile">{{ i18n
  117. "pages.inbounds.generalActions" }}</template>
  118. </a-button>
  119. <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
  120. <a-menu-item key="import">
  121. <a-icon type="import"></a-icon>
  122. {{ i18n "pages.inbounds.importInbound" }}
  123. </a-menu-item>
  124. <a-menu-item key="export">
  125. <a-icon type="export"></a-icon>
  126. {{ i18n "pages.inbounds.export" }}
  127. </a-menu-item>
  128. <a-menu-item key="subs" v-if="subSettings.enable">
  129. <a-icon type="export"></a-icon>
  130. {{ i18n "pages.inbounds.export" }} - {{ i18n
  131. "pages.settings.subSettings" }}
  132. </a-menu-item>
  133. <a-menu-item key="resetInbounds">
  134. <a-icon type="reload"></a-icon>
  135. {{ i18n "pages.inbounds.resetAllTraffic" }}
  136. </a-menu-item>
  137. <a-menu-item key="resetClients">
  138. <a-icon type="file-done"></a-icon>
  139. {{ i18n "pages.inbounds.resetAllClientTraffics" }}
  140. </a-menu-item>
  141. <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
  142. <a-icon type="rest"></a-icon>
  143. {{ i18n "pages.inbounds.delDepletedClients" }}
  144. </a-menu-item>
  145. </a-menu>
  146. </a-dropdown>
  147. </a-space>
  148. </template>
  149. <template #extra>
  150. <a-button-group>
  151. <a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
  152. <a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
  153. <template #title>
  154. <div class="ant-custom-popover-title">
  155. <a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
  156. <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
  157. </div>
  158. </template>
  159. <template #content>
  160. <a-space direction="vertical">
  161. <span>{{ i18n "pages.inbounds.autoRefreshInterval"
  162. }}</span>
  163. <a-select v-model="refreshInterval" :disabled="!isRefreshEnabled" :style="{ width: '100%' }"
  164. @change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.currentTheme">
  165. <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
  166. </a-select>
  167. </a-space>
  168. </template>
  169. <a-button icon="down"></a-button>
  170. </a-popover>
  171. </a-button-group>
  172. </template>
  173. <a-space direction="vertical">
  174. <div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
  175. <a-switch v-model="enableFilter"
  176. :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
  177. @change="toggleFilter">
  178. <a-icon slot="checkedChildren" type="search"></a-icon>
  179. <a-icon slot="unCheckedChildren" type="filter"></a-icon>
  180. </a-switch>
  181. <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus
  182. :style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input>
  183. <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid"
  184. :size="isMobile ? 'small' : ''">
  185. <a-radio-button value>{{ i18n "none" }}</a-radio-button>
  186. <a-radio-button value="active">{{ i18n "subscription.active"
  187. }}</a-radio-button>
  188. <a-radio-button value="deactive">{{ i18n "disabled"
  189. }}</a-radio-button>
  190. <a-radio-button value="depleted">{{ i18n "depleted"
  191. }}</a-radio-button>
  192. <a-radio-button value="expiring">{{ i18n "depletingSoon"
  193. }}</a-radio-button>
  194. <a-radio-button value="online">{{ i18n "online"
  195. }}</a-radio-button>
  196. </a-radio-group>
  197. </div>
  198. <a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
  199. :data-source="searchedInbounds" :scroll="isMobile ? {} : { x: 1000 }"
  200. :pagination=pagination(searchedInbounds) :expand-icon-as-cell="false" :expand-row-by-click="false"
  201. :expand-icon-column-index="0" :indent-size="0"
  202. :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
  203. :style="{ marginTop: '10px' }"
  204. :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}`, emptyText: `{{ i18n "noData" }}` }'>
  205. <template slot="action" slot-scope="text, dbInbound">
  206. <a-dropdown :trigger="['click']">
  207. <a-icon @click="e => e.preventDefault()" type="more"
  208. :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
  209. <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)"
  210. :theme="themeSwitcher.currentTheme">
  211. <a-menu-item key="edit">
  212. <a-icon type="edit"></a-icon>
  213. {{ i18n "edit" }}
  214. </a-menu-item>
  215. <a-menu-item key="qrcode"
  216. v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
  217. <a-icon type="qrcode"></a-icon>
  218. {{ i18n "qrCode" }}
  219. </a-menu-item>
  220. <template v-if="dbInbound.isMultiUser()">
  221. <a-menu-item key="addClient">
  222. <a-icon type="user-add"></a-icon>
  223. {{ i18n "pages.client.add"}}
  224. </a-menu-item>
  225. <a-menu-item key="addBulkClient">
  226. <a-icon type="usergroup-add"></a-icon>
  227. {{ i18n "pages.client.bulk"}}
  228. </a-menu-item>
  229. <a-menu-item key="copyClients">
  230. <a-icon type="copy"></a-icon>
  231. {{ i18n "pages.client.copyFromInbound"}}
  232. </a-menu-item>
  233. <a-menu-item key="resetClients">
  234. <a-icon type="file-done"></a-icon>
  235. {{ i18n
  236. "pages.inbounds.resetInboundClientTraffics"}}
  237. </a-menu-item>
  238. <a-menu-item key="export">
  239. <a-icon type="export"></a-icon>
  240. {{ i18n "pages.inbounds.export"}}
  241. </a-menu-item>
  242. <a-menu-item key="subs" v-if="subSettings.enable">
  243. <a-icon type="export"></a-icon>
  244. {{ i18n "pages.inbounds.export"}} - {{ i18n
  245. "pages.settings.subSettings" }}
  246. </a-menu-item>
  247. <a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }">
  248. <a-icon type="rest"></a-icon>
  249. {{ i18n "pages.inbounds.delDepletedClients" }}
  250. </a-menu-item>
  251. </template>
  252. <template v-else>
  253. <a-menu-item key="showInfo">
  254. <a-icon type="info-circle"></a-icon>
  255. {{ i18n "info"}}
  256. </a-menu-item>
  257. </template>
  258. <a-menu-item key="clipboard">
  259. <a-icon type="copy"></a-icon>
  260. {{ i18n "pages.inbounds.exportInbound" }}
  261. </a-menu-item>
  262. <a-menu-item key="resetTraffic">
  263. <a-icon type="retweet"></a-icon> {{ i18n
  264. "pages.inbounds.resetTraffic" }}
  265. </a-menu-item>
  266. <a-menu-item key="clone">
  267. <a-icon type="block"></a-icon> {{ i18n
  268. "pages.inbounds.clone"}}
  269. </a-menu-item>
  270. <a-menu-item key="delete">
  271. <span :style="{ color: '#FF4D4F' }">
  272. <a-icon type="delete"></a-icon> {{ i18n "delete"}}
  273. </span>
  274. </a-menu-item>
  275. <a-menu-item v-if="isMobile">
  276. <a-switch size="small" v-model="dbInbound.enable"
  277. @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
  278. {{ i18n "pages.inbounds.enable" }}
  279. </a-menu-item>
  280. </a-menu>
  281. </a-dropdown>
  282. </template>
  283. <template slot="protocol" slot-scope="text, dbInbound">
  284. <a-tag :style="{ margin: '0' }" color="purple">[[
  285. dbInbound.protocol ]]</a-tag>
  286. <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
  287. <a-tag :style="{ margin: '0' }" color="green">[[
  288. dbInbound.toInbound().stream.network ]]</a-tag>
  289. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
  290. color="blue">TLS</a-tag>
  291. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
  292. color="blue">Reality</a-tag>
  293. </template>
  294. </template>
  295. <template slot="clients" slot-scope="text, dbInbound">
  296. <template v-if="clientCount[dbInbound.id]">
  297. <a-tag :style="{ margin: '0' }" color="green">[[
  298. clientCount[dbInbound.id].clients ]]</a-tag>
  299. <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
  300. <template slot="content">
  301. <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
  302. class="client-popup-item">
  303. <span>[[ clientEmail ]]</span>
  304. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  305. <template #title>
  306. [[
  307. clientCount[dbInbound.id].comments.get(clientEmail)
  308. ]]
  309. </template>
  310. <a-icon type="message"
  311. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  312. </a-tooltip>
  313. </div>
  314. </template>
  315. <a-tag :style="{ margin: '0', padding: '0 2px' }"
  316. v-if="clientCount[dbInbound.id].deactive.length">[[
  317. clientCount[dbInbound.id].deactive.length ]]</a-tag>
  318. </a-popover>
  319. <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
  320. <template slot="content">
  321. <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
  322. class="client-popup-item">
  323. <span>[[ clientEmail ]]</span>
  324. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  325. <template #title>
  326. [[
  327. clientCount[dbInbound.id].comments.get(clientEmail)
  328. ]]
  329. </template>
  330. <a-icon type="message"
  331. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  332. </a-tooltip>
  333. </div>
  334. </template>
  335. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
  336. v-if="clientCount[dbInbound.id].depleted.length">[[
  337. clientCount[dbInbound.id].depleted.length ]]</a-tag>
  338. </a-popover>
  339. <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
  340. <template slot="content">
  341. <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
  342. class="client-popup-item">
  343. <span>[[ clientEmail ]]</span>
  344. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  345. <template #title>
  346. [[
  347. clientCount[dbInbound.id].comments.get(clientEmail)
  348. ]]
  349. </template>
  350. <a-icon type="message"
  351. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  352. </a-tooltip>
  353. </div>
  354. </template>
  355. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
  356. v-if="clientCount[dbInbound.id].expiring.length">[[
  357. clientCount[dbInbound.id].expiring.length ]]</a-tag>
  358. </a-popover>
  359. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  360. <template slot="content">
  361. <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
  362. class="client-popup-item">
  363. <span>[[ clientEmail ]]</span>
  364. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  365. <template #title>
  366. [[
  367. clientCount[dbInbound.id].comments.get(clientEmail)
  368. ]]
  369. </template>
  370. <a-icon type="message"
  371. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  372. </a-tooltip>
  373. </div>
  374. </template>
  375. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue"
  376. v-if="clientCount[dbInbound.id].online.length">[[
  377. clientCount[dbInbound.id].online.length
  378. ]]</a-tag>
  379. </a-popover>
  380. </template>
  381. </template>
  382. <template slot="traffic" slot-scope="text, dbInbound">
  383. <a-popover :overlay-class-name="themeSwitcher.currentTheme">
  384. <template slot="content">
  385. <table cellpadding="2" width="100%">
  386. <tr>
  387. <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up)
  388. ]]</td>
  389. <td>↓[[ SizeFormatter.sizeFormat(dbInbound.down)
  390. ]]</td>
  391. </tr>
  392. <tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
  393. <td>{{ i18n "remained" }}</td>
  394. <td>[[ SizeFormatter.sizeFormat(dbInbound.total -
  395. dbInbound.up - dbInbound.down) ]]</td>
  396. </tr>
  397. </table>
  398. </template>
  399. <a-tag
  400. :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
  401. [[ SizeFormatter.sizeFormat(dbInbound.up +
  402. dbInbound.down) ]] /
  403. <template v-if="dbInbound.total > 0">
  404. [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
  405. </template>
  406. <template v-else>
  407. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  408. <path
  409. 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"
  410. fill="currentColor"></path>
  411. </svg>
  412. </template>
  413. </a-tag>
  414. </a-popover>
  415. </template>
  416. <template slot="allTimeInbound" slot-scope="text, dbInbound">
  417. <a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0)
  418. ]]</a-tag>
  419. </template>
  420. <template slot="enable" slot-scope="text, dbInbound">
  421. <a-switch v-model="dbInbound.enable"
  422. @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
  423. </template>
  424. <template slot="expiryTime" slot-scope="text, dbInbound">
  425. <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
  426. <template slot="content">
  427. [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
  428. </template>
  429. <a-tag :style="{ minWidth: '50px' }"
  430. :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
  431. [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime)
  432. ]]
  433. </a-tag>
  434. </a-popover>
  435. <a-tag v-else color="purple" class="infinite-tag">
  436. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  437. <path
  438. 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"
  439. fill="currentColor"></path>
  440. </svg>
  441. </a-tag>
  442. </template>
  443. <template slot="info" slot-scope="text, dbInbound">
  444. <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme"
  445. trigger="click">
  446. <template slot="content">
  447. <table cellpadding="2">
  448. <tr>
  449. <td>{{ i18n "pages.inbounds.protocol" }}</td>
  450. <td>
  451. <a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol
  452. ]]</a-tag>
  453. <template
  454. v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
  455. <a-tag :style="{ margin: '0' }" color="blue">[[
  456. dbInbound.toInbound().stream.network
  457. ]]</a-tag>
  458. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls"
  459. color="green">tls</a-tag>
  460. <a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality"
  461. color="green">reality</a-tag>
  462. </template>
  463. </td>
  464. </tr>
  465. <tr>
  466. <td>{{ i18n "pages.inbounds.port" }}</td>
  467. <td><a-tag>[[ dbInbound.port ]]</a-tag></td>
  468. </tr>
  469. <tr v-if="clientCount[dbInbound.id]">
  470. <td>{{ i18n "clients" }}</td>
  471. <td>
  472. <a-tag :style="{ margin: '0' }" color="blue">[[
  473. clientCount[dbInbound.id].clients
  474. ]]</a-tag>
  475. <a-popover title='{{ i18n "disabled" }}'
  476. :overlay-class-name="themeSwitcher.currentTheme">
  477. <template slot="content">
  478. <div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail"
  479. class="client-popup-item">
  480. <span>[[ clientEmail ]]</span>
  481. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  482. <template #title>
  483. [[
  484. clientCount[dbInbound.id].comments.get(clientEmail)
  485. ]]
  486. </template>
  487. <a-icon type="message"
  488. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  489. </a-tooltip>
  490. </div>
  491. </template>
  492. <a-tag :style="{ margin: '0', padding: '0 2px' }"
  493. v-if="clientCount[dbInbound.id].deactive.length">[[
  494. clientCount[dbInbound.id].deactive.length
  495. ]]</a-tag>
  496. </a-popover>
  497. <a-popover title='{{ i18n "depleted" }}'
  498. :overlay-class-name="themeSwitcher.currentTheme">
  499. <template slot="content">
  500. <div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail"
  501. class="client-popup-item">
  502. <span>[[ clientEmail ]]</span>
  503. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  504. <template #title>
  505. [[
  506. clientCount[dbInbound.id].comments.get(clientEmail)
  507. ]]
  508. </template>
  509. <a-icon type="message"
  510. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  511. </a-tooltip>
  512. </div>
  513. </template>
  514. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="red"
  515. v-if="clientCount[dbInbound.id].depleted.length">[[
  516. clientCount[dbInbound.id].depleted.length
  517. ]]</a-tag>
  518. </a-popover>
  519. <a-popover title='{{ i18n "depletingSoon" }}'
  520. :overlay-class-name="themeSwitcher.currentTheme">
  521. <template slot="content">
  522. <div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail"
  523. class="client-popup-item">
  524. <span>[[ clientEmail ]]</span>
  525. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  526. <template #title>
  527. [[
  528. clientCount[dbInbound.id].comments.get(clientEmail)
  529. ]]
  530. </template>
  531. <a-icon type="message"
  532. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  533. </a-tooltip>
  534. </div>
  535. </template>
  536. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange"
  537. v-if="clientCount[dbInbound.id].expiring.length">[[
  538. clientCount[dbInbound.id].expiring.length
  539. ]]</a-tag>
  540. </a-popover>
  541. <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
  542. <template slot="content">
  543. <div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail"
  544. class="client-popup-item">
  545. <span>[[ clientEmail ]]</span>
  546. <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
  547. <template #title>
  548. [[
  549. clientCount[dbInbound.id].comments.get(clientEmail)
  550. ]]
  551. </template>
  552. <a-icon type="message"
  553. v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
  554. </a-tooltip>
  555. </div>
  556. </template>
  557. <a-tag :style="{ margin: '0', padding: '0 2px' }" color="green"
  558. v-if="clientCount[dbInbound.id].online.length">[[
  559. clientCount[dbInbound.id].online.length
  560. ]]</a-tag>
  561. </a-popover>
  562. </td>
  563. </tr>
  564. <tr>
  565. <td>{{ i18n "pages.inbounds.traffic" }}</td>
  566. <td>
  567. <a-popover :overlay-class-name="themeSwitcher.currentTheme">
  568. <template slot="content">
  569. <table cellpadding="2" width="100%">
  570. <tr>
  571. <td>↑[[
  572. SizeFormatter.sizeFormat(dbInbound.up)
  573. ]]</td>
  574. <td>↓[[
  575. SizeFormatter.sizeFormat(dbInbound.down)
  576. ]]</td>
  577. </tr>
  578. <tr
  579. v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
  580. <td>{{ i18n "remained" }}</td>
  581. <td>[[
  582. SizeFormatter.sizeFormat(dbInbound.total
  583. - dbInbound.up - dbInbound.down)
  584. ]]</td>
  585. </tr>
  586. </table>
  587. </template>
  588. <a-tag
  589. :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
  590. [[ SizeFormatter.sizeFormat(dbInbound.up +
  591. dbInbound.down) ]] /
  592. <template v-if="dbInbound.total > 0">
  593. [[
  594. SizeFormatter.sizeFormat(dbInbound.total)
  595. ]]
  596. </template>
  597. <template v-else>
  598. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  599. <path
  600. 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"
  601. fill="currentColor"></path>
  602. </svg>
  603. </template>
  604. </a-tag>
  605. </a-popover>
  606. </td>
  607. </tr>
  608. <tr>
  609. <td>{{ i18n "pages.inbounds.expireDate" }}</td>
  610. <td>
  611. <a-tag :style="{ minWidth: '50px', textAlign: 'center' }"
  612. v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
  613. [[ IntlUtil.formatDate(dbInbound.expiryTime)
  614. ]]
  615. </a-tag>
  616. <a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
  617. <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
  618. <path
  619. 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"
  620. fill="currentColor"></path>
  621. </svg>
  622. </a-tag>
  623. </td>
  624. </tr>
  625. <tr>
  626. <td>{{ i18n
  627. "pages.inbounds.periodicTrafficResetTitle"
  628. }}</td>
  629. <td>
  630. <a-tag color="blue">[[ dbInbound.trafficReset
  631. ]]</a-tag>
  632. </td>
  633. </tr>
  634. </table>
  635. </template>
  636. <a-badge>
  637. <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle"
  638. :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
  639. <a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
  640. <a-icon type="info"></a-icon>
  641. </a-button>
  642. </a-badge>
  643. </a-popover>
  644. </template>
  645. <template slot="expandedRowRender" slot-scope="record">
  646. <a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns"
  647. :data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record))
  648. :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
  649. {{template "component/aClientTable" .}}
  650. </a-table>
  651. </template>
  652. </a-table>
  653. </a-space>
  654. </a-card>
  655. </a-col>
  656. </a-row>
  657. </transition>
  658. </a-spin>
  659. </a-layout-content>
  660. </a-layout>
  661. </a-layout>
  662. {{template "page/body_scripts" .}}
  663. <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
  664. <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
  665. <script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
  666. <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
  667. <script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
  668. {{template "component/aSidebar" .}}
  669. {{template "component/aThemeSwitch" .}}
  670. {{template "component/aCustomStatistic" .}}
  671. {{template "component/aPersianDatepicker" .}}
  672. {{template "modals/inboundModal" .}}
  673. {{template "modals/promptModal" .}}
  674. {{template "modals/qrcodeModal" .}}
  675. {{template "modals/textModal" .}}
  676. {{template "modals/inboundInfoModal" .}}
  677. {{template "modals/clientsModal" .}}
  678. {{template "modals/clientsBulkModal" .}}
  679. <a-modal id="copy-clients-modal" :title="copyClientsModal.title" :visible="copyClientsModal.visible"
  680. :confirm-loading="copyClientsModal.confirmLoading" ok-text='{{ i18n "pages.client.copySelected" }}'
  681. cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme" :closable="true" :mask-closable="false"
  682. @ok="() => copyClientsModal.ok()" @cancel="() => copyClientsModal.close()" width="900px">
  683. <a-space direction="vertical" style="width: 100%;">
  684. <div>
  685. <div style="margin-bottom: 6px;">{{ i18n "pages.client.copySource" }}</div>
  686. <a-select v-model="copyClientsModal.sourceInboundId" style="width: 100%;"
  687. :dropdown-class-name="themeSwitcher.currentTheme" @change="id => copyClientsModal.onSourceChange(id)">
  688. <a-select-option v-for="item in copyClientsModal.sources" :key="item.id" :value="item.id">
  689. [[ item.label ]]
  690. </a-select-option>
  691. </a-select>
  692. </div>
  693. <div v-if="copyClientsModal.sourceInboundId">
  694. <a-space style="margin-bottom: 10px;">
  695. <a-button size="small"
  696. @click="() => copyClientsModal.selectAll()">{{ i18n "pages.client.selectAll" }}</a-button>
  697. <a-button size="small" @click="() => copyClientsModal.clearAll()">{{ i18n "pages.client.clearAll" }}</a-button>
  698. </a-space>
  699. <a-table :columns="copyClientsColumns" :data-source="copyClientsModal.sourceClients" :pagination="false"
  700. size="small" :row-key="item => item.email" :scroll="{ y: 280 }">
  701. <template slot="emailCheckbox" slot-scope="text, record">
  702. <a-checkbox :checked="copyClientsModal.selectedEmails.includes(record.email)"
  703. @change="event => copyClientsModal.toggleEmail(record.email, event.target.checked)">
  704. [[ record.email ]]
  705. </a-checkbox>
  706. </template>
  707. </a-table>
  708. </div>
  709. <div v-if="copyClientsModal.showFlow">
  710. <div style="margin-bottom: 6px;">{{ i18n "pages.client.copyFlowLabel" }}</div>
  711. <a-select v-model="copyClientsModal.flow" style="width: 100%;" :dropdown-class-name="themeSwitcher.currentTheme"
  712. allow-clear>
  713. <a-select-option value="">{{ i18n "none" }}</a-select-option>
  714. <a-select-option value="xtls-rprx-vision">xtls-rprx-vision</a-select-option>
  715. <a-select-option value="xtls-rprx-vision-udp443">xtls-rprx-vision-udp443</a-select-option>
  716. </a-select>
  717. <div style="margin-top: 4px; font-size: 12px; opacity: 0.7;">
  718. {{ i18n "pages.client.copyFlowHint" }}
  719. </div>
  720. </div>
  721. <div v-if="copyClientsModal.selectedEmails.length > 0">
  722. <div style="margin-bottom: 4px;">{{ i18n "pages.client.copyEmailPreview" }}</div>
  723. <div style="max-height: 120px; overflow-y: auto;">
  724. <a-tag v-for="preview in previewEmails" :key="preview" style="margin-bottom: 4px;">
  725. [[ preview ]]
  726. </a-tag>
  727. </div>
  728. </div>
  729. </a-space>
  730. </a-modal>
  731. <script>
  732. const copyClientsColumns = [{
  733. title: '{{ i18n "pages.inbounds.email" }}',
  734. width: 300,
  735. scopedSlots: {
  736. customRender: 'emailCheckbox'
  737. }
  738. },
  739. {
  740. title: '{{ i18n "pages.inbounds.traffic" }}',
  741. width: 160,
  742. dataIndex: 'trafficLabel'
  743. },
  744. {
  745. title: '{{ i18n "pages.inbounds.expireDate" }}',
  746. width: 180,
  747. dataIndex: 'expiryLabel'
  748. },
  749. ];
  750. const copyClientsModal = {
  751. visible: false,
  752. confirmLoading: false,
  753. title: '',
  754. targetInboundId: 0,
  755. targetInboundRemark: '',
  756. targetProtocol: '',
  757. showFlow: false,
  758. flow: '',
  759. sourceInboundId: undefined,
  760. sources: [],
  761. sourceClients: [],
  762. selectedEmails: [],
  763. show(targetDbInbound) {
  764. if (!targetDbInbound) return;
  765. const sources = app.dbInbounds
  766. .filter(row => row.id !== targetDbInbound.id && typeof row.isMultiUser === 'function' && row.isMultiUser())
  767. .map(row => {
  768. const clients = app.getInboundClients(row) || [];
  769. return {
  770. id: row.id,
  771. label: `${row.remark} (${row.protocol}, ${clients.length})`
  772. };
  773. });
  774. let showFlow = false;
  775. try {
  776. const targetInbound = targetDbInbound.toInbound();
  777. showFlow = !!(targetInbound && typeof targetInbound.canEnableTlsFlow === 'function' && targetInbound
  778. .canEnableTlsFlow());
  779. } catch (e) {
  780. showFlow = false;
  781. }
  782. copyClientsModal.targetInboundId = targetDbInbound.id;
  783. copyClientsModal.targetInboundRemark = targetDbInbound.remark;
  784. copyClientsModal.targetProtocol = targetDbInbound.protocol;
  785. copyClientsModal.showFlow = showFlow;
  786. copyClientsModal.flow = '';
  787. copyClientsModal.title = `{{ i18n "pages.client.copyToInbound" }} ${targetDbInbound.remark}`;
  788. copyClientsModal.sources = sources;
  789. copyClientsModal.sourceInboundId = undefined;
  790. copyClientsModal.sourceClients = [];
  791. copyClientsModal.selectedEmails = [];
  792. copyClientsModal.confirmLoading = false;
  793. copyClientsModal.visible = true;
  794. },
  795. close() {
  796. copyClientsModal.visible = false;
  797. copyClientsModal.confirmLoading = false;
  798. },
  799. onSourceChange(sourceInboundId) {
  800. copyClientsModal.selectedEmails = [];
  801. const sourceInbound = app.dbInbounds.find(row => row.id === Number(sourceInboundId));
  802. if (!sourceInbound) {
  803. copyClientsModal.sourceClients = [];
  804. return;
  805. }
  806. const sourceClients = app.getInboundClients(sourceInbound) || [];
  807. copyClientsModal.sourceClients = sourceClients.map(client => {
  808. const stats = app.getClientStats(sourceInbound, client.email);
  809. const used = stats ? ((stats.up || 0) + (stats.down || 0)) : 0;
  810. let expiryLabel = '{{ i18n "unlimited" }}';
  811. if (client.expiryTime > 0) {
  812. expiryLabel = IntlUtil.formatDate(client.expiryTime);
  813. } else if (client.expiryTime < 0) {
  814. expiryLabel = `${-client.expiryTime / 86400000}d`;
  815. }
  816. return {
  817. email: client.email,
  818. trafficLabel: SizeFormatter.sizeFormat(used),
  819. expiryLabel,
  820. };
  821. });
  822. },
  823. toggleEmail(email, checked) {
  824. const selected = copyClientsModal.selectedEmails.slice();
  825. if (checked) {
  826. if (!selected.includes(email)) selected.push(email);
  827. } else {
  828. const idx = selected.indexOf(email);
  829. if (idx >= 0) selected.splice(idx, 1);
  830. }
  831. copyClientsModal.selectedEmails = selected;
  832. },
  833. selectAll() {
  834. copyClientsModal.selectedEmails = copyClientsModal.sourceClients.map(item => item.email);
  835. },
  836. clearAll() {
  837. copyClientsModal.selectedEmails = [];
  838. },
  839. async ok() {
  840. if (!copyClientsModal.sourceInboundId) {
  841. app.$message.error('{{ i18n "pages.client.copySelectSourceFirst" }}');
  842. return;
  843. }
  844. copyClientsModal.confirmLoading = true;
  845. const payload = {
  846. sourceInboundId: copyClientsModal.sourceInboundId,
  847. clientEmails: copyClientsModal.selectedEmails,
  848. };
  849. if (copyClientsModal.showFlow && copyClientsModal.flow) {
  850. payload.flow = copyClientsModal.flow;
  851. }
  852. try {
  853. const msg = await HttpUtil.post(`/panel/api/inbounds/${copyClientsModal.targetInboundId}/copyClients`,
  854. payload);
  855. if (!msg || !msg.success) return;
  856. const obj = msg.obj || {};
  857. const addedCount = (obj.added || []).length;
  858. const errorList = obj.errors || [];
  859. if (addedCount > 0) {
  860. app.$message.success(`{{ i18n "pages.client.copyResultSuccess" }}: ${addedCount}`);
  861. } else {
  862. app.$message.warning('{{ i18n "pages.client.copyResultNone" }}');
  863. }
  864. if (errorList.length > 0) {
  865. app.$message.error(`{{ i18n "pages.client.copyResultErrors" }}: ${errorList.join('; ')}`);
  866. }
  867. copyClientsModal.close();
  868. await app.getDBInbounds();
  869. } finally {
  870. copyClientsModal.confirmLoading = false;
  871. }
  872. },
  873. };
  874. const copyClientsModalApp = new Vue({
  875. delimiters: ['[[', ']]'],
  876. el: '#copy-clients-modal',
  877. data: {
  878. copyClientsModal,
  879. copyClientsColumns,
  880. themeSwitcher,
  881. },
  882. computed: {
  883. previewEmails() {
  884. if (!this.copyClientsModal.targetInboundId) return [];
  885. return this.copyClientsModal.selectedEmails.map(email =>
  886. `${email}_${this.copyClientsModal.targetInboundId}`);
  887. },
  888. },
  889. });
  890. </script>
  891. <script>
  892. const columns = [{
  893. title: "ID",
  894. align: 'right',
  895. dataIndex: "id",
  896. width: 30,
  897. responsive: ["xs"],
  898. }, {
  899. title: '{{ i18n "pages.inbounds.operate" }}',
  900. align: 'center',
  901. width: 30,
  902. scopedSlots: {
  903. customRender: 'action'
  904. },
  905. }, {
  906. title: '{{ i18n "pages.inbounds.enable" }}',
  907. align: 'center',
  908. width: 35,
  909. scopedSlots: {
  910. customRender: 'enable'
  911. },
  912. }, {
  913. title: '{{ i18n "pages.inbounds.remark" }}',
  914. align: 'center',
  915. width: 60,
  916. dataIndex: "remark",
  917. }, {
  918. title: '{{ i18n "pages.inbounds.port" }}',
  919. align: 'center',
  920. dataIndex: "port",
  921. width: 40,
  922. }, {
  923. title: '{{ i18n "pages.inbounds.protocol" }}',
  924. align: 'left',
  925. width: 70,
  926. scopedSlots: {
  927. customRender: 'protocol'
  928. },
  929. }, {
  930. title: '{{ i18n "clients" }}',
  931. align: 'left',
  932. width: 50,
  933. scopedSlots: {
  934. customRender: 'clients'
  935. },
  936. }, {
  937. title: '{{ i18n "pages.inbounds.traffic" }}',
  938. align: 'center',
  939. width: 90,
  940. scopedSlots: {
  941. customRender: 'traffic'
  942. },
  943. }, {
  944. title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
  945. align: 'center',
  946. width: 60,
  947. scopedSlots: {
  948. customRender: 'allTimeInbound'
  949. },
  950. }, {
  951. title: '{{ i18n "pages.inbounds.expireDate" }}',
  952. align: 'center',
  953. width: 40,
  954. scopedSlots: {
  955. customRender: 'expiryTime'
  956. },
  957. }];
  958. const mobileColumns = [{
  959. title: "ID",
  960. align: 'right',
  961. dataIndex: "id",
  962. width: 10,
  963. responsive: ["s"],
  964. }, {
  965. title: '{{ i18n "pages.inbounds.operate" }}',
  966. align: 'center',
  967. width: 25,
  968. scopedSlots: {
  969. customRender: 'action'
  970. },
  971. }, {
  972. title: '{{ i18n "pages.inbounds.remark" }}',
  973. align: 'left',
  974. width: 70,
  975. dataIndex: "remark",
  976. }, {
  977. title: '{{ i18n "pages.inbounds.info" }}',
  978. align: 'center',
  979. width: 10,
  980. scopedSlots: {
  981. customRender: 'info'
  982. },
  983. }];
  984. const innerColumns = [{
  985. title: '{{ i18n "pages.inbounds.operate" }}',
  986. width: 70,
  987. scopedSlots: {
  988. customRender: 'actions'
  989. }
  990. },
  991. {
  992. title: '{{ i18n "pages.inbounds.enable" }}',
  993. width: 30,
  994. scopedSlots: {
  995. customRender: 'enable'
  996. }
  997. },
  998. {
  999. title: '{{ i18n "online" }}',
  1000. width: 32,
  1001. scopedSlots: {
  1002. customRender: 'online'
  1003. }
  1004. },
  1005. {
  1006. title: '{{ i18n "pages.inbounds.client" }}',
  1007. width: 80,
  1008. scopedSlots: {
  1009. customRender: 'client'
  1010. }
  1011. },
  1012. {
  1013. title: '{{ i18n "pages.inbounds.traffic" }}',
  1014. width: 80,
  1015. align: 'center',
  1016. scopedSlots: {
  1017. customRender: 'traffic'
  1018. }
  1019. },
  1020. {
  1021. title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
  1022. width: 60,
  1023. align: 'center',
  1024. scopedSlots: {
  1025. customRender: 'allTime'
  1026. }
  1027. },
  1028. {
  1029. title: '{{ i18n "pages.inbounds.expireDate" }}',
  1030. width: 80,
  1031. align: 'center',
  1032. scopedSlots: {
  1033. customRender: 'expiryTime'
  1034. }
  1035. },
  1036. ];
  1037. const innerMobileColumns = [{
  1038. title: '{{ i18n "pages.inbounds.operate" }}',
  1039. width: 10,
  1040. align: 'center',
  1041. scopedSlots: {
  1042. customRender: 'actionMenu'
  1043. }
  1044. },
  1045. {
  1046. title: '{{ i18n "pages.inbounds.client" }}',
  1047. width: 90,
  1048. align: 'left',
  1049. scopedSlots: {
  1050. customRender: 'client'
  1051. }
  1052. },
  1053. {
  1054. title: '{{ i18n "pages.inbounds.info" }}',
  1055. width: 10,
  1056. align: 'center',
  1057. scopedSlots: {
  1058. customRender: 'info'
  1059. }
  1060. },
  1061. ];
  1062. const app = new Vue({
  1063. delimiters: ['[[', ']]'],
  1064. el: '#app',
  1065. mixins: [MediaQueryMixin],
  1066. data: {
  1067. themeSwitcher,
  1068. persianDatepicker,
  1069. loadingStates: {
  1070. fetched: false,
  1071. spinning: false
  1072. },
  1073. inbounds: [],
  1074. dbInbounds: [],
  1075. searchKey: '',
  1076. enableFilter: false,
  1077. filterBy: '',
  1078. searchedInbounds: [],
  1079. expireDiff: 0,
  1080. trafficDiff: 0,
  1081. defaultCert: '',
  1082. defaultKey: '',
  1083. clientCount: [],
  1084. onlineClients: [],
  1085. lastOnlineMap: {},
  1086. isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
  1087. refreshing: false,
  1088. refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
  1089. subSettings: {
  1090. enable: false,
  1091. subTitle: '',
  1092. subURI: '',
  1093. subJsonURI: '',
  1094. subJsonEnable: false,
  1095. },
  1096. remarkModel: '-ieo',
  1097. datepicker: 'gregorian',
  1098. tgBotEnable: false,
  1099. showAlert: false,
  1100. ipLimitEnable: false,
  1101. pageSize: 0,
  1102. },
  1103. methods: {
  1104. loading(spinning = true) {
  1105. this.loadingStates.spinning = spinning;
  1106. },
  1107. async getDBInbounds() {
  1108. this.refreshing = true;
  1109. const msg = await HttpUtil.get('/panel/api/inbounds/list');
  1110. if (!msg.success) {
  1111. this.refreshing = false;
  1112. return;
  1113. }
  1114. await this.getLastOnlineMap();
  1115. await this.getOnlineUsers();
  1116. this.setInbounds(msg.obj);
  1117. setTimeout(() => {
  1118. this.refreshing = false;
  1119. }, 500);
  1120. },
  1121. async getOnlineUsers() {
  1122. const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
  1123. if (!msg.success) {
  1124. return;
  1125. }
  1126. this.onlineClients = msg.obj != null ? msg.obj : [];
  1127. },
  1128. async getLastOnlineMap() {
  1129. const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
  1130. if (!msg.success || !msg.obj) return;
  1131. this.lastOnlineMap = msg.obj || {}
  1132. },
  1133. async getDefaultSettings() {
  1134. const msg = await HttpUtil.post('/panel/setting/defaultSettings');
  1135. if (!msg.success) {
  1136. return;
  1137. }
  1138. const settings = msg.obj || {};
  1139. this.expireDiff = settings.expireDiff * 86400000;
  1140. this.trafficDiff = settings.trafficDiff * 1073741824;
  1141. this.defaultCert = settings.defaultCert;
  1142. this.defaultKey = settings.defaultKey;
  1143. this.tgBotEnable = settings.tgBotEnable;
  1144. this.subSettings = {
  1145. enable: settings.subEnable,
  1146. subTitle: settings.subTitle,
  1147. subURI: settings.subURI,
  1148. subJsonURI: settings.subJsonURI,
  1149. subJsonEnable: settings.subJsonEnable,
  1150. };
  1151. this.pageSize = settings.pageSize;
  1152. this.remarkModel = settings.remarkModel;
  1153. this.datepicker = settings.datepicker;
  1154. this.ipLimitEnable = settings.ipLimitEnable;
  1155. },
  1156. setInbounds(dbInbounds) {
  1157. this.inbounds.splice(0);
  1158. this.dbInbounds.splice(0);
  1159. this.clientCount.splice(0);
  1160. for (const inbound of dbInbounds) {
  1161. const dbInbound = new DBInbound(inbound);
  1162. to_inbound = dbInbound.toInbound()
  1163. this.inbounds.push(to_inbound);
  1164. this.dbInbounds.push(dbInbound);
  1165. if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(inbound
  1166. .protocol)) {
  1167. if (dbInbound.isSS && (!to_inbound.isSSMultiUser)) {
  1168. continue;
  1169. }
  1170. this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
  1171. }
  1172. }
  1173. if (!this.loadingStates.fetched) {
  1174. this.loadingStates.fetched = true
  1175. }
  1176. if (this.enableFilter) {
  1177. this.filterInbounds();
  1178. } else {
  1179. this.searchInbounds(this.searchKey);
  1180. }
  1181. },
  1182. getClientCounts(dbInbound, inbound) {
  1183. let clientCount = 0,
  1184. active = [],
  1185. deactive = [],
  1186. depleted = [],
  1187. expiring = [],
  1188. online = [],
  1189. comments = new Map();
  1190. clients = inbound.clients;
  1191. clientStats = dbInbound.clientStats
  1192. now = new Date().getTime()
  1193. if (clients) {
  1194. clientCount = clients.length;
  1195. if (dbInbound.enable) {
  1196. clients.forEach(client => {
  1197. if (client.comment) {
  1198. comments.set(client.email, client.comment)
  1199. }
  1200. if (client.enable) {
  1201. active.push(client.email);
  1202. if (this.isClientOnline(client.email)) online.push(client.email);
  1203. } else {
  1204. deactive.push(client.email);
  1205. }
  1206. });
  1207. clientStats.forEach(stats => {
  1208. const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
  1209. const expired = stats.expiryTime > 0 && stats.expiryTime <= now;
  1210. if (expired || exhausted) {
  1211. depleted.push(stats.email);
  1212. } else {
  1213. const expiringSoon = (stats.expiryTime > 0 && (stats.expiryTime - now < this.expireDiff)) ||
  1214. (stats.total > 0 && (stats.total - (stats.up + stats.down) < this.trafficDiff));
  1215. if (expiringSoon) expiring.push(stats.email);
  1216. }
  1217. });
  1218. } else {
  1219. clients.forEach(client => {
  1220. deactive.push(client.email);
  1221. });
  1222. }
  1223. }
  1224. return {
  1225. clients: clientCount,
  1226. active: active,
  1227. deactive: deactive,
  1228. depleted: depleted,
  1229. expiring: expiring,
  1230. online: online,
  1231. comments: comments,
  1232. };
  1233. },
  1234. searchInbounds(key) {
  1235. if (ObjectUtil.isEmpty(key)) {
  1236. this.searchedInbounds = this.dbInbounds.slice();
  1237. } else {
  1238. this.searchedInbounds.splice(0, this.searchedInbounds.length);
  1239. this.dbInbounds.forEach(inbound => {
  1240. if (ObjectUtil.deepSearch(inbound, key)) {
  1241. const newInbound = new DBInbound(inbound);
  1242. const inboundSettings = JSON.parse(inbound.settings);
  1243. if (inboundSettings.hasOwnProperty('clients')) {
  1244. const searchedSettings = {
  1245. "clients": []
  1246. };
  1247. inboundSettings.clients.forEach(client => {
  1248. if (ObjectUtil.deepSearch(client, key)) {
  1249. searchedSettings.clients.push(client);
  1250. }
  1251. });
  1252. newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
  1253. }
  1254. this.searchedInbounds.push(newInbound);
  1255. }
  1256. });
  1257. }
  1258. },
  1259. filterInbounds() {
  1260. if (ObjectUtil.isEmpty(this.filterBy)) {
  1261. this.searchedInbounds = this.dbInbounds.slice();
  1262. } else {
  1263. this.searchedInbounds.splice(0, this.searchedInbounds.length);
  1264. this.dbInbounds.forEach(inbound => {
  1265. const newInbound = new DBInbound(inbound);
  1266. const inboundSettings = JSON.parse(inbound.settings);
  1267. if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)) {
  1268. const list = this.clientCount[inbound.id][this.filterBy];
  1269. if (list.length > 0) {
  1270. const filteredSettings = {
  1271. "clients": []
  1272. };
  1273. if (inboundSettings.clients) {
  1274. inboundSettings.clients.forEach(client => {
  1275. if (list.includes(client.email)) {
  1276. filteredSettings.clients.push(client);
  1277. }
  1278. });
  1279. }
  1280. newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
  1281. this.searchedInbounds.push(newInbound);
  1282. }
  1283. }
  1284. });
  1285. }
  1286. },
  1287. toggleFilter() {
  1288. if (this.enableFilter) {
  1289. this.searchKey = '';
  1290. } else {
  1291. this.filterBy = '';
  1292. this.searchedInbounds = this.dbInbounds.slice();
  1293. }
  1294. },
  1295. generalActions(action) {
  1296. switch (action.key) {
  1297. case "import":
  1298. this.importInbound();
  1299. break;
  1300. case "export":
  1301. this.exportAllLinks();
  1302. break;
  1303. case "subs":
  1304. this.exportAllSubs();
  1305. break;
  1306. case "resetInbounds":
  1307. this.resetAllTraffic();
  1308. break;
  1309. case "resetClients":
  1310. this.resetAllClientTraffics(-1);
  1311. break;
  1312. case "delDepletedClients":
  1313. this.delDepletedClients(-1)
  1314. break;
  1315. }
  1316. },
  1317. clickAction(action, dbInbound) {
  1318. switch (action.key) {
  1319. case "qrcode":
  1320. this.showQrcode(dbInbound.id);
  1321. break;
  1322. case "showInfo":
  1323. this.showInfo(dbInbound.id);
  1324. break;
  1325. case "edit":
  1326. this.openEditInbound(dbInbound.id);
  1327. break;
  1328. case "addClient":
  1329. this.openAddClient(dbInbound.id)
  1330. break;
  1331. case "addBulkClient":
  1332. this.openAddBulkClient(dbInbound.id)
  1333. break;
  1334. case "copyClients":
  1335. copyClientsModal.show(dbInbound);
  1336. break;
  1337. case "export":
  1338. this.inboundLinks(dbInbound.id);
  1339. break;
  1340. case "subs":
  1341. this.exportSubs(dbInbound.id);
  1342. break;
  1343. case "clipboard":
  1344. this.copy(dbInbound.id);
  1345. break;
  1346. case "resetTraffic":
  1347. this.resetTraffic(dbInbound.id);
  1348. break;
  1349. case "resetClients":
  1350. this.resetAllClientTraffics(dbInbound.id);
  1351. break;
  1352. case "clone":
  1353. this.openCloneInbound(dbInbound);
  1354. break;
  1355. case "delete":
  1356. this.delInbound(dbInbound.id);
  1357. break;
  1358. case "delDepletedClients":
  1359. this.delDepletedClients(dbInbound.id)
  1360. break;
  1361. }
  1362. },
  1363. openCloneInbound(dbInbound) {
  1364. this.$confirm({
  1365. title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
  1366. content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
  1367. okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
  1368. class: themeSwitcher.currentTheme,
  1369. cancelText: '{{ i18n "cancel" }}',
  1370. onOk: () => {
  1371. const baseInbound = dbInbound.toInbound();
  1372. dbInbound.up = 0;
  1373. dbInbound.down = 0;
  1374. this.cloneInbound(baseInbound, dbInbound);
  1375. },
  1376. });
  1377. },
  1378. async cloneInbound(baseInbound, dbInbound) {
  1379. const data = {
  1380. up: dbInbound.up,
  1381. down: dbInbound.down,
  1382. total: dbInbound.total,
  1383. remark: dbInbound.remark + " - Cloned",
  1384. enable: dbInbound.enable,
  1385. expiryTime: dbInbound.expiryTime,
  1386. trafficReset: dbInbound.trafficReset,
  1387. lastTrafficResetTime: dbInbound.lastTrafficResetTime,
  1388. listen: '',
  1389. port: RandomUtil.randomInteger(10000, 60000),
  1390. protocol: baseInbound.protocol,
  1391. settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
  1392. streamSettings: baseInbound.stream.toString(),
  1393. sniffing: baseInbound.sniffing.toString(),
  1394. };
  1395. await this.submit('/panel/api/inbounds/add', data, inModal);
  1396. },
  1397. openAddInbound() {
  1398. inModal.show({
  1399. title: '{{ i18n "pages.inbounds.addInbound"}}',
  1400. okText: '{{ i18n "create"}}',
  1401. cancelText: '{{ i18n "close" }}',
  1402. confirm: async (inbound, dbInbound) => {
  1403. await this.addInbound(inbound, dbInbound, inModal);
  1404. },
  1405. isEdit: false
  1406. });
  1407. },
  1408. openEditInbound(dbInboundId) {
  1409. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1410. const inbound = dbInbound.toInbound();
  1411. inModal.show({
  1412. title: '{{ i18n "pages.inbounds.modifyInbound"}}',
  1413. okText: '{{ i18n "update"}}',
  1414. cancelText: '{{ i18n "close" }}',
  1415. inbound: inbound,
  1416. dbInbound: dbInbound,
  1417. confirm: async (inbound, dbInbound) => {
  1418. await this.updateInbound(inbound, dbInbound);
  1419. },
  1420. isEdit: true
  1421. });
  1422. },
  1423. async addInbound(inbound, dbInbound) {
  1424. const data = {
  1425. up: dbInbound.up,
  1426. down: dbInbound.down,
  1427. total: dbInbound.total,
  1428. remark: dbInbound.remark,
  1429. enable: dbInbound.enable,
  1430. expiryTime: dbInbound.expiryTime,
  1431. trafficReset: dbInbound.trafficReset,
  1432. lastTrafficResetTime: dbInbound.lastTrafficResetTime,
  1433. listen: inbound.listen,
  1434. port: inbound.port,
  1435. protocol: inbound.protocol,
  1436. settings: inbound.settings.toString(),
  1437. };
  1438. if (inbound.canEnableStream()) {
  1439. data.streamSettings = inbound.stream.toString();
  1440. } else if (inbound.stream?.sockopt) {
  1441. data.streamSettings = JSON.stringify({
  1442. sockopt: inbound.stream.sockopt.toJson()
  1443. }, null, 2);
  1444. }
  1445. data.sniffing = inbound.sniffing.toString();
  1446. await this.submit('/panel/api/inbounds/add', data, inModal);
  1447. },
  1448. async updateInbound(inbound, dbInbound) {
  1449. const data = {
  1450. up: dbInbound.up,
  1451. down: dbInbound.down,
  1452. total: dbInbound.total,
  1453. remark: dbInbound.remark,
  1454. enable: dbInbound.enable,
  1455. expiryTime: dbInbound.expiryTime,
  1456. trafficReset: dbInbound.trafficReset,
  1457. lastTrafficResetTime: dbInbound.lastTrafficResetTime,
  1458. listen: inbound.listen,
  1459. port: inbound.port,
  1460. protocol: inbound.protocol,
  1461. settings: inbound.settings.toString(),
  1462. };
  1463. if (inbound.canEnableStream()) {
  1464. data.streamSettings = inbound.stream.toString();
  1465. } else if (inbound.stream?.sockopt) {
  1466. data.streamSettings = JSON.stringify({
  1467. sockopt: inbound.stream.sockopt.toJson()
  1468. }, null, 2);
  1469. }
  1470. data.sniffing = inbound.sniffing.toString();
  1471. const formData = new FormData();
  1472. Object.keys(data).forEach(key => formData.append(key, data[key]));
  1473. await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, formData, inModal);
  1474. },
  1475. openAddClient(dbInboundId) {
  1476. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1477. clientModal.show({
  1478. title: '{{ i18n "pages.client.add"}}',
  1479. okText: '{{ i18n "pages.client.submitAdd"}}',
  1480. dbInbound: dbInbound,
  1481. confirm: async (clients, dbInboundId) => {
  1482. await this.addClient(clients, dbInboundId, clientModal);
  1483. },
  1484. isEdit: false
  1485. });
  1486. },
  1487. openAddBulkClient(dbInboundId) {
  1488. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1489. clientsBulkModal.show({
  1490. title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
  1491. okText: '{{ i18n "pages.client.bulk"}}',
  1492. dbInbound: dbInbound,
  1493. confirm: async (clients, dbInboundId) => {
  1494. await this.addClient(clients, dbInboundId, clientsBulkModal);
  1495. },
  1496. });
  1497. },
  1498. openEditClient(dbInboundId, client) {
  1499. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1500. if (!dbInbound) return;
  1501. clients = this.getInboundClients(dbInbound);
  1502. if (!clients || !Array.isArray(clients)) return;
  1503. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1504. if (index < 0) return;
  1505. clientModal.show({
  1506. title: '{{ i18n "pages.client.edit"}}',
  1507. okText: '{{ i18n "pages.client.submitEdit"}}',
  1508. dbInbound: dbInbound,
  1509. index: index,
  1510. confirm: async (client, dbInboundId, clientId) => {
  1511. clientModal.loading();
  1512. await this.updateClient(client, dbInboundId, clientId);
  1513. clientModal.close();
  1514. },
  1515. isEdit: true
  1516. });
  1517. },
  1518. findIndexOfClient(protocol, clients, client) {
  1519. if (!clients || !Array.isArray(clients) || !client) {
  1520. return -1;
  1521. }
  1522. switch (protocol) {
  1523. case Protocols.TROJAN:
  1524. case Protocols.SHADOWSOCKS:
  1525. return clients.findIndex(item => item && item.password === client.password && item.email === client
  1526. .email);
  1527. default:
  1528. return clients.findIndex(item => item && item.id === client.id && item.email === client.email);
  1529. }
  1530. },
  1531. async addClient(clients, dbInboundId, modal) {
  1532. const data = {
  1533. id: dbInboundId,
  1534. settings: '{"clients": [' + clients.toString() + ']}',
  1535. };
  1536. await this.submit(`/panel/api/inbounds/addClient`, data, modal);
  1537. },
  1538. async updateClient(client, dbInboundId, clientId) {
  1539. const data = {
  1540. id: dbInboundId,
  1541. settings: '{"clients": [' + client.toString() + ']}',
  1542. };
  1543. await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
  1544. },
  1545. resetTraffic(dbInboundId) {
  1546. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1547. this.$confirm({
  1548. title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
  1549. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  1550. class: themeSwitcher.currentTheme,
  1551. okText: '{{ i18n "reset"}}',
  1552. cancelText: '{{ i18n "cancel"}}',
  1553. onOk: () => {
  1554. const inbound = dbInbound.toInbound();
  1555. dbInbound.up = 0;
  1556. dbInbound.down = 0;
  1557. this.updateInbound(inbound, dbInbound);
  1558. },
  1559. });
  1560. },
  1561. delInbound(dbInboundId) {
  1562. this.$confirm({
  1563. title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
  1564. content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
  1565. class: themeSwitcher.currentTheme,
  1566. okText: '{{ i18n "delete"}}',
  1567. cancelText: '{{ i18n "cancel"}}',
  1568. onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
  1569. });
  1570. },
  1571. delClient(dbInboundId, client, confirmation = true) {
  1572. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1573. clientId = this.getClientId(dbInbound.protocol, client);
  1574. if (confirmation) {
  1575. this.$confirm({
  1576. title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
  1577. content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
  1578. class: themeSwitcher.currentTheme,
  1579. okText: '{{ i18n "delete"}}',
  1580. cancelText: '{{ i18n "cancel"}}',
  1581. onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
  1582. });
  1583. } else {
  1584. this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
  1585. }
  1586. },
  1587. getSubGroupClients(dbInbounds, currentClient) {
  1588. const response = {
  1589. inbounds: [],
  1590. clients: [],
  1591. editIds: []
  1592. }
  1593. if (dbInbounds && dbInbounds.length > 0 && currentClient) {
  1594. dbInbounds.forEach((dbInboundItem) => {
  1595. const dbInbound = new DBInbound(dbInboundItem);
  1596. if (dbInbound) {
  1597. const inbound = dbInbound.toInbound();
  1598. if (inbound) {
  1599. const clients = inbound.clients;
  1600. if (clients.length > 0) {
  1601. clients.forEach((client) => {
  1602. if (client['subId'] === currentClient['subId']) {
  1603. client['inboundId'] = dbInboundItem.id
  1604. client['clientId'] = this.getClientId(dbInbound.protocol, client)
  1605. response.inbounds.push(dbInboundItem.id)
  1606. response.clients.push(client)
  1607. response.editIds.push(client['clientId'])
  1608. }
  1609. })
  1610. }
  1611. }
  1612. }
  1613. })
  1614. }
  1615. return response;
  1616. },
  1617. getClientId(protocol, client) {
  1618. switch (protocol) {
  1619. case Protocols.TROJAN:
  1620. return client.password;
  1621. case Protocols.SHADOWSOCKS:
  1622. return client.email;
  1623. case Protocols.HYSTERIA:
  1624. return client.auth;
  1625. default:
  1626. return client.id;
  1627. }
  1628. },
  1629. checkFallback(dbInbound) {
  1630. newDbInbound = new DBInbound(dbInbound);
  1631. if (dbInbound.listen.startsWith("@")) {
  1632. rootInbound = this.inbounds.find((i) =>
  1633. i.isTcp && ['trojan', 'vless'].includes(i.protocol) &&
  1634. i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
  1635. );
  1636. if (rootInbound) {
  1637. newDbInbound.listen = rootInbound.listen;
  1638. newDbInbound.port = rootInbound.port;
  1639. newInbound = newDbInbound.toInbound();
  1640. newInbound.stream.security = rootInbound.stream.security;
  1641. newInbound.stream.tls = rootInbound.stream.tls;
  1642. newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
  1643. newDbInbound.streamSettings = newInbound.stream.toString();
  1644. }
  1645. }
  1646. return newDbInbound;
  1647. },
  1648. showQrcode(dbInboundId, client) {
  1649. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1650. newDbInbound = this.checkFallback(dbInbound);
  1651. qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
  1652. },
  1653. showInfo(dbInboundId, client) {
  1654. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1655. if (!dbInbound) return;
  1656. index = 0;
  1657. if (dbInbound.isMultiUser()) {
  1658. inbound = dbInbound.toInbound();
  1659. clients = inbound && inbound.clients ? inbound.clients : null;
  1660. if (clients && Array.isArray(clients)) {
  1661. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1662. if (index < 0) index = 0;
  1663. }
  1664. }
  1665. newDbInbound = this.checkFallback(dbInbound);
  1666. infoModal.show(newDbInbound, index);
  1667. },
  1668. switchEnable(dbInboundId, state) {
  1669. let dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1670. if (!dbInbound) return;
  1671. dbInbound.enable = state;
  1672. let inbound = dbInbound.toInbound();
  1673. const data = {
  1674. up: dbInbound.up,
  1675. down: dbInbound.down,
  1676. total: dbInbound.total,
  1677. remark: dbInbound.remark,
  1678. enable: dbInbound.enable,
  1679. expiryTime: dbInbound.expiryTime,
  1680. trafficReset: dbInbound.trafficReset,
  1681. lastTrafficResetTime: dbInbound.lastTrafficResetTime,
  1682. listen: inbound.listen,
  1683. port: inbound.port,
  1684. protocol: inbound.protocol,
  1685. settings: inbound.settings.toString(),
  1686. };
  1687. if (inbound.canEnableStream()) {
  1688. data.streamSettings = inbound.stream.toString();
  1689. } else if (inbound.stream?.sockopt) {
  1690. data.streamSettings = JSON.stringify({
  1691. sockopt: inbound.stream.sockopt.toJson()
  1692. }, null, 2);
  1693. }
  1694. data.sniffing = inbound.sniffing.toString();
  1695. const formData = new FormData();
  1696. Object.keys(data).forEach(key => formData.append(key, data[key]));
  1697. this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData);
  1698. },
  1699. async switchEnableClient(dbInboundId, client, state) {
  1700. this.loading();
  1701. try {
  1702. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1703. if (!dbInbound) return;
  1704. inbound = dbInbound.toInbound();
  1705. clients = inbound && inbound.clients ? inbound.clients : null;
  1706. if (!clients || !Array.isArray(clients)) return;
  1707. index = this.findIndexOfClient(dbInbound.protocol, clients, client);
  1708. if (index < 0 || !clients[index]) return;
  1709. clients[index].enable = typeof state === 'boolean' ? state : !!client.enable;
  1710. clientId = this.getClientId(dbInbound.protocol, clients[index]);
  1711. await this.updateClient(clients[index], dbInboundId, clientId);
  1712. } finally {
  1713. this.loading(false);
  1714. }
  1715. },
  1716. async submit(url, data, modal) {
  1717. const msg = await HttpUtil.postWithModal(url, data, modal);
  1718. if (msg.success) {
  1719. await this.getDBInbounds();
  1720. }
  1721. },
  1722. getInboundClients(dbInbound) {
  1723. if (!dbInbound) return null;
  1724. const inbound = dbInbound.toInbound();
  1725. return inbound && inbound.clients ? inbound.clients : null;
  1726. },
  1727. resetClientTraffic(client, dbInboundId, confirmation = true) {
  1728. if (confirmation) {
  1729. this.$confirm({
  1730. title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
  1731. content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
  1732. class: themeSwitcher.currentTheme,
  1733. okText: '{{ i18n "reset"}}',
  1734. cancelText: '{{ i18n "cancel"}}',
  1735. onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client
  1736. .email),
  1737. })
  1738. } else {
  1739. this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
  1740. }
  1741. },
  1742. resetAllTraffic() {
  1743. this.$confirm({
  1744. title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
  1745. content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
  1746. class: themeSwitcher.currentTheme,
  1747. okText: '{{ i18n "reset"}}',
  1748. cancelText: '{{ i18n "cancel"}}',
  1749. onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
  1750. });
  1751. },
  1752. resetAllClientTraffics(dbInboundId) {
  1753. this.$confirm({
  1754. title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' :
  1755. '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
  1756. content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' :
  1757. '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
  1758. class: themeSwitcher.currentTheme,
  1759. okText: '{{ i18n "reset"}}',
  1760. cancelText: '{{ i18n "cancel"}}',
  1761. onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
  1762. })
  1763. },
  1764. delDepletedClients(dbInboundId) {
  1765. this.$confirm({
  1766. title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
  1767. content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
  1768. class: themeSwitcher.currentTheme,
  1769. okText: '{{ i18n "delete"}}',
  1770. cancelText: '{{ i18n "cancel"}}',
  1771. onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
  1772. })
  1773. },
  1774. isExpiry(dbInbound, index) {
  1775. return dbInbound.toInbound().isExpiry(index);
  1776. },
  1777. getClientStats(dbInbound, email) {
  1778. if (!dbInbound) return null;
  1779. if (!dbInbound._clientStatsMap) {
  1780. dbInbound._clientStatsMap = new Map();
  1781. if (dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
  1782. for (const stats of dbInbound.clientStats) {
  1783. dbInbound._clientStatsMap.set(stats.email, stats);
  1784. }
  1785. }
  1786. }
  1787. return dbInbound._clientStatsMap.get(email);
  1788. },
  1789. getUpStats(dbInbound, email) {
  1790. if (!email || email.length == 0) return 0;
  1791. let clientStats = this.getClientStats(dbInbound, email);
  1792. return clientStats ? clientStats.up : 0;
  1793. },
  1794. getDownStats(dbInbound, email) {
  1795. if (!email || email.length == 0) return 0;
  1796. let clientStats = this.getClientStats(dbInbound, email);
  1797. return clientStats ? clientStats.down : 0;
  1798. },
  1799. getSumStats(dbInbound, email) {
  1800. if (!email || email.length == 0) return 0;
  1801. let clientStats = this.getClientStats(dbInbound, email);
  1802. return clientStats ? clientStats.up + clientStats.down : 0;
  1803. },
  1804. getAllTimeClient(dbInbound, email) {
  1805. if (!email || email.length == 0) return 0;
  1806. let clientStats = this.getClientStats(dbInbound, email);
  1807. if (!clientStats) return 0;
  1808. return clientStats.allTime || (clientStats.up + clientStats.down);
  1809. },
  1810. getRemStats(dbInbound, email) {
  1811. if (!email || email.length == 0) return 0;
  1812. let clientStats = this.getClientStats(dbInbound, email);
  1813. if (!clientStats) return 0;
  1814. let remained = clientStats.total - (clientStats.up + clientStats.down);
  1815. return remained > 0 ? remained : 0;
  1816. },
  1817. clientStatsColor(dbInbound, email) {
  1818. if (!email || email.length == 0) return ColorUtils.clientUsageColor();
  1819. let clientStats = this.getClientStats(dbInbound, email);
  1820. return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
  1821. },
  1822. statsProgress(dbInbound, email) {
  1823. if (!email || email.length == 0) return 100;
  1824. let clientStats = this.getClientStats(dbInbound, email);
  1825. if (!clientStats) return 0;
  1826. if (clientStats.total == 0) return 100;
  1827. return 100 * (clientStats.down + clientStats.up) / clientStats.total;
  1828. },
  1829. expireProgress(expTime, reset) {
  1830. now = new Date().getTime();
  1831. remainedSeconds = expTime < 0 ? -expTime / 1000 : (expTime - now) / 1000;
  1832. resetSeconds = reset * 86400;
  1833. if (remainedSeconds >= resetSeconds) return 0;
  1834. return 100 * (1 - (remainedSeconds / resetSeconds));
  1835. },
  1836. statsExpColor(dbInbound, email) {
  1837. if (!email || email.length == 0) return '#7a316f';
  1838. let clientStats = this.getClientStats(dbInbound, email);
  1839. if (!clientStats) return '#7a316f';
  1840. let statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats
  1841. .total);
  1842. let expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
  1843. switch (true) {
  1844. case statsColor == "red" || expColor == "red":
  1845. return "#cf3c3c"; // Red
  1846. case statsColor == "orange" || expColor == "orange":
  1847. return "#f37b24"; // Orange
  1848. case statsColor == "green" || expColor == "green":
  1849. return "#008771"; // Green
  1850. default:
  1851. return "#7a316f"; // purple
  1852. }
  1853. },
  1854. isClientEnabled(dbInbound, email) {
  1855. let clientStats = dbInbound ? this.getClientStats(dbInbound, email) : null;
  1856. return clientStats ? clientStats['enable'] : true;
  1857. },
  1858. isClientDepleted(dbInbound, email) {
  1859. if (!email || !dbInbound) return false;
  1860. const stats = this.getClientStats(dbInbound, email);
  1861. if (!stats) return false;
  1862. const total = stats.total ?? 0;
  1863. const used = (stats.up ?? 0) + (stats.down ?? 0);
  1864. const hasTotal = total > 0;
  1865. const exhausted = hasTotal && used >= total;
  1866. const expiryTime = stats.expiryTime ?? 0;
  1867. const hasExpiry = expiryTime > 0;
  1868. const now = Date.now();
  1869. const expired = hasExpiry && expiryTime <= now;
  1870. return expired || exhausted;
  1871. },
  1872. isClientOnline(email) {
  1873. return this.onlineClients.includes(email);
  1874. },
  1875. getLastOnline(email) {
  1876. return this.lastOnlineMap[email] || null
  1877. },
  1878. formatLastOnline(email) {
  1879. const ts = this.getLastOnline(email)
  1880. if (!ts) return '-'
  1881. // Check if IntlUtil is available (may not be loaded yet)
  1882. if (typeof IntlUtil !== 'undefined' && IntlUtil.formatDate) {
  1883. return IntlUtil.formatDate(ts)
  1884. }
  1885. // Fallback to simple date formatting if IntlUtil is not available
  1886. return new Date(ts).toLocaleString()
  1887. },
  1888. isRemovable(dbInboundId) {
  1889. return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
  1890. },
  1891. inboundLinks(dbInboundId) {
  1892. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1893. newDbInbound = this.checkFallback(dbInbound);
  1894. txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel),
  1895. newDbInbound.remark);
  1896. },
  1897. exportSubs(dbInboundId) {
  1898. const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1899. const clients = this.getInboundClients(dbInbound);
  1900. let subLinks = []
  1901. if (clients != null) {
  1902. clients.forEach(c => {
  1903. if (c.subId && c.subId.length > 0) {
  1904. subLinks.push(this.subSettings.subURI + c.subId)
  1905. }
  1906. })
  1907. }
  1908. txtModal.show(
  1909. '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
  1910. [...new Set(subLinks)].join('\n'),
  1911. dbInbound.remark + "-Subs");
  1912. },
  1913. importInbound() {
  1914. promptModal.open({
  1915. title: '{{ i18n "pages.inbounds.importInbound" }}',
  1916. type: 'textarea',
  1917. value: '',
  1918. okText: '{{ i18n "pages.inbounds.import" }}',
  1919. confirm: async (dbInboundText) => {
  1920. await this.submit('/panel/api/inbounds/import', {
  1921. data: dbInboundText
  1922. }, promptModal);
  1923. },
  1924. });
  1925. },
  1926. exportAllSubs() {
  1927. let subLinks = []
  1928. for (const dbInbound of this.dbInbounds) {
  1929. const clients = this.getInboundClients(dbInbound);
  1930. if (clients != null) {
  1931. clients.forEach(c => {
  1932. if (c.subId && c.subId.length > 0) {
  1933. subLinks.push(this.subSettings.subURI + c.subId)
  1934. }
  1935. })
  1936. }
  1937. }
  1938. txtModal.show(
  1939. '{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
  1940. [...new Set(subLinks)].join('\r\n'),
  1941. 'All-Inbounds-Subs');
  1942. },
  1943. exportAllLinks() {
  1944. let copyText = [];
  1945. for (const dbInbound of this.dbInbounds) {
  1946. copyText.push(dbInbound.genInboundLinks(this.remarkModel));
  1947. }
  1948. txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
  1949. },
  1950. copy(dbInboundId) {
  1951. dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
  1952. txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
  1953. },
  1954. async startDataRefreshLoop() {
  1955. while (this.isRefreshEnabled) {
  1956. try {
  1957. await this.getDBInbounds();
  1958. } catch (e) {
  1959. console.error(e);
  1960. }
  1961. await PromiseUtil.sleep(this.refreshInterval);
  1962. }
  1963. },
  1964. toggleRefresh() {
  1965. localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
  1966. if (this.isRefreshEnabled) {
  1967. this.startDataRefreshLoop();
  1968. }
  1969. },
  1970. changeRefreshInterval() {
  1971. localStorage.setItem("refreshInterval", this.refreshInterval);
  1972. },
  1973. async manualRefresh() {
  1974. if (!this.refreshing) {
  1975. this.loadingStates.spinning = true;
  1976. await this.getDBInbounds();
  1977. this.loadingStates.spinning = false;
  1978. }
  1979. },
  1980. pagination(obj) {
  1981. if (this.pageSize > 0 && obj.length > this.pageSize) {
  1982. // Set page options based on object size
  1983. let sizeOptions = [this.pageSize.toString()];
  1984. const increments = [2, 5, 10, 20];
  1985. for (const m of increments) {
  1986. const val = this.pageSize * m;
  1987. if (val < obj.length && val <= 1000) {
  1988. sizeOptions.push(val.toString());
  1989. }
  1990. }
  1991. // Add option to see all in one page
  1992. if (!sizeOptions.includes(obj.length.toString())) {
  1993. sizeOptions.push(obj.length.toString());
  1994. }
  1995. p = {
  1996. showSizeChanger: true,
  1997. size: 'small',
  1998. position: 'bottom',
  1999. pageSize: this.pageSize,
  2000. pageSizeOptions: sizeOptions
  2001. };
  2002. return p;
  2003. }
  2004. return false
  2005. }
  2006. },
  2007. watch: {
  2008. searchKey: Utils.debounce(function(newVal) {
  2009. this.searchInbounds(newVal);
  2010. }, 500)
  2011. },
  2012. mounted() {
  2013. if (window.location.protocol !== "https:") {
  2014. this.showAlert = true;
  2015. }
  2016. this.loading();
  2017. this.getDefaultSettings();
  2018. // Initial data fetch
  2019. this.getDBInbounds().then(() => {
  2020. this.loading(false);
  2021. });
  2022. // Setup WebSocket for real-time updates
  2023. if (window.wsClient) {
  2024. window.wsClient.connect();
  2025. // Listen for inbounds updates
  2026. window.wsClient.on('inbounds', (payload) => {
  2027. if (payload && Array.isArray(payload)) {
  2028. // Use setInbounds to properly convert to DBInbound objects with methods
  2029. this.setInbounds(payload);
  2030. }
  2031. });
  2032. // Listen for invalidate signals (sent when payload is too large for WebSocket)
  2033. // The server sends a lightweight notification and we re-fetch via REST API
  2034. let invalidateTimer = null;
  2035. window.wsClient.on('invalidate', (payload) => {
  2036. if (payload && (payload.type === 'inbounds' || payload.type === 'traffic')) {
  2037. // Debounce to avoid flooding the REST API with multiple invalidate signals
  2038. if (invalidateTimer) clearTimeout(invalidateTimer);
  2039. invalidateTimer = setTimeout(() => {
  2040. invalidateTimer = null;
  2041. this.getDBInbounds();
  2042. }, 1000);
  2043. }
  2044. });
  2045. // Listen for traffic updates
  2046. window.wsClient.on('traffic', (payload) => {
  2047. // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event
  2048. // because clientTraffics contains delta/incremental values, not total accumulated values.
  2049. // Total traffic is updated via the 'inbounds' WebSocket event (or 'invalidate' fallback for large panels).
  2050. // Update online clients list in real-time
  2051. if (payload && Array.isArray(payload.onlineClients)) {
  2052. const nextOnlineClients = payload.onlineClients;
  2053. let onlineChanged = this.onlineClients.length !== nextOnlineClients.length;
  2054. if (!onlineChanged) {
  2055. const prevSet = new Set(this.onlineClients);
  2056. for (const email of nextOnlineClients) {
  2057. if (!prevSet.has(email)) {
  2058. onlineChanged = true;
  2059. break;
  2060. }
  2061. }
  2062. }
  2063. this.onlineClients = nextOnlineClients;
  2064. if (onlineChanged) {
  2065. // Recalculate client counts to update online status
  2066. // Use $set for Vue 2 reactivity — direct array index assignment is not reactive
  2067. this.dbInbounds.forEach(dbInbound => {
  2068. const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
  2069. if (inbound && this.clientCount[dbInbound.id]) {
  2070. this.$set(this.clientCount, dbInbound.id, this.getClientCounts(dbInbound, inbound));
  2071. }
  2072. });
  2073. // Always trigger UI refresh — not just when filter is enabled
  2074. if (this.enableFilter) {
  2075. this.filterInbounds();
  2076. } else {
  2077. this.searchInbounds(this.searchKey);
  2078. }
  2079. }
  2080. }
  2081. // Update last online map in real-time
  2082. // Replace entirely (server sends the full map) to avoid unbounded growth from deleted clients
  2083. if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') {
  2084. this.lastOnlineMap = payload.lastOnlineMap;
  2085. }
  2086. });
  2087. // Fallback to polling if WebSocket fails
  2088. window.wsClient.on('error', () => {
  2089. console.warn('WebSocket connection failed, falling back to polling');
  2090. if (this.isRefreshEnabled) {
  2091. this.startDataRefreshLoop();
  2092. }
  2093. });
  2094. window.wsClient.on('disconnected', () => {
  2095. if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
  2096. console.warn('WebSocket reconnection failed, falling back to polling');
  2097. if (this.isRefreshEnabled) {
  2098. this.startDataRefreshLoop();
  2099. }
  2100. }
  2101. });
  2102. } else {
  2103. // Fallback to polling if WebSocket is not available
  2104. if (this.isRefreshEnabled) {
  2105. this.startDataRefreshLoop();
  2106. }
  2107. }
  2108. },
  2109. computed: {
  2110. total() {
  2111. let down = 0,
  2112. up = 0,
  2113. allTime = 0;
  2114. let clients = 0,
  2115. deactive = [],
  2116. depleted = [],
  2117. expiring = [];
  2118. this.dbInbounds.forEach(dbInbound => {
  2119. down += dbInbound.down;
  2120. up += dbInbound.up;
  2121. allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
  2122. if (this.clientCount[dbInbound.id]) {
  2123. clients += this.clientCount[dbInbound.id].clients;
  2124. deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
  2125. depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
  2126. expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
  2127. }
  2128. });
  2129. return {
  2130. down: down,
  2131. up: up,
  2132. allTime: allTime,
  2133. clients: clients,
  2134. deactive: deactive,
  2135. depleted: depleted,
  2136. expiring: expiring,
  2137. };
  2138. }
  2139. },
  2140. });
  2141. </script>
  2142. <style>
  2143. #content-layout>.ant-layout-content>.ant-spin-nested-loading>div>.ant-spin {
  2144. position: fixed !important;
  2145. top: 50vh !important;
  2146. left: calc(50vw + 100px) !important;
  2147. transform: translate(-50%, -50%);
  2148. z-index: 99999 !important;
  2149. }
  2150. @media (max-width: 768px) {
  2151. #content-layout>.ant-layout-content>.ant-spin-nested-loading>div>.ant-spin {
  2152. left: 50vw !important;
  2153. }
  2154. }
  2155. </style>
  2156. {{ template "page/body_end" .}}