diff --git a/frontend/src/components/PromptModal.vue b/frontend/src/components/PromptModal.vue new file mode 100644 index 00000000..f79a3974 --- /dev/null +++ b/frontend/src/components/PromptModal.vue @@ -0,0 +1,70 @@ + + + diff --git a/frontend/src/components/TextModal.vue b/frontend/src/components/TextModal.vue new file mode 100644 index 00000000..25bd2720 --- /dev/null +++ b/frontend/src/components/TextModal.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue index 6dfd9a5a..f479b36f 100644 --- a/frontend/src/pages/inbounds/InboundsPage.vue +++ b/frontend/src/pages/inbounds/InboundsPage.vue @@ -21,6 +21,8 @@ import ClientFormModal from './ClientFormModal.vue'; import ClientBulkModal from './ClientBulkModal.vue'; import InboundInfoModal from './InboundInfoModal.vue'; import QrCodeModal from './QrCodeModal.vue'; +import TextModal from '@/components/TextModal.vue'; +import PromptModal from '@/components/PromptModal.vue'; import { useInbounds } from './useInbounds.js'; const antdThemeConfig = computed(() => ({ @@ -77,6 +79,127 @@ const infoClientIndex = ref(0); const qrOpen = ref(false); const qrDbInbound = ref(null); +// === Shared text + prompt modal state ================================= +const textOpen = ref(false); +const textTitle = ref(''); +const textContent = ref(''); +const textFileName = ref(''); + +const promptOpen = ref(false); +const promptTitle = ref(''); +const promptOkText = ref('OK'); +const promptType = ref('textarea'); +const promptInitial = ref(''); +const promptLoading = ref(false); +let promptHandler = null; + +function openText({ title, content, fileName = '' }) { + textTitle.value = title; + textContent.value = content; + textFileName.value = fileName; + textOpen.value = true; +} + +function openPrompt({ title, okText, type = 'textarea', value = '', confirm }) { + promptTitle.value = title; + promptOkText.value = okText || 'OK'; + promptType.value = type; + promptInitial.value = value; + promptHandler = confirm; + promptOpen.value = true; +} + +async function onPromptConfirm(value) { + if (!promptHandler) { promptOpen.value = false; return; } + promptLoading.value = true; + try { + const ok = await promptHandler(value); + if (ok !== false) promptOpen.value = false; + } finally { + promptLoading.value = false; + } +} + +// === Export helpers — mirror legacy txtModal call sites ============== +function exportInboundLinks(dbInbound) { + const projected = checkFallback(dbInbound); + openText({ + title: 'Export inbound links', + content: projected.genInboundLinks(remarkModel.value), + fileName: projected.remark || 'inbound', + }); +} + +function exportInboundClipboard(dbInbound) { + openText({ + title: 'Inbound JSON', + content: JSON.stringify(dbInbound, null, 2), + }); +} + +function exportInboundSubs(dbInbound) { + const inbound = dbInbound.toInbound(); + const clients = inbound?.clients || []; + const subLinks = []; + for (const c of clients) { + if (c.subId && subSettings.value.subURI) { + subLinks.push(subSettings.value.subURI + c.subId); + } + } + openText({ + title: 'Export subscription links', + content: [...new Set(subLinks)].join('\n'), + fileName: `${dbInbound.remark || 'inbound'}-Subs`, + }); +} + +function exportAllLinks() { + const out = []; + for (const ib of dbInbounds.value) { + out.push(ib.genInboundLinks(remarkModel.value)); + } + openText({ + title: 'Export all inbound links', + content: out.join('\r\n'), + fileName: 'All-Inbounds', + }); +} + +function exportAllSubs() { + const out = []; + for (const ib of dbInbounds.value) { + const inbound = ib.toInbound(); + const clients = inbound?.clients || []; + for (const c of clients) { + if (c.subId && subSettings.value.subURI) { + out.push(subSettings.value.subURI + c.subId); + } + } + } + openText({ + title: 'Export all subscription links', + content: [...new Set(out)].join('\r\n'), + fileName: 'All-Inbounds-Subs', + }); +} + +function importInbound() { + openPrompt({ + title: 'Import inbound', + okText: 'Import', + type: 'textarea', + value: '', + confirm: async (value) => { + const msg = await HttpUtil.post('/panel/api/inbounds/import', { data: value }); + if (msg?.success) { + await refresh(); + return true; + } + return false; + }, + }); +} + // `checkFallback` mirrors the legacy helper: when an inbound listens // on a unix-socket fallback (`@`), point the link generator at // the root inbound that owns the listen address so QRs/links carry @@ -285,6 +408,15 @@ function confirmClone(dbInbound) { function onGeneralAction(key) { switch (key) { + case 'import': + importInbound(); + break; + case 'export': + exportAllLinks(); + break; + case 'subs': + exportAllSubs(); + break; case 'resetInbounds': Modal.confirm({ title: 'Reset all inbound traffic?', @@ -335,6 +467,21 @@ function onRowAction({ key, dbInbound }) { qrDbInbound.value = checkFallback(dbInbound); qrOpen.value = true; break; + case 'export': + exportInboundLinks(dbInbound); + break; + case 'subs': + exportInboundSubs(dbInbound); + break; + case 'clipboard': + exportInboundClipboard(dbInbound); + break; + case 'copyClients': + // Copy-clients-from-inbound is a tiny dedicated modal in legacy + // (lets you tick clients to copy across inbounds). Defer to a + // future commit — surface a friendly message for now. + message.info('Copy clients across inbounds — coming soon'); + break; case 'delete': confirmDelete(dbInbound); break; @@ -501,6 +648,22 @@ function onRowAction({ key, dbInbound }) { :db-inbound="qrDbInbound" :remark-model="remarkModel" /> + + +