1
0

qrcode_modal.html 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. {{define "modals/qrcodeModal"}}
  2. <a-modal id="qrcode-modal" v-model="qrModal.visible" :closable="true" :class="themeSwitcher.currentTheme"
  3. width="fit-content" :dialog-style="isMobile ? { top: '18px' } : {}" :footer="null">
  4. <template #title>
  5. <a-space direction="horizontal">
  6. <span>[[ qrModal.title ]]</span>
  7. <a-popover :overlay-class-name="themeSwitcher.currentTheme" trigger="click" placement="bottom">
  8. <template slot="content">
  9. <a-space direction="vertical">
  10. <template v-for="(row, index) in qrModal.qrcodes">
  11. <b>[[ row.remark ]]</b>
  12. <a-space direction="horizontal">
  13. <a-switch size="small" :checked="row.useIPv4" @click="toggleIPv4(index)"></a-switch>
  14. <span>{{ i18n "useIPv4ForHost" }}</span>
  15. </a-space>
  16. </template>
  17. </a-space>
  18. </template>
  19. <a-icon type="setting"></a-icon>
  20. </a-popover>
  21. </a-space>
  22. </template>
  23. <tr-qr-modal class="qr-modal">
  24. <template v-if="app.subSettings?.enable && qrModal.subId">
  25. <tr-qr-box class="qr-box">
  26. <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
  27. <tr-qr-bg class="qr-bg-sub">
  28. <tr-qr-bg-inner class="qr-bg-sub-inner">
  29. <canvas @click="copy(genSubLink(qrModal.client.subId))" id="qrCode-sub" class="qr-cv"></canvas>
  30. </tr-qr-bg-inner>
  31. </tr-qr-bg>
  32. </tr-qr-box>
  33. <tr-qr-box class="qr-box" v-if="app.subSettings.subJsonEnable">
  34. <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
  35. <tr-qr-bg class="qr-bg-sub">
  36. <tr-qr-bg-inner class="qr-bg-sub-inner">
  37. <canvas @click="copy(genSubJsonLink(qrModal.client.subId))" id="qrCode-subJson" class="qr-cv"></canvas>
  38. </tr-qr-bg-inner>
  39. </tr-qr-bg>
  40. </tr-qr-box>
  41. </template>
  42. <template v-for="(row, index) in qrModal.qrcodes">
  43. <tr-qr-box class="qr-box">
  44. <a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
  45. <tr-qr-bg class="qr-bg">
  46. <canvas @click="copy(row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
  47. </tr-qr-bg>
  48. </tr-qr-box>
  49. </template>
  50. </tr-qr-modal>
  51. </a-modal>
  52. <style>
  53. .ant-table:not(.ant-table-expanded-row .ant-table) {
  54. outline: 1px solid #f0f0f0;
  55. outline-offset: -1px;
  56. border-radius: 1rem;
  57. overflow-x: hidden;
  58. }
  59. /* QR code transition effects */
  60. .qr-cv {
  61. transition: all 0.3s ease-in-out;
  62. }
  63. .qr-transition-enter-active,
  64. .qr-transition-leave-active {
  65. transition: opacity 0.3s, transform 0.3s;
  66. }
  67. .qr-transition-enter,
  68. .qr-transition-leave-to {
  69. opacity: 0;
  70. transform: scale(0.9);
  71. }
  72. .qr-transition-enter-to,
  73. .qr-transition-leave {
  74. opacity: 1;
  75. transform: scale(1);
  76. }
  77. .qr-flash {
  78. animation: qr-flash-animation 0.6s;
  79. }
  80. @keyframes qr-flash-animation {
  81. 0% {
  82. opacity: 1;
  83. transform: scale(1);
  84. }
  85. 50% {
  86. opacity: 0.5;
  87. transform: scale(0.95);
  88. }
  89. 100% {
  90. opacity: 1;
  91. transform: scale(1);
  92. }
  93. }
  94. </style>
  95. <script>
  96. const qrModal = {
  97. title: '',
  98. dbInbound: new DBInbound(),
  99. client: null,
  100. qrcodes: [],
  101. visible: false,
  102. subId: '',
  103. show: function(title = '', dbInbound, client) {
  104. this.title = title;
  105. this.dbInbound = dbInbound;
  106. this.inbound = dbInbound.toInbound();
  107. this.client = client;
  108. this.subId = '';
  109. this.qrcodes = [];
  110. // Reset the status fetched flag when showing the modal
  111. if (qrModalApp) qrModalApp.statusFetched = false;
  112. if (this.inbound.protocol == Protocols.WIREGUARD) {
  113. this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l, index) => {
  114. this.qrcodes.push({
  115. remark: "Peer " + (index + 1),
  116. link: l,
  117. useIPv4: false,
  118. originalLink: l
  119. });
  120. });
  121. } else {
  122. this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
  123. this.qrcodes.push({
  124. remark: l.remark,
  125. link: l.link,
  126. useIPv4: false,
  127. originalLink: l.link
  128. });
  129. });
  130. }
  131. this.visible = true;
  132. },
  133. close: function() {
  134. this.visible = false;
  135. },
  136. };
  137. const qrModalApp = new Vue({
  138. delimiters: ['[[', ']]'],
  139. el: '#qrcode-modal',
  140. mixins: [MediaQueryMixin],
  141. data: {
  142. qrModal: qrModal,
  143. serverStatus: null,
  144. statusFetched: false,
  145. },
  146. methods: {
  147. async getStatus() {
  148. try {
  149. const msg = await HttpUtil.get('/panel/api/server/status');
  150. if (msg.success) {
  151. this.serverStatus = msg.obj;
  152. }
  153. } catch (e) {
  154. console.error("Failed to get status:", e);
  155. }
  156. },
  157. toggleIPv4(index) {
  158. const row = qrModal.qrcodes[index];
  159. row.useIPv4 = !row.useIPv4;
  160. this.updateLink(index);
  161. },
  162. updateLink(index) {
  163. const row = qrModal.qrcodes[index];
  164. if (!this.serverStatus || !this.serverStatus.publicIP) {
  165. return;
  166. }
  167. if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
  168. // Replace the hostname or IP in the link with the IPv4 address
  169. const originalLink = row.originalLink;
  170. const url = new URL(originalLink);
  171. const ipv4 = this.serverStatus.publicIP.ipv4;
  172. if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
  173. // Special handling for WireGuard config
  174. const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
  175. const match = originalLink.match(endpointRegex);
  176. if (match) {
  177. row.link = originalLink.replace(
  178. `Endpoint = ${match[1]}:${match[2]}`,
  179. `Endpoint = ${ipv4}:${match[2]}`
  180. );
  181. }
  182. } else {
  183. // For other protocols using URL format
  184. url.hostname = ipv4;
  185. row.link = url.toString();
  186. }
  187. } else {
  188. // Restore original link
  189. row.link = row.originalLink;
  190. }
  191. // Update QR code with transition effect
  192. const canvasElement = document.querySelector('#qrCode-' + index);
  193. if (canvasElement) {
  194. // Add flash animation class
  195. canvasElement.classList.add('qr-flash');
  196. // Remove the class after animation completes
  197. setTimeout(() => {
  198. canvasElement.classList.remove('qr-flash');
  199. }, 600);
  200. }
  201. this.setQrCode("qrCode-" + index, row.link);
  202. },
  203. copy(content) {
  204. ClipboardManager
  205. .copyText(content)
  206. .then(() => {
  207. app.$message.success('{{ i18n "copied" }}')
  208. })
  209. },
  210. setQrCode(elementId, content) {
  211. new QRious({
  212. element: document.querySelector('#' + elementId),
  213. size: 400,
  214. value: content,
  215. background: 'white',
  216. backgroundAlpha: 0,
  217. foreground: 'black',
  218. padding: 2,
  219. level: 'L'
  220. });
  221. },
  222. genSubLink(subID) {
  223. return app.subSettings.subURI + subID;
  224. },
  225. genSubJsonLink(subID) {
  226. return app.subSettings.subJsonURI + subID;
  227. },
  228. revertOverflow() {
  229. const elements = document.querySelectorAll(".qr-tag");
  230. elements.forEach((element) => {
  231. element.classList.remove("tr-marquee");
  232. element.children[0].style.animation = '';
  233. while (element.children.length > 1) {
  234. element.removeChild(element.lastChild);
  235. }
  236. });
  237. }
  238. },
  239. updated() {
  240. if (this.qrModal.visible) {
  241. fixOverflow();
  242. if (!this.statusFetched) {
  243. this.getStatus();
  244. this.statusFetched = true;
  245. }
  246. } else {
  247. this.revertOverflow();
  248. // Reset the flag when modal is closed so it will fetch again next time
  249. this.statusFetched = false;
  250. }
  251. if (qrModal.client && qrModal.client.subId) {
  252. qrModal.subId = qrModal.client.subId;
  253. this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
  254. if (app.subSettings.subJsonEnable) {
  255. this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
  256. }
  257. }
  258. qrModal.qrcodes.forEach((element, index) => {
  259. this.setQrCode("qrCode-" + index, element.link);
  260. // Update links based on current toggle state
  261. if (element.useIPv4 && this.serverStatus && this.serverStatus.publicIP) {
  262. this.updateLink(index);
  263. }
  264. });
  265. }
  266. });
  267. function fixOverflow() {
  268. const elements = document.querySelectorAll(".qr-tag");
  269. elements.forEach((element) => {
  270. function isElementOverflowing(element) {
  271. const overflowX = element.offsetWidth < element.scrollWidth,
  272. overflowY = element.offsetHeight < element.scrollHeight;
  273. return overflowX || overflowY;
  274. }
  275. function wrapContentsInMarquee(element) {
  276. element.classList.add("tr-marquee");
  277. element.children[0].style.animation = `move-ltr ${(element.children[0].clientWidth / element.clientWidth) * 5
  278. }s ease-in-out infinite`;
  279. const marqueeText = element.children[0];
  280. if (element.children.length < 2) {
  281. for (let i = 0; i < 1; i++) {
  282. const marqueeText = element.children[0].cloneNode(true);
  283. element.children[0].after(marqueeText);
  284. }
  285. }
  286. }
  287. if (isElementOverflowing(element)) {
  288. wrapContentsInMarquee(element);
  289. }
  290. });
  291. }
  292. </script>
  293. {{end}}