fix: copy clients modal not opening

This commit is contained in:
Нестеров Руслан 2026-04-22 16:54:54 +03:00
parent 55d0929cfe
commit eac7296a5d
5 changed files with 61 additions and 8 deletions

View file

@ -58,6 +58,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
type CopyInboundClientsRequest struct {
SourceInboundID int `form:"sourceInboundId" json:"sourceInboundId"`
ClientEmails []string `form:"clientEmails" json:"clientEmails"`
Flow string `form:"flow" json:"flow"`
}
// getInbounds retrieves the list of inbounds for the logged-in user.
@ -285,7 +286,7 @@ func (a *InboundController) copyInboundClients(c *gin.Context) {
return
}
result, needRestart, err := a.inboundService.CopyInboundClients(targetID, req.SourceInboundID, req.ClientEmails)
result, needRestart, err := a.inboundService.CopyInboundClients(targetID, req.SourceInboundID, req.ClientEmails, req.Flow)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return

View file

@ -826,6 +826,20 @@
</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;">
@ -849,6 +863,9 @@
title: '',
targetInboundId: 0,
targetInboundRemark: '',
targetProtocol: '',
showFlow: false,
flow: '',
sourceInboundId: undefined,
sources: [],
sourceClients: [],
@ -861,8 +878,18 @@
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;
@ -925,14 +952,23 @@
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 skippedCount = (obj.skipped || []).length;
const errorCount = (obj.errors || []).length;
app.$message.success(`{{ i18n "pages.client.copyResult" }}: +${addedCount}, ~${skippedCount}, !${errorCount}`);
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 {

View file

@ -801,7 +801,7 @@ func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol)
}
}
func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string) (model.Client, error) {
func (s *InboundService) buildTargetClientFromSource(source model.Client, targetProtocol model.Protocol, email string, flow string) (model.Client, error) {
nowTs := time.Now().UnixMilli()
target := source
target.Email = email
@ -811,10 +811,16 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target
target.ID = ""
target.Password = ""
target.Auth = ""
target.Flow = ""
switch targetProtocol {
case model.VMESS, model.VLESS:
case model.VMESS:
target.ID = s.generateRandomCredential(targetProtocol)
case model.VLESS:
target.ID = s.generateRandomCredential(targetProtocol)
if flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443" {
target.Flow = flow
}
case model.Trojan, model.Shadowsocks:
target.Password = s.generateRandomCredential(targetProtocol)
case model.Hysteria:
@ -840,7 +846,7 @@ func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID
}
}
func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string) (*CopyClientsResult, bool, error) {
func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string, flow string) (*CopyClientsResult, bool, error) {
result := &CopyClientsResult{
Added: []string{},
Skipped: []string{},
@ -913,7 +919,7 @@ func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID
}
targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails)
targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail)
targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound.Protocol, targetEmail, flow)
if buildErr != nil {
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr))
continue

View file

@ -305,6 +305,11 @@
"copyEmailPreview" = "Resulting email preview"
"copySelectSourceFirst" = "Please select a source inbound first."
"copyResult" = "Copy result"
"copyResultSuccess" = "Copied successfully"
"copyResultNone" = "Nothing to copy: no clients selected or source is empty"
"copyResultErrors" = "Copy errors"
"copyFlowLabel" = "Flow for new clients (VLESS)"
"copyFlowHint" = "Applied to all copied clients. Leave empty to skip."
"selectAll" = "Select all"
"clearAll" = "Clear all"
"method" = "Method"

View file

@ -305,6 +305,11 @@
"copyEmailPreview" = "Предпросмотр итоговых email"
"copySelectSourceFirst" = "Сначала выберите источник."
"copyResult" = "Результат копирования"
"copyResultSuccess" = "Успешно скопировано"
"copyResultNone" = "Нечего копировать: ни одного клиента не выбрано или список источника пуст"
"copyResultErrors" = "Ошибки при копировании"
"copyFlowLabel" = "Flow для новых клиентов (VLESS)"
"copyFlowHint" = "Применится ко всем копируемым клиентам. Оставьте пустым, чтобы не задавать."
"selectAll" = "Выбрать всех"
"clearAll" = "Снять всё"
"method" = "Метод"