mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-07 21:34:21 +00:00
cb37dd55ca
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>
116 lines
3.1 KiB
Vue
116 lines
3.1 KiB
Vue
<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.
|
|
record: { type: Object, default: null },
|
|
});
|
|
|
|
const emit = defineEmits(['update:open', 'saved']);
|
|
|
|
const form = reactive({ type: 'geosite', alias: '', url: '' });
|
|
const saving = ref(false);
|
|
|
|
const editing = ref(false);
|
|
const editId = ref(null);
|
|
|
|
watch(() => props.open, (next) => {
|
|
if (!next) return;
|
|
if (props.record) {
|
|
editing.value = true;
|
|
editId.value = props.record.id;
|
|
form.type = props.record.type;
|
|
form.alias = props.record.alias;
|
|
form.url = props.record.url;
|
|
} else {
|
|
editing.value = false;
|
|
editId.value = null;
|
|
form.type = 'geosite';
|
|
form.alias = '';
|
|
form.url = '';
|
|
}
|
|
});
|
|
|
|
function close() {
|
|
emit('update:open', false);
|
|
}
|
|
|
|
function validate() {
|
|
// Backend expects a filesystem-safe alias; legacy enforces the same regex.
|
|
if (!/^[a-z0-9_-]+$/.test(form.alias || '')) {
|
|
message.error(t('pages.index.customGeoValidationAlias'));
|
|
return false;
|
|
}
|
|
const u = (form.url || '').trim();
|
|
if (!/^https?:\/\//i.test(u)) {
|
|
message.error(t('pages.index.customGeoValidationUrl'));
|
|
return false;
|
|
}
|
|
try {
|
|
const parsed = new URL(u);
|
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
message.error(t('pages.index.customGeoValidationUrl'));
|
|
return false;
|
|
}
|
|
} catch (_e) {
|
|
message.error(t('pages.index.customGeoValidationUrl'));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function submit() {
|
|
if (!validate()) return;
|
|
saving.value = true;
|
|
try {
|
|
const url = editing.value
|
|
? `/panel/api/custom-geo/update/${editId.value}`
|
|
: '/panel/api/custom-geo/add';
|
|
const msg = await HttpUtil.post(url, form);
|
|
if (msg?.success) {
|
|
emit('saved');
|
|
close();
|
|
}
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<a-modal
|
|
:open="open"
|
|
:title="editing ? t('pages.index.customGeoModalEdit') : t('pages.index.customGeoModalAdd')"
|
|
:confirm-loading="saving"
|
|
:ok-text="t('pages.index.customGeoModalSave')"
|
|
:cancel-text="t('close')"
|
|
@ok="submit"
|
|
@cancel="close"
|
|
>
|
|
<a-form layout="vertical">
|
|
<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="t('pages.index.customGeoAlias')">
|
|
<a-input
|
|
v-model:value="form.alias"
|
|
:disabled="editing"
|
|
:placeholder="t('pages.index.customGeoAliasPlaceholder')"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item :label="t('pages.index.customGeoUrl')">
|
|
<a-input v-model:value="form.url" placeholder="https://" />
|
|
</a-form-item>
|
|
</a-form>
|
|
</a-modal>
|
|
</template>
|