mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards
Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
35efeb983e
commit
e7d117f11f
8 changed files with 136 additions and 107 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
|
@ -13,6 +14,8 @@ import {
|
||||||
import { currentTheme } from '@/composables/useTheme.js';
|
import { currentTheme } from '@/composables/useTheme.js';
|
||||||
import ThemeSwitch from '@/components/ThemeSwitch.vue';
|
import ThemeSwitch from '@/components/ThemeSwitch.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -42,13 +45,15 @@ const iconByName = {
|
||||||
// would turn /panel/settings + 'panel/...' into /panel/panel/...).
|
// would turn /panel/settings + 'panel/...' into /panel/panel/...).
|
||||||
const prefix = props.basePath?.startsWith('/') ? props.basePath : `/${props.basePath || ''}`;
|
const prefix = props.basePath?.startsWith('/') ? props.basePath : `/${props.basePath || ''}`;
|
||||||
|
|
||||||
const tabs = [
|
// Labels are i18n-driven so the sidebar matches the locale picked
|
||||||
{ key: `${prefix}panel/`, icon: 'dashboard', title: 'Dashboard' },
|
// in panel settings without a page reload of the sidebar component.
|
||||||
{ key: `${prefix}panel/inbounds`, icon: 'user', title: 'Inbounds' },
|
const tabs = computed(() => [
|
||||||
{ key: `${prefix}panel/settings`, icon: 'setting', title: 'Settings' },
|
{ key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') },
|
||||||
{ key: `${prefix}panel/xray`, icon: 'tool', title: 'Xray' },
|
{ key: `${prefix}panel/inbounds`, icon: 'user', title: t('menu.inbounds') },
|
||||||
{ key: `${prefix}logout/`, icon: 'logout', title: 'Logout' },
|
{ key: `${prefix}panel/settings`, icon: 'setting', title: t('menu.settings') },
|
||||||
];
|
{ key: `${prefix}panel/xray`, icon: 'tool', title: t('menu.xray') },
|
||||||
|
{ key: `${prefix}logout/`, icon: 'logout', title: t('logout') },
|
||||||
|
]);
|
||||||
|
|
||||||
const activeTab = ref([props.requestUri]);
|
const activeTab = ref([props.requestUri]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
|
|
@ -29,6 +30,8 @@ import { DBInbound } from '@/models/dbinbound.js';
|
||||||
import { Inbound } from '@/models/inbound.js';
|
import { Inbound } from '@/models/inbound.js';
|
||||||
import ClientRowTable from './ClientRowTable.vue';
|
import ClientRowTable from './ClientRowTable.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dbInbounds: { type: Array, required: true },
|
dbInbounds: { type: Array, required: true },
|
||||||
clientCount: { type: Object, required: true },
|
clientCount: { type: Object, required: true },
|
||||||
|
|
@ -136,26 +139,27 @@ const visibleInbounds = computed(() => {
|
||||||
|
|
||||||
// ============ Columns =================================================
|
// ============ Columns =================================================
|
||||||
// `key`-driven so we can render via the body-cell slot below. AD-Vue 4's
|
// `key`-driven so we can render via the body-cell slot below. AD-Vue 4's
|
||||||
// `responsive` array still works on column defs.
|
// `responsive` array still works on column defs. Computed so column
|
||||||
const desktopColumns = [
|
// labels react to live locale switches.
|
||||||
|
const desktopColumns = computed(() => [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30, responsive: ['xs'] },
|
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30, responsive: ['xs'] },
|
||||||
{ title: 'Action', key: 'action', align: 'center', width: 30 },
|
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 30 },
|
||||||
{ title: 'Enable', key: 'enable', align: 'center', width: 35 },
|
{ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 },
|
||||||
{ title: 'Remark', dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
|
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
|
||||||
{ title: 'Port', dataIndex: 'port', key: 'port', align: 'center', width: 40 },
|
{ title: t('pages.inbounds.port'), dataIndex: 'port', key: 'port', align: 'center', width: 40 },
|
||||||
{ title: 'Protocol', key: 'protocol', align: 'left', width: 70 },
|
{ title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 70 },
|
||||||
{ title: 'Clients', key: 'clients', align: 'left', width: 50 },
|
{ title: t('clients'), key: 'clients', align: 'left', width: 50 },
|
||||||
{ title: 'Traffic', key: 'traffic', align: 'center', width: 90 },
|
{ title: t('pages.inbounds.traffic'), key: 'traffic', align: 'center', width: 90 },
|
||||||
{ title: 'All-time', key: 'allTimeInbound', align: 'center', width: 60 },
|
{ title: t('pages.inbounds.allTimeTraffic'), key: 'allTimeInbound', align: 'center', width: 60 },
|
||||||
{ title: 'Expiry', key: 'expiryTime', align: 'center', width: 40 },
|
{ title: t('pages.inbounds.expireDate'), key: 'expiryTime', align: 'center', width: 40 },
|
||||||
];
|
]);
|
||||||
const mobileColumns = [
|
const mobileColumns = computed(() => [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 10, responsive: ['s'] },
|
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 10, responsive: ['s'] },
|
||||||
{ title: 'Action', key: 'action', align: 'center', width: 25 },
|
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 25 },
|
||||||
{ title: 'Remark', dataIndex: 'remark', key: 'remark', align: 'left', width: 70 },
|
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'left', width: 70 },
|
||||||
{ title: 'Info', key: 'info', align: 'center', width: 10 },
|
{ title: t('info'), key: 'info', align: 'center', width: 10 },
|
||||||
];
|
]);
|
||||||
const columns = computed(() => (props.isMobile ? mobileColumns : desktopColumns));
|
const columns = computed(() => (props.isMobile ? mobileColumns.value : desktopColumns.value));
|
||||||
|
|
||||||
// ============ Pagination ============================================
|
// ============ Pagination ============================================
|
||||||
function paginationFor(rows) {
|
function paginationFor(rows) {
|
||||||
|
|
@ -208,32 +212,32 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" @click="emit('add-inbound')">
|
<a-button type="primary" @click="emit('add-inbound')">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon><PlusOutlined /></template>
|
||||||
<template v-if="!isMobile">Add inbound</template>
|
<template v-if="!isMobile">{{ t('pages.inbounds.addInbound') }}</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a-button type="primary">
|
<a-button type="primary">
|
||||||
<template #icon><MenuOutlined /></template>
|
<template #icon><MenuOutlined /></template>
|
||||||
<template v-if="!isMobile">General actions</template>
|
<template v-if="!isMobile">{{ t('pages.inbounds.generalActions') }}</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="(a) => emit('general-action', a.key)">
|
<a-menu @click="(a) => emit('general-action', a.key)">
|
||||||
<a-menu-item key="import">
|
<a-menu-item key="import">
|
||||||
<ImportOutlined /> Import inbound
|
<ImportOutlined /> {{ t('pages.inbounds.importInbound') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="export">
|
<a-menu-item key="export">
|
||||||
<ExportOutlined /> Export
|
<ExportOutlined /> {{ t('pages.inbounds.export') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item v-if="subEnable" key="subs">
|
<a-menu-item v-if="subEnable" key="subs">
|
||||||
<ExportOutlined /> Export — Subscription
|
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="resetInbounds">
|
<a-menu-item key="resetInbounds">
|
||||||
<ReloadOutlined /> Reset all traffic
|
<ReloadOutlined /> {{ t('pages.inbounds.resetAllTraffic') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="resetClients">
|
<a-menu-item key="resetClients">
|
||||||
<FileDoneOutlined /> Reset all client traffic
|
<FileDoneOutlined /> {{ t('pages.inbounds.resetAllClientTraffics') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="delDepletedClients" class="danger-item">
|
<a-menu-item key="delDepletedClients" class="danger-item">
|
||||||
<RestOutlined /> Delete depleted clients
|
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -250,12 +254,12 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="auto-refresh-title">
|
<div class="auto-refresh-title">
|
||||||
<a-switch v-model:checked="isRefreshEnabled" size="small" />
|
<a-switch v-model:checked="isRefreshEnabled" size="small" />
|
||||||
<span>Auto refresh</span>
|
<span>{{ t('pages.inbounds.autoRefresh') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-space direction="vertical">
|
<a-space direction="vertical">
|
||||||
<span>Auto-refresh interval</span>
|
<span>{{ t('pages.inbounds.autoRefreshInterval') }}</span>
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="refreshIntervalMs"
|
v-model:value="refreshIntervalMs"
|
||||||
:disabled="!isRefreshEnabled"
|
:disabled="!isRefreshEnabled"
|
||||||
|
|
@ -284,7 +288,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<a-input
|
<a-input
|
||||||
v-if="!enableFilter"
|
v-if="!enableFilter"
|
||||||
v-model:value="searchKey"
|
v-model:value="searchKey"
|
||||||
placeholder="Search"
|
:placeholder="t('search')"
|
||||||
autofocus
|
autofocus
|
||||||
:size="isMobile ? 'small' : 'middle'"
|
:size="isMobile ? 'small' : 'middle'"
|
||||||
:style="{ maxWidth: '300px' }"
|
:style="{ maxWidth: '300px' }"
|
||||||
|
|
@ -295,12 +299,12 @@ function showQrCodeMenu(dbInbound) {
|
||||||
button-style="solid"
|
button-style="solid"
|
||||||
:size="isMobile ? 'small' : 'middle'"
|
:size="isMobile ? 'small' : 'middle'"
|
||||||
>
|
>
|
||||||
<a-radio-button value="">None</a-radio-button>
|
<a-radio-button value="">{{ t('none') }}</a-radio-button>
|
||||||
<a-radio-button value="active">Active</a-radio-button>
|
<a-radio-button value="active">{{ t('subscription.active') }}</a-radio-button>
|
||||||
<a-radio-button value="deactive">Disabled</a-radio-button>
|
<a-radio-button value="deactive">{{ t('disabled') }}</a-radio-button>
|
||||||
<a-radio-button value="depleted">Depleted</a-radio-button>
|
<a-radio-button value="depleted">{{ t('depleted') }}</a-radio-button>
|
||||||
<a-radio-button value="expiring">Depleting</a-radio-button>
|
<a-radio-button value="expiring">{{ t('depletingSoon') }}</a-radio-button>
|
||||||
<a-radio-button value="online">Online</a-radio-button>
|
<a-radio-button value="online">{{ t('online') }}</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -343,31 +347,31 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<MoreOutlined class="row-action-trigger" @click.prevent />
|
<MoreOutlined class="row-action-trigger" @click.prevent />
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
<a-menu @click="(a) => emit('row-action', { key: a.key, dbInbound: record })">
|
||||||
<a-menu-item key="edit"><EditOutlined /> Edit</a-menu-item>
|
<a-menu-item key="edit"><EditOutlined /> {{ t('edit') }}</a-menu-item>
|
||||||
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
<a-menu-item v-if="showQrCodeMenu(record)" key="qrcode">
|
||||||
<QrcodeOutlined /> QR code
|
<QrcodeOutlined /> {{ t('qrCode') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<template v-if="record.isMultiUser()">
|
<template v-if="record.isMultiUser()">
|
||||||
<a-menu-item key="addClient"><UserAddOutlined /> Add client</a-menu-item>
|
<a-menu-item key="addClient"><UserAddOutlined /> {{ t('pages.client.add') }}</a-menu-item>
|
||||||
<a-menu-item key="addBulkClient"><UsergroupAddOutlined /> Add bulk clients</a-menu-item>
|
<a-menu-item key="addBulkClient"><UsergroupAddOutlined /> {{ t('pages.client.bulk') }}</a-menu-item>
|
||||||
<a-menu-item key="copyClients"><CopyOutlined /> Copy clients from inbound</a-menu-item>
|
<a-menu-item key="copyClients"><CopyOutlined /> {{ t('pages.client.copyFromInbound') }}</a-menu-item>
|
||||||
<a-menu-item key="resetClients"><FileDoneOutlined /> Reset client traffic</a-menu-item>
|
<a-menu-item key="resetClients"><FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}</a-menu-item>
|
||||||
<a-menu-item key="export"><ExportOutlined /> Export</a-menu-item>
|
<a-menu-item key="export"><ExportOutlined /> {{ t('pages.inbounds.export') }}</a-menu-item>
|
||||||
<a-menu-item v-if="subEnable" key="subs">
|
<a-menu-item v-if="subEnable" key="subs">
|
||||||
<ExportOutlined /> Export — Subscription
|
<ExportOutlined /> {{ t('pages.inbounds.export') }} — {{ t('pages.settings.subSettings') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="delDepletedClients" class="danger-item">
|
<a-menu-item key="delDepletedClients" class="danger-item">
|
||||||
<RestOutlined /> Delete depleted clients
|
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-menu-item key="showInfo"><InfoCircleOutlined /> Info</a-menu-item>
|
<a-menu-item key="showInfo"><InfoCircleOutlined /> {{ t('info') }}</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item key="clipboard"><CopyOutlined /> Export inbound</a-menu-item>
|
<a-menu-item key="clipboard"><CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}</a-menu-item>
|
||||||
<a-menu-item key="resetTraffic"><RetweetOutlined /> Reset traffic</a-menu-item>
|
<a-menu-item key="resetTraffic"><RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}</a-menu-item>
|
||||||
<a-menu-item key="clone"><BlockOutlined /> Clone</a-menu-item>
|
<a-menu-item key="clone"><BlockOutlined /> {{ t('pages.inbounds.clone') }}</a-menu-item>
|
||||||
<a-menu-item key="delete" class="danger-item">
|
<a-menu-item key="delete" class="danger-item">
|
||||||
<DeleteOutlined /> Delete
|
<DeleteOutlined /> {{ t('delete') }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -398,25 +402,25 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<template v-else-if="column.key === 'clients'">
|
<template v-else-if="column.key === 'clients'">
|
||||||
<template v-if="clientCount[record.id]">
|
<template v-if="clientCount[record.id]">
|
||||||
<a-tag color="green" style="margin: 0">{{ clientCount[record.id].clients }}</a-tag>
|
<a-tag color="green" style="margin: 0">{{ clientCount[record.id].clients }}</a-tag>
|
||||||
<a-popover v-if="clientCount[record.id].deactive.length" title="Disabled">
|
<a-popover v-if="clientCount[record.id].deactive.length" :title="t('disabled')">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].deactive" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].deactive" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
<a-tag style="margin: 0; padding: 0 2px">{{ clientCount[record.id].deactive.length }}</a-tag>
|
<a-tag style="margin: 0; padding: 0 2px">{{ clientCount[record.id].deactive.length }}</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-popover v-if="clientCount[record.id].depleted.length" title="Depleted">
|
<a-popover v-if="clientCount[record.id].depleted.length" :title="t('depleted')">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
<a-tag color="red" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].depleted.length }}</a-tag>
|
<a-tag color="red" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].depleted.length }}</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-popover v-if="clientCount[record.id].expiring.length" title="Depleting soon">
|
<a-popover v-if="clientCount[record.id].expiring.length" :title="t('depletingSoon')">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
<a-tag color="orange" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].expiring.length }}</a-tag>
|
<a-tag color="orange" style="margin: 0; padding: 0 2px">{{ clientCount[record.id].expiring.length }}</a-tag>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
<a-popover v-if="clientCount[record.id].online.length" title="Online">
|
<a-popover v-if="clientCount[record.id].online.length" :title="t('online')">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-for="email in clientCount[record.id].online" :key="email">{{ email }}</div>
|
<div v-for="email in clientCount[record.id].online" :key="email">{{ email }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -436,7 +440,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<td>↓ {{ SizeFormatter.sizeFormat(record.down) }}</td>
|
<td>↓ {{ SizeFormatter.sizeFormat(record.down) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="record.total > 0 && record.up + record.down < record.total">
|
<tr v-if="record.total > 0 && record.up + record.down < record.total">
|
||||||
<td>Remaining</td>
|
<td>{{ t('remained') }}</td>
|
||||||
<td>{{ SizeFormatter.sizeFormat(record.total - record.up - record.down) }}</td>
|
<td>{{ SizeFormatter.sizeFormat(record.total - record.up - record.down) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -476,19 +480,19 @@ function showQrCodeMenu(dbInbound) {
|
||||||
<table cellpadding="2">
|
<table cellpadding="2">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Protocol</td>
|
<td>{{ t('pages.inbounds.protocol') }}</td>
|
||||||
<td><a-tag color="purple">{{ record.protocol }}</a-tag></td>
|
<td><a-tag color="purple">{{ record.protocol }}</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Port</td>
|
<td>{{ t('pages.inbounds.port') }}</td>
|
||||||
<td><a-tag>{{ record.port }}</a-tag></td>
|
<td><a-tag>{{ record.port }}</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="clientCount[record.id]">
|
<tr v-if="clientCount[record.id]">
|
||||||
<td>Clients</td>
|
<td>{{ t('clients') }}</td>
|
||||||
<td><a-tag color="blue">{{ clientCount[record.id].clients }}</a-tag></td>
|
<td><a-tag color="blue">{{ clientCount[record.id].clients }}</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Traffic</td>
|
<td>{{ t('pages.inbounds.traffic') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag>
|
<a-tag>
|
||||||
{{ SizeFormatter.sizeFormat(record.up + record.down) }} /
|
{{ SizeFormatter.sizeFormat(record.up + record.down) }} /
|
||||||
|
|
@ -498,7 +502,7 @@ function showQrCodeMenu(dbInbound) {
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Expiry</td>
|
<td>{{ t('pages.inbounds.expireDate') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="record.expiryTime > 0">{{ IntlUtil.formatRelativeTime(record.expiryTime) }}</a-tag>
|
<a-tag v-if="record.expiryTime > 0">{{ IntlUtil.formatRelativeTime(record.expiryTime) }}</a-tag>
|
||||||
<a-tag v-else color="purple">∞</a-tag>
|
<a-tag v-else color="purple">∞</a-tag>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { theme as antdTheme, Modal, message } from 'ant-design-vue';
|
import { theme as antdTheme, Modal, message } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
SwapOutlined,
|
SwapOutlined,
|
||||||
|
|
@ -25,6 +26,8 @@ import TextModal from '@/components/TextModal.vue';
|
||||||
import PromptModal from '@/components/PromptModal.vue';
|
import PromptModal from '@/components/PromptModal.vue';
|
||||||
import { useInbounds } from './useInbounds.js';
|
import { useInbounds } from './useInbounds.js';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const antdThemeConfig = computed(() => ({
|
const antdThemeConfig = computed(() => ({
|
||||||
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||||
}));
|
}));
|
||||||
|
|
@ -531,7 +534,7 @@ function onRowAction({ key, dbInbound }) {
|
||||||
<a-row :gutter="[16, 12]">
|
<a-row :gutter="[16, 12]">
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic
|
||||||
title="Total ↑ / ↓"
|
:title="t('pages.inbounds.totalDownUp')"
|
||||||
:value="`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`"
|
:value="`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`"
|
||||||
>
|
>
|
||||||
<template #prefix><SwapOutlined /></template>
|
<template #prefix><SwapOutlined /></template>
|
||||||
|
|
@ -539,7 +542,7 @@ function onRowAction({ key, dbInbound }) {
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic
|
||||||
title="Total usage"
|
:title="t('pages.inbounds.totalUsage')"
|
||||||
:value="SizeFormatter.sizeFormat(totals.up + totals.down)"
|
:value="SizeFormatter.sizeFormat(totals.up + totals.down)"
|
||||||
>
|
>
|
||||||
<template #prefix><PieChartOutlined /></template>
|
<template #prefix><PieChartOutlined /></template>
|
||||||
|
|
@ -547,19 +550,19 @@ function onRowAction({ key, dbInbound }) {
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic
|
<CustomStatistic
|
||||||
title="All-time traffic"
|
:title="t('pages.inbounds.allTimeTrafficUsage')"
|
||||||
:value="SizeFormatter.sizeFormat(totals.allTime)"
|
:value="SizeFormatter.sizeFormat(totals.allTime)"
|
||||||
>
|
>
|
||||||
<template #prefix><HistoryOutlined /></template>
|
<template #prefix><HistoryOutlined /></template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="12" :md="5">
|
<a-col :sm="12" :md="5">
|
||||||
<CustomStatistic title="Inbounds" :value="String(dbInbounds.length)">
|
<CustomStatistic :title="t('pages.inbounds.inboundCount')" :value="String(dbInbounds.length)">
|
||||||
<template #prefix><BarsOutlined /></template>
|
<template #prefix><BarsOutlined /></template>
|
||||||
</CustomStatistic>
|
</CustomStatistic>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="4">
|
<a-col :sm="24" :md="4">
|
||||||
<CustomStatistic title="Clients" value=" ">
|
<CustomStatistic :title="t('clients')" value=" ">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<TeamOutlined />
|
<TeamOutlined />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { theme as antdTheme } from 'ant-design-vue';
|
import { theme as antdTheme } from 'ant-design-vue';
|
||||||
import { BarsOutlined, CloudServerOutlined, CloudDownloadOutlined } from '@ant-design/icons-vue';
|
import { BarsOutlined, CloudServerOutlined, CloudDownloadOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
import { theme as themeState } from '@/composables/useTheme.js';
|
import { theme as themeState } from '@/composables/useTheme.js';
|
||||||
import { useStatus } from '@/composables/useStatus.js';
|
import { useStatus } from '@/composables/useStatus.js';
|
||||||
|
|
@ -104,29 +107,31 @@ function openVersionSwitch() { versionOpen.value = true; }
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :sm="24" :lg="12">
|
<a-col :sm="24" :lg="12">
|
||||||
<a-card title="Quick actions" hoverable>
|
<a-card :title="t('menu.link')" hoverable>
|
||||||
<template v-if="panelUpdateInfo.updateAvailable" #extra>
|
<template v-if="panelUpdateInfo.updateAvailable" #extra>
|
||||||
<a-tooltip :title="`Update panel: ${panelUpdateInfo.latestVersion}`">
|
<a-tooltip :title="`${t('pages.index.updatePanel')}: ${panelUpdateInfo.latestVersion}`">
|
||||||
<a-tag color="orange" class="update-tag" @click="panelUpdateOpen = true">
|
<a-tag color="orange" class="update-tag" @click="panelUpdateOpen = true">
|
||||||
<CloudDownloadOutlined />
|
<CloudDownloadOutlined />
|
||||||
{{ panelUpdateInfo.latestVersion }}
|
{{ panelUpdateInfo.latestVersion }}
|
||||||
<span v-if="!isMobile">Update</span>
|
<span v-if="!isMobile">{{ t('update') }}</span>
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<a-space class="action" @click="logsOpen = true">
|
<a-space class="action" @click="logsOpen = true">
|
||||||
<BarsOutlined />
|
<BarsOutlined />
|
||||||
<span v-if="!isMobile">Logs</span>
|
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space class="action" @click="backupOpen = true">
|
<a-space class="action" @click="backupOpen = true">
|
||||||
<CloudServerOutlined />
|
<CloudServerOutlined />
|
||||||
<span v-if="!isMobile">Backup</span>
|
<span v-if="!isMobile">{{ t('pages.index.backupTitle') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space class="action" @click="panelUpdateOpen = true">
|
<a-space class="action" @click="panelUpdateOpen = true">
|
||||||
<CloudDownloadOutlined />
|
<CloudDownloadOutlined />
|
||||||
<span v-if="!isMobile">
|
<span v-if="!isMobile">
|
||||||
{{ panelUpdateInfo.updateAvailable ? `Update → ${panelUpdateInfo.latestVersion}` : 'Up to date' }}
|
{{ panelUpdateInfo.updateAvailable
|
||||||
|
? `${t('update')} → ${panelUpdateInfo.latestVersion}`
|
||||||
|
: t('pages.index.panelUpToDate') }}
|
||||||
</span>
|
</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { AreaChartOutlined, HistoryOutlined } from '@ant-design/icons-vue';
|
import { AreaChartOutlined, HistoryOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
import { CPUFormatter, SizeFormatter } from '@/utils';
|
import { CPUFormatter, SizeFormatter } from '@/utils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
status: { type: Object, required: true },
|
status: { type: Object, required: true },
|
||||||
isMobile: { type: Boolean, default: false },
|
isMobile: { type: Boolean, default: false },
|
||||||
|
|
@ -25,16 +28,16 @@ defineEmits(['open-cpu-history']);
|
||||||
:percent="status.cpu.percent"
|
:percent="status.cpu.percent"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<b>CPU:</b> {{ CPUFormatter.cpuCoreFormat(status.cpuCores) }}
|
<b>{{ t('pages.index.cpu') }}:</b> {{ CPUFormatter.cpuCoreFormat(status.cpuCores) }}
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div><b>Logical processors:</b> {{ status.logicalPro }}</div>
|
<div><b>{{ t('pages.index.logicalProcessors') }}:</b> {{ status.logicalPro }}</div>
|
||||||
<div><b>Frequency:</b> {{ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) }}</div>
|
<div><b>{{ t('pages.index.frequency') }}:</b> {{ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) }}</div>
|
||||||
</template>
|
</template>
|
||||||
<AreaChartOutlined />
|
<AreaChartOutlined />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template #title>CPU history</template>
|
<template #title>{{ t('pages.index.cpu') }}</template>
|
||||||
<a-button size="small" shape="circle" class="ml-8" @click="$emit('open-cpu-history')">
|
<a-button size="small" shape="circle" class="ml-8" @click="$emit('open-cpu-history')">
|
||||||
<template #icon><HistoryOutlined /></template>
|
<template #icon><HistoryOutlined /></template>
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
@ -50,7 +53,7 @@ defineEmits(['open-cpu-history']);
|
||||||
:percent="status.mem.percent"
|
:percent="status.mem.percent"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<b>Memory:</b> {{ SizeFormatter.sizeFormat(status.mem.current) }} /
|
<b>{{ t('pages.index.memory') }}:</b> {{ SizeFormatter.sizeFormat(status.mem.current) }} /
|
||||||
{{ SizeFormatter.sizeFormat(status.mem.total) }}
|
{{ SizeFormatter.sizeFormat(status.mem.total) }}
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -68,7 +71,7 @@ defineEmits(['open-cpu-history']);
|
||||||
:percent="status.swap.percent"
|
:percent="status.swap.percent"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<b>Swap:</b> {{ SizeFormatter.sizeFormat(status.swap.current) }} /
|
<b>{{ t('pages.index.swap') }}:</b> {{ SizeFormatter.sizeFormat(status.swap.current) }} /
|
||||||
{{ SizeFormatter.sizeFormat(status.swap.total) }}
|
{{ SizeFormatter.sizeFormat(status.swap.total) }}
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -81,7 +84,7 @@ defineEmits(['open-cpu-history']);
|
||||||
:percent="status.disk.percent"
|
:percent="status.disk.percent"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<b>Storage:</b> {{ SizeFormatter.sizeFormat(status.disk.current) }} /
|
<b>{{ t('pages.index.storage') }}:</b> {{ SizeFormatter.sizeFormat(status.disk.current) }} /
|
||||||
{{ SizeFormatter.sizeFormat(status.disk.total) }}
|
{{ SizeFormatter.sizeFormat(status.disk.total) }}
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
BarsOutlined,
|
BarsOutlined,
|
||||||
PoweroffOutlined,
|
PoweroffOutlined,
|
||||||
|
|
@ -6,6 +7,8 @@ import {
|
||||||
ToolOutlined,
|
ToolOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
status: { type: Object, required: true },
|
status: { type: Object, required: true },
|
||||||
isMobile: { type: Boolean, default: false },
|
isMobile: { type: Boolean, default: false },
|
||||||
|
|
@ -30,7 +33,7 @@ function badgeAnimationClass(color) {
|
||||||
<a-card hoverable>
|
<a-card hoverable>
|
||||||
<template #title>
|
<template #title>
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<span>Xray status</span>
|
<span>{{ t('pages.index.xrayStatus') }}</span>
|
||||||
<a-tag v-if="isMobile && status.xray.version && status.xray.version !== 'Unknown'" color="green">
|
<a-tag v-if="isMobile && status.xray.version && status.xray.version !== 'Unknown'" color="green">
|
||||||
v{{ status.xray.version }}
|
v{{ status.xray.version }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
|
|
@ -50,7 +53,7 @@ function badgeAnimationClass(color) {
|
||||||
<a-popover>
|
<a-popover>
|
||||||
<template #title>
|
<template #title>
|
||||||
<a-row type="flex" align="middle" justify="space-between">
|
<a-row type="flex" align="middle" justify="space-between">
|
||||||
<a-col><span>Xray error</span></a-col>
|
<a-col><span>{{ t('pages.index.xrayStatusError') }}</span></a-col>
|
||||||
<a-col>
|
<a-col>
|
||||||
<BarsOutlined class="cursor-pointer" @click="$emit('open-logs')" />
|
<BarsOutlined class="cursor-pointer" @click="$emit('open-logs')" />
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -74,22 +77,22 @@ function badgeAnimationClass(color) {
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<a-space v-if="ipLimitEnable" direction="horizontal" class="action" @click="$emit('open-xray-logs')">
|
<a-space v-if="ipLimitEnable" direction="horizontal" class="action" @click="$emit('open-xray-logs')">
|
||||||
<BarsOutlined />
|
<BarsOutlined />
|
||||||
<span v-if="!isMobile">Logs</span>
|
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" class="action" @click="$emit('stop-xray')">
|
<a-space direction="horizontal" class="action" @click="$emit('stop-xray')">
|
||||||
<PoweroffOutlined />
|
<PoweroffOutlined />
|
||||||
<span v-if="!isMobile">Stop xray</span>
|
<span v-if="!isMobile">{{ t('pages.index.stopXray') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" class="action" @click="$emit('restart-xray')">
|
<a-space direction="horizontal" class="action" @click="$emit('restart-xray')">
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
<span v-if="!isMobile">Restart xray</span>
|
<span v-if="!isMobile">{{ t('pages.index.restartXray') }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" class="action" @click="$emit('open-version-switch')">
|
<a-space direction="horizontal" class="action" @click="$emit('open-version-switch')">
|
||||||
<ToolOutlined />
|
<ToolOutlined />
|
||||||
<span v-if="!isMobile">
|
<span v-if="!isMobile">
|
||||||
{{ status.xray.version && status.xray.version !== 'Unknown'
|
{{ status.xray.version && status.xray.version !== 'Unknown'
|
||||||
? `v${status.xray.version}`
|
? `v${status.xray.version}`
|
||||||
: 'Switch xray' }}
|
: t('pages.index.xraySwitch') }}
|
||||||
</span>
|
</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
|
@ -20,6 +21,8 @@ import TelegramTab from './TelegramTab.vue';
|
||||||
import SubscriptionGeneralTab from './SubscriptionGeneralTab.vue';
|
import SubscriptionGeneralTab from './SubscriptionGeneralTab.vue';
|
||||||
import SubscriptionFormatsTab from './SubscriptionFormatsTab.vue';
|
import SubscriptionFormatsTab from './SubscriptionFormatsTab.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const antdThemeConfig = computed(() => ({
|
const antdThemeConfig = computed(() => ({
|
||||||
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||||
}));
|
}));
|
||||||
|
|
@ -185,10 +188,10 @@ const alertVisible = ref(true);
|
||||||
<a-col :xs="24" :sm="10" class="header-actions">
|
<a-col :xs="24" :sm="10" class="header-actions">
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
||||||
Save
|
{{ t('pages.settings.save') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" danger :disabled="!saveDisabled" @click="restartPanel">
|
<a-button type="primary" danger :disabled="!saveDisabled" @click="restartPanel">
|
||||||
Restart panel
|
{{ t('pages.settings.restartPanel') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -197,7 +200,7 @@ const alertVisible = ref(true);
|
||||||
<a-alert
|
<a-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
show-icon
|
show-icon
|
||||||
message="Save before restarting — unsaved changes are dropped on restart."
|
:message="t('pages.settings.infoDesc')"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
@ -209,28 +212,28 @@ const alertVisible = ref(true);
|
||||||
<a-tab-pane key="1" class="tab-pane">
|
<a-tab-pane key="1" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
<span>Panel</span>
|
<span>{{ t('pages.settings.panelSettings') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<GeneralTab :all-setting="allSetting" />
|
<GeneralTab :all-setting="allSetting" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="2" class="tab-pane">
|
<a-tab-pane key="2" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SafetyOutlined />
|
<SafetyOutlined />
|
||||||
<span>Security</span>
|
<span>{{ t('pages.settings.securitySettings') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<SecurityTab :all-setting="allSetting" />
|
<SecurityTab :all-setting="allSetting" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="3" class="tab-pane">
|
<a-tab-pane key="3" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<MessageOutlined />
|
<MessageOutlined />
|
||||||
<span>Telegram</span>
|
<span>{{ t('pages.settings.TGBotSettings') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<TelegramTab :all-setting="allSetting" />
|
<TelegramTab :all-setting="allSetting" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="4" class="tab-pane">
|
<a-tab-pane key="4" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<CloudServerOutlined />
|
<CloudServerOutlined />
|
||||||
<span>Subscription</span>
|
<span>{{ t('pages.settings.subSettings') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<SubscriptionGeneralTab :all-setting="allSetting" />
|
<SubscriptionGeneralTab :all-setting="allSetting" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
@ -241,7 +244,7 @@ const alertVisible = ref(true);
|
||||||
>
|
>
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<CodeOutlined />
|
<CodeOutlined />
|
||||||
<span>Subscription (Formats)</span>
|
<span>{{ t('pages.settings.subSettings') }} (Formats)</span>
|
||||||
</template>
|
</template>
|
||||||
<SubscriptionFormatsTab :all-setting="allSetting" />
|
<SubscriptionFormatsTab :all-setting="allSetting" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
|
@ -24,6 +25,8 @@ import WarpModal from './WarpModal.vue';
|
||||||
import NordModal from './NordModal.vue';
|
import NordModal from './NordModal.vue';
|
||||||
import { useXraySetting } from './useXraySetting.js';
|
import { useXraySetting } from './useXraySetting.js';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
// Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing,
|
// Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing,
|
||||||
// Outbounds, Balancers, DNS) land in subsequent 6-ii…vi commits — they
|
// Outbounds, Balancers, DNS) land in subsequent 6-ii…vi commits — they
|
||||||
// each need their own tree of structured forms or a dedicated modal.
|
// each need their own tree of structured forms or a dedicated modal.
|
||||||
|
|
@ -161,10 +164,10 @@ function confirmRestart() {
|
||||||
<a-col :xs="24" :sm="14" class="header-actions">
|
<a-col :xs="24" :sm="14" class="header-actions">
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
||||||
Save
|
{{ t('pages.xray.save') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" danger :disabled="!saveDisabled" @click="confirmRestart">
|
<a-button type="primary" danger :disabled="!saveDisabled" @click="confirmRestart">
|
||||||
Restart xray
|
{{ t('pages.xray.restart') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-popover v-if="restartResult" placement="rightTop">
|
<a-popover v-if="restartResult" placement="rightTop">
|
||||||
<template #title>Xray restart output</template>
|
<template #title>Xray restart output</template>
|
||||||
|
|
@ -180,7 +183,7 @@ function confirmRestart() {
|
||||||
<a-alert
|
<a-alert
|
||||||
type="warning"
|
type="warning"
|
||||||
show-icon
|
show-icon
|
||||||
message="Save before restarting — unsaved changes are dropped on restart."
|
:message="t('pages.settings.infoDesc')"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
@ -192,7 +195,7 @@ function confirmRestart() {
|
||||||
<a-tabs default-active-key="tpl-basic">
|
<a-tabs default-active-key="tpl-basic">
|
||||||
<a-tab-pane key="tpl-basic" class="tab-pane">
|
<a-tab-pane key="tpl-basic" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SettingOutlined /> <span>Basic template</span>
|
<SettingOutlined /> <span>{{ t('pages.xray.basicTemplate') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<BasicsTab
|
<BasicsTab
|
||||||
:template-settings="templateSettings"
|
:template-settings="templateSettings"
|
||||||
|
|
@ -207,7 +210,7 @@ function confirmRestart() {
|
||||||
|
|
||||||
<a-tab-pane key="tpl-routing" class="tab-pane">
|
<a-tab-pane key="tpl-routing" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SwapOutlined /> <span>Routing</span>
|
<SwapOutlined /> <span>{{ t('pages.xray.Routings') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<RoutingTab
|
<RoutingTab
|
||||||
:template-settings="templateSettings"
|
:template-settings="templateSettings"
|
||||||
|
|
@ -219,7 +222,7 @@ function confirmRestart() {
|
||||||
|
|
||||||
<a-tab-pane key="tpl-outbound" class="tab-pane">
|
<a-tab-pane key="tpl-outbound" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<UploadOutlined /> <span>Outbounds</span>
|
<UploadOutlined /> <span>{{ t('pages.xray.Outbounds') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<OutboundsTab
|
<OutboundsTab
|
||||||
:template-settings="templateSettings"
|
:template-settings="templateSettings"
|
||||||
|
|
@ -236,7 +239,7 @@ function confirmRestart() {
|
||||||
|
|
||||||
<a-tab-pane key="tpl-balancer" class="tab-pane">
|
<a-tab-pane key="tpl-balancer" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<ClusterOutlined /> <span>Balancers</span>
|
<ClusterOutlined /> <span>{{ t('pages.xray.Balancers') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<BalancersTab :template-settings="templateSettings" />
|
<BalancersTab :template-settings="templateSettings" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
@ -250,7 +253,7 @@ function confirmRestart() {
|
||||||
|
|
||||||
<a-tab-pane key="tpl-advanced" class="tab-pane">
|
<a-tab-pane key="tpl-advanced" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<CodeOutlined /> <span>Advanced (JSON)</span>
|
<CodeOutlined /> <span>{{ t('pages.xray.advancedTemplate') }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="Outbound test URL">
|
<a-form-item label="Outbound test URL">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue