|
|
@@ -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;
|