Files
3x-ui/frontend/src/pages/index/IndexPage.vue
T
MHSanaei e7d117f11f 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>
2026-05-08 15:07:41 +02:00

207 lines
6.9 KiB
Vue

<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';
import { useMediaQuery } from '@/composables/useMediaQuery.js';
import AppSidebar from '@/components/AppSidebar.vue';
import StatusCard from './StatusCard.vue';
import XrayStatusCard from './XrayStatusCard.vue';
import PanelUpdateModal from './PanelUpdateModal.vue';
import LogModal from './LogModal.vue';
import BackupModal from './BackupModal.vue';
import CpuHistoryModal from './CpuHistoryModal.vue';
import XrayLogModal from './XrayLogModal.vue';
import VersionModal from './VersionModal.vue';
// Drive AD-Vue 4's built-in dark algorithm from our reactive theme.
const antdThemeConfig = computed(() => ({
algorithm: themeState.isDark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm,
}));
const { status, fetched, refresh } = useStatus();
const { isMobile } = useMediaQuery();
// `/panel/setting/defaultSettings` returns ipLimitEnable; the xray
// card hides its log button when access logs are off.
const ipLimitEnable = ref(false);
HttpUtil.post('/panel/setting/defaultSettings').then((msg) => {
if (msg?.success && msg.obj) ipLimitEnable.value = !!msg.obj.ipLimitEnable;
});
// Panel-update info — fetched once on mount, drives both the badge
// in QuickActions and the contents of PanelUpdateModal.
const panelUpdateInfo = ref({ currentVersion: '', latestVersion: '', updateAvailable: false });
onMounted(() => {
HttpUtil.get('/panel/api/server/getPanelUpdateInfo').then((msg) => {
if (msg?.success && msg.obj) panelUpdateInfo.value = msg.obj;
});
});
const basePath = window.__X_UI_BASE_PATH__ || '';
const requestUri = window.location.pathname;
// Modal open state.
const logsOpen = ref(false);
const backupOpen = ref(false);
const panelUpdateOpen = ref(false);
const cpuHistoryOpen = ref(false);
const xrayLogsOpen = ref(false);
const versionOpen = ref(false);
// Page-level loading overlay; modals can request it via @busy.
const loading = ref(false);
const loadingTip = ref('Loading…');
function setBusy({ busy, tip }) {
loading.value = busy;
if (tip) loadingTip.value = tip;
}
// Xray controls
async function stopXray() {
await HttpUtil.post('/panel/api/server/stopXrayService');
await refresh();
}
async function restartXray() {
await HttpUtil.post('/panel/api/server/restartXrayService');
await refresh();
}
function openCpuHistory() { cpuHistoryOpen.value = true; }
function openXrayLogs() { xrayLogsOpen.value = true; }
function openVersionSwitch() { versionOpen.value = true; }
</script>
<template>
<a-config-provider :theme="antdThemeConfig">
<a-layout class="index-page" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
<AppSidebar :base-path="basePath" :request-uri="requestUri" />
<a-layout class="content-shell">
<a-layout-content class="content-area">
<a-spin :spinning="loading || !fetched" :delay="200" :tip="loading ? loadingTip : 'Loading…'" size="large">
<div v-if="!fetched" class="loading-spacer" />
<a-row v-else :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]">
<a-col :span="24">
<StatusCard :status="status" :is-mobile="isMobile" @open-cpu-history="openCpuHistory" />
</a-col>
<a-col :sm="24" :lg="12">
<XrayStatusCard
:status="status"
:is-mobile="isMobile"
:ip-limit-enable="ipLimitEnable"
@stop-xray="stopXray"
@restart-xray="restartXray"
@open-xray-logs="openXrayLogs"
@open-logs="logsOpen = true"
@open-version-switch="openVersionSwitch"
/>
</a-col>
<a-col :sm="24" :lg="12">
<a-card :title="t('menu.link')" hoverable>
<template v-if="panelUpdateInfo.updateAvailable" #extra>
<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">{{ t('update') }}</span>
</a-tag>
</a-tooltip>
</template>
<template #actions>
<a-space class="action" @click="logsOpen = true">
<BarsOutlined />
<span v-if="!isMobile">{{ t('pages.index.logs') }}</span>
</a-space>
<a-space class="action" @click="backupOpen = true">
<CloudServerOutlined />
<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
? `${t('update')} → ${panelUpdateInfo.latestVersion}`
: t('pages.index.panelUpToDate') }}
</span>
</a-space>
</template>
</a-card>
</a-col>
</a-row>
</a-spin>
</a-layout-content>
</a-layout>
<PanelUpdateModal
v-model:open="panelUpdateOpen"
:info="panelUpdateInfo"
@busy="setBusy"
/>
<LogModal v-model:open="logsOpen" />
<BackupModal
v-model:open="backupOpen"
:base-path="basePath"
@busy="setBusy"
/>
<CpuHistoryModal v-model:open="cpuHistoryOpen" :status="status" />
<XrayLogModal v-model:open="xrayLogsOpen" />
<VersionModal v-model:open="versionOpen" :status="status" @busy="setBusy" />
</a-layout>
</a-config-provider>
</template>
<style scoped>
.index-page {
--bg-page: #f0f2f5;
--bg-card: #ffffff;
min-height: 100vh;
background: var(--bg-page);
}
.index-page.is-dark {
--bg-page: #0a1222;
--bg-card: #151f31;
}
.index-page.is-dark.is-ultra {
--bg-page: #21242a;
--bg-card: #0c0e12;
}
.index-page :deep(.ant-layout),
.index-page :deep(.ant-layout-content) {
background: transparent;
}
.content-shell { background: transparent; }
.content-area { padding: 24px; }
.loading-spacer {
min-height: calc(100vh - 120px);
}
.action {
cursor: pointer;
justify-content: center;
}
.update-tag {
cursor: pointer;
margin: 0;
display: inline-flex;
align-items: center;
gap: 4px;
}
</style>