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>
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
|
|
@ -13,6 +14,8 @@ import {
|
|||
import { currentTheme } from '@/composables/useTheme.js';
|
||||
import ThemeSwitch from '@/components/ThemeSwitch.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -42,13 +45,15 @@ const iconByName = {
|
|||
// would turn /panel/settings + 'panel/...' into /panel/panel/...).
|
||||
const prefix = props.basePath?.startsWith('/') ? props.basePath : `/${props.basePath || ''}`;
|
||||
|
||||
const tabs = [
|
||||
{ key: `${prefix}panel/`, icon: 'dashboard', title: 'Dashboard' },
|
||||
{ key: `${prefix}panel/inbounds`, icon: 'user', title: 'Inbounds' },
|
||||
{ key: `${prefix}panel/settings`, icon: 'setting', title: 'Settings' },
|
||||
{ key: `${prefix}panel/xray`, icon: 'tool', title: 'Xray' },
|
||||
{ key: `${prefix}logout/`, icon: 'logout', title: 'Logout' },
|
||||
];
|
||||
// Labels are i18n-driven so the sidebar matches the locale picked
|
||||
// in panel settings without a page reload of the sidebar component.
|
||||
const tabs = computed(() => [
|
||||
{ key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') },
|
||||
{ key: `${prefix}panel/inbounds`, icon: 'user', title: t('menu.inbounds') },
|
||||
{ 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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
PlusOutlined,
|
||||
MenuOutlined,
|
||||
|
|
@ -29,6 +30,8 @@ import { DBInbound } from '@/models/dbinbound.js';
|
|||
import { Inbound } from '@/models/inbound.js';
|
||||
import ClientRowTable from './ClientRowTable.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
dbInbounds: { type: Array, required: true },
|
||||
clientCount: { type: Object, required: true },
|
||||
|
|
@ -136,26 +139,27 @@ const visibleInbounds = computed(() => {
|
|||
|
||||
// ============ Columns =================================================
|
||||
// `key`-driven so we can render via the body-cell slot below. AD-Vue 4's
|
||||
// `responsive` array still works on column defs.
|
||||
const desktopColumns = [
|
||||
// `responsive` array still works on column defs. Computed so column
|
||||
// labels react to live locale switches.
|
||||
const desktopColumns = computed(() => [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 30, responsive: ['xs'] },
|
||||
{ title: 'Action', key: 'action', align: 'center', width: 30 },
|
||||
{ title: 'Enable', key: 'enable', align: 'center', width: 35 },
|
||||
{ title: 'Remark', dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
|
||||
{ title: 'Port', dataIndex: 'port', key: 'port', align: 'center', width: 40 },
|
||||
{ title: 'Protocol', key: 'protocol', align: 'left', width: 70 },
|
||||
{ title: 'Clients', key: 'clients', align: 'left', width: 50 },
|
||||
{ title: 'Traffic', key: 'traffic', align: 'center', width: 90 },
|
||||
{ title: 'All-time', key: 'allTimeInbound', align: 'center', width: 60 },
|
||||
{ title: 'Expiry', key: 'expiryTime', align: 'center', width: 40 },
|
||||
];
|
||||
const mobileColumns = [
|
||||
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 30 },
|
||||
{ title: t('pages.inbounds.enable'), key: 'enable', align: 'center', width: 35 },
|
||||
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'center', width: 60 },
|
||||
{ title: t('pages.inbounds.port'), dataIndex: 'port', key: 'port', align: 'center', width: 40 },
|
||||
{ title: t('pages.inbounds.protocol'), key: 'protocol', align: 'left', width: 70 },
|
||||
{ title: t('clients'), key: 'clients', align: 'left', width: 50 },
|
||||
{ title: t('pages.inbounds.traffic'), key: 'traffic', align: 'center', width: 90 },
|
||||
{ title: t('pages.inbounds.allTimeTraffic'), key: 'allTimeInbound', align: 'center', width: 60 },
|
||||
{ title: t('pages.inbounds.expireDate'), key: 'expiryTime', align: 'center', width: 40 },
|
||||
]);
|
||||
const mobileColumns = computed(() => [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', align: 'right', width: 10, responsive: ['s'] },
|
||||
{ title: 'Action', key: 'action', align: 'center', width: 25 },
|
||||
{ title: 'Remark', dataIndex: 'remark', key: 'remark', align: 'left', width: 70 },
|
||||
{ title: 'Info', key: 'info', align: 'center', width: 10 },
|
||||
];
|
||||
const columns = computed(() => (props.isMobile ? mobileColumns : desktopColumns));
|
||||
{ title: t('pages.inbounds.operate'), key: 'action', align: 'center', width: 25 },
|
||||
{ title: t('pages.inbounds.remark'), dataIndex: 'remark', key: 'remark', align: 'left', width: 70 },
|
||||
{ title: t('info'), key: 'info', align: 'center', width: 10 },
|
||||
]);
|
||||
const columns = computed(() => (props.isMobile ? mobileColumns.value : desktopColumns.value));
|
||||
|
||||
// ============ Pagination ============================================
|
||||
function paginationFor(rows) {
|
||||
|
|
@ -208,32 +212,32 @@ function showQrCodeMenu(dbInbound) {
|
|||
<a-space direction="horizontal">
|
||||
<a-button type="primary" @click="emit('add-inbound')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
<template v-if="!isMobile">Add inbound</template>
|
||||
<template v-if="!isMobile">{{ t('pages.inbounds.addInbound') }}</template>
|
||||
</a-button>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="primary">
|
||||
<template #icon><MenuOutlined /></template>
|
||||
<template v-if="!isMobile">General actions</template>
|
||||
<template v-if="!isMobile">{{ t('pages.inbounds.generalActions') }}</template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="(a) => emit('general-action', a.key)">
|
||||
<a-menu-item key="import">
|
||||
<ImportOutlined /> Import inbound
|
||||
<ImportOutlined /> {{ t('pages.inbounds.importInbound') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<ExportOutlined /> Export
|
||||
<ExportOutlined /> {{ t('pages.inbounds.export') }}
|
||||
</a-menu-item>
|
||||
<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 key="resetInbounds">
|
||||
<ReloadOutlined /> Reset all traffic
|
||||
<ReloadOutlined /> {{ t('pages.inbounds.resetAllTraffic') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetClients">
|
||||
<FileDoneOutlined /> Reset all client traffic
|
||||
<FileDoneOutlined /> {{ t('pages.inbounds.resetAllClientTraffics') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients" class="danger-item">
|
||||
<RestOutlined /> Delete depleted clients
|
||||
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
|
@ -250,12 +254,12 @@ function showQrCodeMenu(dbInbound) {
|
|||
<template #title>
|
||||
<div class="auto-refresh-title">
|
||||
<a-switch v-model:checked="isRefreshEnabled" size="small" />
|
||||
<span>Auto refresh</span>
|
||||
<span>{{ t('pages.inbounds.autoRefresh') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<a-space direction="vertical">
|
||||
<span>Auto-refresh interval</span>
|
||||
<span>{{ t('pages.inbounds.autoRefreshInterval') }}</span>
|
||||
<a-select
|
||||
v-model:value="refreshIntervalMs"
|
||||
:disabled="!isRefreshEnabled"
|
||||
|
|
@ -284,7 +288,7 @@ function showQrCodeMenu(dbInbound) {
|
|||
<a-input
|
||||
v-if="!enableFilter"
|
||||
v-model:value="searchKey"
|
||||
placeholder="Search"
|
||||
:placeholder="t('search')"
|
||||
autofocus
|
||||
:size="isMobile ? 'small' : 'middle'"
|
||||
:style="{ maxWidth: '300px' }"
|
||||
|
|
@ -295,12 +299,12 @@ function showQrCodeMenu(dbInbound) {
|
|||
button-style="solid"
|
||||
:size="isMobile ? 'small' : 'middle'"
|
||||
>
|
||||
<a-radio-button value="">None</a-radio-button>
|
||||
<a-radio-button value="active">Active</a-radio-button>
|
||||
<a-radio-button value="deactive">Disabled</a-radio-button>
|
||||
<a-radio-button value="depleted">Depleted</a-radio-button>
|
||||
<a-radio-button value="expiring">Depleting</a-radio-button>
|
||||
<a-radio-button value="online">Online</a-radio-button>
|
||||
<a-radio-button value="">{{ t('none') }}</a-radio-button>
|
||||
<a-radio-button value="active">{{ t('subscription.active') }}</a-radio-button>
|
||||
<a-radio-button value="deactive">{{ t('disabled') }}</a-radio-button>
|
||||
<a-radio-button value="depleted">{{ t('depleted') }}</a-radio-button>
|
||||
<a-radio-button value="expiring">{{ t('depletingSoon') }}</a-radio-button>
|
||||
<a-radio-button value="online">{{ t('online') }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
|
|
@ -343,31 +347,31 @@ function showQrCodeMenu(dbInbound) {
|
|||
<MoreOutlined class="row-action-trigger" @click.prevent />
|
||||
<template #overlay>
|
||||
<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">
|
||||
<QrcodeOutlined /> QR code
|
||||
<QrcodeOutlined /> {{ t('qrCode') }}
|
||||
</a-menu-item>
|
||||
<template v-if="record.isMultiUser()">
|
||||
<a-menu-item key="addClient"><UserAddOutlined /> Add client</a-menu-item>
|
||||
<a-menu-item key="addBulkClient"><UsergroupAddOutlined /> Add bulk clients</a-menu-item>
|
||||
<a-menu-item key="copyClients"><CopyOutlined /> Copy clients from inbound</a-menu-item>
|
||||
<a-menu-item key="resetClients"><FileDoneOutlined /> Reset client traffic</a-menu-item>
|
||||
<a-menu-item key="export"><ExportOutlined /> Export</a-menu-item>
|
||||
<a-menu-item key="addClient"><UserAddOutlined /> {{ t('pages.client.add') }}</a-menu-item>
|
||||
<a-menu-item key="addBulkClient"><UsergroupAddOutlined /> {{ t('pages.client.bulk') }}</a-menu-item>
|
||||
<a-menu-item key="copyClients"><CopyOutlined /> {{ t('pages.client.copyFromInbound') }}</a-menu-item>
|
||||
<a-menu-item key="resetClients"><FileDoneOutlined /> {{ t('pages.inbounds.resetInboundClientTraffics') }}</a-menu-item>
|
||||
<a-menu-item key="export"><ExportOutlined /> {{ t('pages.inbounds.export') }}</a-menu-item>
|
||||
<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 key="delDepletedClients" class="danger-item">
|
||||
<RestOutlined /> Delete depleted clients
|
||||
<RestOutlined /> {{ t('pages.inbounds.delDepletedClients') }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
<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>
|
||||
<a-menu-item key="clipboard"><CopyOutlined /> Export inbound</a-menu-item>
|
||||
<a-menu-item key="resetTraffic"><RetweetOutlined /> Reset traffic</a-menu-item>
|
||||
<a-menu-item key="clone"><BlockOutlined /> Clone</a-menu-item>
|
||||
<a-menu-item key="clipboard"><CopyOutlined /> {{ t('pages.inbounds.exportInbound') }}</a-menu-item>
|
||||
<a-menu-item key="resetTraffic"><RetweetOutlined /> {{ t('pages.inbounds.resetTraffic') }}</a-menu-item>
|
||||
<a-menu-item key="clone"><BlockOutlined /> {{ t('pages.inbounds.clone') }}</a-menu-item>
|
||||
<a-menu-item key="delete" class="danger-item">
|
||||
<DeleteOutlined /> Delete
|
||||
<DeleteOutlined /> {{ t('delete') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
|
@ -398,25 +402,25 @@ function showQrCodeMenu(dbInbound) {
|
|||
<template v-else-if="column.key === 'clients'">
|
||||
<template v-if="clientCount[record.id]">
|
||||
<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>
|
||||
<div v-for="email in clientCount[record.id].deactive" :key="email">{{ email }}</div>
|
||||
</template>
|
||||
<a-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="Depleted">
|
||||
<a-popover v-if="clientCount[record.id].depleted.length" :title="t('depleted')">
|
||||
<template #content>
|
||||
<div v-for="email in clientCount[record.id].depleted" :key="email">{{ email }}</div>
|
||||
</template>
|
||||
<a-tag color="red" 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="Depleting soon">
|
||||
<a-popover v-if="clientCount[record.id].expiring.length" :title="t('depletingSoon')">
|
||||
<template #content>
|
||||
<div v-for="email in clientCount[record.id].expiring" :key="email">{{ email }}</div>
|
||||
</template>
|
||||
<a-tag color="orange" 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="Online">
|
||||
<a-popover v-if="clientCount[record.id].online.length" :title="t('online')">
|
||||
<template #content>
|
||||
<div v-for="email in clientCount[record.id].online" :key="email">{{ email }}</div>
|
||||
</template>
|
||||
|
|
@ -436,7 +440,7 @@ function showQrCodeMenu(dbInbound) {
|
|||
<td>↓ {{ SizeFormatter.sizeFormat(record.down) }}</td>
|
||||
</tr>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -476,19 +480,19 @@ function showQrCodeMenu(dbInbound) {
|
|||
<table cellpadding="2">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Protocol</td>
|
||||
<td>{{ t('pages.inbounds.protocol') }}</td>
|
||||
<td><a-tag color="purple">{{ record.protocol }}</a-tag></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Port</td>
|
||||
<td>{{ t('pages.inbounds.port') }}</td>
|
||||
<td><a-tag>{{ record.port }}</a-tag></td>
|
||||
</tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Traffic</td>
|
||||
<td>{{ t('pages.inbounds.traffic') }}</td>
|
||||
<td>
|
||||
<a-tag>
|
||||
{{ SizeFormatter.sizeFormat(record.up + record.down) }} /
|
||||
|
|
@ -498,7 +502,7 @@ function showQrCodeMenu(dbInbound) {
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Expiry</td>
|
||||
<td>{{ t('pages.inbounds.expireDate') }}</td>
|
||||
<td>
|
||||
<a-tag v-if="record.expiryTime > 0">{{ IntlUtil.formatRelativeTime(record.expiryTime) }}</a-tag>
|
||||
<a-tag v-else color="purple">∞</a-tag>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { theme as antdTheme, Modal, message } from 'ant-design-vue';
|
||||
import {
|
||||
SwapOutlined,
|
||||
|
|
@ -25,6 +26,8 @@ import TextModal from '@/components/TextModal.vue';
|
|||
import PromptModal from '@/components/PromptModal.vue';
|
||||
import { useInbounds } from './useInbounds.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const antdThemeConfig = computed(() => ({
|
||||
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
|
||||
}));
|
||||
|
|
@ -531,7 +534,7 @@ function onRowAction({ key, dbInbound }) {
|
|||
<a-row :gutter="[16, 12]">
|
||||
<a-col :sm="12" :md="5">
|
||||
<CustomStatistic
|
||||
title="Total ↑ / ↓"
|
||||
:title="t('pages.inbounds.totalDownUp')"
|
||||
:value="`${SizeFormatter.sizeFormat(totals.up)} / ${SizeFormatter.sizeFormat(totals.down)}`"
|
||||
>
|
||||
<template #prefix><SwapOutlined /></template>
|
||||
|
|
@ -539,7 +542,7 @@ function onRowAction({ key, dbInbound }) {
|
|||
</a-col>
|
||||
<a-col :sm="12" :md="5">
|
||||
<CustomStatistic
|
||||
title="Total usage"
|
||||
:title="t('pages.inbounds.totalUsage')"
|
||||
:value="SizeFormatter.sizeFormat(totals.up + totals.down)"
|
||||
>
|
||||
<template #prefix><PieChartOutlined /></template>
|
||||
|
|
@ -547,19 +550,19 @@ function onRowAction({ key, dbInbound }) {
|
|||
</a-col>
|
||||
<a-col :sm="12" :md="5">
|
||||
<CustomStatistic
|
||||
title="All-time traffic"
|
||||
:title="t('pages.inbounds.allTimeTrafficUsage')"
|
||||
:value="SizeFormatter.sizeFormat(totals.allTime)"
|
||||
>
|
||||
<template #prefix><HistoryOutlined /></template>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<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>
|
||||
</CustomStatistic>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="4">
|
||||
<CustomStatistic title="Clients" value=" ">
|
||||
<CustomStatistic :title="t('clients')" value=" ">
|
||||
<template #prefix>
|
||||
<a-space direction="horizontal">
|
||||
<TeamOutlined />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { theme as antdTheme } from 'ant-design-vue';
|
||||
import { BarsOutlined, CloudServerOutlined, CloudDownloadOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { theme as themeState } from '@/composables/useTheme.js';
|
||||
import { useStatus } from '@/composables/useStatus.js';
|
||||
|
|
@ -104,29 +107,31 @@ function openVersionSwitch() { versionOpen.value = true; }
|
|||
</a-col>
|
||||
|
||||
<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>
|
||||
<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">
|
||||
<CloudDownloadOutlined />
|
||||
{{ panelUpdateInfo.latestVersion }}
|
||||
<span v-if="!isMobile">Update</span>
|
||||
<span v-if="!isMobile">{{ t('update') }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-space class="action" @click="logsOpen = true">
|
||||
<BarsOutlined />
|
||||
<span v-if="!isMobile">Logs</span>
|
||||
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
|
||||
</a-space>
|
||||
<a-space class="action" @click="backupOpen = true">
|
||||
<CloudServerOutlined />
|
||||
<span v-if="!isMobile">Backup</span>
|
||||
<span v-if="!isMobile">{{ t('pages.index.backupTitle') }}</span>
|
||||
</a-space>
|
||||
<a-space class="action" @click="panelUpdateOpen = true">
|
||||
<CloudDownloadOutlined />
|
||||
<span v-if="!isMobile">
|
||||
{{ panelUpdateInfo.updateAvailable ? `Update → ${panelUpdateInfo.latestVersion}` : 'Up to date' }}
|
||||
{{ panelUpdateInfo.updateAvailable
|
||||
? `${t('update')} → ${panelUpdateInfo.latestVersion}`
|
||||
: t('pages.index.panelUpToDate') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AreaChartOutlined, HistoryOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { CPUFormatter, SizeFormatter } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
status: { type: Object, required: true },
|
||||
isMobile: { type: Boolean, default: false },
|
||||
|
|
@ -25,16 +28,16 @@ defineEmits(['open-cpu-history']);
|
|||
:percent="status.cpu.percent"
|
||||
/>
|
||||
<div>
|
||||
<b>CPU:</b> {{ CPUFormatter.cpuCoreFormat(status.cpuCores) }}
|
||||
<b>{{ t('pages.index.cpu') }}:</b> {{ CPUFormatter.cpuCoreFormat(status.cpuCores) }}
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<div><b>Logical processors:</b> {{ status.logicalPro }}</div>
|
||||
<div><b>Frequency:</b> {{ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) }}</div>
|
||||
<div><b>{{ t('pages.index.logicalProcessors') }}:</b> {{ status.logicalPro }}</div>
|
||||
<div><b>{{ t('pages.index.frequency') }}:</b> {{ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) }}</div>
|
||||
</template>
|
||||
<AreaChartOutlined />
|
||||
</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')">
|
||||
<template #icon><HistoryOutlined /></template>
|
||||
</a-button>
|
||||
|
|
@ -50,7 +53,7 @@ defineEmits(['open-cpu-history']);
|
|||
:percent="status.mem.percent"
|
||||
/>
|
||||
<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) }}
|
||||
</div>
|
||||
</a-col>
|
||||
|
|
@ -68,7 +71,7 @@ defineEmits(['open-cpu-history']);
|
|||
:percent="status.swap.percent"
|
||||
/>
|
||||
<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) }}
|
||||
</div>
|
||||
</a-col>
|
||||
|
|
@ -81,7 +84,7 @@ defineEmits(['open-cpu-history']);
|
|||
:percent="status.disk.percent"
|
||||
/>
|
||||
<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) }}
|
||||
</div>
|
||||
</a-col>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
BarsOutlined,
|
||||
PoweroffOutlined,
|
||||
|
|
@ -6,6 +7,8 @@ import {
|
|||
ToolOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
status: { type: Object, required: true },
|
||||
isMobile: { type: Boolean, default: false },
|
||||
|
|
@ -30,7 +33,7 @@ function badgeAnimationClass(color) {
|
|||
<a-card hoverable>
|
||||
<template #title>
|
||||
<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">
|
||||
v{{ status.xray.version }}
|
||||
</a-tag>
|
||||
|
|
@ -50,7 +53,7 @@ function badgeAnimationClass(color) {
|
|||
<a-popover>
|
||||
<template #title>
|
||||
<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>
|
||||
<BarsOutlined class="cursor-pointer" @click="$emit('open-logs')" />
|
||||
</a-col>
|
||||
|
|
@ -74,22 +77,22 @@ function badgeAnimationClass(color) {
|
|||
<template #actions>
|
||||
<a-space v-if="ipLimitEnable" direction="horizontal" class="action" @click="$emit('open-xray-logs')">
|
||||
<BarsOutlined />
|
||||
<span v-if="!isMobile">Logs</span>
|
||||
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" class="action" @click="$emit('stop-xray')">
|
||||
<PoweroffOutlined />
|
||||
<span v-if="!isMobile">Stop xray</span>
|
||||
<span v-if="!isMobile">{{ t('pages.index.stopXray') }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" class="action" @click="$emit('restart-xray')">
|
||||
<ReloadOutlined />
|
||||
<span v-if="!isMobile">Restart xray</span>
|
||||
<span v-if="!isMobile">{{ t('pages.index.restartXray') }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" class="action" @click="$emit('open-version-switch')">
|
||||
<ToolOutlined />
|
||||
<span v-if="!isMobile">
|
||||
{{ status.xray.version && status.xray.version !== 'Unknown'
|
||||
? `v${status.xray.version}`
|
||||
: 'Switch xray' }}
|
||||
: t('pages.index.xraySwitch') }}
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
SettingOutlined,
|
||||
|
|
@ -20,6 +21,8 @@ import TelegramTab from './TelegramTab.vue';
|
|||
import SubscriptionGeneralTab from './SubscriptionGeneralTab.vue';
|
||||
import SubscriptionFormatsTab from './SubscriptionFormatsTab.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const antdThemeConfig = computed(() => ({
|
||||
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-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
||||
Save
|
||||
{{ t('pages.settings.save') }}
|
||||
</a-button>
|
||||
<a-button type="primary" danger :disabled="!saveDisabled" @click="restartPanel">
|
||||
Restart panel
|
||||
{{ t('pages.settings.restartPanel') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
|
|
@ -197,7 +200,7 @@ const alertVisible = ref(true);
|
|||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="Save before restarting — unsaved changes are dropped on restart."
|
||||
:message="t('pages.settings.infoDesc')"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -209,28 +212,28 @@ const alertVisible = ref(true);
|
|||
<a-tab-pane key="1" class="tab-pane">
|
||||
<template #tab>
|
||||
<SettingOutlined />
|
||||
<span>Panel</span>
|
||||
<span>{{ t('pages.settings.panelSettings') }}</span>
|
||||
</template>
|
||||
<GeneralTab :all-setting="allSetting" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" class="tab-pane">
|
||||
<template #tab>
|
||||
<SafetyOutlined />
|
||||
<span>Security</span>
|
||||
<span>{{ t('pages.settings.securitySettings') }}</span>
|
||||
</template>
|
||||
<SecurityTab :all-setting="allSetting" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" class="tab-pane">
|
||||
<template #tab>
|
||||
<MessageOutlined />
|
||||
<span>Telegram</span>
|
||||
<span>{{ t('pages.settings.TGBotSettings') }}</span>
|
||||
</template>
|
||||
<TelegramTab :all-setting="allSetting" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" class="tab-pane">
|
||||
<template #tab>
|
||||
<CloudServerOutlined />
|
||||
<span>Subscription</span>
|
||||
<span>{{ t('pages.settings.subSettings') }}</span>
|
||||
</template>
|
||||
<SubscriptionGeneralTab :all-setting="allSetting" />
|
||||
</a-tab-pane>
|
||||
|
|
@ -241,7 +244,7 @@ const alertVisible = ref(true);
|
|||
>
|
||||
<template #tab>
|
||||
<CodeOutlined />
|
||||
<span>Subscription (Formats)</span>
|
||||
<span>{{ t('pages.settings.subSettings') }} (Formats)</span>
|
||||
</template>
|
||||
<SubscriptionFormatsTab :all-setting="allSetting" />
|
||||
</a-tab-pane>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { theme as antdTheme, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
SettingOutlined,
|
||||
|
|
@ -24,6 +25,8 @@ import WarpModal from './WarpModal.vue';
|
|||
import NordModal from './NordModal.vue';
|
||||
import { useXraySetting } from './useXraySetting.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Phase 6-i: scaffold + advanced JSON tab. Other tabs (Basics, Routing,
|
||||
// Outbounds, Balancers, DNS) land in subsequent 6-ii…vi commits — they
|
||||
// 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-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveDisabled" @click="saveAll">
|
||||
Save
|
||||
{{ t('pages.xray.save') }}
|
||||
</a-button>
|
||||
<a-button type="primary" danger :disabled="!saveDisabled" @click="confirmRestart">
|
||||
Restart xray
|
||||
{{ t('pages.xray.restart') }}
|
||||
</a-button>
|
||||
<a-popover v-if="restartResult" placement="rightTop">
|
||||
<template #title>Xray restart output</template>
|
||||
|
|
@ -180,7 +183,7 @@ function confirmRestart() {
|
|||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="Save before restarting — unsaved changes are dropped on restart."
|
||||
:message="t('pages.settings.infoDesc')"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
|
@ -192,7 +195,7 @@ function confirmRestart() {
|
|||
<a-tabs default-active-key="tpl-basic">
|
||||
<a-tab-pane key="tpl-basic" class="tab-pane">
|
||||
<template #tab>
|
||||
<SettingOutlined /> <span>Basic template</span>
|
||||
<SettingOutlined /> <span>{{ t('pages.xray.basicTemplate') }}</span>
|
||||
</template>
|
||||
<BasicsTab
|
||||
:template-settings="templateSettings"
|
||||
|
|
@ -207,7 +210,7 @@ function confirmRestart() {
|
|||
|
||||
<a-tab-pane key="tpl-routing" class="tab-pane">
|
||||
<template #tab>
|
||||
<SwapOutlined /> <span>Routing</span>
|
||||
<SwapOutlined /> <span>{{ t('pages.xray.Routings') }}</span>
|
||||
</template>
|
||||
<RoutingTab
|
||||
:template-settings="templateSettings"
|
||||
|
|
@ -219,7 +222,7 @@ function confirmRestart() {
|
|||
|
||||
<a-tab-pane key="tpl-outbound" class="tab-pane">
|
||||
<template #tab>
|
||||
<UploadOutlined /> <span>Outbounds</span>
|
||||
<UploadOutlined /> <span>{{ t('pages.xray.Outbounds') }}</span>
|
||||
</template>
|
||||
<OutboundsTab
|
||||
:template-settings="templateSettings"
|
||||
|
|
@ -236,7 +239,7 @@ function confirmRestart() {
|
|||
|
||||
<a-tab-pane key="tpl-balancer" class="tab-pane">
|
||||
<template #tab>
|
||||
<ClusterOutlined /> <span>Balancers</span>
|
||||
<ClusterOutlined /> <span>{{ t('pages.xray.Balancers') }}</span>
|
||||
</template>
|
||||
<BalancersTab :template-settings="templateSettings" />
|
||||
</a-tab-pane>
|
||||
|
|
@ -250,7 +253,7 @@ function confirmRestart() {
|
|||
|
||||
<a-tab-pane key="tpl-advanced" class="tab-pane">
|
||||
<template #tab>
|
||||
<CodeOutlined /> <span>Advanced (JSON)</span>
|
||||
<CodeOutlined /> <span>{{ t('pages.xray.advancedTemplate') }}</span>
|
||||
</template>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="Outbound test URL">
|
||||
|
|
|
|||
Loading…
Reference in a new issue