mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
i18n(frontend): translate every remaining English string on the index page
Closes the index page's i18n coverage. Combined with the page-chrome
commit, every label users see on the dashboard is now sourced from
the TOML translation files.
Per file:
- IndexPage.vue: loading-spinner tip (initial + dynamic).
- BackupModal.vue: modal title, both list-item titles + descriptions
("Back up" / "Restore"), in-flight busy tips ("Importing database…"
/ "Restarting panel…").
- PanelUpdateModal.vue: modal title, update-available alert,
current/latest version row labels, "Up to date" tag + label,
primary action button. Modal.confirm now uses the translated
panelUpdateDialog / panelUpdateDialogDesc with #version#
substitution; success toast uses panelUpdateStartedPopover.
- LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/
Error log-level options stay literal — they're xray's wire values,
not user-facing labels (matches the existing settings-page choice).
- XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay
literal for the same reason.
- VersionModal.vue: modal title + xray-switch alert + per-file
tooltip + "Update all" button + custom-geo collapse header. The
Modal.confirm flows for switchXrayVersion + updateGeofile use
translated dialog/desc with #version# / #filename# substitution.
- CpuHistoryModal.vue: title slot.
- CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons,
every column title (computed for live locale), copy/edit/download/
delete tooltips, copy toast, delete-confirm modal, empty-state
text.
- CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/
Alias/URL field labels, alias placeholder, all three validation
toasts.
Total: ~50 strings localised across 8 index-page files. The Hello /
Welcome login headline cycle and a handful of literal xray wire
values (Direct/Blocked/Proxy/log levels) are intentionally kept
hardcoded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e7d117f11f
commit
cb37dd55ca
9 changed files with 101 additions and 77 deletions
|
|
@ -1,7 +1,10 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { DownloadOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { HttpUtil, PromiseUtil } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
basePath: { type: String, default: '' },
|
||||
|
|
@ -32,7 +35,7 @@ function importDb() {
|
|||
formData.append('db', dbFile);
|
||||
|
||||
close();
|
||||
emit('busy', { busy: true, tip: 'Importing database…' });
|
||||
emit('busy', { busy: true, tip: t('pages.index.importDatabase') + '…' });
|
||||
|
||||
const upload = await HttpUtil.post('/panel/api/server/importDB', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
|
|
@ -42,7 +45,7 @@ function importDb() {
|
|||
return;
|
||||
}
|
||||
|
||||
emit('busy', { busy: true, tip: 'Restarting panel…' });
|
||||
emit('busy', { busy: true, tip: t('pages.settings.restartPanel') + '…' });
|
||||
const restart = await HttpUtil.post('/panel/setting/restartPanel');
|
||||
if (restart?.success) {
|
||||
await PromiseUtil.sleep(5000);
|
||||
|
|
@ -56,12 +59,12 @@ function importDb() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" title="Database backup & restore" :closable="true" :footer="null" @cancel="close">
|
||||
<a-modal :open="open" :title="t('pages.index.backupTitle')" :closable="true" :footer="null" @cancel="close">
|
||||
<a-list bordered class="backup-list">
|
||||
<a-list-item class="backup-item">
|
||||
<a-list-item-meta>
|
||||
<template #title>Back up</template>
|
||||
<template #description>Click to download a .db file containing a backup of your current database to your device.</template>
|
||||
<template #title>{{ t('pages.index.exportDatabase') }}</template>
|
||||
<template #description>{{ t('pages.index.exportDatabaseDesc') }}</template>
|
||||
</a-list-item-meta>
|
||||
<a-button type="primary" @click="exportDb">
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
|
|
@ -70,8 +73,8 @@ function importDb() {
|
|||
|
||||
<a-list-item class="backup-item">
|
||||
<a-list-item-meta>
|
||||
<template #title>Restore</template>
|
||||
<template #description>Click to upload a .db file. The panel restarts after restore — your session will reconnect automatically.</template>
|
||||
<template #title>{{ t('pages.index.importDatabase') }}</template>
|
||||
<template #description>{{ t('pages.index.importDatabaseDesc') }}</template>
|
||||
</a-list-item-meta>
|
||||
<a-button type="primary" @click="importDb">
|
||||
<template #icon><UploadOutlined /></template>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import Sparkline from '@/components/Sparkline.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
status: { type: Object, required: true },
|
||||
|
|
@ -48,7 +51,7 @@ watch(bucket, () => { if (props.open) fetchBucket(); });
|
|||
<template>
|
||||
<a-modal :open="open" :closable="true" :footer="null" width="900px" @cancel="close">
|
||||
<template #title>
|
||||
CPU history
|
||||
{{ t('pages.index.cpu') }}
|
||||
<a-select v-model:value="bucket" size="small" class="bucket-select">
|
||||
<a-select-option :value="2">2m</a-select-option>
|
||||
<a-select-option :value="30">30m</a-select-option>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { HttpUtil } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
// Populate with the record when editing; null/undefined when adding.
|
||||
|
|
@ -41,22 +44,22 @@ function close() {
|
|||
function validate() {
|
||||
// Backend expects a filesystem-safe alias; legacy enforces the same regex.
|
||||
if (!/^[a-z0-9_-]+$/.test(form.alias || '')) {
|
||||
message.error('Alias must contain only lowercase letters, digits, dashes or underscores.');
|
||||
message.error(t('pages.index.customGeoValidationAlias'));
|
||||
return false;
|
||||
}
|
||||
const u = (form.url || '').trim();
|
||||
if (!/^https?:\/\//i.test(u)) {
|
||||
message.error('URL must start with http:// or https://');
|
||||
message.error(t('pages.index.customGeoValidationUrl'));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(u);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
message.error('URL must start with http:// or https://');
|
||||
message.error(t('pages.index.customGeoValidationUrl'));
|
||||
return false;
|
||||
}
|
||||
} catch (_e) {
|
||||
message.error('URL must start with http:// or https://');
|
||||
message.error(t('pages.index.customGeoValidationUrl'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -83,28 +86,28 @@ async function submit() {
|
|||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="editing ? 'Edit custom geo entry' : 'Add custom geo entry'"
|
||||
:title="editing ? t('pages.index.customGeoModalEdit') : t('pages.index.customGeoModalAdd')"
|
||||
:confirm-loading="saving"
|
||||
ok-text="Save"
|
||||
cancel-text="Close"
|
||||
:ok-text="t('pages.index.customGeoModalSave')"
|
||||
:cancel-text="t('close')"
|
||||
@ok="submit"
|
||||
@cancel="close"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="Type">
|
||||
<a-form-item :label="t('pages.index.customGeoType')">
|
||||
<a-select v-model:value="form.type" :disabled="editing">
|
||||
<a-select-option value="geosite">geosite</a-select-option>
|
||||
<a-select-option value="geoip">geoip</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Alias">
|
||||
<a-form-item :label="t('pages.index.customGeoAlias')">
|
||||
<a-input
|
||||
v-model:value="form.alias"
|
||||
:disabled="editing"
|
||||
placeholder="lowercase letters, digits, dashes, underscores"
|
||||
:placeholder="t('pages.index.customGeoAliasPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="URL">
|
||||
<a-form-item :label="t('pages.index.customGeoUrl')">
|
||||
<a-input v-model:value="form.url" placeholder="https://" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import {
|
||||
PlusOutlined,
|
||||
|
|
@ -12,6 +13,8 @@ import {
|
|||
import { HttpUtil, ClipboardManager } from '@/utils';
|
||||
import CustomGeoFormModal from './CustomGeoFormModal.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
// Re-fetch the list when the parent collapse expands this section.
|
||||
active: { type: Boolean, default: false },
|
||||
|
|
@ -25,13 +28,14 @@ const actionId = ref(null);
|
|||
const formOpen = ref(false);
|
||||
const editingRecord = ref(null);
|
||||
|
||||
const columns = [
|
||||
{ title: 'Alias', key: 'alias', width: 200 },
|
||||
{ title: 'URL', key: 'url', ellipsis: true },
|
||||
{ title: 'Ext', key: 'extDat', width: 220 },
|
||||
{ title: 'Last updated', key: 'lastUpdatedAt', width: 140 },
|
||||
{ title: 'Actions', key: 'action', width: 120 },
|
||||
];
|
||||
// Computed so column titles re-render after a locale swap.
|
||||
const columns = computed(() => [
|
||||
{ title: t('pages.index.customGeoAlias'), key: 'alias', width: 200 },
|
||||
{ title: t('pages.index.customGeoUrl'), key: 'url', ellipsis: true },
|
||||
{ title: t('pages.index.customGeoExtColumn'), key: 'extDat', width: 220 },
|
||||
{ title: t('pages.index.customGeoLastUpdated'), key: 'lastUpdatedAt', width: 140 },
|
||||
{ title: t('pages.index.customGeoActions'), key: 'action', width: 120 },
|
||||
]);
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
|
|
@ -63,7 +67,7 @@ function extDisplay(record) {
|
|||
async function copyExt(record) {
|
||||
const text = extDisplay(record);
|
||||
const ok = await ClipboardManager.copyText(text);
|
||||
if (ok) message.success(`Copied: ${text}`);
|
||||
if (ok) message.success(`${t('copied')}: ${text}`);
|
||||
}
|
||||
|
||||
function formatTime(ts) {
|
||||
|
|
@ -87,11 +91,11 @@ function relativeTime(ts) {
|
|||
|
||||
function confirmDelete(record) {
|
||||
Modal.confirm({
|
||||
title: 'Delete custom geo entry',
|
||||
content: `Delete "${record.alias}"? This cannot be undone.`,
|
||||
okText: 'Delete',
|
||||
title: t('pages.index.customGeoDelete'),
|
||||
content: t('pages.index.customGeoDeleteConfirm'),
|
||||
okText: t('delete'),
|
||||
okType: 'danger',
|
||||
cancelText: 'Cancel',
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const msg = await HttpUtil.post(`/panel/api/custom-geo/delete/${record.id}`);
|
||||
if (msg?.success) await loadList();
|
||||
|
|
@ -134,17 +138,17 @@ watch(() => props.active, (next) => { if (next) loadList(); }, { immediate: true
|
|||
type="info"
|
||||
show-icon
|
||||
class="mb-10"
|
||||
message="Reference custom files in routing rules with ext:<filename>:tag"
|
||||
:message="t('pages.index.customGeoRoutingHint')"
|
||||
/>
|
||||
|
||||
<div class="toolbar">
|
||||
<a-button type="primary" :loading="loading" @click="openAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
Add
|
||||
{{ t('pages.index.customGeoAdd') }}
|
||||
</a-button>
|
||||
<a-button :loading="updatingAll" :disabled="!list.length" @click="updateAll">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
Update all
|
||||
{{ t('pages.index.geofilesUpdateAll') }}
|
||||
</a-button>
|
||||
<span v-if="list.length" class="custom-geo-count">{{ list.length }}</span>
|
||||
</div>
|
||||
|
|
@ -177,7 +181,7 @@ watch(() => props.active, (next) => { if (next) loadList(); }, { immediate: true
|
|||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'extDat'">
|
||||
<a-tooltip title="Copy">
|
||||
<a-tooltip :title="t('copy')">
|
||||
<code class="custom-geo-ext-code custom-geo-copyable" @click="copyExt(record)">
|
||||
{{ extDisplay(record) }}
|
||||
</code>
|
||||
|
|
@ -193,12 +197,12 @@ watch(() => props.active, (next) => { if (next) loadList(); }, { immediate: true
|
|||
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space size="small">
|
||||
<a-tooltip title="Edit">
|
||||
<a-tooltip :title="t('pages.index.customGeoEdit')">
|
||||
<a-button type="link" size="small" @click="openEdit(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="Download">
|
||||
<a-tooltip :title="t('pages.index.customGeoDownload')">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
|
|
@ -208,7 +212,7 @@ watch(() => props.active, (next) => { if (next) loadList(); }, { immediate: true
|
|||
<template #icon><ReloadOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="Delete">
|
||||
<a-tooltip :title="t('pages.index.customGeoDelete')">
|
||||
<a-button type="link" size="small" danger @click="confirmDelete(record)">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
|
|
@ -220,7 +224,7 @@ watch(() => props.active, (next) => { if (next) loadList(); }, { immediate: true
|
|||
<template #emptyText>
|
||||
<div class="custom-geo-empty">
|
||||
<InboxOutlined class="custom-geo-empty-icon" />
|
||||
<div>No custom geo entries yet</div>
|
||||
<div>{{ t('pages.index.customGeoEmpty') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const versionOpen = ref(false);
|
|||
|
||||
// Page-level loading overlay; modals can request it via @busy.
|
||||
const loading = ref(false);
|
||||
const loadingTip = ref('Loading…');
|
||||
const loadingTip = ref(t('loading'));
|
||||
function setBusy({ busy, tip }) {
|
||||
loading.value = busy;
|
||||
if (tip) loadingTip.value = tip;
|
||||
|
|
@ -85,7 +85,7 @@ function openVersionSwitch() { versionOpen.value = true; }
|
|||
|
||||
<a-layout class="content-shell">
|
||||
<a-layout-content class="content-area">
|
||||
<a-spin :spinning="loading || !fetched" :delay="200" :tip="loading ? loadingTip : 'Loading…'" size="large">
|
||||
<a-spin :spinning="loading || !fetched" :delay="200" :tip="loading ? loadingTip : t('loading')" size="large">
|
||||
<div v-if="!fetched" class="loading-spacer" />
|
||||
|
||||
<a-row v-else :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { HttpUtil, FileManager, PromiseUtil } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
});
|
||||
|
|
@ -94,7 +97,7 @@ watch([rows, level, syslog], () => { if (props.open) refresh(); });
|
|||
<template>
|
||||
<a-modal :open="open" :closable="true" :footer="null" width="800px" @cancel="close">
|
||||
<template #title>
|
||||
Logs
|
||||
{{ t('pages.index.logs') }}
|
||||
<SyncOutlined :spin="loading" class="reload-icon" @click="refresh" />
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Modal, message } from 'ant-design-vue';
|
||||
import { CloudDownloadOutlined } from '@ant-design/icons-vue';
|
||||
import { HttpUtil, PromiseUtil } from '@/utils';
|
||||
import axios from 'axios';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
info: {
|
||||
|
|
@ -20,14 +23,13 @@ function close() {
|
|||
|
||||
function updatePanel() {
|
||||
Modal.confirm({
|
||||
title: 'Update panel',
|
||||
content: `The panel will be updated to ${props.info.latestVersion || ''} and restarted. Continue?`,
|
||||
okText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
title: t('pages.index.panelUpdateDialog'),
|
||||
content: t('pages.index.panelUpdateDialogDesc').replace('#version#', props.info.latestVersion || ''),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const tip = props.info.latestVersion
|
||||
? `Installation in progress, please do not refresh (${props.info.latestVersion})`
|
||||
: 'Installation in progress, please do not refresh';
|
||||
const baseTip = t('pages.index.dontRefresh');
|
||||
const tip = props.info.latestVersion ? `${baseTip} (${props.info.latestVersion})` : baseTip;
|
||||
close();
|
||||
emit('busy', { busy: true, tip });
|
||||
const msg = await HttpUtil.post('/panel/api/server/updatePanel');
|
||||
|
|
@ -48,7 +50,7 @@ function updatePanel() {
|
|||
await PromiseUtil.sleep(2000);
|
||||
}
|
||||
if (back) {
|
||||
message.success('Panel update started');
|
||||
message.success(t('pages.index.panelUpdateStartedPopover'));
|
||||
await PromiseUtil.sleep(800);
|
||||
}
|
||||
window.location.reload();
|
||||
|
|
@ -58,34 +60,34 @@ function updatePanel() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" title="Update panel" :closable="true" :footer="null" @cancel="close">
|
||||
<a-modal :open="open" :title="t('pages.index.updatePanel')" :closable="true" :footer="null" @cancel="close">
|
||||
<a-alert
|
||||
v-if="info.updateAvailable"
|
||||
type="warning"
|
||||
class="mb-12"
|
||||
message="A new panel version is available. Update will restart the service."
|
||||
:message="t('pages.index.panelUpdateDesc')"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<a-list bordered class="version-list">
|
||||
<a-list-item class="version-list-item">
|
||||
<span>Current version</span>
|
||||
<a-tag color="green">v{{ info.currentVersion || 'unknown' }}</a-tag>
|
||||
<span>{{ t('pages.index.currentPanelVersion') }}</span>
|
||||
<a-tag color="green">v{{ info.currentVersion || '?' }}</a-tag>
|
||||
</a-list-item>
|
||||
<a-list-item v-if="info.updateAvailable" class="version-list-item">
|
||||
<span>Latest version</span>
|
||||
<span>{{ t('pages.index.latestPanelVersion') }}</span>
|
||||
<a-tag color="purple">{{ info.latestVersion || '-' }}</a-tag>
|
||||
</a-list-item>
|
||||
<a-list-item v-else class="version-list-item">
|
||||
<span>Panel is up to date</span>
|
||||
<a-tag color="green">Up to date</a-tag>
|
||||
<span>{{ t('pages.index.panelUpToDate') }}</span>
|
||||
<a-tag color="green">{{ t('pages.index.panelUpToDate') }}</a-tag>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
|
||||
<div class="actions-row">
|
||||
<a-button type="primary" :disabled="!info.updateAvailable" @click="updatePanel">
|
||||
<template #icon><CloudDownloadOutlined /></template>
|
||||
Update panel
|
||||
{{ t('pages.index.updatePanel') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import CustomGeoSection from './CustomGeoSection.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
status: { type: Object, required: true },
|
||||
|
|
@ -36,13 +39,13 @@ function close() {
|
|||
|
||||
function switchXrayVersion(version) {
|
||||
Modal.confirm({
|
||||
title: 'Switch xray version',
|
||||
content: `Are you sure you want to install ${version}? This will restart xray.`,
|
||||
okText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
title: t('pages.index.xraySwitchVersionDialog'),
|
||||
content: t('pages.index.xraySwitchVersionDialogDesc').replace('#version#', version),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
close();
|
||||
emit('busy', { busy: true, tip: `Installing ${version}…` });
|
||||
emit('busy', { busy: true, tip: t('pages.index.dontRefresh') });
|
||||
try {
|
||||
await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
||||
} finally {
|
||||
|
|
@ -55,15 +58,15 @@ function switchXrayVersion(version) {
|
|||
function updateGeofile(fileName) {
|
||||
const isSingle = !!fileName;
|
||||
Modal.confirm({
|
||||
title: 'Update geofile',
|
||||
title: t('pages.index.geofileUpdateDialog'),
|
||||
content: isSingle
|
||||
? `Update ${fileName}? Xray will restart after the file is replaced.`
|
||||
: 'Update all geofiles? Xray will restart after the files are replaced.',
|
||||
okText: 'Confirm',
|
||||
cancelText: 'Cancel',
|
||||
? t('pages.index.geofileUpdateDialogDesc').replace('#filename#', fileName)
|
||||
: t('pages.index.geofilesUpdateDialogDesc'),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
close();
|
||||
emit('busy', { busy: true, tip: 'Updating geofiles…' });
|
||||
emit('busy', { busy: true, tip: t('pages.index.dontRefresh') });
|
||||
const url = isSingle
|
||||
? `/panel/api/server/updateGeofile/${fileName}`
|
||||
: '/panel/api/server/updateGeofile';
|
||||
|
|
@ -80,14 +83,14 @@ watch(() => props.open, (next) => { if (next) fetchVersions(); });
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :open="open" title="Xray updates" :closable="true" :footer="null" @cancel="close">
|
||||
<a-modal :open="open" :title="t('pages.index.xrayUpdates')" :closable="true" :footer="null" @cancel="close">
|
||||
<a-spin :spinning="loading">
|
||||
<a-collapse v-model:active-key="activeKey" accordion>
|
||||
<a-collapse-panel key="1" header="Xray">
|
||||
<a-alert
|
||||
type="warning"
|
||||
class="mb-12"
|
||||
message="Click a version to install it. Xray will restart automatically."
|
||||
:message="t('pages.index.xraySwitchClickDesk')"
|
||||
show-icon
|
||||
/>
|
||||
<a-list bordered class="version-list">
|
||||
|
|
@ -105,17 +108,17 @@ watch(() => props.open, (next) => { if (next) fetchVersions(); });
|
|||
<a-list bordered class="version-list">
|
||||
<a-list-item v-for="(file, index) in GEOFILES" :key="file" class="version-list-item">
|
||||
<a-tag :color="index % 2 === 0 ? 'purple' : 'green'">{{ file }}</a-tag>
|
||||
<a-tooltip title="Update this file">
|
||||
<a-tooltip :title="t('update')">
|
||||
<ReloadOutlined class="reload-icon" @click="updateGeofile(file)" />
|
||||
</a-tooltip>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<div class="actions-row">
|
||||
<a-button @click="updateGeofile('')">Update all</a-button>
|
||||
<a-button @click="updateGeofile('')">{{ t('pages.index.geofilesUpdateAll') }}</a-button>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
|
||||
<a-collapse-panel key="3" header="Custom geo">
|
||||
<a-collapse-panel key="3" :header="t('pages.index.customGeoTitle')">
|
||||
<CustomGeoSection :active="activeKey === '3'" />
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { HttpUtil, FileManager, IntlUtil, PromiseUtil } from '@/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
});
|
||||
|
|
@ -102,7 +105,7 @@ watch([rows, showDirect, showBlocked, showProxy], () => { if (props.open) refres
|
|||
<template>
|
||||
<a-modal :open="open" :closable="true" :footer="null" width="80vw" @cancel="close">
|
||||
<template #title>
|
||||
Xray logs
|
||||
{{ t('pages.index.logs') }}
|
||||
<SyncOutlined :spin="loading" class="reload-icon" @click="refresh" />
|
||||
</template>
|
||||
|
||||
|
|
@ -116,7 +119,7 @@ watch([rows, showDirect, showBlocked, showProxy], () => { if (props.open) refres
|
|||
<a-select-option value="500">500</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Filter">
|
||||
<a-form-item :label="t('filter')">
|
||||
<a-input v-model:value="filter" size="small" @keyup.enter="refresh" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
|
|
|
|||
Loading…
Reference in a new issue