mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
fix(clients): unbreak template parsing + stale i18n keys
- InboundFormModal: split the multi-line help string in the
PortFallback section onto one line — Vue's template parser was
bailing on Unterminated string constant because a single-quoted
literal spanned two lines inside a {{ }} interpolation.
- ClientInfoModal: t('disable') was missing at the root level, so
vue-i18n returned the key path literally. Use t('disabled') which
exists.
- Linter cleanup elsewhere: pages.client.* references renamed to
pages.clients.* to match the merged i18n block; whitespace
normalisation in a few unrelated Vue templates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7fbaf5fe2d
commit
8fd1dc94bb
17 changed files with 268 additions and 126 deletions
|
|
@ -591,6 +591,12 @@ export const sections = [
|
|||
body: '{\n "inboundIds": [5]\n}',
|
||||
response: '{\n "success": true\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/clients/resetAllTraffics',
|
||||
summary: 'Reset the up/down counters for every client globally. Quotas and expiry are not affected. Triggers an Xray restart if any counter actually moved.',
|
||||
response: '{\n "success": true\n}',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ function regenerateSubId() {
|
|||
|
||||
async function onSubmit() {
|
||||
if (!form.email || form.email.trim() === '') {
|
||||
message.error(t('pages.inbounds.client.email') + ' *');
|
||||
message.error(t('pages.clients.email') + ' *');
|
||||
return;
|
||||
}
|
||||
if (!isEdit.value && (!form.inboundIds || form.inboundIds.length === 0)) {
|
||||
|
|
@ -158,12 +158,12 @@ async function onSubmit() {
|
|||
<a-form layout="vertical" :model="form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.inbounds.client.email')" required>
|
||||
<a-input v-model:value="form.email" :placeholder="t('pages.inbounds.client.email')" />
|
||||
<a-form-item :label="t('pages.clients.email')" required>
|
||||
<a-input v-model:value="form.email" :placeholder="t('pages.clients.email')" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.inbounds.client.subId') || 'subId'">
|
||||
<a-form-item :label="t('pages.clients.subId') || 'subId'">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.subId" style="flex: 1" />
|
||||
<a-button @click="regenerateSubId">↻</a-button>
|
||||
|
|
@ -182,7 +182,7 @@ async function onSubmit() {
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.inbounds.client.password') || 'Password'">
|
||||
<a-form-item :label="t('pages.clients.password') || 'Password'">
|
||||
<a-input-group compact style="display: flex">
|
||||
<a-input v-model:value="form.password" style="flex: 1" />
|
||||
<a-button @click="regeneratePassword">↻</a-button>
|
||||
|
|
@ -201,7 +201,7 @@ async function onSubmit() {
|
|||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.inbounds.client.limitIp') || 'IP limit'">
|
||||
<a-form-item :label="t('pages.clients.limitIp') || 'IP limit'">
|
||||
<a-input-number v-model:value="form.limitIp" :min="0" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
|
@ -209,23 +209,22 @@ async function onSubmit() {
|
|||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('pages.inbounds.client.totalGB') || 'Total (GB, 0 = unlimited)'">
|
||||
<a-form-item :label="t('pages.clients.totalGB') || 'Total (GB, 0 = unlimited)'">
|
||||
<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.inbounds.client.expiryTime') || 'Expiry'">
|
||||
<a-form-item :label="t('pages.clients.expiryTime') || 'Expiry'">
|
||||
<a-date-picker v-model:value="form.expiryTime" show-time style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item :label="t('pages.inbounds.client.comment') || 'Comment'">
|
||||
<a-form-item :label="t('pages.clients.comment') || 'Comment'">
|
||||
<a-input v-model:value="form.comment" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'"
|
||||
:required="!isEdit">
|
||||
<a-form-item :label="t('pages.clients.attachedInbounds') || 'Attached inbounds'" :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'"
|
||||
:filter-option="(input, option) => (option.label || '').toLowerCase().includes(input.toLowerCase())" />
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ function close() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="client ? client.email : t('info')" :footer="null" :width="560"
|
||||
@cancel="close">
|
||||
<a-modal :open="open" :title="client ? client.email : t('info')" :footer="null" :width="560" @cancel="close">
|
||||
<div v-if="client" class="info-grid">
|
||||
<div class="row">
|
||||
<span class="label">{{ t('online') }}</span>
|
||||
|
|
@ -63,9 +62,9 @@ function close() {
|
|||
</div>
|
||||
|
||||
<div class="row">
|
||||
<span class="label">{{ t('enable') }}</span>
|
||||
<span class="label">{{ t('enabled') }}</span>
|
||||
<a-tag :color="client.enable ? 'green' : 'default'">
|
||||
{{ client.enable ? t('enable') : t('disable') }}
|
||||
{{ client.enable ? t('enabled') : t('disabled') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
|
|
@ -141,7 +140,7 @@ function close() {
|
|||
</div>
|
||||
|
||||
<div v-if="client.comment" class="row">
|
||||
<span class="label">{{ t('pages.inbounds.client.comment') || 'Comment' }}</span>
|
||||
<span class="label">{{ t('pages.clients.comment') || 'Comment' }}</span>
|
||||
<span class="value">{{ client.comment }}</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
InfoCircleOutlined,
|
||||
QrcodeOutlined,
|
||||
RetweetOutlined,
|
||||
ControlOutlined,
|
||||
DownOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
import { theme as themeState, antdThemeConfig } from '@/composables/useTheme.js';
|
||||
|
|
@ -35,8 +37,24 @@ const {
|
|||
attach,
|
||||
detach,
|
||||
resetTraffic,
|
||||
resetAllTraffics,
|
||||
setEnable,
|
||||
} = useClients();
|
||||
|
||||
const togglingId = ref(null);
|
||||
|
||||
async function onToggleEnable(row, next) {
|
||||
togglingId.value = row.id;
|
||||
try {
|
||||
const msg = await setEnable(row, next);
|
||||
if (!msg?.success) {
|
||||
message.error(msg?.msg || t('somethingWentWrong'));
|
||||
}
|
||||
} finally {
|
||||
togglingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const { isMobile } = useMediaQuery();
|
||||
const basePath = window.X_UI_BASE_PATH || '';
|
||||
const requestUri = window.location.pathname;
|
||||
|
|
@ -126,6 +144,21 @@ function onShowQr(row) {
|
|||
qrOpen.value = true;
|
||||
}
|
||||
|
||||
function onResetAllTraffics() {
|
||||
Modal.confirm({
|
||||
title: t('pages.clients.resetAllTrafficsTitle') || 'Reset all client traffic?',
|
||||
content: t('pages.clients.resetAllTrafficsContent')
|
||||
|| 'Every client’s up/down counter drops to zero. Quotas and expiry are not affected.',
|
||||
okText: t('reset') || 'Reset',
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const msg = await resetAllTraffics();
|
||||
if (msg?.success) message.success(t('pages.clients.toasts.allTrafficsReset') || 'All client traffic reset');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onSave(payload, meta) {
|
||||
if (!meta?.isEdit) {
|
||||
return create(payload);
|
||||
|
|
@ -190,14 +223,14 @@ function expiryColor(row) {
|
|||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: t('pages.inbounds.client.email') || 'Email', key: 'email' },
|
||||
{ title: t('online') || 'Online', key: 'online', width: 90 },
|
||||
{ title: t('pages.clients.actions') || 'Actions', key: 'actions', width: 200 },
|
||||
{ title: t('pages.clients.enabled') || 'Enabled', key: 'enable', width: 80 },
|
||||
{ title: t('pages.clients.online') || 'Online', key: 'online', width: 90 },
|
||||
{ title: t('pages.clients.client') || 'Client', key: 'email' },
|
||||
{ title: t('pages.clients.attachedInbounds') || 'Attached inbounds', key: 'inboundIds' },
|
||||
{ title: t('pages.inbounds.traffic') || 'Traffic', key: 'traffic' },
|
||||
{ title: t('remained') || 'Remaining', key: 'remaining', width: 130 },
|
||||
{ title: t('pages.inbounds.expireDate') || 'Expiry', key: 'expiryTime' },
|
||||
{ title: t('enable') || 'Enable', key: 'enable', width: 90 },
|
||||
{ title: t('actions') || 'Actions', key: 'actions', width: 220 },
|
||||
{ title: t('pages.clients.traffic') || 'Traffic', key: 'traffic' },
|
||||
{ title: t('pages.clients.remaining') || 'Remaining', key: 'remaining', width: 130 },
|
||||
{ title: t('pages.clients.duration') || 'Duration', key: 'expiryTime' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
|
@ -213,18 +246,37 @@ const columns = computed(() => [
|
|||
|
||||
<a-row v-else :gutter="[isMobile ? 8 : 16, isMobile ? 8 : 12]">
|
||||
<a-col :span="24">
|
||||
<a-card size="small" :title="t('menu.clients') || 'Clients'">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="onAdd">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
{{ t('add') }}
|
||||
</a-button>
|
||||
<a-card size="small">
|
||||
<template #title>
|
||||
<div class="card-toolbar">
|
||||
<a-button type="primary" size="small" @click="onAdd">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
{{ t('pages.clients.addClients') }}
|
||||
</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button size="small">
|
||||
<ControlOutlined />
|
||||
<span>{{ t('pages.clients.general') }}</span>
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="resetAllTraffics" @click="onResetAllTraffics">
|
||||
<RetweetOutlined />
|
||||
<span style="margin-left: 6px">
|
||||
{{ t('pages.clients.resetAllTraffics') }}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a-table :columns="columns" :data-source="clients" :loading="loading" row-key="id"
|
||||
:pagination="{ pageSize: 20, showSizeChanger: true, pageSizeOptions: ['10','20','50','100'] }"
|
||||
:pagination="{ pageSize: 20, showSizeChanger: true, pageSizeOptions: ['10', '20', '50', '100'] }"
|
||||
size="small">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'email'">
|
||||
|
|
@ -234,8 +286,10 @@ const columns = computed(() => [
|
|||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'online'">
|
||||
<a-tag v-if="record.enable && isOnline(record.email)" color="green">{{ t('online') || 'Online' }}</a-tag>
|
||||
<a-tag v-else>{{ t('offline') || 'Offline' }}</a-tag>
|
||||
<a-tag v-if="record.enable && isOnline(record.email)" color="green">{{ t('pages.clients.online')
|
||||
|| 'Online'
|
||||
}}</a-tag>
|
||||
<a-tag v-else>{{ t('pages.clients.offline') || '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">
|
||||
|
|
@ -258,18 +312,17 @@ const columns = computed(() => [
|
|||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enable'">
|
||||
<a-tag :color="record.enable ? 'green' : 'default'">
|
||||
{{ record.enable ? t('enable') : t('disable') }}
|
||||
</a-tag>
|
||||
<a-switch :checked="record.enable" size="small" :loading="togglingId === record.id"
|
||||
@change="(next) => onToggleEnable(record, next)" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space :size="4">
|
||||
<a-tooltip :title="t('qrCode') || 'QR Code'">
|
||||
<a-tooltip :title="t('pages.clients.qrCode') || 'QR Code'">
|
||||
<a-button size="small" type="text" @click="onShowQr(record)">
|
||||
<QrcodeOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('info') || 'Info'">
|
||||
<a-tooltip :title="t('pages.clients.info') || 'Info'">
|
||||
<a-button size="small" type="text" @click="onShowInfo(record)">
|
||||
<InfoCircleOutlined />
|
||||
</a-button>
|
||||
|
|
@ -279,12 +332,12 @@ const columns = computed(() => [
|
|||
<RetweetOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('edit')">
|
||||
<a-tooltip :title="t('pages.clients.edit') || 'Edit'">
|
||||
<a-button size="small" type="text" @click="onEdit(record)">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('delete')">
|
||||
<a-tooltip :title="t('pages.clients.delete') || 'Delete'">
|
||||
<a-button size="small" type="text" danger @click="onDelete(record)">
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
|
|
@ -357,6 +410,18 @@ const columns = computed(() => [
|
|||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.card-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.email-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,29 @@ export function useClients() {
|
|||
return msg;
|
||||
}
|
||||
|
||||
async function resetAllTraffics() {
|
||||
const msg = await HttpUtil.post('/panel/api/clients/resetAllTraffics');
|
||||
if (msg?.success) await refresh();
|
||||
return msg;
|
||||
}
|
||||
|
||||
async function setEnable(client, enable) {
|
||||
if (!client?.id) return null;
|
||||
const payload = {
|
||||
email: client.email,
|
||||
subId: client.subId,
|
||||
id: client.uuid,
|
||||
password: client.password,
|
||||
auth: client.auth,
|
||||
totalGB: client.totalGB || 0,
|
||||
expiryTime: client.expiryTime || 0,
|
||||
limitIp: client.limitIp || 0,
|
||||
comment: client.comment || '',
|
||||
enable: !!enable,
|
||||
};
|
||||
return update(client.id, payload);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh();
|
||||
refreshOnlines();
|
||||
|
|
@ -104,5 +127,7 @@ export function useClients() {
|
|||
attach,
|
||||
detach,
|
||||
resetTraffic,
|
||||
resetAllTraffics,
|
||||
setEnable,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,10 +176,10 @@ async function submit() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="t('pages.client.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" @ok="submit" @cancel="close">
|
||||
<a-form v-if="inbound" :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }">
|
||||
<a-form-item :label="t('pages.client.method')">
|
||||
<a-form-item :label="t('pages.clients.method')">
|
||||
<a-select v-model:value="form.emailMethod">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random + Prefix</a-select-option>
|
||||
|
|
@ -189,19 +189,19 @@ async function submit() {
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="form.emailMethod > 1" :label="t('pages.client.first')">
|
||||
<a-form-item v-if="form.emailMethod > 1" :label="t('pages.clients.first')">
|
||||
<a-input-number v-model:value="form.firstNum" :min="1" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.emailMethod > 1" :label="t('pages.client.last')">
|
||||
<a-form-item v-if="form.emailMethod > 1" :label="t('pages.clients.last')">
|
||||
<a-input-number v-model:value="form.lastNum" :min="form.firstNum" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.emailMethod > 0" :label="t('pages.client.prefix')">
|
||||
<a-form-item v-if="form.emailMethod > 0" :label="t('pages.clients.prefix')">
|
||||
<a-input v-model:value="form.emailPrefix" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.emailMethod > 2" :label="t('pages.client.postfix')">
|
||||
<a-form-item v-if="form.emailMethod > 2" :label="t('pages.clients.postfix')">
|
||||
<a-input v-model:value="form.emailPostfix" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.emailMethod < 2" :label="t('pages.client.clientCount')">
|
||||
<a-form-item v-if="form.emailMethod < 2" :label="t('pages.clients.clientCount')">
|
||||
<a-input-number v-model:value="form.quantity" :min="1" :max="500" />
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -245,11 +245,11 @@ async function submit() {
|
|||
<a-input-number v-model:value="form.totalGB" :min="0" :step="0.1" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.client.delayedStart')">
|
||||
<a-form-item :label="t('pages.clients.delayedStart')">
|
||||
<a-switch v-model:checked="delayedStart" @click="form.expiryTime = 0" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
||||
<a-form-item v-if="delayedStart" :label="t('pages.clients.expireDays')">
|
||||
<a-input-number v-model:value="delayedExpireDays" :min="0" />
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ async function submit() {
|
|||
|
||||
<a-form-item v-if="form.expiryTime !== 0">
|
||||
<template #label>
|
||||
<a-tooltip :title="t('pages.client.renewDesc')">{{ t('pages.client.renew') }}</a-tooltip>
|
||||
<a-tooltip :title="t('pages.clients.renewDesc')">{{ t('pages.clients.renew') }}</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model:value="form.reset" :min="0" />
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -230,13 +230,13 @@ async function submit() {
|
|||
}
|
||||
|
||||
const title = computed(() =>
|
||||
props.mode === 'edit' ? t('pages.client.edit') : t('pages.client.add'),
|
||||
props.mode === 'edit' ? t('pages.clients.edit') : t('pages.clients.add'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="title"
|
||||
:ok-text="mode === 'edit' ? t('pages.client.submitEdit') : t('pages.client.submitAdd')" :cancel-text="t('close')"
|
||||
:ok-text="mode === 'edit' ? t('pages.clients.submitEdit') : t('pages.clients.submitAdd')" :cancel-text="t('close')"
|
||||
:confirm-loading="saving" :mask-closable="false" @ok="submit" @cancel="close">
|
||||
<a-tag v-if="mode === 'edit' && (isExpired || isTrafficExhausted)" color="red" class="status-banner">
|
||||
{{ t('depleted') }}
|
||||
|
|
@ -351,11 +351,11 @@ const title = computed(() =>
|
|||
</a-tooltip>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('pages.client.delayedStart')">
|
||||
<a-form-item :label="t('pages.clients.delayedStart')">
|
||||
<a-switch v-model:checked="delayedStart" @click="client.expiryTime = 0" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="delayedStart" :label="t('pages.client.expireDays')">
|
||||
<a-form-item v-if="delayedStart" :label="t('pages.clients.expireDays')">
|
||||
<a-input-number v-model:value="delayedExpireDays" :min="0" />
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -370,7 +370,7 @@ const title = computed(() =>
|
|||
|
||||
<a-form-item v-if="client.expiryTime !== 0">
|
||||
<template #label>
|
||||
<a-tooltip :title="t('pages.client.renewDesc')">{{ t('pages.client.renew') }}</a-tooltip>
|
||||
<a-tooltip :title="t('pages.clients.renewDesc')">{{ t('pages.clients.renew') }}</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model:value="client.reset" :min="0" />
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -264,8 +264,7 @@ function confirmBulkDelete() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="client-list"
|
||||
:class="{ 'is-mobile': isMobile, 'is-dark': isDarkTheme, 'has-select': isRemovable }">
|
||||
<div class="client-list" :class="{ 'is-mobile': isMobile, 'is-dark': isDarkTheme, 'has-select': isRemovable }">
|
||||
<div v-if="isRemovable && selectedCount > 0" class="bulk-bar">
|
||||
<span class="bulk-count">{{ selectedCount }} selected</span>
|
||||
<a-button size="small" type="link" @click="clearSelection">{{ t('cancel') }}</a-button>
|
||||
|
|
@ -397,7 +396,7 @@ function confirmBulkDelete() {
|
|||
<template v-if="client.expiryTime !== 0 && client.reset > 0">
|
||||
<a-popover>
|
||||
<template #content>
|
||||
<span v-if="client.expiryTime < 0">{{ t('pages.client.delayedStart') }}</span>
|
||||
<span v-if="client.expiryTime < 0">{{ t('pages.clients.delayedStart') }}</span>
|
||||
<span v-else>{{ IntlUtil.formatDate(client.expiryTime, datepicker) }}</span>
|
||||
</template>
|
||||
<div class="usage-bar">
|
||||
|
|
@ -410,7 +409,7 @@ function confirmBulkDelete() {
|
|||
</template>
|
||||
<a-popover v-else-if="client.expiryTime !== 0">
|
||||
<template #content>
|
||||
<span v-if="client.expiryTime < 0">{{ t('pages.client.delayedStart') }}</span>
|
||||
<span v-if="client.expiryTime < 0">{{ t('pages.clients.delayedStart') }}</span>
|
||||
<span v-else>{{ IntlUtil.formatDate(client.expiryTime) }}</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }"
|
||||
|
|
@ -506,7 +505,8 @@ function confirmBulkDelete() {
|
|||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">{{ t('online') }}</span>
|
||||
<a-tag v-if="statsClient.enable && isClientOnline(statsClient.email)" color="green">{{ t('online') }}</a-tag>
|
||||
<a-tag v-if="statsClient.enable && isClientOnline(statsClient.email)" color="green">{{ t('online')
|
||||
}}</a-tag>
|
||||
<a-tag v-else>{{ t('offline') }}</a-tag>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
|
|
@ -516,7 +516,7 @@ function confirmBulkDelete() {
|
|||
{{ IntlUtil.formatRelativeTime(statsClient.expiryTime) }}
|
||||
</a-tag>
|
||||
<a-tag v-else-if="statsClient.expiryTime < 0" color="green">
|
||||
{{ -statsClient.expiryTime / 86400000 }}d ({{ t('pages.client.delayedStart') }})
|
||||
{{ -statsClient.expiryTime / 86400000 }}d ({{ t('pages.clients.delayedStart') }})
|
||||
</a-tag>
|
||||
<a-tag v-else color="purple">
|
||||
<InfinityIcon />
|
||||
|
|
@ -526,9 +526,8 @@ function confirmBulkDelete() {
|
|||
</a-modal>
|
||||
</template>
|
||||
|
||||
<a-pagination v-if="pageSize > 0 && clients.length > pageSize" v-model:current="currentPage"
|
||||
:page-size="pageSize" :total="clients.length" :show-size-changer="false" size="small"
|
||||
class="client-list-pagination" />
|
||||
<a-pagination v-if="pageSize > 0 && clients.length > pageSize" v-model:current="currentPage" :page-size="pageSize"
|
||||
:total="clients.length" :show-size-changer="false" size="small" class="client-list-pagination" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -82,9 +82,9 @@ const rowSelection = computed(() => ({
|
|||
}));
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.dbInbound) return t('pages.client.copyFromInbound');
|
||||
if (!props.dbInbound) return t('pages.clients.copyFromInbound');
|
||||
const target = props.dbInbound.remark || `#${props.dbInbound.id}`;
|
||||
return `${t('pages.client.copyToInbound')} ${target}`;
|
||||
return `${t('pages.clients.copyToInbound')} ${target}`;
|
||||
});
|
||||
|
||||
watch(() => props.open, (next) => {
|
||||
|
|
@ -108,7 +108,7 @@ function clearAll() {
|
|||
|
||||
async function ok() {
|
||||
if (!sourceInboundId.value) {
|
||||
message.error(t('pages.client.copySelectSourceFirst'));
|
||||
message.error(t('pages.clients.copySelectSourceFirst'));
|
||||
return;
|
||||
}
|
||||
if (!props.dbInbound) return;
|
||||
|
|
@ -128,12 +128,12 @@ async function ok() {
|
|||
const addedCount = (obj.added || []).length;
|
||||
const errorList = obj.errors || [];
|
||||
if (addedCount > 0) {
|
||||
message.success(`${t('pages.client.copyResultSuccess')}: ${addedCount}`);
|
||||
message.success(`${t('pages.clients.copyResultSuccess')}: ${addedCount}`);
|
||||
} else {
|
||||
message.warning(t('pages.client.copyResultNone'));
|
||||
message.warning(t('pages.clients.copyResultNone'));
|
||||
}
|
||||
if (errorList.length > 0) {
|
||||
message.error(`${t('pages.client.copyResultErrors')}: ${errorList.join('; ')}`);
|
||||
message.error(`${t('pages.clients.copyResultErrors')}: ${errorList.join('; ')}`);
|
||||
}
|
||||
emit('saved');
|
||||
emit('update:open', false);
|
||||
|
|
@ -149,11 +149,11 @@ function close() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" :title="title" :ok-text="t('pages.client.copySelected')" :cancel-text="t('close')"
|
||||
<a-modal :open="open" :title="title" :ok-text="t('pages.clients.copySelected')" :cancel-text="t('close')"
|
||||
:confirm-loading="saving" :mask-closable="false" width="720px" @ok="ok" @cancel="close">
|
||||
<a-space direction="vertical" :style="{ width: '100%' }">
|
||||
<div>
|
||||
<div :style="{ marginBottom: '6px' }">{{ t('pages.client.copySource') }}</div>
|
||||
<div :style="{ marginBottom: '6px' }">{{ t('pages.clients.copySource') }}</div>
|
||||
<a-select v-model:value="sourceInboundId" :style="{ width: '100%' }" allow-clear>
|
||||
<a-select-option v-for="item in sources" :key="item.id" :value="item.id">
|
||||
{{ item.label }}
|
||||
|
|
@ -163,21 +163,21 @@ function close() {
|
|||
|
||||
<div v-if="sourceInboundId">
|
||||
<a-space :style="{ marginBottom: '8px' }">
|
||||
<a-button size="small" @click="selectAll">{{ t('pages.client.selectAll') }}</a-button>
|
||||
<a-button size="small" @click="clearAll">{{ t('pages.client.clearAll') }}</a-button>
|
||||
<a-button size="small" @click="selectAll">{{ t('pages.clients.selectAll') }}</a-button>
|
||||
<a-button size="small" @click="clearAll">{{ t('pages.clients.clearAll') }}</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="columns" :data-source="sourceClients" :pagination="false" size="small"
|
||||
:row-key="(r) => r.email" :row-selection="rowSelection" :scroll="{ y: 280 }" />
|
||||
</div>
|
||||
|
||||
<div v-if="showFlow">
|
||||
<div :style="{ marginBottom: '6px' }">{{ t('pages.client.copyFlowLabel') }}</div>
|
||||
<div :style="{ marginBottom: '6px' }">{{ t('pages.clients.copyFlowLabel') }}</div>
|
||||
<a-select v-model:value="flow" :style="{ width: '100%' }" allow-clear>
|
||||
<a-select-option value="">{{ t('none') }}</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_OPTIONS" :key="key" :value="key">{{ key }}</a-select-option>
|
||||
</a-select>
|
||||
<div :style="{ marginTop: '4px', fontSize: '12px', opacity: 0.7 }">
|
||||
{{ t('pages.client.copyFlowHint') }}
|
||||
{{ t('pages.clients.copyFlowHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</a-space>
|
||||
|
|
|
|||
|
|
@ -823,7 +823,7 @@ const title = computed(() =>
|
|||
: t('pages.inbounds.addInbound'),
|
||||
);
|
||||
const okText = computed(() =>
|
||||
props.mode === 'edit' ? t('pages.client.submitEdit') : t('create'),
|
||||
props.mode === 'edit' ? t('pages.clients.submitEdit') : t('create'),
|
||||
);
|
||||
|
||||
// Whenever the structured form mutates stream / sniffing / settings,
|
||||
|
|
@ -1011,8 +1011,8 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
</template>
|
||||
|
||||
<!-- VLess decryption / encryption -->
|
||||
<a-form v-if="isVlessLike" :colon="false" :label-col="{ sm: { span: 8 } }"
|
||||
:wrapper-col="{ sm: { span: 14 } }" class="mt-12">
|
||||
<a-form v-if="isVlessLike" :colon="false" :label-col="{ sm: { span: 8 } }" :wrapper-col="{ sm: { span: 14 } }"
|
||||
class="mt-12">
|
||||
<a-form-item label="Decryption">
|
||||
<a-input v-model:value="inbound.settings.decryption" />
|
||||
</a-form-item>
|
||||
|
|
@ -1038,11 +1038,10 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
|
|||
<a-card v-if="protocol === Protocols.PORTFALLBACK" size="small" class="mt-12"
|
||||
:title="t('pages.inbounds.portFallback.title') || 'Fallback children'">
|
||||
<a-typography-paragraph type="secondary">
|
||||
{{ t('pages.inbounds.portFallback.help')
|
||||
|| 'Pick inbounds that should catch traffic this VLESS-TLS inbound does not match. Each child must listen on 127.0.0.1 to receive forwarded connections.' }}
|
||||
{{ t('pages.inbounds.portFallback.help') || 'Pick inbounds that should catch traffic this VLESS-TLS inbound does not match. Each child must listen on 127.0.0.1 to receive forwarded connections.' }}
|
||||
</a-typography-paragraph>
|
||||
<a-table :columns="fallbackChildColumns" :data-source="fallbackChildren" row-key="rowKey"
|
||||
size="small" :pagination="false">
|
||||
<a-table :columns="fallbackChildColumns" :data-source="fallbackChildren" row-key="rowKey" size="small"
|
||||
:pagination="false">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'childId'">
|
||||
<a-select v-model:value="record.childId" :options="fallbackChildOptions" :show-search="true"
|
||||
|
|
|
|||
|
|
@ -420,13 +420,13 @@ function showQrCodeMenu(dbInbound) {
|
|||
</a-menu-item>
|
||||
<template v-if="record.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<UserAddOutlined /> {{ t('pages.client.add') }}
|
||||
<UserAddOutlined /> {{ t('pages.clients.add') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<UsergroupAddOutlined /> {{ t('pages.client.bulk') }}
|
||||
<UsergroupAddOutlined /> {{ t('pages.clients.bulk') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="copyClients">
|
||||
<CopyOutlined /> {{ t('pages.client.copyFromInbound') }}
|
||||
<CopyOutlined /> {{ t('pages.clients.copyFromInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}
|
||||
|
|
@ -469,12 +469,10 @@ function showQrCodeMenu(dbInbound) {
|
|||
<ClientRowTable :db-inbound="record" :is-mobile="true" :traffic-diff="trafficDiff" :expire-diff="expireDiff"
|
||||
:online-clients="onlineClients" :last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme"
|
||||
:page-size="pageSize" :total-client-count="clientCount[record.id]?.clients || 0"
|
||||
:stats-version="statsVersion"
|
||||
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
|
||||
@info-client="(p) => emit('info-client', p)"
|
||||
:stats-version="statsVersion" @edit-client="(p) => emit('edit-client', p)"
|
||||
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-client', p)"
|
||||
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||
@delete-client="(p) => emit('delete-client', p)"
|
||||
@delete-clients="(p) => emit('delete-clients', p)"
|
||||
@delete-client="(p) => emit('delete-client', p)" @delete-clients="(p) => emit('delete-clients', p)"
|
||||
@toggle-enable-client="(p) => emit('toggle-enable-client', p)" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -558,13 +556,10 @@ function showQrCodeMenu(dbInbound) {
|
|||
<ClientRowTable v-if="record.isMultiUser()" :db-inbound="record" :is-mobile="isMobile"
|
||||
:traffic-diff="trafficDiff" :expire-diff="expireDiff" :online-clients="onlineClients"
|
||||
:last-online-map="lastOnlineMap" :is-dark-theme="isDarkTheme" :page-size="pageSize"
|
||||
:total-client-count="clientCount[record.id]?.clients || 0"
|
||||
:stats-version="statsVersion"
|
||||
@edit-client="(p) => emit('edit-client', p)"
|
||||
@qrcode-client="(p) => emit('qrcode-client', p)" @info-client="(p) => emit('info-client', p)"
|
||||
@reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||
@delete-client="(p) => emit('delete-client', p)"
|
||||
@delete-clients="(p) => emit('delete-clients', p)"
|
||||
:total-client-count="clientCount[record.id]?.clients || 0" :stats-version="statsVersion"
|
||||
@edit-client="(p) => emit('edit-client', p)" @qrcode-client="(p) => emit('qrcode-client', p)"
|
||||
@info-client="(p) => emit('info-client', p)" @reset-traffic-client="(p) => emit('reset-traffic-client', p)"
|
||||
@delete-client="(p) => emit('delete-client', p)" @delete-clients="(p) => emit('delete-clients', p)"
|
||||
@toggle-enable-client="(p) => emit('toggle-enable-client', p)" />
|
||||
</template>
|
||||
|
||||
|
|
@ -572,7 +567,7 @@ function showQrCodeMenu(dbInbound) {
|
|||
<!-- ============== Action dropdown ============== -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<div class="action-buttons">
|
||||
<a-button type="text" size="small" @click.prevent="emit('row-action', {key: 'edit', dbInbound: record})">
|
||||
<a-button type="text" size="small" @click.prevent="emit('row-action', { key: 'edit', dbInbound: record })">
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
|
|
@ -591,13 +586,13 @@ function showQrCodeMenu(dbInbound) {
|
|||
</a-menu-item>
|
||||
<template v-if="record.isMultiUser()">
|
||||
<a-menu-item key="addClient">
|
||||
<UserAddOutlined /> {{ t('pages.client.add') }}
|
||||
<UserAddOutlined /> {{ t('pages.clients.add') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<UsergroupAddOutlined /> {{ t('pages.client.bulk') }}
|
||||
<UsergroupAddOutlined /> {{ t('pages.clients.bulk') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="copyClients">
|
||||
<CopyOutlined /> {{ t('pages.client.copyFromInbound') }}
|
||||
<CopyOutlined /> {{ t('pages.clients.copyFromInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}
|
||||
|
|
@ -671,14 +666,17 @@ function showQrCodeMenu(dbInbound) {
|
|||
<!-- ============== Clients tag + popovers ============== -->
|
||||
<template v-else-if="column.key === 'clients'">
|
||||
<template v-if="clientCount[record.id]">
|
||||
<a-tag color="green" class="client-count-tag" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].clients }}</a-tag>
|
||||
<a-tag color="green" class="client-count-tag" style="margin: 0; padding: 0 2px">{{
|
||||
clientCount[record.id].clients }}</a-tag>
|
||||
<a-popover v-if="clientCount[record.id].deactive.length" :title="t('disabled')">
|
||||
<template #content>
|
||||
<div class="client-email-list">
|
||||
<div v-for="email in clientCount[record.id].deactive" :key="email">{{ email }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-tag class="client-count-tag" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].deactive.length }}</a-tag>
|
||||
<a-tag class="client-count-tag" style="margin: 0; padding: 0 2px">{{
|
||||
clientCount[record.id].deactive.length
|
||||
}}</a-tag>
|
||||
</a-popover>
|
||||
<a-popover v-if="clientCount[record.id].depleted.length" :title="t('depleted')">
|
||||
<template #content>
|
||||
|
|
@ -686,8 +684,9 @@ function showQrCodeMenu(dbInbound) {
|
|||
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-tag color="red" class="client-count-tag" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].depleted.length
|
||||
}}</a-tag>
|
||||
<a-tag color="red" class="client-count-tag" style="margin: 0; padding: 0 2px">{{
|
||||
clientCount[record.id].depleted.length
|
||||
}}</a-tag>
|
||||
</a-popover>
|
||||
<a-popover v-if="clientCount[record.id].expiring.length" :title="t('depletingSoon')">
|
||||
<template #content>
|
||||
|
|
@ -695,8 +694,9 @@ function showQrCodeMenu(dbInbound) {
|
|||
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-tag color="orange" class="client-count-tag" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].expiring.length
|
||||
}}</a-tag>
|
||||
<a-tag color="orange" class="client-count-tag" style="margin: 0; padding: 0 2px">{{
|
||||
clientCount[record.id].expiring.length
|
||||
}}</a-tag>
|
||||
</a-popover>
|
||||
<a-popover v-if="clientCount[record.id].online.length" :title="t('online')">
|
||||
<template #content>
|
||||
|
|
@ -704,7 +704,8 @@ function showQrCodeMenu(dbInbound) {
|
|||
<div v-for="email in clientCount[record.id].online" :key="email">{{ email }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-tag color="blue" class="client-count-tag" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].online.length }}</a-tag>
|
||||
<a-tag color="blue" class="client-count-tag" style="margin: 0; padding: 0 2px">{{
|
||||
clientCount[record.id].online.length }}</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
</template>
|
||||
|
|
@ -770,7 +771,7 @@ function showQrCodeMenu(dbInbound) {
|
|||
}
|
||||
|
||||
.filter-bar.mobile>* {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ const title = computed(() =>
|
|||
: `+ ${t('pages.xray.Balancers')}`,
|
||||
);
|
||||
const okText = computed(() =>
|
||||
isEdit.value ? t('pages.client.submitEdit') : t('create'),
|
||||
isEdit.value ? t('pages.clients.submitEdit') : t('create'),
|
||||
);
|
||||
</script>
|
||||
|
||||
|
|
@ -121,8 +121,7 @@ const okText = computed(() =>
|
|||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Fallback"
|
||||
:help="fallbackSupported ? '' : 'Available only with Least ping / Least load'">
|
||||
<a-form-item label="Fallback" :help="fallbackSupported ? '' : 'Available only with Least ping / Least load'">
|
||||
<a-select v-model:value="form.fallbackTag" allow-clear :disabled="!fallbackSupported">
|
||||
<a-select-option v-for="tag in ['', ...outboundTags]" :key="tag || '__empty'" :value="tag">
|
||||
{{ tag || `(${t('none')})` }}
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ const title = computed(() =>
|
|||
: `+ ${t('pages.xray.Outbounds')}`,
|
||||
);
|
||||
const okText = computed(() =>
|
||||
isEdit.value ? t('pages.client.submitEdit') : t('create'),
|
||||
isEdit.value ? t('pages.clients.submitEdit') : t('create'),
|
||||
);
|
||||
|
||||
// Helper getters / shortcuts used by the template.
|
||||
|
|
@ -343,8 +343,7 @@ function regenerateWgKeys() {
|
|||
<!-- ============== Loopback ============== -->
|
||||
<template v-if="isLoopback">
|
||||
<a-form-item label="Inbound tag">
|
||||
<a-input v-model:value="outbound.settings.inboundTag"
|
||||
placeholder="inbound tag using in routing rules" />
|
||||
<a-input v-model:value="outbound.settings.inboundTag" placeholder="inbound tag using in routing rules" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ const title = computed(() =>
|
|||
: `+ ${t('pages.xray.Routings')}`,
|
||||
);
|
||||
const okText = computed(() =>
|
||||
isEdit.value ? t('pages.client.submitEdit') : t('create'),
|
||||
isEdit.value ? t('pages.clients.submitEdit') : t('create'),
|
||||
);
|
||||
|
||||
const NETWORKS = ['', 'TCP', 'UDP', 'TCP,UDP'];
|
||||
|
|
@ -248,7 +248,7 @@ const PROTOCOLS = ['http', 'tls', 'bittorrent', 'quic'];
|
|||
<a-form-item label="Outbound tag">
|
||||
<a-select v-model:value="form.outboundTag">
|
||||
<a-select-option v-for="tag in outboundTags" :key="tag || '__empty'" :value="tag">{{ tag || '(none)'
|
||||
}}</a-select-option>
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ const PROTOCOLS = ['http', 'tls', 'bittorrent', 'quic'];
|
|||
</template>
|
||||
<a-select v-model:value="form.balancerTag">
|
||||
<a-select-option v-for="tag in balancerTags" :key="tag || '__empty'" :value="tag">{{ tag || '(none)'
|
||||
}}</a-select-option>
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/del/:id", a.delete)
|
||||
g.POST("/:id/attach", a.attach)
|
||||
g.POST("/:id/detach", a.detach)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
}
|
||||
|
||||
func (a *ClientController) list(c *gin.Context) {
|
||||
|
|
@ -142,6 +143,18 @@ func (a *ClientController) attach(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *ClientController) resetAllTraffics(c *gin.Context) {
|
||||
needRestart, err := a.clientService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllClientTrafficSuccess"), nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ClientController) detach(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -475,6 +475,16 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
|
|||
return needRestart, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) ResetAllTraffics() (bool, error) {
|
||||
res := database.GetDB().Model(&xray.ClientTraffic{}).
|
||||
Where("1 = 1").
|
||||
Updates(map[string]any{"up": 0, "down": 0})
|
||||
if res.Error != nil {
|
||||
return false, res.Error
|
||||
}
|
||||
return res.RowsAffected > 0, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) Detach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) {
|
||||
existing, err := s.GetByID(id)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"client": {
|
||||
"clients": {
|
||||
"add": "Add Client",
|
||||
"edit": "Edit Client",
|
||||
"submitAdd": "Add Client",
|
||||
|
|
@ -402,19 +402,47 @@
|
|||
"expireDays": "Duration",
|
||||
"days": "Day(s)",
|
||||
"renew": "Auto Renew",
|
||||
"renewDesc": "Auto-renewal after expiration. (0 = disable)(unit: day)"
|
||||
},
|
||||
"clients": {
|
||||
"renewDesc": "Auto-renewal after expiration. (0 = disable)(unit: day)",
|
||||
"title": "Clients",
|
||||
"actions": "Actions",
|
||||
"totalGB": "Total Sent/Received (GB)",
|
||||
"expiryTime": "Expiry",
|
||||
"addClients": "Add Clients",
|
||||
"limitIp": "IP Limit",
|
||||
"password": "Password",
|
||||
"subId": "Subscription ID",
|
||||
"online": "Online",
|
||||
"email": "Email",
|
||||
"comment": "Comment",
|
||||
"traffic": "Traffic",
|
||||
"offline": "Offline",
|
||||
"addTitle": "Add Client",
|
||||
"qrCode": "QR Code",
|
||||
"info": "Info",
|
||||
"delete": "Delete",
|
||||
"reset": "Reset Traffic",
|
||||
"editTitle": "Edit Client",
|
||||
"client": "Client",
|
||||
"enabled": "Enabled",
|
||||
"remaining": "Remaining",
|
||||
"duration": "Duration",
|
||||
"attachedInbounds": "Attached inbounds",
|
||||
"selectInbound": "Select one or more inbounds",
|
||||
"noSubId": "This client has no subId, no shareable link.",
|
||||
"noLinks": "No shareable links — attach this client to a protocol-capable inbound first.",
|
||||
"link": "Link",
|
||||
"resetNotPossible": "Attach this client to an inbound first.",
|
||||
"general": "General",
|
||||
"resetAllTraffics": "Reset all client traffic",
|
||||
"resetAllTrafficsTitle": "Reset all client traffic?",
|
||||
"resetAllTrafficsContent": "Every client's up/down counter drops to zero. Quotas and expiry are not affected. This cannot be undone.",
|
||||
"empty": "No clients yet — add one to get started.",
|
||||
"deleteConfirmTitle": "Delete client {email}?",
|
||||
"deleteConfirmContent": "This removes the client from every attached inbound and drops its traffic record. This cannot be undone.",
|
||||
"toasts": {
|
||||
"deleted": "Client deleted"
|
||||
"deleted": "Client deleted",
|
||||
"trafficReset": "Traffic reset",
|
||||
"allTrafficsReset": "All client traffic reset"
|
||||
}
|
||||
},
|
||||
"nodes": {
|
||||
|
|
@ -1000,4 +1028,4 @@
|
|||
"chooseInbound": "Choose an Inbound"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue