diff options
Diffstat (limited to 'web/html')
| -rw-r--r-- | web/html/inbounds.html | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 60de0750..b8485702 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -262,6 +262,10 @@ <a-icon type="usergroup-add"></a-icon> {{ i18n "pages.client.bulk"}} </a-menu-item> + <a-menu-item key="copyClients"> + <a-icon type="copy"></a-icon> + {{ i18n "pages.client.copyFromInbound"}} + </a-menu-item> <a-menu-item key="resetClients"> <a-icon type="file-done"></a-icon> {{ i18n @@ -777,6 +781,218 @@ {{template "modals/inboundInfoModal"}} {{template "modals/clientsModal"}} {{template "modals/clientsBulkModal"}} +<a-modal id="copy-clients-modal" + :title="copyClientsModal.title" + :visible="copyClientsModal.visible" + :confirm-loading="copyClientsModal.confirmLoading" + ok-text='{{ i18n "pages.client.copySelected" }}' + cancel-text='{{ i18n "close" }}' + :class="themeSwitcher.currentTheme" + :closable="true" + :mask-closable="false" + @ok="() => copyClientsModal.ok()" + @cancel="() => copyClientsModal.close()" + width="900px"> + <a-space direction="vertical" style="width: 100%;"> + <div> + <div style="margin-bottom: 6px;">{{ i18n "pages.client.copySource" }}</div> + <a-select v-model="copyClientsModal.sourceInboundId" + style="width: 100%;" + :dropdown-class-name="themeSwitcher.currentTheme" + @change="id => copyClientsModal.onSourceChange(id)"> + <a-select-option v-for="item in copyClientsModal.sources" + :key="item.id" + :value="item.id"> + [[ item.label ]] + </a-select-option> + </a-select> + </div> + <div v-if="copyClientsModal.sourceInboundId"> + <a-space style="margin-bottom: 10px;"> + <a-button size="small" @click="() => copyClientsModal.selectAll()">{{ i18n "pages.client.selectAll" }}</a-button> + <a-button size="small" @click="() => copyClientsModal.clearAll()">{{ i18n "pages.client.clearAll" }}</a-button> + </a-space> + <a-table :columns="copyClientsColumns" + :data-source="copyClientsModal.sourceClients" + :pagination="false" + size="small" + :row-key="item => item.email" + :scroll="{ y: 280 }"> + <template slot="emailCheckbox" slot-scope="text, record"> + <a-checkbox :checked="copyClientsModal.selectedEmails.includes(record.email)" + @change="event => copyClientsModal.toggleEmail(record.email, event.target.checked)"> + [[ record.email ]] + </a-checkbox> + </template> + </a-table> + </div> + <div v-if="copyClientsModal.showFlow"> + <div style="margin-bottom: 6px;">{{ i18n "pages.client.copyFlowLabel" }}</div> + <a-select v-model="copyClientsModal.flow" + style="width: 100%;" + :dropdown-class-name="themeSwitcher.currentTheme" + allow-clear> + <a-select-option value="">{{ i18n "none" }}</a-select-option> + <a-select-option value="xtls-rprx-vision">xtls-rprx-vision</a-select-option> + <a-select-option value="xtls-rprx-vision-udp443">xtls-rprx-vision-udp443</a-select-option> + </a-select> + <div style="margin-top: 4px; font-size: 12px; opacity: 0.7;"> + {{ i18n "pages.client.copyFlowHint" }} + </div> + </div> + <div v-if="copyClientsModal.selectedEmails.length > 0"> + <div style="margin-bottom: 4px;">{{ i18n "pages.client.copyEmailPreview" }}</div> + <div style="max-height: 120px; overflow-y: auto;"> + <a-tag v-for="preview in previewEmails" :key="preview" style="margin-bottom: 4px;"> + [[ preview ]] + </a-tag> + </div> + </div> + </a-space> +</a-modal> +<script> + const copyClientsColumns = [ + { title: '{{ i18n "pages.inbounds.email" }}', width: 300, scopedSlots: { customRender: 'emailCheckbox' } }, + { title: '{{ i18n "pages.inbounds.traffic" }}', width: 160, dataIndex: 'trafficLabel' }, + { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 180, dataIndex: 'expiryLabel' }, + ]; + + const copyClientsModal = { + visible: false, + confirmLoading: false, + title: '', + targetInboundId: 0, + targetInboundRemark: '', + targetProtocol: '', + showFlow: false, + flow: '', + sourceInboundId: undefined, + sources: [], + sourceClients: [], + selectedEmails: [], + show(targetDbInbound) { + if (!targetDbInbound) return; + const sources = app.dbInbounds + .filter(row => row.id !== targetDbInbound.id && typeof row.isMultiUser === 'function' && row.isMultiUser()) + .map(row => { + const clients = app.getInboundClients(row) || []; + return { id: row.id, label: `${row.remark} (${row.protocol}, ${clients.length})` }; + }); + let showFlow = false; + try { + const targetInbound = targetDbInbound.toInbound(); + showFlow = !!(targetInbound && typeof targetInbound.canEnableTlsFlow === 'function' && targetInbound.canEnableTlsFlow()); + } catch (e) { + showFlow = false; + } + copyClientsModal.targetInboundId = targetDbInbound.id; + copyClientsModal.targetInboundRemark = targetDbInbound.remark; + copyClientsModal.targetProtocol = targetDbInbound.protocol; + copyClientsModal.showFlow = showFlow; + copyClientsModal.flow = ''; + copyClientsModal.title = `{{ i18n "pages.client.copyToInbound" }} ${targetDbInbound.remark}`; + copyClientsModal.sources = sources; + copyClientsModal.sourceInboundId = undefined; + copyClientsModal.sourceClients = []; + copyClientsModal.selectedEmails = []; + copyClientsModal.confirmLoading = false; + copyClientsModal.visible = true; + }, + close() { + copyClientsModal.visible = false; + copyClientsModal.confirmLoading = false; + }, + onSourceChange(sourceInboundId) { + copyClientsModal.selectedEmails = []; + const sourceInbound = app.dbInbounds.find(row => row.id === Number(sourceInboundId)); + if (!sourceInbound) { + copyClientsModal.sourceClients = []; + return; + } + const sourceClients = app.getInboundClients(sourceInbound) || []; + copyClientsModal.sourceClients = sourceClients.map(client => { + const stats = app.getClientStats(sourceInbound, client.email); + const used = stats ? ((stats.up || 0) + (stats.down || 0)) : 0; + let expiryLabel = '{{ i18n "unlimited" }}'; + if (client.expiryTime > 0) { + expiryLabel = IntlUtil.formatDate(client.expiryTime); + } else if (client.expiryTime < 0) { + expiryLabel = `${-client.expiryTime / 86400000}d`; + } + return { + email: client.email, + trafficLabel: SizeFormatter.sizeFormat(used), + expiryLabel, + }; + }); + }, + toggleEmail(email, checked) { + const selected = copyClientsModal.selectedEmails.slice(); + if (checked) { + if (!selected.includes(email)) selected.push(email); + } else { + const idx = selected.indexOf(email); + if (idx >= 0) selected.splice(idx, 1); + } + copyClientsModal.selectedEmails = selected; + }, + selectAll() { + copyClientsModal.selectedEmails = copyClientsModal.sourceClients.map(item => item.email); + }, + clearAll() { + copyClientsModal.selectedEmails = []; + }, + async ok() { + if (!copyClientsModal.sourceInboundId) { + app.$message.error('{{ i18n "pages.client.copySelectSourceFirst" }}'); + return; + } + copyClientsModal.confirmLoading = true; + const payload = { + sourceInboundId: copyClientsModal.sourceInboundId, + clientEmails: copyClientsModal.selectedEmails, + }; + if (copyClientsModal.showFlow && copyClientsModal.flow) { + payload.flow = copyClientsModal.flow; + } + try { + const msg = await HttpUtil.post(`/panel/api/inbounds/${copyClientsModal.targetInboundId}/copyClients`, payload); + if (!msg || !msg.success) return; + const obj = msg.obj || {}; + const addedCount = (obj.added || []).length; + const errorList = obj.errors || []; + if (addedCount > 0) { + app.$message.success(`{{ i18n "pages.client.copyResultSuccess" }}: ${addedCount}`); + } else { + app.$message.warning('{{ i18n "pages.client.copyResultNone" }}'); + } + if (errorList.length > 0) { + app.$message.error(`{{ i18n "pages.client.copyResultErrors" }}: ${errorList.join('; ')}`); + } + copyClientsModal.close(); + await app.getDBInbounds(); + } finally { + copyClientsModal.confirmLoading = false; + } + }, + }; + + const copyClientsModalApp = new Vue({ + delimiters: ['[[', ']]'], + el: '#copy-clients-modal', + data: { + copyClientsModal, + copyClientsColumns, + themeSwitcher, + }, + computed: { + previewEmails() { + if (!this.copyClientsModal.targetInboundId) return []; + return this.copyClientsModal.selectedEmails.map(email => `${email}_${this.copyClientsModal.targetInboundId}`); + }, + }, + }); +</script> <script> const columns = [{ title: "ID", @@ -1135,6 +1351,9 @@ case "addBulkClient": this.openAddBulkClient(dbInbound.id) break; + case "copyClients": + copyClientsModal.show(dbInbound); + break; case "export": this.inboundLinks(dbInbound.id); break; |
