mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
i18n(clients): replace English fallbacks with proper translation keys
Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings.
This commit is contained in:
parent
106adca414
commit
b57cafed94
6 changed files with 111 additions and 104 deletions
|
|
@ -129,7 +129,7 @@ function buildEmails() {
|
|||
|
||||
async function submit() {
|
||||
if (!Array.isArray(form.inboundIds) || form.inboundIds.length === 0) {
|
||||
message.error(t('pages.clients.selectInbound') || 'Select one or more inbounds.');
|
||||
message.error(t('pages.clients.selectInbound'));
|
||||
return;
|
||||
}
|
||||
const emails = buildEmails();
|
||||
|
|
@ -159,9 +159,9 @@ async function submit() {
|
|||
else failed++;
|
||||
}
|
||||
if (failed === 0) {
|
||||
message.success(t('pages.clients.toasts.bulkCreated', { count: ok }) || `${ok} clients created`);
|
||||
message.success(t('pages.clients.toasts.bulkCreated', { count: ok }));
|
||||
} else {
|
||||
message.warning(`${ok} created, ${failed} failed`);
|
||||
message.warning(t('pages.clients.toasts.bulkCreatedMixed', { ok, failed }));
|
||||
}
|
||||
emit('saved');
|
||||
close();
|
||||
|
|
@ -172,10 +172,10 @@ async function submit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="t('pages.clients.bulk') || 'Add Bulk'" :ok-text="t('create')" :cancel-text="t('close')"
|
||||
<a-modal :open="open" :title="t('pages.clients.bulk')" :ok-text="t('create')" :cancel-text="t('close')"
|
||||
:confirm-loading="saving" :mask-closable="false" :width="640" @ok="submit" @cancel="close">
|
||||
<a-form :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" required>
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds')" required>
|
||||
<a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions"
|
||||
:placeholder="t('pages.clients.selectInbound')" :show-search="true"
|
||||
:filter-option="(input, option) => (option.label || '').toLowerCase().includes(input.toLowerCase())" />
|
||||
|
|
@ -219,14 +219,14 @@ async function submit() {
|
|||
<a-input v-model:value="form.comment" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="showFlow" label="Flow">
|
||||
<a-form-item v-if="showFlow" :label="t('pages.clients.flow')">
|
||||
<a-select v-model:value="form.flow" :style="{ width: '220px' }">
|
||||
<a-select-option value="">none</a-select-option>
|
||||
<a-select-option value="">{{ t('none') }}</a-select-option>
|
||||
<a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="ipLimitEnable" :label="t('pages.clients.limitIp') || 'IP Limit'">
|
||||
<a-form-item v-if="ipLimitEnable" :label="t('pages.clients.limitIp')">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" />
|
||||
</a-form-item>
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ function regenerateEmail() {
|
|||
|
||||
async function onSubmit() {
|
||||
if (!form.email || form.email.trim() === '') {
|
||||
message.error(t('pages.clients.email') + ' *');
|
||||
message.error(`${t('pages.clients.email')} *`);
|
||||
return;
|
||||
}
|
||||
if (!isEdit.value && (!form.inboundIds || form.inboundIds.length === 0)) {
|
||||
|
|
@ -243,7 +243,7 @@ async function onSubmit() {
|
|||
|
||||
<template>
|
||||
<a-modal :open="open" :title="isEdit ? t('pages.clients.editTitle') : t('pages.clients.addTitle')"
|
||||
:destroy-on-close="true" :ok-text="isEdit ? t('save') : t('add')" :cancel-text="t('cancel')"
|
||||
:destroy-on-close="true" :ok-text="isEdit ? t('save') : t('create')" :cancel-text="t('cancel')"
|
||||
:ok-button-props="{ loading: submitting }" :width="720" @ok="onSubmit" @cancel="close">
|
||||
<a-form layout="vertical" :model="form">
|
||||
<a-row :gutter="16">
|
||||
|
|
@ -256,7 +256,7 @@ async function onSubmit() {
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.subId') || 'subId'">
|
||||
<a-form-item :label="t('pages.clients.subId')">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.subId" style="flex: 1" />
|
||||
<a-button @click="regenerateSubId">↻</a-button>
|
||||
|
|
@ -267,7 +267,7 @@ async function onSubmit() {
|
|||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="Auth (Hysteria)">
|
||||
<a-form-item :label="t('pages.clients.hysteriaAuth')">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.auth" style="flex: 1" />
|
||||
<a-button @click="regenerateAuth">↻</a-button>
|
||||
|
|
@ -275,7 +275,7 @@ async function onSubmit() {
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.password') || 'Password'">
|
||||
<a-form-item :label="t('pages.clients.password')">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.password" style="flex: 1" />
|
||||
<a-button @click="regeneratePassword">↻</a-button>
|
||||
|
|
@ -286,7 +286,7 @@ async function onSubmit() {
|
|||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="ipLimitEnable ? 12 : 24">
|
||||
<a-form-item label="UUID">
|
||||
<a-form-item :label="t('pages.clients.uuid')">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.uuid" style="flex: 1" />
|
||||
<a-button @click="regenerateUUID">↻</a-button>
|
||||
|
|
@ -294,7 +294,7 @@ async function onSubmit() {
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="ipLimitEnable" :span="12">
|
||||
<a-form-item :label="t('pages.clients.limitIp') || 'IP limit'">
|
||||
<a-form-item :label="t('pages.clients.limitIp')">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
|
@ -302,12 +302,12 @@ async function onSubmit() {
|
|||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.totalGB') || 'Total (GB, 0 = unlimited)'">
|
||||
<a-form-item :label="t('pages.clients.totalGB')">
|
||||
<a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.expiryTime') || 'Expiry'">
|
||||
<a-form-item :label="t('pages.clients.expiryTime')">
|
||||
<a-date-picker v-model:value="form.expiryTime" show-time style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
|
@ -315,38 +315,38 @@ async function onSubmit() {
|
|||
|
||||
<a-row v-if="showFlow || showReverseTag" :gutter="16">
|
||||
<a-col v-if="showFlow" :span="12">
|
||||
<a-form-item label="Flow">
|
||||
<a-form-item :label="t('pages.clients.flow')">
|
||||
<a-select v-model:value="form.flow">
|
||||
<a-select-option value="">none</a-select-option>
|
||||
<a-select-option value="">{{ t('none') }}</a-select-option>
|
||||
<a-select-option v-for="k in FLOW_OPTIONS" :key="k" :value="k">{{ k }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="showReverseTag" :span="12">
|
||||
<a-form-item label="Reverse tag">
|
||||
<a-input v-model:value="form.reverseTag" placeholder="Optional reverse tag" />
|
||||
<a-form-item :label="t('pages.clients.reverseTag')">
|
||||
<a-input v-model:value="form.reverseTag" :placeholder="t('pages.clients.reverseTagPlaceholder')" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="'Telegram user ID'">
|
||||
<a-form-item :label="t('pages.clients.telegramId')">
|
||||
<a-input-number v-model:value="form.tgId" :min="0" :controls="false"
|
||||
:placeholder="t('pages.clients.telegramIdPlaceholder') || 'Numeric Telegram user ID (0 = none)'"
|
||||
:placeholder="t('pages.clients.telegramIdPlaceholder')"
|
||||
style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.clients.comment') || 'Comment'">
|
||||
<a-form-item :label="t('pages.clients.comment')">
|
||||
<a-input v-model:value="form.comment" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" :required="!isEdit">
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds')" :required="!isEdit">
|
||||
<a-select v-model:value="form.inboundIds" mode="multiple" :options="inboundOptions" :show-search="true"
|
||||
:placeholder="t('pages.clients.selectInbound') || 'Select one or more inbounds'"
|
||||
:placeholder="t('pages.clients.selectInbound')"
|
||||
:filter-option="(input, option) => (option.label || '').toLowerCase().includes(input.toLowerCase())" />
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -355,17 +355,17 @@ async function onSubmit() {
|
|||
<span style="margin-left: 8px">{{ t('enable') }}</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="isEdit && ipLimitEnable" :label="t('pages.clients.ipLog') || 'IP Log'">
|
||||
<a-form-item v-if="isEdit && ipLimitEnable" :label="t('pages.clients.ipLog')">
|
||||
<a-space style="margin-bottom: 8px">
|
||||
<a-button size="small" :loading="ipsLoading" @click="loadIps">{{ t('refresh') }}</a-button>
|
||||
<a-button size="small" danger :loading="ipsClearing" :disabled="clientIps.length === 0" @click="clearIps">
|
||||
{{ t('clearAll') || 'Clear' }}
|
||||
{{ t('pages.clients.clearAll') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
<div v-if="clientIps.length > 0">
|
||||
<a-tag v-for="(ip, idx) in clientIps" :key="idx" color="blue" style="margin-bottom: 4px">{{ ip }}</a-tag>
|
||||
</div>
|
||||
<a-tag v-else>{{ t('tgbot.noIpRecord') || 'No IP record' }}</a-tag>
|
||||
<a-tag v-else>{{ t('tgbot.noIpRecord') }}</a-tag>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
|
|
|||
|
|
@ -105,10 +105,10 @@ function close() {
|
|||
<table class="info-table block">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ t('pages.clients.online') || 'Online' }}</td>
|
||||
<td>{{ t('pages.clients.online') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="client.enable && isOnline" color="green">{{ t('pages.clients.online') || 'Online' }}</a-tag>
|
||||
<a-tag v-else>{{ t('pages.clients.offline') || 'Offline' }}</a-tag>
|
||||
<a-tag v-if="client.enable && isOnline" color="green">{{ t('pages.clients.online') }}</a-tag>
|
||||
<a-tag v-else>{{ t('pages.clients.offline') }}</a-tag>
|
||||
<span class="hint">{{ t('lastOnline') }}: {{ lastOnlineLabel(traffic?.lastOnline) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -123,7 +123,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('pages.clients.email') || 'Email' }}</td>
|
||||
<td>{{ t('pages.clients.email') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="client.email" color="green">{{ client.email }}</a-tag>
|
||||
<a-tag v-else color="red">{{ t('none') }}</a-tag>
|
||||
|
|
@ -131,7 +131,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>subId</td>
|
||||
<td>{{ t('pages.clients.subId') }}</td>
|
||||
<td>
|
||||
<a-tag class="info-large-tag">{{ client.subId || '-' }}</a-tag>
|
||||
<a-button v-if="client.subId" size="small" type="text" @click="copyValue(client.subId)">
|
||||
|
|
@ -141,7 +141,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr v-if="client.uuid">
|
||||
<td>UUID</td>
|
||||
<td>{{ t('pages.clients.uuid') }}</td>
|
||||
<td>
|
||||
<a-tag class="info-large-tag">{{ client.uuid }}</a-tag>
|
||||
<a-button size="small" type="text" @click="copyValue(client.uuid)">
|
||||
|
|
@ -161,7 +161,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr v-if="client.auth">
|
||||
<td>Auth</td>
|
||||
<td>{{ t('pages.clients.auth') }}</td>
|
||||
<td>
|
||||
<a-tag class="info-large-tag">{{ client.auth }}</a-tag>
|
||||
<a-button size="small" type="text" @click="copyValue(client.auth)">
|
||||
|
|
@ -171,7 +171,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Flow</td>
|
||||
<td>{{ t('pages.clients.flow') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="client.flow">{{ client.flow }}</a-tag>
|
||||
<a-tag v-else color="orange">{{ t('none') }}</a-tag>
|
||||
|
|
@ -194,7 +194,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('remained') || 'Remaining' }}</td>
|
||||
<td>{{ t('remained') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="remaining < 0" color="purple">∞</a-tag>
|
||||
<a-tag v-else :color="remaining > 0 ? '' : 'red'">
|
||||
|
|
@ -204,7 +204,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('pages.inbounds.expireDate') || 'Expiry' }}</td>
|
||||
<td>{{ t('pages.inbounds.expireDate') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="!client.expiryTime || client.expiryTime <= 0" color="purple">∞</a-tag>
|
||||
<a-tag v-else>{{ expiryLabel(client.expiryTime) }}</a-tag>
|
||||
|
|
@ -213,7 +213,7 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>IP limit</td>
|
||||
<td>{{ t('pages.clients.ipLimit') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="!client.limitIp">∞</a-tag>
|
||||
<a-tag v-else>{{ client.limitIp }}</a-tag>
|
||||
|
|
@ -221,28 +221,28 @@ function close() {
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('pages.inbounds.createdAt') || 'Created' }}</td>
|
||||
<td>{{ t('pages.inbounds.createdAt') }}</td>
|
||||
<td>
|
||||
<a-tag>{{ dateLabel(client.createdAt) }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('pages.inbounds.updatedAt') || 'Updated' }}</td>
|
||||
<td>{{ t('pages.inbounds.updatedAt') }}</td>
|
||||
<td>
|
||||
<a-tag>{{ dateLabel(client.updatedAt) }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="client.comment">
|
||||
<td>{{ t('pages.clients.comment') || 'Comment' }}</td>
|
||||
<td>{{ t('pages.clients.comment') }}</td>
|
||||
<td>
|
||||
<a-tag class="info-large-tag">{{ client.comment }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ t('pages.clients.attachedInbounds') || 'Attached inbounds' }}</td>
|
||||
<td>{{ t('pages.clients.attachedInbounds') }}</td>
|
||||
<td>
|
||||
<div class="chips">
|
||||
<a-tag v-for="id in (client.inboundIds || [])" :key="id" color="blue">
|
||||
|
|
@ -259,10 +259,10 @@ function close() {
|
|||
</table>
|
||||
|
||||
<template v-if="links.length > 0">
|
||||
<a-divider>{{ t('pages.inbounds.copyLink') || 'URL' }}</a-divider>
|
||||
<a-divider>{{ t('pages.inbounds.copyLink') }}</a-divider>
|
||||
<div v-for="(link, idx) in links" :key="idx" class="link-panel">
|
||||
<div class="link-panel-header">
|
||||
<a-tag color="green">{{ `${t('pages.clients.link') || 'Link'} ${idx + 1}` }}</a-tag>
|
||||
<a-tag color="green">{{ `${t('pages.clients.link')} ${idx + 1}` }}</a-tag>
|
||||
<a-tooltip :title="t('copy')">
|
||||
<a-button size="small" @click="copyValue(link)">
|
||||
<template #icon>
|
||||
|
|
@ -276,10 +276,10 @@ function close() {
|
|||
</template>
|
||||
|
||||
<template v-if="showSubscription && subLink">
|
||||
<a-divider>{{ t('subscription.title') || 'Subscription info' }}</a-divider>
|
||||
<a-divider>{{ t('subscription.title') }}</a-divider>
|
||||
<div class="link-panel">
|
||||
<div class="link-panel-header">
|
||||
<a-tag color="green">{{ t('subscription.title') || 'Subscription info' }}</a-tag>
|
||||
<a-tag color="green">{{ t('subscription.title') }}</a-tag>
|
||||
<a-tooltip :title="t('copy')">
|
||||
<a-button size="small" @click="copyValue(subLink)">
|
||||
<template #icon>
|
||||
|
|
|
|||
|
|
@ -67,20 +67,20 @@ function close() {
|
|||
@cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="!client?.subId && !loading" class="empty">
|
||||
{{ t('pages.clients.noSubId') || 'This client has no subId, no shareable link.' }}
|
||||
{{ t('pages.clients.noSubId') }}
|
||||
</div>
|
||||
<div v-else-if="!hasAnything && !loading" class="empty">
|
||||
{{ t('pages.clients.noLinks') || 'No shareable links — attach this client to a protocol-capable inbound first.' }}
|
||||
{{ t('pages.clients.noLinks') }}
|
||||
</div>
|
||||
<a-collapse v-else :active-key="activeKeys" accordion>
|
||||
<a-collapse-panel v-if="subLink" key="sub" :header="t('subscription.title') || 'Subscription info'">
|
||||
<QrPanel :value="subLink" :remark="`${client?.email || ''} — ${t('subscription.title') || 'Subscription'}`" />
|
||||
<a-collapse-panel v-if="subLink" key="sub" :header="t('subscription.title')">
|
||||
<QrPanel :value="subLink" :remark="`${client?.email || ''} — ${t('subscription.title')}`" />
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel v-if="subJsonLink" key="subJson" :header="`${t('subscription.title') || 'Subscription info'} (JSON)`">
|
||||
<a-collapse-panel v-if="subJsonLink" key="subJson" :header="`${t('subscription.title')} (JSON)`">
|
||||
<QrPanel :value="subJsonLink" :remark="`${client?.email || ''} — JSON`" />
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel v-for="(link, idx) in links" :key="`l${idx}`"
|
||||
:header="`${t('pages.clients.link') || 'Link'} ${idx + 1}`">
|
||||
:header="`${t('pages.clients.link')} ${idx + 1}`">
|
||||
<QrPanel :value="link" :remark="`${client?.email || ''} #${idx + 1}`" />
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
|
|
|||
|
|
@ -130,10 +130,8 @@ function onBulkDelete() {
|
|||
const emails = [...selectedRowKeys.value];
|
||||
if (emails.length === 0) return;
|
||||
Modal.confirm({
|
||||
title: t('pages.clients.bulkDeleteConfirmTitle', { count: emails.length })
|
||||
|| `Delete ${emails.length} clients?`,
|
||||
content: t('pages.clients.bulkDeleteConfirmContent')
|
||||
|| 'Each client is removed from every attached inbound and its traffic record is dropped. This cannot be undone.',
|
||||
title: t('pages.clients.bulkDeleteConfirmTitle', { count: emails.length }),
|
||||
content: t('pages.clients.bulkDeleteConfirmContent'),
|
||||
okText: t('delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
|
|
@ -147,9 +145,9 @@ function onBulkDelete() {
|
|||
}
|
||||
selectedRowKeys.value = [];
|
||||
if (failed === 0) {
|
||||
message.success(t('pages.clients.toasts.bulkDeleted', { count: ok }) || `${ok} clients deleted`);
|
||||
message.success(t('pages.clients.toasts.bulkDeleted', { count: ok }));
|
||||
} else {
|
||||
message.warning(`${ok} deleted, ${failed} failed`);
|
||||
message.warning(t('pages.clients.toasts.bulkDeletedMixed', { ok, failed }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -161,9 +159,8 @@ async function onBulkAddSaved() {
|
|||
|
||||
function onDelDepleted() {
|
||||
Modal.confirm({
|
||||
title: t('pages.clients.delDepletedConfirmTitle') || 'Delete depleted clients?',
|
||||
content: t('pages.clients.delDepletedConfirmContent')
|
||||
|| 'Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.',
|
||||
title: t('pages.clients.delDepletedConfirmTitle'),
|
||||
content: t('pages.clients.delDepletedConfirmContent'),
|
||||
okText: t('delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
|
|
@ -171,8 +168,7 @@ function onDelDepleted() {
|
|||
const msg = await delDepleted();
|
||||
if (msg?.success) {
|
||||
const deleted = msg.obj?.deleted ?? 0;
|
||||
message.success(t('pages.clients.toasts.delDepleted', { count: deleted })
|
||||
|| `${deleted} depleted clients deleted`);
|
||||
message.success(t('pages.clients.toasts.delDepleted', { count: deleted }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -311,33 +307,31 @@ function onEdit(row) {
|
|||
|
||||
function onDelete(row) {
|
||||
Modal.confirm({
|
||||
title: t('pages.clients.deleteConfirmTitle', { email: row.email }) || `Delete ${row.email}?`,
|
||||
content: t('pages.clients.deleteConfirmContent')
|
||||
|| 'This removes the client from every attached inbound and drops its traffic record.',
|
||||
title: t('pages.clients.deleteConfirmTitle', { email: row.email }),
|
||||
content: t('pages.clients.deleteConfirmContent'),
|
||||
okText: t('delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const msg = await remove(row.email);
|
||||
if (msg?.success) message.success(t('pages.clients.toasts.deleted') || 'Client deleted');
|
||||
if (msg?.success) message.success(t('pages.clients.toasts.deleted'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onResetTraffic(row) {
|
||||
if (!row?.email || !Array.isArray(row.inboundIds) || row.inboundIds.length === 0) {
|
||||
message.warning(t('pages.clients.resetNotPossible') || 'Attach this client to an inbound first.');
|
||||
message.warning(t('pages.clients.resetNotPossible'));
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: `${t('pages.inbounds.resetTraffic') || 'Reset traffic'} — ${row.email}`,
|
||||
content: t('pages.inbounds.resetTrafficContent')
|
||||
|| 'Counters drop to zero. Quota and expiry stay as-is.',
|
||||
okText: t('reset') || 'Reset',
|
||||
title: `${t('pages.inbounds.resetTraffic')} — ${row.email}`,
|
||||
content: t('pages.inbounds.resetTrafficContent'),
|
||||
okText: t('reset'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const msg = await resetTraffic(row);
|
||||
if (msg?.success) message.success(t('pages.clients.toasts.trafficReset') || 'Traffic reset');
|
||||
if (msg?.success) message.success(t('pages.clients.toasts.trafficReset'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -356,7 +350,7 @@ function onResetAllTraffics() {
|
|||
Modal.confirm({
|
||||
title: t('pages.clients.resetAllTrafficsTitle'),
|
||||
content: t('pages.clients.resetAllTrafficsContent'),
|
||||
okText: t('reset') || 'Reset',
|
||||
okText: t('reset'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
|
|
@ -479,14 +473,14 @@ function onTableChange(_pag, _filters, sorter) {
|
|||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: t('pages.clients.actions') || 'Actions', key: 'actions', width: 200 },
|
||||
sortableCol({ title: t('pages.clients.enabled') || 'Enabled', key: 'enable', width: 80 }, 'enable'),
|
||||
{ title: t('pages.clients.online') || 'Online', key: 'online', width: 90 },
|
||||
sortableCol({ title: t('pages.clients.client') || 'Client', key: 'email' }, 'email'),
|
||||
sortableCol({ title: t('pages.clients.attachedInbounds') || 'Attached inbounds', key: 'inboundIds' }, 'inboundIds'),
|
||||
sortableCol({ title: t('pages.clients.traffic') || 'Traffic', key: 'traffic' }, 'traffic'),
|
||||
sortableCol({ title: t('pages.clients.remaining') || 'Remaining', key: 'remaining', width: 130 }, 'remaining'),
|
||||
sortableCol({ title: t('pages.clients.duration') || 'Duration', key: 'expiryTime' }, 'expiryTime'),
|
||||
{ title: t('pages.clients.actions'), key: 'actions', width: 200 },
|
||||
sortableCol({ title: t('pages.clients.enabled'), key: 'enable', width: 80 }, 'enable'),
|
||||
{ title: t('pages.clients.online'), key: 'online', width: 90 },
|
||||
sortableCol({ title: t('pages.clients.client'), key: 'email' }, 'email'),
|
||||
sortableCol({ title: t('pages.clients.attachedInbounds'), key: 'inboundIds' }, 'inboundIds'),
|
||||
sortableCol({ title: t('pages.clients.traffic'), key: 'traffic' }, 'traffic'),
|
||||
sortableCol({ title: t('pages.clients.remaining'), key: 'remaining', width: 130 }, 'remaining'),
|
||||
sortableCol({ title: t('pages.clients.duration'), key: 'expiryTime' }, 'expiryTime'),
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
|
@ -497,7 +491,7 @@ const columns = computed(() => [
|
|||
|
||||
<a-layout class="content-shell">
|
||||
<a-layout-content id="content-layout" class="content-area">
|
||||
<a-spin :spinning="!fetched" :delay="200" tip="Loading…" size="large">
|
||||
<a-spin :spinning="!fetched" :delay="200" :tip="t('loading')" size="large">
|
||||
<div v-if="!fetched" class="loading-spacer" />
|
||||
|
||||
<a-row v-else :gutter="[isMobile ? 8 : 16, isMobile ? 8 : 12]">
|
||||
|
|
@ -592,14 +586,13 @@ const columns = computed(() => [
|
|||
<template #icon>
|
||||
<UsergroupAddOutlined />
|
||||
</template>
|
||||
<template v-if="!isMobile">{{ t('pages.clients.bulk') || 'Add Bulk' }}</template>
|
||||
<template v-if="!isMobile">{{ t('pages.clients.bulk') }}</template>
|
||||
</a-button>
|
||||
<a-button v-if="selectedRowKeys.length > 0" danger size="small" @click="onBulkDelete">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
{{ t('pages.clients.deleteSelected', { count: selectedRowKeys.length })
|
||||
|| `Delete (${selectedRowKeys.length})` }}
|
||||
{{ t('pages.clients.deleteSelected', { count: selectedRowKeys.length }) }}
|
||||
</a-button>
|
||||
<a-button size="small" @click="onResetAllTraffics">
|
||||
<template #icon>
|
||||
|
|
@ -611,7 +604,7 @@ const columns = computed(() => [
|
|||
<template #icon>
|
||||
<RestOutlined />
|
||||
</template>
|
||||
<template v-if="!isMobile">{{ t('pages.clients.delDepleted') || 'Delete depleted' }}</template>
|
||||
<template v-if="!isMobile">{{ t('pages.clients.delDepleted') }}</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -660,13 +653,13 @@ const columns = computed(() => [
|
|||
{{ t('depleted') }}
|
||||
</a-tag>
|
||||
<a-tag v-else-if="record.enable && isOnline(record.email)" color="green">
|
||||
{{ t('pages.clients.online') || 'Online' }}
|
||||
{{ t('pages.clients.online') }}
|
||||
</a-tag>
|
||||
<a-tag v-else-if="!record.enable">{{ t('disabled') }}</a-tag>
|
||||
<a-tag v-else-if="clientBucket(record) === 'expiring'" color="orange">
|
||||
{{ t('depletingSoon') }}
|
||||
</a-tag>
|
||||
<a-tag v-else>{{ t('pages.clients.offline') || 'Offline' }}</a-tag>
|
||||
<a-tag v-else>{{ t('pages.clients.offline') }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'inboundIds'">
|
||||
<a-tag v-for="id in record.inboundIds" :key="id" color="blue" style="margin: 2px">
|
||||
|
|
@ -694,27 +687,27 @@ const columns = computed(() => [
|
|||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="4">
|
||||
<a-tooltip :title="t('pages.clients.qrCode') || 'QR Code'">
|
||||
<a-tooltip :title="t('pages.clients.qrCode')">
|
||||
<a-button size="small" type="text" @click="onShowQr(record)">
|
||||
<QrcodeOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('pages.clients.moreInformation') || 'More Information'">
|
||||
<a-tooltip :title="t('pages.clients.moreInformation')">
|
||||
<a-button size="small" type="text" @click="onShowInfo(record)">
|
||||
<InfoCircleOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('pages.inbounds.resetTraffic') || 'Reset traffic'">
|
||||
<a-tooltip :title="t('pages.inbounds.resetTraffic')">
|
||||
<a-button size="small" type="text" @click="onResetTraffic(record)">
|
||||
<RetweetOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('pages.clients.edit') || 'Edit'">
|
||||
<a-tooltip :title="t('edit')">
|
||||
<a-button size="small" type="text" @click="onEdit(record)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('pages.clients.delete') || 'Delete'">
|
||||
<a-tooltip :title="t('delete')">
|
||||
<a-button size="small" type="text" danger @click="onDelete(record)">
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
|
|
@ -726,7 +719,7 @@ const columns = computed(() => [
|
|||
<template #emptyText>
|
||||
<div class="clients-empty">
|
||||
<UserOutlined style="font-size: 32px; margin-bottom: 8px" />
|
||||
<div>{{ t('pages.clients.empty') || 'No clients yet.' }}</div>
|
||||
<div>{{ t('pages.clients.empty') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
|
@ -736,7 +729,7 @@ const columns = computed(() => [
|
|||
<div v-if="filteredClients.length > 0" class="card-bulk-bar">
|
||||
<a-checkbox :checked="allSelected" :indeterminate="someSelected"
|
||||
@change="(e) => selectAll(e.target.checked)">
|
||||
{{ t('pages.clients.selectAll') || 'Select all' }}
|
||||
{{ t('pages.clients.selectAll') }}
|
||||
</a-checkbox>
|
||||
<span v-if="selectedRowKeys.length > 0" class="bulk-count">
|
||||
{{ selectedRowKeys.length }}
|
||||
|
|
@ -745,7 +738,7 @@ const columns = computed(() => [
|
|||
|
||||
<div v-if="filteredClients.length === 0" class="card-empty">
|
||||
<UserOutlined style="font-size: 28px; opacity: 0.5" />
|
||||
<div>{{ t('pages.clients.empty') || 'No clients yet.' }}</div>
|
||||
<div>{{ t('pages.clients.empty') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="row in filteredClients" :key="row.email" class="client-card"
|
||||
|
|
@ -762,7 +755,7 @@ const columns = computed(() => [
|
|||
{{ t('depletingSoon') }}
|
||||
</a-tag>
|
||||
<div class="card-actions" @click.stop>
|
||||
<a-tooltip :title="t('pages.clients.moreInformation') || 'Info'">
|
||||
<a-tooltip :title="t('pages.clients.moreInformation')">
|
||||
<InfoCircleOutlined class="row-action-trigger" @click="onShowInfo(row)" />
|
||||
</a-tooltip>
|
||||
<a-switch :checked="row.enable" size="small" :loading="togglingEmail === row.email"
|
||||
|
|
@ -772,16 +765,16 @@ const columns = computed(() => [
|
|||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="qr" @click="onShowQr(row)">
|
||||
<QrcodeOutlined /> {{ t('pages.clients.qrCode') || 'QR Code' }}
|
||||
<QrcodeOutlined /> {{ t('pages.clients.qrCode') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="reset" @click="onResetTraffic(row)">
|
||||
<RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') || 'Reset traffic' }}
|
||||
<RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="edit" @click="onEdit(row)">
|
||||
<EditOutlined /> {{ t('pages.clients.edit') || 'Edit' }}
|
||||
<EditOutlined /> {{ t('edit') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" class="danger-item" @click="onDelete(row)">
|
||||
<DeleteOutlined /> {{ t('pages.clients.delete') || 'Delete' }}
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"loading": "Loading...",
|
||||
"refresh": "Refresh",
|
||||
"second": "Second",
|
||||
"minute": "Minute",
|
||||
"hour": "Hour",
|
||||
|
|
@ -444,12 +445,25 @@
|
|||
"delDepleted": "Delete depleted",
|
||||
"delDepletedConfirmTitle": "Delete depleted clients?",
|
||||
"delDepletedConfirmContent": "Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.",
|
||||
"auth": "Auth",
|
||||
"hysteriaAuth": "Hysteria Auth",
|
||||
"uuid": "UUID",
|
||||
"flow": "Flow",
|
||||
"reverseTag": "Reverse tag",
|
||||
"reverseTagPlaceholder": "Optional reverse tag",
|
||||
"telegramId": "Telegram user ID",
|
||||
"telegramIdPlaceholder": "Numeric Telegram user ID (0 = none)",
|
||||
"created": "Created",
|
||||
"updated": "Updated",
|
||||
"ipLimit": "IP limit",
|
||||
"toasts": {
|
||||
"deleted": "Client deleted",
|
||||
"trafficReset": "Traffic reset",
|
||||
"allTrafficsReset": "All client traffic reset",
|
||||
"bulkDeleted": "{count} clients deleted",
|
||||
"bulkDeletedMixed": "{ok} deleted, {failed} failed",
|
||||
"bulkCreated": "{count} clients created",
|
||||
"bulkCreatedMixed": "{ok} created, {failed} failed",
|
||||
"delDepleted": "{count} depleted clients deleted"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue