ClientBulkModal.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <script setup>
  2. import { computed, reactive, ref, watch } from 'vue';
  3. import { useI18n } from 'vue-i18n';
  4. import dayjs from 'dayjs';
  5. import { SyncOutlined } from '@ant-design/icons-vue';
  6. import { HttpUtil, RandomUtil, SizeFormatter } from '@/utils';
  7. const { t } = useI18n();
  8. import {
  9. Inbound,
  10. Protocols,
  11. USERS_SECURITY,
  12. TLS_FLOW_CONTROL,
  13. } from '@/models/inbound.js';
  14. import DateTimePicker from '@/components/DateTimePicker.vue';
  15. // Bulk-add up to 500 clients in one go. The legacy panel offers five
  16. // generation modes — this component preserves them all:
  17. // 0: Random — N fully-random emails (no prefix)
  18. // 1: Random+Prefix — N random emails preceded by `prefix`
  19. // 2: Random+Prefix+Num — emails like `<rand><prefix><num>` for num in [first..last]
  20. // 3: Random+Prefix+Num+Postfix — same + appended postfix
  21. // 4: Prefix+Num+Postfix — no random part, just `<prefix><num><postfix>`
  22. const props = defineProps({
  23. open: { type: Boolean, default: false },
  24. dbInbound: { type: Object, default: null },
  25. subEnable: { type: Boolean, default: false },
  26. tgBotEnable: { type: Boolean, default: false },
  27. ipLimitEnable: { type: Boolean, default: false },
  28. });
  29. const emit = defineEmits(['update:open', 'saved']);
  30. const SECURITY_OPTIONS = Object.values(USERS_SECURITY);
  31. const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
  32. // === Reactive form state ===========================================
  33. // Cloned inbound (so canEnableTlsFlow() works).
  34. const inbound = ref(null);
  35. const saving = ref(false);
  36. const delayedStart = ref(false);
  37. const form = reactive({
  38. emailMethod: 0,
  39. firstNum: 1,
  40. lastNum: 1,
  41. emailPrefix: '',
  42. emailPostfix: '',
  43. quantity: 1,
  44. security: USERS_SECURITY.AUTO,
  45. flow: '',
  46. subId: '',
  47. tgId: 0,
  48. limitIp: 0,
  49. totalGB: 0,
  50. expiryTime: 0, // ms epoch; negative => delayed start days
  51. reset: 0,
  52. });
  53. const expiryDate = computed({
  54. get: () => (form.expiryTime > 0 ? dayjs(form.expiryTime) : null),
  55. set: (next) => { form.expiryTime = next ? next.valueOf() : 0; },
  56. });
  57. const delayedExpireDays = computed({
  58. get: () => (form.expiryTime < 0 ? form.expiryTime / -86400000 : 0),
  59. set: (days) => { form.expiryTime = -86400000 * (days || 0); },
  60. });
  61. watch(() => props.open, (next) => {
  62. if (!next) return;
  63. if (!props.dbInbound) return;
  64. inbound.value = Inbound.fromJson(props.dbInbound.toInbound().toJson());
  65. // Reset all form fields on every open — bulk add is intentionally
  66. // stateless between sessions (legacy resets on .show()).
  67. form.emailMethod = 0;
  68. form.firstNum = 1;
  69. form.lastNum = 1;
  70. form.emailPrefix = '';
  71. form.emailPostfix = '';
  72. form.quantity = 1;
  73. form.security = USERS_SECURITY.AUTO;
  74. form.flow = '';
  75. form.subId = '';
  76. form.tgId = 0;
  77. form.limitIp = 0;
  78. form.totalGB = 0;
  79. form.expiryTime = 0;
  80. form.reset = 0;
  81. delayedStart.value = false;
  82. });
  83. function close() {
  84. emit('update:open', false);
  85. }
  86. function makeNewClient(parsed) {
  87. switch (parsed.protocol) {
  88. case Protocols.VMESS: return new Inbound.VmessSettings.VMESS();
  89. case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
  90. case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
  91. case Protocols.SHADOWSOCKS: {
  92. const method = parsed.settings.shadowsockses[0]?.method || parsed.settings.method;
  93. return new Inbound.ShadowsocksSettings.Shadowsocks(method);
  94. }
  95. case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria();
  96. default: return null;
  97. }
  98. }
  99. function buildClients() {
  100. if (!inbound.value) return [];
  101. const out = [];
  102. const method = form.emailMethod;
  103. let start;
  104. let end;
  105. if (method > 1) {
  106. start = form.firstNum;
  107. end = form.lastNum + 1;
  108. } else {
  109. start = 0;
  110. end = form.quantity;
  111. }
  112. const prefix = method > 0 && form.emailPrefix.length > 0 ? form.emailPrefix : '';
  113. const useNum = method > 1;
  114. const postfix = method > 2 && form.emailPostfix.length > 0 ? form.emailPostfix : '';
  115. for (let i = start; i < end; i++) {
  116. const c = makeNewClient(inbound.value);
  117. if (!c) continue;
  118. if (method === 4) c.email = '';
  119. c.email += useNum ? prefix + String(i) + postfix : prefix + postfix;
  120. if (form.subId.length > 0) c.subId = form.subId;
  121. c.tgId = form.tgId;
  122. c.security = form.security;
  123. c.limitIp = form.limitIp;
  124. // Use the clien's totalGB setter (ms epoch and bytes already handled
  125. // identically for bulk and single client paths).
  126. c.totalGB = Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB);
  127. c.expiryTime = form.expiryTime;
  128. if (inbound.value.canEnableTlsFlow()) c.flow = form.flow;
  129. c.reset = form.reset;
  130. out.push(c);
  131. }
  132. return out;
  133. }
  134. async function submit() {
  135. const clients = buildClients();
  136. if (clients.length === 0) return;
  137. saving.value = true;
  138. try {
  139. const payload = {
  140. id: props.dbInbound.id,
  141. // Clients all serialize via toString() — same shape the single-
  142. // client modal posts. Joining with `,` lets the Go side parse the
  143. // outer array directly.
  144. settings: `{"clients": [${clients.map((c) => c.toString()).join(',')}]}`,
  145. };
  146. const msg = await HttpUtil.post('/panel/api/inbounds/addClient', payload);
  147. if (msg?.success) {
  148. emit('saved');
  149. close();
  150. }
  151. } finally {
  152. saving.value = false;
  153. }
  154. }
  155. </script>
  156. <template>
  157. <a-modal :open="open" :title="t('pages.client.bulk')" :ok-text="t('create')" :cancel-text="t('close')"
  158. :confirm-loading="saving" :mask-closable="false" @ok="submit" @cancel="close">
  159. <a-form v-if="inbound" :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
  160. <a-form-item :label="t('pages.client.method')">
  161. <a-select v-model:value="form.emailMethod">
  162. <a-select-option :value="0">Random</a-select-option>
  163. <a-select-option :value="1">Random + Prefix</a-select-option>
  164. <a-select-option :value="2">Random + Prefix + Num</a-select-option>
  165. <a-select-option :value="3">Random + Prefix + Num + Postfix</a-select-option>
  166. <a-select-option :value="4">Prefix + Num + Postfix</a-select-option>
  167. </a-select>
  168. </a-form-item>
  169. <a-form-item v-if="form.emailMethod > 1" :label="t('pages.client.first')">
  170. <a-input-number v-model:value="form.firstNum" :min="1" />
  171. </a-form-item>
  172. <a-form-item v-if="form.emailMethod > 1" :label="t('pages.client.last')">
  173. <a-input-number v-model:value="form.lastNum" :min="form.firstNum" />
  174. </a-form-item>
  175. <a-form-item v-if="form.emailMethod > 0" :label="t('pages.client.prefix')">
  176. <a-input v-model:value="form.emailPrefix" />
  177. </a-form-item>
  178. <a-form-item v-if="form.emailMethod > 2" :label="t('pages.client.postfix')">
  179. <a-input v-model:value="form.emailPostfix" />
  180. </a-form-item>
  181. <a-form-item v-if="form.emailMethod < 2" :label="t('pages.client.clientCount')">
  182. <a-input-number v-model:value="form.quantity" :min="1" :max="500" />
  183. </a-form-item>
  184. <a-form-item v-if="inbound.protocol === Protocols.VMESS" :label="t('security')">
  185. <a-select v-model:value="form.security">
  186. <a-select-option v-for="key in SECURITY_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
  187. </a-select>
  188. </a-form-item>
  189. <a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow">
  190. <a-select v-model:value="form.flow">
  191. <a-select-option value="">{{ t('none') }}</a-select-option>
  192. <a-select-option v-for="key in FLOW_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
  193. </a-select>
  194. </a-form-item>
  195. <a-form-item v-if="subEnable">
  196. <template #label>
  197. {{ t('subscription.title') }}
  198. <SyncOutlined class="random-icon" @click="form.subId = RandomUtil.randomLowerAndNum(16)" />
  199. </template>
  200. <a-input v-model:value="form.subId" />
  201. </a-form-item>
  202. <a-form-item v-if="tgBotEnable" label="Telegram ID">
  203. <a-input-number v-model:value="form.tgId" :min="0" :style="{ width: '50%' }" />
  204. </a-form-item>
  205. <a-form-item v-if="ipLimitEnable" :label="t('pages.inbounds.IPLimit')">
  206. <a-input-number v-model:value="form.limitIp" :min="0" />
  207. </a-form-item>
  208. <a-form-item>
  209. <template #label>
  210. <a-tooltip :title="t('pages.inbounds.meansNoLimit')">{{ t('pages.inbounds.totalFlow') }}</a-tooltip>
  211. </template>
  212. <a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" />
  213. </a-form-item>
  214. <a-form-item :label="t('pages.client.delayedStart')">
  215. <a-switch v-model:checked="delayedStart" @click="form.expiryTime = 0" />
  216. </a-form-item>
  217. <a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
  218. <a-input-number v-model:value="delayedExpireDays" :min="0" />
  219. </a-form-item>
  220. <a-form-item v-else>
  221. <template #label>
  222. <a-tooltip :title="t('pages.inbounds.leaveBlankToNeverExpire')">{{ t('pages.inbounds.expireDate')
  223. }}</a-tooltip>
  224. </template>
  225. <DateTimePicker v-model:value="expiryDate" />
  226. </a-form-item>
  227. <a-form-item v-if="form.expiryTime !== 0">
  228. <template #label>
  229. <a-tooltip :title="t('pages.client.renewDesc')">{{ t('pages.client.renew') }}</a-tooltip>
  230. </template>
  231. <a-input-number v-model:value="form.reset" :min="0" />
  232. </a-form-item>
  233. </a-form>
  234. </a-modal>
  235. </template>
  236. <style scoped>
  237. .random-icon {
  238. margin-left: 4px;
  239. cursor: pointer;
  240. color: var(--ant-primary-color, #1890ff);
  241. }
  242. </style>