xray.html 85 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441
  1. {{ template "page/head_start" .}}
  2. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
  3. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
  4. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
  5. <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
  6. {{ template "page/head_end" .}}
  7. {{ template "page/body_start" .}}
  8. <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
  9. <a-sidebar></a-sidebar>
  10. <a-layout id="content-layout">
  11. <a-layout-content>
  12. <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
  13. <transition name="list" appear>
  14. <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
  15. message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
  16. </a-alert>
  17. </transition>
  18. <transition name="list" appear>
  19. <a-row v-if="!loadingStates.fetched">
  20. <div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
  21. </a-row>
  22. <a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
  23. <a-col>
  24. <a-card hoverable>
  25. <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
  26. <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
  27. <a-space direction="horizontal">
  28. <a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
  29. {{ i18n "pages.xray.save" }}
  30. </a-button>
  31. <a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">
  32. {{ i18n "pages.xray.restart" }}
  33. </a-button>
  34. <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
  35. <span slot="title">{{ i18n
  36. "pages.index.xrayErrorPopoverTitle" }}</span>
  37. <template slot="content">
  38. <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
  39. ]]</span>
  40. </template>
  41. <a-icon type="question-circle"></a-icon>
  42. </a-popover>
  43. </a-space>
  44. </a-col>
  45. <a-col :xs="24" :sm="14">
  46. <template>
  47. <div>
  48. <a-back-top :target="() => document.getElementById('content-layout')"
  49. visibility-height="200"></a-back-top>
  50. <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
  51. message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
  52. </a-alert>
  53. </div>
  54. </template>
  55. </a-col>
  56. </a-row>
  57. </a-card>
  58. </a-col>
  59. <a-col>
  60. <a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
  61. :class="themeSwitcher.currentTheme">
  62. <a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
  63. <template #tab>
  64. <a-icon type="setting"></a-icon>
  65. <span>{{ i18n "pages.xray.basicTemplate"}}</span>
  66. </template>
  67. {{ template "settings/xray/basics" . }}
  68. </a-tab-pane>
  69. <a-tab-pane key="tpl-routing" :style="{ paddingTop: '20px' }">
  70. <template #tab>
  71. <a-icon type="swap"></a-icon>
  72. <span>{{ i18n "pages.xray.Routings"}}</span>
  73. </template>
  74. {{ template "settings/xray/routing" . }}
  75. </a-tab-pane>
  76. <a-tab-pane key="tpl-outbound" force-render="true">
  77. <template #tab>
  78. <a-icon type="upload"></a-icon>
  79. <span>{{ i18n "pages.xray.Outbounds"}}</span>
  80. </template>
  81. {{ template "settings/xray/outbounds" . }}
  82. </a-tab-pane>
  83. <a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" force-render="true">
  84. <template #tab>
  85. <a-icon type="import"></a-icon>
  86. <span>{{ i18n "pages.xray.outbound.reverse"}}</span>
  87. </template>
  88. {{ template "settings/xray/reverse" . }}
  89. </a-tab-pane>
  90. <a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true">
  91. <template #tab>
  92. <a-icon type="cluster"></a-icon>
  93. <span>{{ i18n "pages.xray.Balancers"}}</span>
  94. </template>
  95. {{ template "settings/xray/balancers" . }}
  96. </a-tab-pane>
  97. <a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true">
  98. <template #tab>
  99. <a-icon type="database"></a-icon>
  100. <span>DNS</span>
  101. </template>
  102. {{ template "settings/xray/dns" . }}
  103. </a-tab-pane>
  104. <a-tab-pane key="tpl-advanced" force-render="true">
  105. <template #tab>
  106. <a-icon type="code"></a-icon>
  107. <span>{{ i18n "pages.xray.advancedTemplate"}}</span>
  108. </template>
  109. {{ template "settings/xray/advanced" . }}
  110. </a-tab-pane>
  111. </a-tabs>
  112. </a-col>
  113. </a-row>
  114. </transition>
  115. </a-spin>
  116. </a-layout-content>
  117. </a-layout>
  118. </a-layout>
  119. {{template "page/body_scripts" .}}
  120. <script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
  121. <script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
  122. <script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
  123. <script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
  124. <script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
  125. <script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
  126. <script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
  127. <script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
  128. <script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
  129. <script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
  130. <script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
  131. {{template "component/aSidebar" .}}
  132. {{template "component/aThemeSwitch" .}}
  133. {{template "component/aTableSortable" .}}
  134. {{template "component/aSettingListItem" .}}
  135. {{template "modals/ruleModal" .}}
  136. {{template "modals/outModal" .}}
  137. {{template "modals/reverseModal" .}}
  138. {{template "modals/balancerModal" .}}
  139. {{template "modals/dnsModal" .}}
  140. {{template "modals/dnsPresetsModal" .}}
  141. {{template "modals/fakednsModal" .}}
  142. {{template "modals/warpModal" .}}
  143. {{template "modals/nordModal" .}}
  144. <script>
  145. // Modernised rules layout — 6 cells (#, source, network, destination,
  146. // inbound, target). Each criterion renders as a single self-labelled
  147. // pill that shows the first value plus a "+N" remainder badge for the
  148. // rest; the full list is surfaced via tooltip on hover. The destination
  149. // column has no fixed width and absorbs leftover horizontal space so the
  150. // table fits typical viewports without a horizontal scrollbar.
  151. const rulesColumns = [
  152. { title: '#', align: 'center', width: 70, scopedSlots: { customRender: 'action' } },
  153. { title: '{{ i18n "pages.xray.rules.source"}}', align: 'left', width: 180, scopedSlots: { customRender: 'source' } },
  154. { title: '{{ i18n "pages.inbounds.network"}}', align: 'left', width: 180, scopedSlots: { customRender: 'network' } },
  155. { title: '{{ i18n "pages.xray.rules.dest"}}', align: 'left', scopedSlots: { customRender: 'destination' } },
  156. { title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'left', width: 180, scopedSlots: { customRender: 'inbound' } },
  157. { title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'left', width: 170, scopedSlots: { customRender: 'target' } },
  158. ];
  159. // Mobile: 3-column table — #, Inbound, Outbound. Source / Network /
  160. // Destination criteria are dropped to keep the table readable on
  161. // narrow viewports. Users see the rule's identity (Inbound) and
  162. // what it does (Outbound) at a glance; full criteria are accessible
  163. // by tapping Edit in the actions menu.
  164. // # column is wider than desktop (110 vs 70) to fit the touch-friendly
  165. // drag handle (padding: 6px → ~28px) alongside the index and dropdown.
  166. const rulesMobileColumns = [
  167. { title: '#', align: 'center', width: 110, scopedSlots: { customRender: 'action' } },
  168. { title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'left', scopedSlots: { customRender: 'inbound' } },
  169. { title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'left', width: 140, scopedSlots: { customRender: 'target' } },
  170. ];
  171. const outboundColumns = [
  172. { title: '#', align: 'center', width: 70, scopedSlots: { customRender: 'action' } },
  173. // Combined "Tag / Protocol" — saves a column. Tag stays on top, protocol +
  174. // network + security pills sit underneath it. Width chosen so the three
  175. // longest tonal pills (e.g. vless + httpupgrade + reality) fit on a
  176. // single line without wrapping.
  177. { title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'left', width: 280, scopedSlots: { customRender: 'identity' } },
  178. { title: '{{ i18n "pages.xray.outbound.address"}}', align: 'left', scopedSlots: { customRender: 'address' } },
  179. { title: '{{ i18n "pages.inbounds.traffic" }}', align: 'left', width: 190, scopedSlots: { customRender: 'traffic' } },
  180. { title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'left', width: 130, scopedSlots: { customRender: 'testResult' } },
  181. { title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 70, scopedSlots: { customRender: 'test' } },
  182. ];
  183. const reverseColumns = [{
  184. title: "#",
  185. align: 'center',
  186. width: 20,
  187. scopedSlots: {
  188. customRender: 'action'
  189. }
  190. },
  191. {
  192. title: '{{ i18n "pages.xray.outbound.type"}}',
  193. dataIndex: 'type',
  194. align: 'center',
  195. width: 50
  196. },
  197. {
  198. title: '{{ i18n "pages.xray.outbound.tag"}}',
  199. dataIndex: 'tag',
  200. align: 'center',
  201. width: 50
  202. },
  203. {
  204. title: '{{ i18n "pages.xray.outbound.domain"}}',
  205. dataIndex: 'domain',
  206. align: 'center',
  207. width: 50
  208. },
  209. ];
  210. const balancerColumns = [{
  211. title: "#",
  212. align: 'center',
  213. width: 20,
  214. scopedSlots: {
  215. customRender: 'action'
  216. }
  217. },
  218. {
  219. title: '{{ i18n "pages.xray.balancer.tag"}}',
  220. dataIndex: 'tag',
  221. align: 'center',
  222. width: 50
  223. },
  224. {
  225. title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}',
  226. align: 'center',
  227. width: 50,
  228. scopedSlots: {
  229. customRender: 'strategy'
  230. }
  231. },
  232. {
  233. title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}',
  234. align: 'center',
  235. width: 100,
  236. scopedSlots: {
  237. customRender: 'selector'
  238. }
  239. },
  240. ];
  241. const dnsColumns = [{
  242. title: "#",
  243. align: 'center',
  244. width: 20,
  245. scopedSlots: {
  246. customRender: 'action'
  247. }
  248. },
  249. {
  250. title: '{{ i18n "pages.xray.outbound.address"}}',
  251. align: 'center',
  252. width: 50,
  253. scopedSlots: {
  254. customRender: 'address'
  255. }
  256. },
  257. {
  258. title: '{{ i18n "pages.xray.dns.domains"}}',
  259. align: 'center',
  260. width: 50,
  261. scopedSlots: {
  262. customRender: 'domain'
  263. }
  264. },
  265. {
  266. title: '{{ i18n "pages.xray.dns.expectIPs"}}',
  267. align: 'center',
  268. width: 50,
  269. scopedSlots: {
  270. customRender: 'expectIPs'
  271. }
  272. },
  273. ];
  274. const fakednsColumns = [{
  275. title: "#",
  276. align: 'center',
  277. width: 20,
  278. scopedSlots: {
  279. customRender: 'action'
  280. }
  281. },
  282. {
  283. title: '{{ i18n "pages.xray.fakedns.ipPool"}}',
  284. dataIndex: 'ipPool',
  285. align: 'center',
  286. width: 50
  287. },
  288. {
  289. title: '{{ i18n "pages.xray.fakedns.poolSize"}}',
  290. dataIndex: 'poolSize',
  291. align: 'center',
  292. width: 50
  293. },
  294. ];
  295. const app = new Vue({
  296. delimiters: ['[[', ']]'],
  297. mixins: [MediaQueryMixin],
  298. el: '#app',
  299. data: {
  300. themeSwitcher,
  301. isDarkTheme: themeSwitcher.isDarkTheme,
  302. loadingStates: {
  303. fetched: false,
  304. spinning: false
  305. },
  306. oldXraySetting: '',
  307. xraySetting: '',
  308. outboundTestUrl: 'https://www.google.com/generate_204',
  309. oldOutboundTestUrl: 'https://www.google.com/generate_204',
  310. inboundTags: [],
  311. outboundsTraffic: [],
  312. outboundTestStates: {}, // Track testing state and results for each outbound
  313. saveBtnDisable: true,
  314. refreshing: false,
  315. restartResult: '',
  316. showAlert: false,
  317. customGeoAliasLabelSuffix: '{{ i18n "pages.index.customGeoAliasLabelSuffix" }}',
  318. advSettings: 'xraySetting',
  319. obsSettings: '',
  320. cm: null,
  321. cmOptions: {
  322. lineNumbers: true,
  323. mode: "application/json",
  324. lint: true,
  325. styleActiveLine: true,
  326. matchBrackets: true,
  327. theme: "xq",
  328. autoCloseTags: true,
  329. lineWrapping: true,
  330. indentUnit: 2,
  331. indentWithTabs: true,
  332. smartIndent: true,
  333. tabSize: 2,
  334. lineWiseCopyCut: false,
  335. foldGutter: true,
  336. gutters: [
  337. "CodeMirror-lint-markers",
  338. "CodeMirror-linenumbers",
  339. "CodeMirror-foldgutter",
  340. ],
  341. },
  342. ipv4Settings: {
  343. tag: "IPv4",
  344. protocol: "freedom",
  345. settings: {
  346. domainStrategy: "UseIPv4"
  347. }
  348. },
  349. directSettings: {
  350. tag: "direct",
  351. protocol: "freedom"
  352. },
  353. routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
  354. log: {
  355. loglevel: ["none", "debug", "info", "warning", "error"],
  356. access: ["none", "./access.log"],
  357. error: ["none", "./error.log"],
  358. dnsLog: false,
  359. maskAddress: ["quarter", "half", "full"],
  360. },
  361. settingsData: {
  362. protocols: {
  363. bittorrent: ["bittorrent"],
  364. },
  365. IPsOptions: [{
  366. label: 'Private IPs',
  367. value: 'geoip:private'
  368. },
  369. {
  370. label: '🇮🇷 Iran',
  371. value: 'ext:geoip_IR.dat:ir'
  372. },
  373. {
  374. label: '🇨🇳 China',
  375. value: 'geoip:cn'
  376. },
  377. {
  378. label: '🇷🇺 Russia',
  379. value: 'ext:geoip_RU.dat:ru'
  380. },
  381. {
  382. label: '🇻🇳 Vietnam',
  383. value: 'geoip:vn'
  384. },
  385. {
  386. label: '🇪🇸 Spain',
  387. value: 'geoip:es'
  388. },
  389. {
  390. label: '🇮🇩 Indonesia',
  391. value: 'geoip:id'
  392. },
  393. {
  394. label: '🇺🇦 Ukraine',
  395. value: 'geoip:ua'
  396. },
  397. {
  398. label: '🇹🇷 Türkiye',
  399. value: 'geoip:tr'
  400. },
  401. {
  402. label: '🇧🇷 Brazil',
  403. value: 'geoip:br'
  404. },
  405. ],
  406. DomainsOptions: [{
  407. label: '🇮🇷 Iran',
  408. value: 'ext:geosite_IR.dat:ir'
  409. },
  410. {
  411. label: '🇮🇷 .ir',
  412. value: 'regexp:.*\\.ir$'
  413. },
  414. {
  415. label: '🇮🇷 .ایران',
  416. value: 'regexp:.*\\.xn--mgba3a4f16a$'
  417. },
  418. {
  419. label: '🇨🇳 China',
  420. value: 'geosite:cn'
  421. },
  422. {
  423. label: '🇨🇳 .cn',
  424. value: 'regexp:.*\\.cn$'
  425. },
  426. {
  427. label: '🇷🇺 Russia',
  428. value: 'ext:geosite_RU.dat:ru-available-only-inside'
  429. },
  430. {
  431. label: '🇷🇺 .ru',
  432. value: 'regexp:.*\\.ru$'
  433. },
  434. {
  435. label: '🇷🇺 .su',
  436. value: 'regexp:.*\\.su$'
  437. },
  438. {
  439. label: '🇷🇺 .рф',
  440. value: 'regexp:.*\\.xn--p1ai$'
  441. },
  442. {
  443. label: '🇻🇳 .vn',
  444. value: 'regexp:.*\\.vn$'
  445. },
  446. ],
  447. BlockDomainsOptions: [{
  448. label: 'Ads All',
  449. value: 'geosite:category-ads-all'
  450. },
  451. {
  452. label: 'Ads IR 🇮🇷',
  453. value: 'ext:geosite_IR.dat:category-ads-all'
  454. },
  455. {
  456. label: 'Ads RU 🇷🇺',
  457. value: 'ext:geosite_RU.dat:category-ads-all'
  458. },
  459. {
  460. label: 'Malware 🇮🇷',
  461. value: 'ext:geosite_IR.dat:malware'
  462. },
  463. {
  464. label: 'Phishing 🇮🇷',
  465. value: 'ext:geosite_IR.dat:phishing'
  466. },
  467. {
  468. label: 'Cryptominers 🇮🇷',
  469. value: 'ext:geosite_IR.dat:cryptominers'
  470. },
  471. {
  472. label: 'Adult +18',
  473. value: 'geosite:category-porn'
  474. },
  475. {
  476. label: '🇮🇷 Iran',
  477. value: 'ext:geosite_IR.dat:ir'
  478. },
  479. {
  480. label: '🇮🇷 .ir',
  481. value: 'regexp:.*\\.ir$'
  482. },
  483. {
  484. label: '🇮🇷 .ایران',
  485. value: 'regexp:.*\\.xn--mgba3a4f16a$'
  486. },
  487. {
  488. label: '🇨🇳 China',
  489. value: 'geosite:cn'
  490. },
  491. {
  492. label: '🇨🇳 .cn',
  493. value: 'regexp:.*\\.cn$'
  494. },
  495. {
  496. label: '🇷🇺 Russia',
  497. value: 'ext:geosite_RU.dat:ru-available-only-inside'
  498. },
  499. {
  500. label: '🇷🇺 .ru',
  501. value: 'regexp:.*\\.ru$'
  502. },
  503. {
  504. label: '🇷🇺 .su',
  505. value: 'regexp:.*\\.su$'
  506. },
  507. {
  508. label: '🇷🇺 .рф',
  509. value: 'regexp:.*\\.xn--p1ai$'
  510. },
  511. {
  512. label: '🇻🇳 .vn',
  513. value: 'regexp:.*\\.vn$'
  514. },
  515. ],
  516. ServicesOptions: [{
  517. label: 'Apple',
  518. value: 'geosite:apple'
  519. },
  520. {
  521. label: 'Meta',
  522. value: 'geosite:meta'
  523. },
  524. {
  525. label: 'Google',
  526. value: 'geosite:google'
  527. },
  528. {
  529. label: 'OpenAI',
  530. value: 'geosite:openai'
  531. },
  532. {
  533. label: 'Spotify',
  534. value: 'geosite:spotify'
  535. },
  536. {
  537. label: 'Netflix',
  538. value: 'geosite:netflix'
  539. },
  540. {
  541. label: 'Reddit',
  542. value: 'geosite:reddit'
  543. },
  544. {
  545. label: 'Speedtest',
  546. value: 'geosite:speedtest'
  547. },
  548. ]
  549. },
  550. defaultObservatory: {
  551. subjectSelector: [],
  552. probeURL: "https://www.google.com/generate_204",
  553. probeInterval: "1m",
  554. enableConcurrency: true
  555. },
  556. defaultBurstObservatory: {
  557. subjectSelector: [],
  558. pingConfig: {
  559. destination: "https://www.google.com/generate_204",
  560. interval: "1m",
  561. connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
  562. timeout: "5s",
  563. sampling: 2
  564. }
  565. }
  566. },
  567. methods: {
  568. loading(spinning = true) {
  569. this.loadingStates.spinning = spinning;
  570. },
  571. async getOutboundsTraffic() {
  572. const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
  573. if (msg.success) {
  574. this.outboundsTraffic = msg.obj;
  575. }
  576. },
  577. async getXraySetting() {
  578. const msg = await HttpUtil.post("/panel/xray/");
  579. if (msg.success) {
  580. if (!this.loadingStates.fetched) {
  581. this.loadingStates.fetched = true
  582. }
  583. result = JSON.parse(msg.obj);
  584. xs = JSON.stringify(result.xraySetting, null, 2);
  585. this.oldXraySetting = xs;
  586. this.xraySetting = xs;
  587. this.inboundTags = result.inboundTags;
  588. this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
  589. this.oldOutboundTestUrl = this.outboundTestUrl;
  590. this.saveBtnDisable = true;
  591. }
  592. },
  593. async updateXraySetting() {
  594. this.loading(true);
  595. const msg = await HttpUtil.post("/panel/xray/update", {
  596. xraySetting: this.xraySetting,
  597. outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
  598. });
  599. this.loading(false);
  600. if (msg.success) {
  601. await this.getXraySetting();
  602. }
  603. },
  604. async restartXray() {
  605. this.loading(true);
  606. const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
  607. this.loading(false);
  608. if (msg.success) {
  609. await PromiseUtil.sleep(500);
  610. await this.getXrayResult();
  611. }
  612. this.loading(false);
  613. },
  614. async getXrayResult() {
  615. const msg = await HttpUtil.get("/panel/xray/getXrayResult");
  616. if (msg.success) {
  617. this.restartResult = msg.obj;
  618. if (msg.obj.length > 1) Vue.prototype.$message.error(msg.obj);
  619. }
  620. },
  621. async resetXrayConfigToDefault() {
  622. this.loading(true);
  623. const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig");
  624. this.loading(false);
  625. if (msg.success) {
  626. this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
  627. this.saveBtnDisable = true;
  628. }
  629. },
  630. changePage(pageKey) {
  631. if (pageKey == 'tpl-advanced') this.changeCode();
  632. if (pageKey == 'tpl-balancer') this.changeObsCode();
  633. },
  634. syncRulesWithOutbound(tag, setting) {
  635. const newTemplateSettings = this.templateSettings;
  636. const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
  637. const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
  638. if (!haveRules && outboundIndex > 0) {
  639. newTemplateSettings.outbounds.splice(outboundIndex);
  640. }
  641. if (haveRules && outboundIndex < 0) {
  642. newTemplateSettings.outbounds.push(setting);
  643. }
  644. this.templateSettings = newTemplateSettings;
  645. },
  646. templateRuleGetter(routeSettings) {
  647. const {
  648. property,
  649. outboundTag
  650. } = routeSettings;
  651. let result = [];
  652. if (this.templateSettings != null) {
  653. this.templateSettings.routing.rules.forEach(
  654. (routingRule) => {
  655. if (
  656. routingRule.hasOwnProperty(property) &&
  657. routingRule.hasOwnProperty("outboundTag") &&
  658. routingRule.outboundTag === outboundTag
  659. ) {
  660. result.push(...routingRule[property]);
  661. }
  662. }
  663. );
  664. }
  665. return result;
  666. },
  667. templateRuleSetter(routeSettings) {
  668. const {
  669. data,
  670. property,
  671. outboundTag
  672. } = routeSettings;
  673. const oldTemplateSettings = this.templateSettings;
  674. const newTemplateSettings = oldTemplateSettings;
  675. currentProperty = this.templateRuleGetter({
  676. outboundTag,
  677. property
  678. })
  679. if (currentProperty.length == 0) {
  680. const propertyRule = {
  681. type: "field",
  682. outboundTag,
  683. [property]: data
  684. };
  685. newTemplateSettings.routing.rules.push(propertyRule);
  686. } else {
  687. const newRules = [];
  688. insertedOnce = false;
  689. newTemplateSettings.routing.rules.forEach(
  690. (routingRule) => {
  691. if (
  692. routingRule.hasOwnProperty(property) &&
  693. routingRule.hasOwnProperty("outboundTag") &&
  694. routingRule.outboundTag === outboundTag
  695. ) {
  696. if (!insertedOnce && data.length > 0) {
  697. insertedOnce = true;
  698. routingRule[property] = data;
  699. newRules.push(routingRule);
  700. }
  701. } else {
  702. newRules.push(routingRule);
  703. }
  704. }
  705. );
  706. newTemplateSettings.routing.rules = newRules;
  707. }
  708. this.templateSettings = newTemplateSettings;
  709. },
  710. changeCode() {
  711. if (this.cm != null) {
  712. this.cm.toTextArea();
  713. }
  714. textAreaObj = document.getElementById('xraySetting');
  715. textAreaObj.value = this[this.advSettings];
  716. this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
  717. this.cm.on('change', editor => {
  718. value = editor.getValue();
  719. if (this.isJsonString(value)) {
  720. this[this.advSettings] = value;
  721. }
  722. });
  723. },
  724. changeObsCode() {
  725. if (this.cm != null) {
  726. this.cm.toTextArea();
  727. }
  728. if (this.obsSettings == '') {
  729. this.cm = null;
  730. return
  731. }
  732. textAreaObj = document.getElementById('obsSetting');
  733. textAreaObj.value = this[this.obsSettings];
  734. this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
  735. this.cm.on('change', editor => {
  736. value = editor.getValue();
  737. if (this.isJsonString(value)) {
  738. this[this.obsSettings] = value;
  739. }
  740. });
  741. },
  742. isJsonString(str) {
  743. try {
  744. JSON.parse(str);
  745. } catch (e) {
  746. return false;
  747. }
  748. return true;
  749. },
  750. // outboundTrafficFor returns {up, down} for an outbound by tag,
  751. // defaulting to zeros when no traffic row has been reported yet.
  752. // Templates use the up/down accessors below — keeping the lookup in
  753. // one place avoids drift if the data shape changes.
  754. outboundTrafficFor(o) {
  755. const t = this.outboundsTraffic.find(t => t.tag == o.tag);
  756. return { up: t ? t.up : 0, down: t ? t.down : 0 };
  757. },
  758. findOutboundUp(o) { return this.outboundTrafficFor(o).up; },
  759. findOutboundDown(o) { return this.outboundTrafficFor(o).down; },
  760. // One tone per category instead of per-value. Adding a new protocol or
  761. // transport inherits the category colour — no styling work required.
  762. // Hierarchy: emerald (protocol — primary identity, matches brand) →
  763. // slate (network — transport is plumbing, sits back) → violet (security —
  764. // accent, only rendered for tls/reality so a stand-out hue is earned).
  765. outboundProtocolTone() { return 'tone-emerald'; },
  766. outboundNetworkTone() { return 'tone-slate'; },
  767. outboundSecurityTone() { return 'tone-violet'; },
  768. // Whether the security label is one we render as a pill in the table.
  769. isOutboundSecurityVisible(security) {
  770. return security === 'tls' || security === 'reality';
  771. },
  772. // Null-safe accessor for the address list — collapses null/undefined
  773. // returns from findOutboundAddress() into an empty array so the template
  774. // can rely on .length and v-for without extra guards.
  775. outboundAddresses(o) {
  776. return this.findOutboundAddress(o) || [];
  777. },
  778. // Test-state accessors — sparse arrays + per-row state make raw checks
  779. // verbose; these helpers keep the template readable and consistent.
  780. isOutboundTesting(index) {
  781. const s = this.outboundTestStates[index];
  782. return !!(s && s.testing);
  783. },
  784. outboundTestResult(index) {
  785. const s = this.outboundTestStates[index];
  786. return s ? s.result : null;
  787. },
  788. isOutboundUntestable(outbound) {
  789. return outbound.protocol === 'blackhole' || outbound.tag === 'blocked';
  790. },
  791. // csv splits a comma-separated rule field into trimmed non-empty values.
  792. // Routing rule data uses CSV strings for multi-value criteria (e.g.
  793. // sourceIP "1.2.3.0/24,4.5.6.0/24"); the modern table renders each
  794. // criterion as a single summary pill, so values are normally re-joined
  795. // via joinCsv() but this helper is kept for callers that need an array.
  796. csv(value) {
  797. if (!value) return [];
  798. return String(value)
  799. .split(',')
  800. .map(v => v.trim())
  801. .filter(v => v.length > 0);
  802. },
  803. // joinCsv normalises a CSV-style rule field into a single comma-space
  804. // separated string suitable for tooltips. Returns '' for empty inputs
  805. // so v-if guards can short-circuit on the raw rule field.
  806. joinCsv(value) {
  807. return this.csv(value).join(', ');
  808. },
  809. findOutboundAddress(o) {
  810. serverObj = null;
  811. switch (o.protocol) {
  812. case Protocols.VMess:
  813. serverObj = o.settings.vnext;
  814. break;
  815. case Protocols.VLESS:
  816. return [o.settings?.address + ':' + o.settings?.port];
  817. case Protocols.HTTP:
  818. case Protocols.Socks:
  819. case Protocols.Shadowsocks:
  820. case Protocols.Trojan:
  821. serverObj = o.settings.servers;
  822. break;
  823. case Protocols.DNS:
  824. return [o.settings?.address + ':' + o.settings?.port];
  825. case Protocols.Wireguard:
  826. return o.settings.peers.map(peer => peer.endpoint);
  827. default:
  828. return null;
  829. }
  830. return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
  831. },
  832. addOutbound() {
  833. outModal.show({
  834. title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
  835. okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
  836. confirm: (outbound) => {
  837. outModal.loading();
  838. if (outbound.tag.length > 0) {
  839. this.templateSettings.outbounds.push(outbound);
  840. this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
  841. }
  842. outModal.close();
  843. },
  844. isEdit: false,
  845. tags: this.templateSettings.outbounds.map(obj => obj.tag)
  846. });
  847. },
  848. editOutbound(index) {
  849. outModal.show({
  850. title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index + 1),
  851. outbound: app.templateSettings.outbounds[index],
  852. confirm: (outbound) => {
  853. outModal.loading();
  854. this.templateSettings.outbounds[index] = outbound;
  855. this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
  856. outModal.close();
  857. },
  858. isEdit: true,
  859. tags: this.outboundData.filter((o) => o.key != index).map(obj => obj.tag)
  860. });
  861. },
  862. deleteOutbound(index) {
  863. outbounds = this.templateSettings.outbounds;
  864. outbounds.splice(index, 1);
  865. this.outboundSettings = JSON.stringify(outbounds);
  866. },
  867. setFirstOutbound(index) {
  868. outbounds = this.templateSettings.outbounds;
  869. outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
  870. this.outboundSettings = JSON.stringify(outbounds);
  871. },
  872. async testOutbound(index) {
  873. const outbound = this.templateSettings.outbounds[index];
  874. if (!outbound) {
  875. Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}');
  876. return;
  877. }
  878. if (outbound.protocol === 'blackhole' || outbound.tag === 'blocked') {
  879. Vue.prototype.$message.warning(
  880. '{{ i18n "pages.xray.outbound.testError" }}: blocked/blackhole outbound');
  881. return;
  882. }
  883. // Initialize test state for this outbound if not exists
  884. if (!this.outboundTestStates[index]) {
  885. this.$set(this.outboundTestStates, index, {
  886. testing: false,
  887. result: null
  888. });
  889. }
  890. // Set testing state
  891. this.$set(this.outboundTestStates[index], 'testing', true);
  892. this.$set(this.outboundTestStates[index], 'result', null);
  893. try {
  894. const outboundJSON = JSON.stringify(outbound);
  895. const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);
  896. const msg = await HttpUtil.post("/panel/xray/testOutbound", {
  897. outbound: outboundJSON,
  898. allOutbounds: allOutboundsJSON
  899. });
  900. // Update test state
  901. this.$set(this.outboundTestStates[index], 'testing', false);
  902. if (msg.success && msg.obj) {
  903. const result = msg.obj;
  904. this.$set(this.outboundTestStates[index], 'result', result);
  905. if (result.success) {
  906. Vue.prototype.$message.success(
  907. `{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (${result.statusCode})`
  908. );
  909. } else {
  910. Vue.prototype.$message.error(
  911. `{{ i18n "pages.xray.outbound.testFailed" }}: ${result.error || 'Unknown error'}`
  912. );
  913. }
  914. } else {
  915. this.$set(this.outboundTestStates[index], 'result', {
  916. success: false,
  917. error: msg.msg || '{{ i18n "pages.xray.outbound.testError" }}'
  918. });
  919. Vue.prototype.$message.error(msg.msg || '{{ i18n "pages.xray.outbound.testError" }}');
  920. }
  921. } catch (error) {
  922. this.$set(this.outboundTestStates[index], 'testing', false);
  923. this.$set(this.outboundTestStates[index], 'result', {
  924. success: false,
  925. error: error.message || '{{ i18n "pages.xray.outbound.testError" }}'
  926. });
  927. Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}: ' + error.message);
  928. }
  929. },
  930. addReverse() {
  931. reverseModal.show({
  932. title: '{{ i18n "pages.xray.outbound.addReverse"}}',
  933. okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
  934. confirm: (reverse, rules) => {
  935. reverseModal.loading();
  936. if (reverse.tag.length > 0) {
  937. newTemplateSettings = this.templateSettings;
  938. if (newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
  939. if (newTemplateSettings.reverse[reverse.type + 's'] == undefined) newTemplateSettings.reverse[
  940. reverse.type + 's'] = [];
  941. newTemplateSettings.reverse[reverse.type + 's'].push({
  942. tag: reverse.tag,
  943. domain: reverse.domain
  944. });
  945. this.templateSettings = newTemplateSettings;
  946. // Add related rules
  947. this.templateSettings.routing.rules.push(...rules);
  948. this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
  949. }
  950. reverseModal.close();
  951. },
  952. isEdit: false
  953. });
  954. },
  955. editReverse(index) {
  956. if (this.reverseData[index].type == "bridge") {
  957. oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this
  958. .reverseData[index].tag);
  959. } else {
  960. oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this
  961. .reverseData[index].tag);
  962. }
  963. reverseModal.show({
  964. title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index + 1),
  965. reverse: this.reverseData[index],
  966. rules: oldRules,
  967. confirm: (reverse, rules) => {
  968. reverseModal.loading();
  969. if (reverse.tag.length > 0) {
  970. oldData = this.reverseData[index];
  971. newTemplateSettings = this.templateSettings;
  972. oldReverseIndex = newTemplateSettings.reverse[oldData.type + 's'].findIndex(rs => rs.tag ==
  973. oldData.tag);
  974. oldRuleIndex0 = oldRules.length > 0 ? newTemplateSettings.routing.rules.findIndex(r => JSON
  975. .stringify(r) == JSON.stringify(oldRules[0])) : -1;
  976. oldRuleIndex1 = oldRules.length == 2 ? newTemplateSettings.routing.rules.findIndex(r => JSON
  977. .stringify(r) == JSON.stringify(oldRules[1])) : -1;
  978. if (oldData.type == reverse.type) {
  979. newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = {
  980. tag: reverse.tag,
  981. domain: reverse.domain
  982. };
  983. } else {
  984. newTemplateSettings.reverse[oldData.type + 's'].splice(oldReverseIndex, 1);
  985. // delete empty object
  986. if (newTemplateSettings.reverse[oldData.type + 's'].length == 0) Reflect.deleteProperty(
  987. newTemplateSettings.reverse, oldData.type + 's');
  988. // add other type of reverse if it is not exist
  989. if (!newTemplateSettings.reverse[reverse.type + 's']) newTemplateSettings.reverse[reverse
  990. .type + 's'] = [];
  991. newTemplateSettings.reverse[reverse.type + 's'].push({
  992. tag: reverse.tag,
  993. domain: reverse.domain
  994. });
  995. }
  996. this.templateSettings = newTemplateSettings;
  997. // Adjust Rules
  998. newRules = this.templateSettings.routing.rules;
  999. oldRuleIndex0 != -1 ? newRules[oldRuleIndex0] = rules[0] : newRules.push(rules[0]);
  1000. oldRuleIndex1 != -1 ? newRules[oldRuleIndex1] = rules[1] : newRules.push(rules[1]);
  1001. this.routingRuleSettings = JSON.stringify(newRules);
  1002. }
  1003. reverseModal.close();
  1004. },
  1005. isEdit: true
  1006. });
  1007. },
  1008. deleteReverse(index) {
  1009. oldData = this.reverseData[index];
  1010. newTemplateSettings = this.templateSettings;
  1011. reverseTypeObj = newTemplateSettings.reverse[oldData.type + 's'];
  1012. realIndex = reverseTypeObj.findIndex(r => r.tag == oldData.tag && r.domain == oldData.domain);
  1013. newTemplateSettings.reverse[oldData.type + 's'].splice(realIndex, 1);
  1014. // delete empty objects
  1015. if (reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type + 's');
  1016. if (Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings,
  1017. 'reverse');
  1018. // delete related routing rules
  1019. newRules = newTemplateSettings.routing.rules;
  1020. if (oldData.type == "bridge") {
  1021. newRules = newTemplateSettings.routing.rules.filter(r => !(r.inboundTag && r.inboundTag.length == 1 && r
  1022. .inboundTag[0] == oldData.tag));
  1023. } else if (oldData.type == "portal") {
  1024. newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
  1025. }
  1026. newTemplateSettings.routing.rules = newRules;
  1027. this.templateSettings = newTemplateSettings;
  1028. },
  1029. async refreshOutboundTraffic() {
  1030. if (!this.refreshing) {
  1031. this.refreshing = true;
  1032. await this.getOutboundsTraffic();
  1033. data = []
  1034. if (this.templateSettings != null) {
  1035. this.templateSettings.outbounds.forEach((o, index) => {
  1036. data.push({
  1037. 'key': index,
  1038. ...o
  1039. });
  1040. });
  1041. }
  1042. this.outboundData = data;
  1043. this.refreshing = false;
  1044. }
  1045. },
  1046. async resetOutboundTraffic(index) {
  1047. let tag = "-alltags-";
  1048. if (index >= 0) {
  1049. tag = this.outboundData[index].tag ? this.outboundData[index].tag : ""
  1050. }
  1051. const msg = await HttpUtil.post("/panel/xray/resetOutboundsTraffic", {
  1052. tag: tag
  1053. });
  1054. if (msg.success) {
  1055. await this.refreshOutboundTraffic();
  1056. }
  1057. },
  1058. addBalancer() {
  1059. balancerModal.show({
  1060. title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
  1061. okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
  1062. balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
  1063. balancer: {
  1064. tag: '',
  1065. strategy: 'random',
  1066. selector: [],
  1067. fallbackTag: ''
  1068. },
  1069. confirm: (balancer) => {
  1070. balancerModal.loading();
  1071. newTemplateSettings = this.templateSettings;
  1072. if (newTemplateSettings.routing.balancers == undefined) {
  1073. newTemplateSettings.routing.balancers = [];
  1074. }
  1075. let tmpBalancer = {
  1076. 'tag': balancer.tag,
  1077. 'selector': balancer.selector,
  1078. 'fallbackTag': balancer.fallbackTag
  1079. };
  1080. if (balancer.strategy && balancer.strategy != 'random') {
  1081. tmpBalancer.strategy = {
  1082. 'type': balancer.strategy
  1083. };
  1084. }
  1085. newTemplateSettings.routing.balancers.push(tmpBalancer);
  1086. this.templateSettings = newTemplateSettings;
  1087. this.updateObservatorySelectors();
  1088. balancerModal.close();
  1089. this.changeObsCode();
  1090. },
  1091. isEdit: false
  1092. });
  1093. },
  1094. editBalancer(index) {
  1095. const oldTag = this.balancersData[index].tag;
  1096. balancerModal.show({
  1097. title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
  1098. okText: '{{ i18n "sure" }}',
  1099. balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
  1100. balancer: this.balancersData[index],
  1101. confirm: (balancer) => {
  1102. balancerModal.loading();
  1103. newTemplateSettings = this.templateSettings;
  1104. let tmpBalancer = {
  1105. 'tag': balancer.tag,
  1106. 'selector': balancer.selector,
  1107. 'fallbackTag': balancer.fallbackTag
  1108. };
  1109. // Remove old tag
  1110. if (newTemplateSettings.observatory) {
  1111. newTemplateSettings.observatory.subjectSelector = newTemplateSettings.observatory
  1112. .subjectSelector.filter(s => s != oldTag);
  1113. }
  1114. if (newTemplateSettings.burstObservatory) {
  1115. newTemplateSettings.burstObservatory.subjectSelector = newTemplateSettings.burstObservatory
  1116. .subjectSelector.filter(s => s != oldTag);
  1117. }
  1118. if (balancer.strategy && balancer.strategy != 'random') {
  1119. tmpBalancer.strategy = {
  1120. 'type': balancer.strategy
  1121. };
  1122. }
  1123. newTemplateSettings.routing.balancers[index] = tmpBalancer;
  1124. // change edited tag if used in rule section
  1125. if (oldTag != balancer.tag) {
  1126. newTemplateSettings.routing.rules.forEach((rule) => {
  1127. if (rule.balancerTag && rule.balancerTag == oldTag) {
  1128. rule.balancerTag = balancer.tag;
  1129. }
  1130. });
  1131. }
  1132. this.templateSettings = newTemplateSettings;
  1133. this.updateObservatorySelectors();
  1134. balancerModal.close();
  1135. this.changeObsCode();
  1136. },
  1137. isEdit: true
  1138. });
  1139. },
  1140. updateObservatorySelectors() {
  1141. newTemplateSettings = this.templateSettings;
  1142. const leastPings = this.balancersData.filter((b) => b.strategy == 'leastPing');
  1143. const leastLoads = this.balancersData.filter((b) =>
  1144. b.strategy === 'leastLoad' ||
  1145. b.strategy === 'roundRobin' ||
  1146. b.strategy === 'random'
  1147. );
  1148. if (leastPings.length > 0) {
  1149. if (!newTemplateSettings.observatory)
  1150. newTemplateSettings.observatory = this.defaultObservatory;
  1151. newTemplateSettings.observatory.subjectSelector = [];
  1152. leastPings.forEach((b) => {
  1153. b.selector.forEach((s) => {
  1154. if (!newTemplateSettings.observatory.subjectSelector.includes(s))
  1155. newTemplateSettings.observatory.subjectSelector.push(s);
  1156. });
  1157. });
  1158. } else {
  1159. delete newTemplateSettings.observatory
  1160. }
  1161. if (leastLoads.length > 0) {
  1162. if (!newTemplateSettings.burstObservatory)
  1163. newTemplateSettings.burstObservatory = this.defaultBurstObservatory;
  1164. newTemplateSettings.burstObservatory.subjectSelector = [];
  1165. leastLoads.forEach((b) => {
  1166. b.selector.forEach((s) => {
  1167. if (!newTemplateSettings.burstObservatory.subjectSelector.includes(s))
  1168. newTemplateSettings.burstObservatory.subjectSelector.push(s);
  1169. });
  1170. });
  1171. } else {
  1172. delete newTemplateSettings.burstObservatory
  1173. }
  1174. this.templateSettings = newTemplateSettings;
  1175. this.changeObsCode();
  1176. },
  1177. deleteBalancer(index) {
  1178. newTemplateSettings = this.templateSettings;
  1179. // Remove from balancers
  1180. const removedBalancer = this.balancersData.splice(index, 1)[0];
  1181. // Remove from settings
  1182. let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag === removedBalancer.tag);
  1183. newTemplateSettings.routing.balancers.splice(realIndex, 1);
  1184. // Update balancers property to an empty array if there are no more balancers
  1185. if (newTemplateSettings.routing.balancers.length === 0) {
  1186. delete newTemplateSettings.routing.balancers;
  1187. }
  1188. // Remove orphaned balancer references from routing rules
  1189. if (newTemplateSettings.routing.rules) {
  1190. newTemplateSettings.routing.rules.forEach((rule) => {
  1191. if (rule.balancerTag && rule.balancerTag === removedBalancer.tag) {
  1192. delete rule.balancerTag;
  1193. }
  1194. });
  1195. }
  1196. this.templateSettings = newTemplateSettings;
  1197. this.updateObservatorySelectors();
  1198. this.obsSettings = '';
  1199. this.changeObsCode()
  1200. },
  1201. openDNSPresets() {
  1202. dnsPresetsModal.show({
  1203. title: '{{ i18n "pages.xray.dns.dnsPresetTitle" }}',
  1204. selected: (selectedPreset) => {
  1205. this.dnsServers = selectedPreset;
  1206. dnsPresetsModal.close();
  1207. }
  1208. });
  1209. },
  1210. addDNSServer() {
  1211. dnsModal.show({
  1212. title: '{{ i18n "pages.xray.dns.add" }}',
  1213. confirm: (dnsServer) => {
  1214. dnsServers = this.dnsServers;
  1215. dnsServers.push(dnsServer);
  1216. this.dnsServers = dnsServers;
  1217. dnsModal.close();
  1218. },
  1219. isEdit: false
  1220. });
  1221. },
  1222. editDNSServer(index) {
  1223. dnsModal.show({
  1224. title: '{{ i18n "pages.xray.dns.edit" }} #' + (index + 1),
  1225. dnsServer: this.dnsServers[index],
  1226. confirm: (dnsServer) => {
  1227. dnsServers = this.dnsServers;
  1228. dnsServers[index] = dnsServer;
  1229. this.dnsServers = dnsServers;
  1230. dnsModal.close();
  1231. },
  1232. isEdit: true
  1233. });
  1234. },
  1235. deleteDNSServer(index) {
  1236. newDnsServers = this.dnsServers;
  1237. newDnsServers.splice(index, 1);
  1238. this.dnsServers = newDnsServers;
  1239. },
  1240. addFakedns() {
  1241. fakednsModal.show({
  1242. title: '{{ i18n "pages.xray.fakedns.add" }}',
  1243. confirm: (item) => {
  1244. fakeDns = this.fakeDns ?? [];
  1245. fakeDns.push(item);
  1246. this.fakeDns = fakeDns;
  1247. fakednsModal.close();
  1248. },
  1249. isEdit: false
  1250. });
  1251. },
  1252. editFakedns(index) {
  1253. fakednsModal.show({
  1254. title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index + 1),
  1255. fakeDns: this.fakeDns[index],
  1256. confirm: (item) => {
  1257. fakeDns = this.fakeDns;
  1258. fakeDns[index] = item;
  1259. this.fakeDns = fakeDns;
  1260. fakednsModal.close();
  1261. },
  1262. isEdit: true
  1263. });
  1264. },
  1265. deleteFakedns(index) {
  1266. fakeDns = this.fakeDns;
  1267. fakeDns.splice(index, 1);
  1268. this.fakeDns = fakeDns;
  1269. },
  1270. addRule() {
  1271. ruleModal.show({
  1272. title: '{{ i18n "pages.xray.rules.add"}}',
  1273. okText: '{{ i18n "pages.xray.rules.add" }}',
  1274. confirm: (rule) => {
  1275. ruleModal.loading();
  1276. if (JSON.stringify(rule).length > 3) {
  1277. this.templateSettings.routing.rules.push(rule);
  1278. this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
  1279. }
  1280. ruleModal.close();
  1281. },
  1282. isEdit: false
  1283. });
  1284. },
  1285. editRule(index) {
  1286. ruleModal.show({
  1287. title: '{{ i18n "pages.xray.rules.edit"}} ' + (index + 1),
  1288. rule: app.templateSettings.routing.rules[index],
  1289. confirm: (rule) => {
  1290. ruleModal.loading();
  1291. if (JSON.stringify(rule).length > 3) {
  1292. this.templateSettings.routing.rules[index] = rule;
  1293. this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
  1294. }
  1295. ruleModal.close();
  1296. },
  1297. isEdit: true
  1298. });
  1299. },
  1300. replaceRule(old_index, new_index) {
  1301. rules = this.templateSettings.routing.rules;
  1302. if (new_index >= rules.length) rules.push(undefined);
  1303. rules.splice(new_index, 0, rules.splice(old_index, 1)[0]);
  1304. this.routingRuleSettings = JSON.stringify(rules);
  1305. },
  1306. deleteRule(index) {
  1307. rules = this.templateSettings.routing.rules;
  1308. rules.splice(index, 1);
  1309. this.routingRuleSettings = JSON.stringify(rules);
  1310. },
  1311. showWarp() {
  1312. warpModal.show();
  1313. },
  1314. showNord() {
  1315. nordModal.show();
  1316. },
  1317. async loadCustomGeoAliases() {
  1318. try {
  1319. const msg = await HttpUtil.get('/panel/api/custom-geo/aliases');
  1320. if (!msg.success) {
  1321. console.warn('Failed to load custom geo aliases:', msg.msg || 'request failed');
  1322. return;
  1323. }
  1324. if (!msg.obj) return;
  1325. const geoip = msg.obj.geoip ?? [];
  1326. const geosite = msg.obj.geosite ?? [];
  1327. const geoSuffix = this.customGeoAliasLabelSuffix || '';
  1328. geoip.forEach((x) => {
  1329. this.settingsData.IPsOptions.push({
  1330. label: x.alias + geoSuffix,
  1331. value: x.extExample,
  1332. });
  1333. });
  1334. geosite.forEach((x) => {
  1335. const opt = {
  1336. label: x.alias + geoSuffix,
  1337. value: x.extExample
  1338. };
  1339. this.settingsData.DomainsOptions.push(opt);
  1340. this.settingsData.BlockDomainsOptions.push(opt);
  1341. });
  1342. } catch (e) {
  1343. console.error('Failed to load custom geo aliases:', e);
  1344. }
  1345. }
  1346. },
  1347. async mounted() {
  1348. if (window.location.protocol !== "https:") {
  1349. this.showAlert = true;
  1350. }
  1351. await this.getXraySetting();
  1352. await this.loadCustomGeoAliases();
  1353. await this.getXrayResult();
  1354. await this.getOutboundsTraffic();
  1355. if (window.wsClient) {
  1356. window.wsClient.connect();
  1357. window.wsClient.on('outbounds', (payload) => {
  1358. if (payload) {
  1359. this.outboundsTraffic = payload;
  1360. this.$forceUpdate();
  1361. }
  1362. });
  1363. // Handle invalidate signals (sent when payload is too large for WebSocket,
  1364. // or when traffic job notifies about data changes)
  1365. window.wsClient.on('invalidate', (payload) => {
  1366. if (payload && payload.type === 'outbounds') {
  1367. this.refreshOutboundTraffic();
  1368. }
  1369. });
  1370. }
  1371. while (true) {
  1372. await PromiseUtil.sleep(800);
  1373. this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this
  1374. .outboundTestUrl;
  1375. }
  1376. },
  1377. computed: {
  1378. templateSettings: {
  1379. get: function() {
  1380. const parsedSettings = this.xraySetting ? JSON.parse(this.xraySetting) : null;
  1381. return parsedSettings;
  1382. },
  1383. set: function(newValue) {
  1384. if (newValue) {
  1385. this.xraySetting = JSON.stringify(newValue, null, 2);
  1386. }
  1387. },
  1388. },
  1389. inboundSettings: {
  1390. get: function() {
  1391. return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null;
  1392. },
  1393. set: function(newValue) {
  1394. newTemplateSettings = this.templateSettings;
  1395. newTemplateSettings.inbounds = JSON.parse(newValue);
  1396. this.templateSettings = newTemplateSettings;
  1397. },
  1398. },
  1399. outboundSettings: {
  1400. get: function() {
  1401. return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null;
  1402. },
  1403. set: function(newValue) {
  1404. newTemplateSettings = this.templateSettings;
  1405. newTemplateSettings.outbounds = JSON.parse(newValue);
  1406. this.templateSettings = newTemplateSettings;
  1407. },
  1408. },
  1409. outboundData: {
  1410. get: function() {
  1411. data = []
  1412. if (this.templateSettings != null) {
  1413. this.templateSettings.outbounds.forEach((o, index) => {
  1414. data.push({
  1415. 'key': index,
  1416. ...o
  1417. });
  1418. });
  1419. }
  1420. return data;
  1421. },
  1422. },
  1423. reverseData: {
  1424. get: function() {
  1425. data = []
  1426. if (this.templateSettings != null && this.templateSettings.reverse != null) {
  1427. if (this.templateSettings.reverse.bridges) {
  1428. this.templateSettings.reverse.bridges.forEach((o, index) => {
  1429. data.push({
  1430. 'key': index,
  1431. 'type': 'bridge',
  1432. ...o
  1433. });
  1434. });
  1435. }
  1436. if (this.templateSettings.reverse.portals) {
  1437. this.templateSettings.reverse.portals.forEach((o, index) => {
  1438. data.push({
  1439. 'key': index,
  1440. 'type': 'portal',
  1441. ...o
  1442. });
  1443. });
  1444. }
  1445. }
  1446. return data;
  1447. },
  1448. },
  1449. routingRuleSettings: {
  1450. get: function() {
  1451. return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null;
  1452. },
  1453. set: function(newValue) {
  1454. newTemplateSettings = this.templateSettings;
  1455. newTemplateSettings.routing.rules = JSON.parse(newValue);
  1456. this.templateSettings = newTemplateSettings;
  1457. },
  1458. },
  1459. routingRuleData: {
  1460. get: function() {
  1461. data = [];
  1462. if (this.templateSettings != null) {
  1463. this.templateSettings.routing.rules.forEach((r, index) => {
  1464. data.push({
  1465. 'key': index,
  1466. ...r
  1467. });
  1468. });
  1469. // Make rules readable
  1470. data.forEach(r => {
  1471. if (r.domain) r.domain = r.domain.join(',')
  1472. if (r.ip) r.ip = r.ip.join(',')
  1473. if (r.source) r.source = r.source.join(',');
  1474. if (r.user) r.user = r.user.join(',')
  1475. if (r.inboundTag) r.inboundTag = r.inboundTag.join(',')
  1476. if (r.protocol) r.protocol = r.protocol.join(',')
  1477. if (r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2)
  1478. });
  1479. }
  1480. return data;
  1481. }
  1482. },
  1483. balancersData: {
  1484. get: function() {
  1485. data = []
  1486. if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings
  1487. .routing.balancers != null) {
  1488. this.templateSettings.routing.balancers.forEach((o, index) => {
  1489. data.push({
  1490. 'key': index,
  1491. 'tag': o.tag ? o.tag : "",
  1492. 'strategy': o.strategy?.type ?? "random",
  1493. 'selector': o.selector ? o.selector : [],
  1494. 'fallbackTag': o.fallbackTag ?? '',
  1495. });
  1496. });
  1497. }
  1498. return data;
  1499. }
  1500. },
  1501. observatory: {
  1502. get: function() {
  1503. return this.templateSettings?.observatory ? JSON.stringify(this.templateSettings.observatory, null, 2) :
  1504. null;
  1505. },
  1506. set: function(newValue) {
  1507. newTemplateSettings = this.templateSettings;
  1508. newTemplateSettings.observatory = JSON.parse(newValue);
  1509. this.templateSettings = newTemplateSettings;
  1510. },
  1511. },
  1512. burstObservatory: {
  1513. get: function() {
  1514. return this.templateSettings?.burstObservatory ? JSON.stringify(this.templateSettings.burstObservatory,
  1515. null, 2) : null;
  1516. },
  1517. set: function(newValue) {
  1518. newTemplateSettings = this.templateSettings;
  1519. newTemplateSettings.burstObservatory = JSON.parse(newValue);
  1520. this.templateSettings = newTemplateSettings;
  1521. },
  1522. },
  1523. observatoryEnable: function() {
  1524. return this.templateSettings != null && this.templateSettings.observatory != undefined
  1525. },
  1526. burstObservatoryEnable: function() {
  1527. return this.templateSettings != null && this.templateSettings.burstObservatory != undefined
  1528. },
  1529. freedomStrategy: {
  1530. get: function() {
  1531. if (!this.templateSettings) return "AsIs";
  1532. freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && o.tag ==
  1533. "direct");
  1534. if (!freedomOutbound) return "AsIs";
  1535. if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
  1536. return freedomOutbound.settings.domainStrategy;
  1537. },
  1538. set: function(newValue) {
  1539. newTemplateSettings = this.templateSettings;
  1540. freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o
  1541. .tag == "direct");
  1542. if (freedomOutboundIndex == -1) {
  1543. newTemplateSettings.outbounds.push({
  1544. protocol: "freedom",
  1545. tag: "direct",
  1546. settings: {
  1547. "domainStrategy": newValue
  1548. }
  1549. });
  1550. } else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
  1551. newTemplateSettings.outbounds[freedomOutboundIndex].settings = {
  1552. "domainStrategy": newValue
  1553. };
  1554. } else {
  1555. newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
  1556. }
  1557. this.templateSettings = newTemplateSettings;
  1558. }
  1559. },
  1560. routingStrategy: {
  1561. get: function() {
  1562. if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing
  1563. .domainStrategy) return "AsIs";
  1564. return this.templateSettings.routing.domainStrategy;
  1565. },
  1566. set: function(newValue) {
  1567. newTemplateSettings = this.templateSettings;
  1568. newTemplateSettings.routing.domainStrategy = newValue;
  1569. this.templateSettings = newTemplateSettings;
  1570. }
  1571. },
  1572. logLevel: {
  1573. get: function() {
  1574. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel)
  1575. return "warning";
  1576. return this.templateSettings.log.loglevel;
  1577. },
  1578. set: function(newValue) {
  1579. newTemplateSettings = this.templateSettings;
  1580. newTemplateSettings.log.loglevel = newValue;
  1581. this.templateSettings = newTemplateSettings;
  1582. }
  1583. },
  1584. accessLog: {
  1585. get: function() {
  1586. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access)
  1587. return "";
  1588. return this.templateSettings.log.access;
  1589. },
  1590. set: function(newValue) {
  1591. newTemplateSettings = this.templateSettings;
  1592. newTemplateSettings.log.access = newValue;
  1593. this.templateSettings = newTemplateSettings;
  1594. }
  1595. },
  1596. errorLog: {
  1597. get: function() {
  1598. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.error) return "";
  1599. return this.templateSettings.log.error;
  1600. },
  1601. set: function(newValue) {
  1602. newTemplateSettings = this.templateSettings;
  1603. newTemplateSettings.log.error = newValue;
  1604. this.templateSettings = newTemplateSettings;
  1605. }
  1606. },
  1607. dnslog: {
  1608. get: function() {
  1609. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.dnsLog)
  1610. return false;
  1611. return this.templateSettings.log.dnsLog;
  1612. },
  1613. set: function(newValue) {
  1614. newTemplateSettings = this.templateSettings;
  1615. newTemplateSettings.log.dnsLog = newValue;
  1616. this.templateSettings = newTemplateSettings;
  1617. }
  1618. },
  1619. statsInboundUplink: {
  1620. get: function() {
  1621. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1622. .system.statsInboundUplink) return false;
  1623. return this.templateSettings.policy.system.statsInboundUplink;
  1624. },
  1625. set: function(newValue) {
  1626. newTemplateSettings = this.templateSettings;
  1627. newTemplateSettings.policy.system.statsInboundUplink = newValue;
  1628. this.templateSettings = newTemplateSettings;
  1629. }
  1630. },
  1631. statsInboundDownlink: {
  1632. get: function() {
  1633. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1634. .system.statsInboundDownlink) return false;
  1635. return this.templateSettings.policy.system.statsInboundDownlink;
  1636. },
  1637. set: function(newValue) {
  1638. newTemplateSettings = this.templateSettings;
  1639. newTemplateSettings.policy.system.statsInboundDownlink = newValue;
  1640. this.templateSettings = newTemplateSettings;
  1641. }
  1642. },
  1643. statsOutboundUplink: {
  1644. get: function() {
  1645. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1646. .system.statsOutboundUplink) return false;
  1647. return this.templateSettings.policy.system.statsOutboundUplink;
  1648. },
  1649. set: function(newValue) {
  1650. newTemplateSettings = this.templateSettings;
  1651. newTemplateSettings.policy.system.statsOutboundUplink = newValue;
  1652. this.templateSettings = newTemplateSettings;
  1653. }
  1654. },
  1655. statsOutboundDownlink: {
  1656. get: function() {
  1657. if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy
  1658. .system.statsOutboundDownlink) return false;
  1659. return this.templateSettings.policy.system.statsOutboundDownlink;
  1660. },
  1661. set: function(newValue) {
  1662. newTemplateSettings = this.templateSettings;
  1663. newTemplateSettings.policy.system.statsOutboundDownlink = newValue;
  1664. this.templateSettings = newTemplateSettings;
  1665. }
  1666. },
  1667. maskAddressLog: {
  1668. get: function() {
  1669. if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress)
  1670. return "";
  1671. return this.templateSettings.log.maskAddress;
  1672. },
  1673. set: function(newValue) {
  1674. newTemplateSettings = this.templateSettings;
  1675. newTemplateSettings.log.maskAddress = newValue;
  1676. this.templateSettings = newTemplateSettings;
  1677. }
  1678. },
  1679. blockedIPs: {
  1680. get: function() {
  1681. return this.templateRuleGetter({
  1682. outboundTag: "blocked",
  1683. property: "ip"
  1684. });
  1685. },
  1686. set: function(newValue) {
  1687. this.templateRuleSetter({
  1688. outboundTag: "blocked",
  1689. property: "ip",
  1690. data: newValue
  1691. });
  1692. }
  1693. },
  1694. blockedDomains: {
  1695. get: function() {
  1696. return this.templateRuleGetter({
  1697. outboundTag: "blocked",
  1698. property: "domain"
  1699. });
  1700. },
  1701. set: function(newValue) {
  1702. this.templateRuleSetter({
  1703. outboundTag: "blocked",
  1704. property: "domain",
  1705. data: newValue
  1706. });
  1707. }
  1708. },
  1709. blockedProtocols: {
  1710. get: function() {
  1711. return this.templateRuleGetter({
  1712. outboundTag: "blocked",
  1713. property: "protocol"
  1714. });
  1715. },
  1716. set: function(newValue) {
  1717. this.templateRuleSetter({
  1718. outboundTag: "blocked",
  1719. property: "protocol",
  1720. data: newValue
  1721. });
  1722. }
  1723. },
  1724. directIPs: {
  1725. get: function() {
  1726. return this.templateRuleGetter({
  1727. outboundTag: "direct",
  1728. property: "ip"
  1729. });
  1730. },
  1731. set: function(newValue) {
  1732. this.templateRuleSetter({
  1733. outboundTag: "direct",
  1734. property: "ip",
  1735. data: newValue
  1736. });
  1737. this.syncRulesWithOutbound("direct", this.directSettings);
  1738. }
  1739. },
  1740. directDomains: {
  1741. get: function() {
  1742. return this.templateRuleGetter({
  1743. outboundTag: "direct",
  1744. property: "domain"
  1745. });
  1746. },
  1747. set: function(newValue) {
  1748. this.templateRuleSetter({
  1749. outboundTag: "direct",
  1750. property: "domain",
  1751. data: newValue
  1752. });
  1753. this.syncRulesWithOutbound("direct", this.directSettings);
  1754. }
  1755. },
  1756. ipv4Domains: {
  1757. get: function() {
  1758. return this.templateRuleGetter({
  1759. outboundTag: "IPv4",
  1760. property: "domain"
  1761. });
  1762. },
  1763. set: function(newValue) {
  1764. this.templateRuleSetter({
  1765. outboundTag: "IPv4",
  1766. property: "domain",
  1767. data: newValue
  1768. });
  1769. this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
  1770. }
  1771. },
  1772. warpDomains: {
  1773. get: function() {
  1774. return this.templateRuleGetter({
  1775. outboundTag: "warp",
  1776. property: "domain"
  1777. });
  1778. },
  1779. set: function(newValue) {
  1780. this.templateRuleSetter({
  1781. outboundTag: "warp",
  1782. property: "domain",
  1783. data: newValue
  1784. });
  1785. }
  1786. },
  1787. nordTag: {
  1788. get: function() {
  1789. return this.templateSettings ? (this.templateSettings.outbounds.find((o) => o.tag.startsWith(
  1790. "nord-")) || {
  1791. tag: "nord"
  1792. }).tag : "nord";
  1793. }
  1794. },
  1795. nordDomains: {
  1796. get: function() {
  1797. return this.templateRuleGetter({
  1798. outboundTag: this.nordTag,
  1799. property: "domain"
  1800. });
  1801. },
  1802. set: function(newValue) {
  1803. this.templateRuleSetter({
  1804. outboundTag: this.nordTag,
  1805. property: "domain",
  1806. data: newValue
  1807. });
  1808. }
  1809. },
  1810. torrentSettings: {
  1811. get: function() {
  1812. return ArrayUtils.doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
  1813. },
  1814. set: function(newValue) {
  1815. if (newValue) {
  1816. this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
  1817. } else {
  1818. this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent
  1819. .includes(data));
  1820. }
  1821. },
  1822. },
  1823. WarpExist: {
  1824. get: function() {
  1825. return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag == "warp") >= 0 :
  1826. false;
  1827. },
  1828. },
  1829. NordExist: {
  1830. get: function() {
  1831. return this.templateSettings ? this.templateSettings.outbounds.findIndex((o) => o.tag.startsWith(
  1832. "nord-")) >= 0 : false;
  1833. },
  1834. },
  1835. enableDNS: {
  1836. get: function() {
  1837. return this.templateSettings ? this.templateSettings.dns != null : false;
  1838. },
  1839. set: function(newValue) {
  1840. newTemplateSettings = this.templateSettings;
  1841. if (newValue) {
  1842. newTemplateSettings.dns = {
  1843. servers: [],
  1844. queryStrategy: "UseIP",
  1845. tag: "dns_inbound",
  1846. enableParallelQuery: false
  1847. };
  1848. newTemplateSettings.fakedns = null;
  1849. } else {
  1850. delete newTemplateSettings.dns;
  1851. delete newTemplateSettings.fakedns;
  1852. }
  1853. this.templateSettings = newTemplateSettings;
  1854. }
  1855. },
  1856. dnsTag: {
  1857. get: function() {
  1858. return this.enableDNS ? this.templateSettings.dns.tag : "";
  1859. },
  1860. set: function(newValue) {
  1861. newTemplateSettings = this.templateSettings;
  1862. newTemplateSettings.dns.tag = newValue;
  1863. this.templateSettings = newTemplateSettings;
  1864. }
  1865. },
  1866. dnsClientIp: {
  1867. get: function() {
  1868. return this.enableDNS ? this.templateSettings.dns.clientIp : null;
  1869. },
  1870. set: function(newValue) {
  1871. newTemplateSettings = this.templateSettings;
  1872. if (newValue) {
  1873. newTemplateSettings.dns.clientIp = newValue;
  1874. } else {
  1875. delete newTemplateSettings.dns.clientIp;
  1876. }
  1877. this.templateSettings = newTemplateSettings;
  1878. }
  1879. },
  1880. dnsDisableCache: {
  1881. get: function() {
  1882. return this.enableDNS ? this.templateSettings.dns.disableCache : false;
  1883. },
  1884. set: function(newValue) {
  1885. newTemplateSettings = this.templateSettings;
  1886. if (newValue) {
  1887. newTemplateSettings.dns.disableCache = newValue;
  1888. } else {
  1889. delete newTemplateSettings.dns.disableCache
  1890. }
  1891. this.templateSettings = newTemplateSettings;
  1892. }
  1893. },
  1894. dnsDisableFallback: {
  1895. get: function() {
  1896. return this.enableDNS ? this.templateSettings.dns.disableFallback : false;
  1897. },
  1898. set: function(newValue) {
  1899. newTemplateSettings = this.templateSettings;
  1900. if (newValue) {
  1901. newTemplateSettings.dns.disableFallback = newValue;
  1902. } else {
  1903. delete newTemplateSettings.dns.disableFallback
  1904. }
  1905. this.templateSettings = newTemplateSettings;
  1906. }
  1907. },
  1908. dnsDisableFallbackIfMatch: {
  1909. get: function() {
  1910. return this.enableDNS ? this.templateSettings.dns.disableFallbackIfMatch : false;
  1911. },
  1912. set: function(newValue) {
  1913. newTemplateSettings = this.templateSettings;
  1914. if (newValue) {
  1915. newTemplateSettings.dns.disableFallbackIfMatch = newValue;
  1916. } else {
  1917. delete newTemplateSettings.dns.disableFallbackIfMatch
  1918. }
  1919. this.templateSettings = newTemplateSettings;
  1920. }
  1921. },
  1922. dnsEnableParallelQuery: {
  1923. get: function() {
  1924. return this.enableDNS ? (this.templateSettings.dns.enableParallelQuery || false) : false;
  1925. },
  1926. set: function(newValue) {
  1927. newTemplateSettings = this.templateSettings;
  1928. if (newValue) {
  1929. newTemplateSettings.dns.enableParallelQuery = newValue;
  1930. } else {
  1931. delete newTemplateSettings.dns.enableParallelQuery
  1932. }
  1933. this.templateSettings = newTemplateSettings;
  1934. }
  1935. },
  1936. dnsUseSystemHosts: {
  1937. get: function() {
  1938. return this.enableDNS ? this.templateSettings.dns.useSystemHosts : false;
  1939. },
  1940. set: function(newValue) {
  1941. newTemplateSettings = this.templateSettings;
  1942. if (newValue) {
  1943. newTemplateSettings.dns.useSystemHosts = newValue;
  1944. } else {
  1945. delete newTemplateSettings.dns.useSystemHosts
  1946. }
  1947. this.templateSettings = newTemplateSettings;
  1948. }
  1949. },
  1950. dnsStrategy: {
  1951. get: function() {
  1952. return this.enableDNS ? this.templateSettings.dns.queryStrategy : null;
  1953. },
  1954. set: function(newValue) {
  1955. newTemplateSettings = this.templateSettings;
  1956. newTemplateSettings.dns.queryStrategy = newValue;
  1957. this.templateSettings = newTemplateSettings;
  1958. }
  1959. },
  1960. dnsServers: {
  1961. get: function() {
  1962. return this.enableDNS ? this.templateSettings.dns.servers : [];
  1963. },
  1964. set: function(newValue) {
  1965. newTemplateSettings = this.templateSettings;
  1966. newTemplateSettings.dns.servers = newValue;
  1967. this.templateSettings = newTemplateSettings;
  1968. }
  1969. },
  1970. fakeDns: {
  1971. get: function() {
  1972. return this.templateSettings && this.templateSettings.fakedns ? this.templateSettings.fakedns : [];
  1973. },
  1974. set: function(newValue) {
  1975. newTemplateSettings = this.templateSettings;
  1976. if (this.enableDNS) {
  1977. newTemplateSettings.fakedns = newValue.length > 0 ? newValue : null;
  1978. } else {
  1979. delete newTemplateSettings.fakedns;
  1980. }
  1981. this.templateSettings = newTemplateSettings;
  1982. }
  1983. }
  1984. },
  1985. });
  1986. </script>
  1987. <style>
  1988. /* ───────── Modern outbounds table ─────────
  1989. Visual goals:
  1990. • flat surface, no inner cell borders, only subtle row dividers
  1991. • rounded pill badges for protocol / tag / addresses
  1992. • dual-arrow traffic widget that aligns across rows
  1993. • consistent hover/loading/result states
  1994. Scoped under .xray-page .outbounds-modern so it doesn't bleed into other tables. */
  1995. .xray-page .outbounds-modern { width: 100%; }
  1996. .xray-page .outbounds-toolbar-right { text-align: right; }
  1997. /* Table chrome */
  1998. .xray-page .outbounds-table .ant-table {
  1999. background: transparent;
  2000. border-radius: 14px;
  2001. overflow: hidden;
  2002. }
  2003. .xray-page .outbounds-table .ant-table-thead > tr > th {
  2004. background: rgba(255, 255, 255, 0.025);
  2005. color: rgba(255, 255, 255, 0.55);
  2006. font-weight: 500;
  2007. font-size: 12px;
  2008. letter-spacing: 0.04em;
  2009. text-transform: uppercase;
  2010. white-space: nowrap;
  2011. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  2012. padding: 14px 18px;
  2013. }
  2014. .light .xray-page .outbounds-table .ant-table-thead > tr > th {
  2015. background: rgba(0, 0, 0, 0.02);
  2016. color: rgba(0, 0, 0, 0.55);
  2017. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  2018. }
  2019. .xray-page .outbounds-table .ant-table-tbody > tr > td {
  2020. border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  2021. padding: 16px 18px;
  2022. transition: background-color 0.15s ease;
  2023. vertical-align: middle;
  2024. }
  2025. /* Force every cell to honour its column width — long content (especially
  2026. long tags) must clip via cell-level ellipsis instead of pushing the row
  2027. taller. */
  2028. .xray-page .outbounds-table .ant-table-tbody > tr > td,
  2029. .xray-page .outbounds-table .ant-table-thead > tr > th {
  2030. overflow: hidden;
  2031. }
  2032. .light .xray-page .outbounds-table .ant-table-tbody > tr > td {
  2033. border-bottom: 1px solid rgba(0, 0, 0, 0.04);
  2034. }
  2035. .xray-page .outbounds-table .ant-table-tbody > tr:last-child > td {
  2036. border-bottom: none;
  2037. }
  2038. .xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
  2039. background: rgba(255, 255, 255, 0.035) !important;
  2040. }
  2041. .light .xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
  2042. background: rgba(0, 0, 0, 0.025) !important;
  2043. }
  2044. /* Index + actions column */
  2045. .xray-page .outbound-action-cell {
  2046. display: inline-flex;
  2047. align-items: center;
  2048. gap: 8px;
  2049. }
  2050. .xray-page .outbound-index {
  2051. font-weight: 600;
  2052. color: rgba(255, 255, 255, 0.7);
  2053. font-variant-numeric: tabular-nums;
  2054. min-width: 18px;
  2055. text-align: end;
  2056. }
  2057. .light .xray-page .outbound-index { color: rgba(0, 0, 0, 0.7); }
  2058. .xray-page .outbound-action-btn {
  2059. border: none;
  2060. background: rgba(255, 255, 255, 0.05);
  2061. color: rgba(255, 255, 255, 0.75);
  2062. transition: background 0.15s ease;
  2063. }
  2064. .xray-page .outbound-action-btn:hover {
  2065. background: rgba(255, 255, 255, 0.12);
  2066. color: #fff;
  2067. }
  2068. .light .xray-page .outbound-action-btn {
  2069. background: rgba(0, 0, 0, 0.05);
  2070. color: rgba(0, 0, 0, 0.75);
  2071. }
  2072. .light .xray-page .outbound-action-btn:hover {
  2073. background: rgba(0, 0, 0, 0.1);
  2074. color: #000;
  2075. }
  2076. /* Identity cell — tag on top, protocol/network/security pills underneath.
  2077. Combining the two columns lets the table fit common viewports without
  2078. a horizontal scrollbar. */
  2079. .xray-page .outbound-identity-cell {
  2080. display: flex;
  2081. flex-direction: column;
  2082. gap: 8px;
  2083. min-width: 0;
  2084. }
  2085. /* Tag — inherits the table's font for visual parity, single line with
  2086. ellipsis on overflow. A long tag (e.g. "vless_jphttp-ksjpnggl") would
  2087. otherwise wrap and inflate the row's height; the inline tooltip surfaces
  2088. the full value on hover. */
  2089. .xray-page .outbound-tag {
  2090. font-size: 13px;
  2091. color: rgba(255, 255, 255, 0.92);
  2092. font-weight: 500;
  2093. display: block;
  2094. max-width: 100%;
  2095. white-space: nowrap;
  2096. overflow: hidden;
  2097. text-overflow: ellipsis;
  2098. }
  2099. .light .xray-page .outbound-tag { color: rgba(0, 0, 0, 0.85); }
  2100. /* Address pills (monospace, monoline) */
  2101. .xray-page .outbound-address-list {
  2102. display: flex;
  2103. flex-wrap: wrap;
  2104. gap: 6px;
  2105. align-items: center;
  2106. }
  2107. .xray-page .outbound-address-pill {
  2108. font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;
  2109. font-size: 12px;
  2110. padding: 3px 10px;
  2111. border-radius: 8px;
  2112. background: rgba(255, 255, 255, 0.045);
  2113. color: rgba(255, 255, 255, 0.78);
  2114. line-height: 1.5;
  2115. border: 1px solid rgba(255, 255, 255, 0.06);
  2116. display: inline-block;
  2117. max-width: 240px;
  2118. overflow: hidden;
  2119. text-overflow: ellipsis;
  2120. white-space: nowrap;
  2121. vertical-align: middle;
  2122. }
  2123. .light .xray-page .outbound-address-pill {
  2124. background: rgba(0, 0, 0, 0.035);
  2125. color: rgba(0, 0, 0, 0.78);
  2126. border: 1px solid rgba(0, 0, 0, 0.06);
  2127. }
  2128. .xray-page .outbound-address-empty {
  2129. color: rgba(255, 255, 255, 0.3);
  2130. font-style: italic;
  2131. }
  2132. /* Protocol/network/tls pills — shared "outbound-pill" with tonal modifiers.
  2133. The pill row stays on a single line; if the column is somehow too narrow
  2134. for all pills it overflows out of view (rare — column width is sized to
  2135. fit the worst case) but never pushes the row taller. */
  2136. .xray-page .outbound-protocol-cell {
  2137. display: flex;
  2138. flex-wrap: nowrap;
  2139. gap: 6px;
  2140. align-items: center;
  2141. overflow: hidden;
  2142. }
  2143. .xray-page .outbound-pill {
  2144. display: inline-flex;
  2145. align-items: center;
  2146. min-height: 22px;
  2147. padding: 2px 9px;
  2148. border-radius: 11px;
  2149. font-size: 12px;
  2150. font-weight: 500;
  2151. line-height: 1.4;
  2152. letter-spacing: 0.01em;
  2153. border: 1px solid transparent;
  2154. white-space: nowrap;
  2155. flex: 0 0 auto;
  2156. }
  2157. /* Outbound pill tones: emerald = protocol, slate = network, violet = security.
  2158. tone-emerald and tone-violet are also consumed by routing.html for the
  2159. outboundTag / balancerTag pills. */
  2160. .xray-page .outbound-pill.tone-emerald { background: rgba(0, 191, 165, 0.14); color: #4dd4be; border-color: rgba(0, 191, 165, 0.28); }
  2161. .xray-page .outbound-pill.tone-slate { background: rgba(160, 174, 192, 0.14); color: #b8c2d0; border-color: rgba(160, 174, 192, 0.26); }
  2162. .xray-page .outbound-pill.tone-violet { background: rgba(155, 89, 219, 0.16); color: #b489e8; border-color: rgba(155, 89, 219, 0.32); }
  2163. /* Traffic — dual arrow widget, fixed columns so all rows align */
  2164. .xray-page .outbound-traffic-cell {
  2165. display: inline-grid;
  2166. grid-template-columns: 1fr auto 1fr;
  2167. align-items: center;
  2168. gap: 10px;
  2169. padding: 5px 12px;
  2170. border-radius: 100px;
  2171. background: rgba(255, 255, 255, 0.04);
  2172. font-variant-numeric: tabular-nums;
  2173. font-size: 13px;
  2174. min-width: 0;
  2175. }
  2176. .light .xray-page .outbound-traffic-cell {
  2177. background: rgba(0, 0, 0, 0.035);
  2178. }
  2179. .xray-page .outbound-traffic-up,
  2180. .xray-page .outbound-traffic-down {
  2181. display: inline-flex;
  2182. align-items: center;
  2183. gap: 6px;
  2184. white-space: nowrap;
  2185. }
  2186. .xray-page .outbound-traffic-up { justify-content: flex-end; color: #4dd4be; }
  2187. .xray-page .outbound-traffic-down { justify-content: flex-start; color: #82a7ee; }
  2188. .xray-page .outbound-traffic-up .anticon,
  2189. .xray-page .outbound-traffic-down .anticon { font-size: 11px; }
  2190. .xray-page .outbound-traffic-sep {
  2191. width: 1px;
  2192. height: 14px;
  2193. background: rgba(255, 255, 255, 0.12);
  2194. border-radius: 1px;
  2195. }
  2196. .light .xray-page .outbound-traffic-sep { background: rgba(0, 0, 0, 0.12); }
  2197. /* Test result pills */
  2198. .xray-page .outbound-result-cell { display: inline-flex; }
  2199. .xray-page .outbound-result-pill {
  2200. display: inline-flex;
  2201. align-items: center;
  2202. gap: 6px;
  2203. padding: 4px 10px;
  2204. border-radius: 100px;
  2205. font-size: 12px;
  2206. font-weight: 500;
  2207. font-variant-numeric: tabular-nums;
  2208. white-space: nowrap;
  2209. border: 1px solid transparent;
  2210. }
  2211. .xray-page .outbound-result-pill .anticon { font-size: 12px; }
  2212. .xray-page .outbound-result-ok {
  2213. background: rgba(0, 191, 165, 0.14);
  2214. color: #4dd4be;
  2215. border-color: rgba(0, 191, 165, 0.28);
  2216. }
  2217. .xray-page .outbound-result-fail {
  2218. background: rgba(255, 77, 79, 0.14);
  2219. color: #ff7a7c;
  2220. border-color: rgba(255, 77, 79, 0.32);
  2221. }
  2222. .xray-page .outbound-result-status { opacity: 0.75; }
  2223. .xray-page .outbound-result-loading,
  2224. .xray-page .outbound-result-idle {
  2225. color: rgba(255, 255, 255, 0.4);
  2226. font-size: 13px;
  2227. }
  2228. .light .xray-page .outbound-result-loading,
  2229. .light .xray-page .outbound-result-idle { color: rgba(0, 0, 0, 0.4); }
  2230. /* Test button — sleek circular with subtle glow */
  2231. .xray-page .outbound-test-btn {
  2232. box-shadow: 0 2px 8px rgba(0, 191, 165, 0.18);
  2233. transition: transform 0.12s ease, box-shadow 0.18s ease;
  2234. }
  2235. .xray-page .outbound-test-btn:hover:not([disabled]) {
  2236. transform: translateY(-1px);
  2237. box-shadow: 0 4px 14px rgba(0, 191, 165, 0.32);
  2238. }
  2239. .xray-page .outbound-test-btn[disabled] {
  2240. box-shadow: none;
  2241. opacity: 0.45;
  2242. }
  2243. /* ───────── Modern routing-rules table ─────────
  2244. Reuses the .outbound-pill tonal primitive (identical visual) so the
  2245. routing tab feels like the same panel as outbounds. Each cell groups
  2246. a routing criterion (Source / Network / Destination / Inbound) and
  2247. shows its values as labelled pills. */
  2248. .xray-page .routing-modern { width: 100%; }
  2249. .xray-page .routing-table .ant-table {
  2250. background: transparent;
  2251. border-radius: 14px;
  2252. overflow: hidden;
  2253. }
  2254. .xray-page .routing-table .ant-table-thead > tr > th {
  2255. background: rgba(255, 255, 255, 0.025);
  2256. color: rgba(255, 255, 255, 0.55);
  2257. font-weight: 500;
  2258. font-size: 12px;
  2259. letter-spacing: 0.04em;
  2260. text-transform: uppercase;
  2261. white-space: nowrap;
  2262. border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  2263. padding: 14px 18px;
  2264. }
  2265. .light .xray-page .routing-table .ant-table-thead > tr > th {
  2266. background: rgba(0, 0, 0, 0.02);
  2267. color: rgba(0, 0, 0, 0.55);
  2268. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  2269. }
  2270. .xray-page .routing-table .ant-table-tbody > tr > td {
  2271. border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  2272. padding: 16px 18px;
  2273. transition: background-color 0.15s ease;
  2274. vertical-align: top;
  2275. }
  2276. .light .xray-page .routing-table .ant-table-tbody > tr > td {
  2277. border-bottom: 1px solid rgba(0, 0, 0, 0.04);
  2278. }
  2279. .xray-page .routing-table .ant-table-tbody > tr:last-child > td {
  2280. border-bottom: none;
  2281. }
  2282. .xray-page .routing-table .ant-table-tbody > tr:hover > td {
  2283. background: rgba(255, 255, 255, 0.035) !important;
  2284. }
  2285. .light .xray-page .routing-table .ant-table-tbody > tr:hover > td {
  2286. background: rgba(0, 0, 0, 0.025) !important;
  2287. }
  2288. /* Sort handle / # / actions */
  2289. .xray-page .routing-action-cell {
  2290. display: inline-flex;
  2291. align-items: center;
  2292. gap: 8px;
  2293. }
  2294. .xray-page .routing-index {
  2295. font-weight: 600;
  2296. color: rgba(255, 255, 255, 0.7);
  2297. font-variant-numeric: tabular-nums;
  2298. min-width: 18px;
  2299. text-align: end;
  2300. }
  2301. .light .xray-page .routing-index { color: rgba(0, 0, 0, 0.7); }
  2302. .xray-page .routing-action-btn {
  2303. border: none;
  2304. background: rgba(255, 255, 255, 0.05);
  2305. color: rgba(255, 255, 255, 0.75);
  2306. transition: background 0.15s ease;
  2307. }
  2308. .xray-page .routing-action-btn:hover {
  2309. background: rgba(255, 255, 255, 0.12);
  2310. color: #fff;
  2311. }
  2312. .light .xray-page .routing-action-btn {
  2313. background: rgba(0, 0, 0, 0.05);
  2314. color: rgba(0, 0, 0, 0.75);
  2315. }
  2316. .light .xray-page .routing-action-btn:hover {
  2317. background: rgba(0, 0, 0, 0.1);
  2318. color: #000;
  2319. }
  2320. /* Plain-text criterion rows — replaces pill primitives in condition
  2321. columns. Each criterion is a row of "label value (+N)" with form-label
  2322. styling on the label. No bg, no border, no color tones — keeps cells
  2323. light and lets the column header carry the type semantic. The cell's
  2324. visual weight is now proportional only to the data length, not to
  2325. decoration. The single colored pill in Outbound/Balancer remains as
  2326. the row's focal point. */
  2327. .xray-page .criterion-flow {
  2328. display: flex;
  2329. flex-direction: column;
  2330. gap: 3px;
  2331. min-width: 0;
  2332. }
  2333. .xray-page .criterion-row {
  2334. display: flex;
  2335. align-items: baseline;
  2336. gap: 8px;
  2337. min-width: 0;
  2338. font-size: 13px;
  2339. line-height: 1.5;
  2340. }
  2341. .xray-page .criterion-label {
  2342. flex: 0 0 auto;
  2343. font-size: 11px;
  2344. color: rgba(255, 255, 255, 0.42);
  2345. font-weight: 400;
  2346. letter-spacing: 0;
  2347. text-transform: none;
  2348. }
  2349. .light .xray-page .criterion-label { color: rgba(0, 0, 0, 0.45); }
  2350. .xray-page .criterion-value {
  2351. flex: 1 1 auto;
  2352. min-width: 0;
  2353. overflow: hidden;
  2354. text-overflow: ellipsis;
  2355. white-space: nowrap;
  2356. color: rgba(255, 255, 255, 0.85);
  2357. }
  2358. .light .xray-page .criterion-value { color: rgba(0, 0, 0, 0.85); }
  2359. .xray-page .criterion-more {
  2360. flex: 0 0 auto;
  2361. font-size: 11px;
  2362. color: rgba(255, 255, 255, 0.42);
  2363. font-weight: 500;
  2364. }
  2365. .light .xray-page .criterion-more { color: rgba(0, 0, 0, 0.45); }
  2366. .xray-page .routing-criteria-empty {
  2367. color: rgba(255, 255, 255, 0.3);
  2368. font-style: italic;
  2369. }
  2370. .light .xray-page .routing-criteria-empty { color: rgba(0, 0, 0, 0.3); }
  2371. /* Target cell (outbound / balancer) — vertically stacked rows of icon + pill */
  2372. .xray-page .routing-target-cell {
  2373. display: flex;
  2374. flex-direction: column;
  2375. gap: 4px;
  2376. min-width: 0;
  2377. }
  2378. .xray-page .routing-target-row {
  2379. display: inline-flex;
  2380. align-items: center;
  2381. gap: 8px;
  2382. }
  2383. .xray-page .routing-target-icon {
  2384. color: rgba(255, 255, 255, 0.45);
  2385. font-size: 13px;
  2386. }
  2387. .light .xray-page .routing-target-icon { color: rgba(0, 0, 0, 0.45); }
  2388. </style>
  2389. {{ template "page/body_end" .}}