feat: add footer navigation component and update translations (#13642)

This commit is contained in:
2026-08-26 14:30:45 +08:00
committed by GitHub
parent 2dffd06b1b
commit 3506c5dd3b
20 changed files with 459 additions and 71 deletions
+14
View File
@@ -1508,6 +1508,20 @@
"formatZH": "更新角色 [name]",
"formatEN": "update role [name]"
},
"/core/enterprise/settings/footer/reset": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重置企业版底部链接设置",
"formatEN": "reset Enterprise footer link settings"
},
"/core/enterprise/settings/footer/update": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新企业版底部链接设置",
"formatEN": "update Enterprise footer link settings"
},
"/core/enterprise/skills-hub/delete": {
"bodyKeys": [
"id"
+14
View File
@@ -1508,6 +1508,20 @@
"formatZH": "更新角色 [name]",
"formatEN": "update role [name]"
},
"/core/enterprise/settings/footer/reset": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "重置企业版底部链接设置",
"formatEN": "reset Enterprise footer link settings"
},
"/core/enterprise/settings/footer/update": {
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新企业版底部链接设置",
"formatEN": "update Enterprise footer link settings"
},
"/core/enterprise/skills-hub/delete": {
"bodyKeys": [
"id"
@@ -0,0 +1,5 @@
export const FOOTER_NAVIGATION_REFRESH_EVENT = '1panel:footer-navigation-refresh';
export const refreshFooterNavigation = () => {
window.dispatchEvent(new Event(FOOTER_NAVIGATION_REFRESH_EVENT));
};
@@ -0,0 +1,115 @@
<template>
<div v-if="visibleLinks.length" class="footer-navigation">
<template v-for="(item, index) in visibleLinks" :key="item.key">
<el-link type="primary" underline="never" @click="openLink(item.url)">
<span class="font-normal">{{ $t(item.label) }}</span>
</el-link>
<el-divider v-if="index < visibleLinks.length - 1" direction="vertical" />
</template>
<el-divider class="footer-navigation__tail" direction="vertical" />
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { getEnterpriseFooterSetting } from '@/extensions/footer-setting';
import { useGlobalStore } from '@/composables/useGlobalStore';
import {
createDefaultFooterNavigationLinks,
footerNavigationKeys,
isSafeExternalUrl,
mergeFooterNavigationLinks,
} from './model';
import type { FooterNavigationKey, FooterNavigationSetting } from './model';
import { FOOTER_NAVIGATION_REFRESH_EVENT } from './event';
const { docsUrl, isEE, isFxplay, isIntl } = useGlobalStore();
const setting = ref<FooterNavigationSetting | null>(null);
let loadGeneration = 0;
const defaults = computed(() => createDefaultFooterNavigationLinks(isIntl.value, docsUrl.value));
const links = computed(() => mergeFooterNavigationLinks(setting.value, defaults.value));
const labels: Record<FooterNavigationKey, string> = {
learnMore: 'license.knowMorePro',
forum: 'setting.forum',
documentation: 'setting.doc2',
project: 'setting.project',
};
const visibleLinks = computed(() => {
return footerNavigationKeys
.filter((key) => {
if (isFxplay.value && key !== 'documentation') {
return false;
}
return links.value[key].visible;
})
.map((key) => ({
key,
label: labels[key],
url: links.value[key].url,
}));
});
const loadSetting = async () => {
const generation = ++loadGeneration;
if (!isEE.value) {
setting.value = null;
return;
}
try {
const res = await getEnterpriseFooterSetting(true);
if (generation !== loadGeneration || !isEE.value) {
return;
}
setting.value = res?.data || null;
} catch {
if (generation !== loadGeneration || !isEE.value) {
return;
}
setting.value = null;
}
};
const openLink = (url: string) => {
if (!isSafeExternalUrl(url)) {
return;
}
window.open(url, '_blank', 'noopener,noreferrer');
};
watch(isEE, loadSetting, { immediate: true });
onMounted(() => {
window.addEventListener(FOOTER_NAVIGATION_REFRESH_EVENT, loadSetting);
});
onBeforeUnmount(() => {
loadGeneration += 1;
window.removeEventListener(FOOTER_NAVIGATION_REFRESH_EVENT, loadSetting);
});
</script>
<style scoped lang="scss">
.footer-navigation {
display: flex;
flex-wrap: wrap;
align-items: center;
row-gap: 8px;
}
:deep(.el-link__inner) {
font-weight: 400;
}
@media (max-width: 767px) {
.footer-navigation {
column-gap: 12px;
justify-content: center;
}
.footer-navigation :deep(.el-divider--vertical) {
display: none;
}
}
</style>
@@ -0,0 +1,128 @@
export const footerNavigationKeys = ['learnMore', 'forum', 'documentation', 'project'] as const;
export type FooterNavigationKey = (typeof footerNavigationKeys)[number];
export interface FooterNavigationLinkSetting {
visible: boolean;
url: string;
}
export type FooterNavigationLinks = Record<FooterNavigationKey, FooterNavigationLinkSetting>;
export interface FooterNavigationSetting {
customized: boolean;
links: FooterNavigationLinks;
}
export interface FooterNavigationSettingEditor {
validate: () => Promise<boolean>;
save: () => Promise<void>;
restoreDefaults: () => Promise<void>;
reload: () => Promise<boolean>;
isDirty: () => boolean;
}
export const createDefaultFooterNavigationLinks = (isIntl: boolean, docsUrl: string): FooterNavigationLinks => ({
learnMore: {
visible: true,
url: isIntl ? 'https://1panel.pro/pricing' : 'https://1panel.cn/versions.html',
},
forum: {
visible: true,
url: isIntl ? 'https://github.com/1Panel-dev/1Panel/discussions' : 'https://bbs.fit2cloud.com/c/1p/7',
},
documentation: {
visible: true,
url: docsUrl.endsWith('/') ? docsUrl : `${docsUrl}/`,
},
project: {
visible: true,
url: 'https://github.com/1Panel-dev/1Panel',
},
});
const controlCharacterPattern = /[\u0000-\u001f\u007f-\u009f]/u;
const schemeAuthorityPattern = /^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/i;
const percentBytePattern = /^[\da-f]{2}$/i;
const containsControlCharacter = (value: string) => controlCharacterPattern.test(value);
const decodePercentEscapes = (value: string): string | null => {
if (!value.includes('%')) {
return value;
}
const encoder = new TextEncoder();
const bytes: number[] = [];
let segmentStart = 0;
for (let index = 0; index < value.length; index += 1) {
if (value[index] !== '%') {
continue;
}
bytes.push(...encoder.encode(value.slice(segmentStart, index)));
const escapedByte = value.slice(index + 1, index + 3);
if (!percentBytePattern.test(escapedByte)) {
return null;
}
bytes.push(Number.parseInt(escapedByte, 16));
index += 2;
segmentStart = index + 1;
}
bytes.push(...encoder.encode(value.slice(segmentStart)));
return new TextDecoder().decode(Uint8Array.from(bytes));
};
export const isSafeExternalUrl = (value: unknown): value is string => {
if (typeof value !== 'string') {
return false;
}
if (containsControlCharacter(value)) {
return false;
}
const normalizedURL = value.trim();
if (normalizedURL.includes('\\')) {
return false;
}
const authority = normalizedURL.match(schemeAuthorityPattern)?.[1];
if (!authority || authority.endsWith(':') || authority.includes('@') || authority.includes('%')) {
return false;
}
const decodedURL = decodePercentEscapes(normalizedURL);
if (decodedURL === null || containsControlCharacter(decodedURL)) {
return false;
}
try {
const url = new URL(normalizedURL);
const port = url.port ? Number(url.port) : null;
return (
(url.protocol === 'http:' || url.protocol === 'https:') &&
Boolean(url.hostname) &&
!url.username &&
!url.password &&
(port === null || (Number.isInteger(port) && port >= 1 && port <= 65535))
);
} catch {
return false;
}
};
export const mergeFooterNavigationLinks = (
setting: FooterNavigationSetting | null,
defaults: FooterNavigationLinks,
): FooterNavigationLinks => {
if (!setting?.customized || !setting.links) {
return defaults;
}
return footerNavigationKeys.reduce((links, key) => {
const customized = setting.links[key];
links[key] = {
visible: typeof customized?.visible === 'boolean' ? customized.visible : defaults[key].visible,
url: isSafeExternalUrl(customized?.url) ? customized.url : defaults[key].url,
};
return links;
}, {} as FooterNavigationLinks);
};
@@ -1,53 +1,33 @@
<template>
<div>
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
<div class="flex flex-wrap gap-y-2 items-center">
<span v-if="props.footer">
<el-link type="primary" underline="never" @click="toEdition" v-if="!isFxplay">
<span class="font-normal">{{ $t('license.knowMorePro') }}</span>
<div class="flex flex-wrap items-center">
<div class="flex flex-wrap items-center">
<el-link v-if="isEE" underline="never" type="primary" @click="toEdition">
{{ $t('license.ee') }}
</el-link>
<el-link v-else-if="isMasterPro" underline="never" type="primary" @click="toLxware">
{{ $t('license.pro') }}
</el-link>
<el-link v-else-if="isOffline" underline="never" type="primary" @click="to1Panel">
{{ $t('license.offLine') }}
</el-link>
<el-link v-else underline="never" type="primary" @click="toEdition">
{{ $t('license.community') }}
</el-link>
<el-link underline="never" class="version" type="primary" @click="getVersionLog()">
{{ version }}
</el-link>
<el-badge
is-dot
v-if="isAdmin && !isOffline && !isEE"
class="-mt-0.5"
:hidden="version === 'Waiting' || !hasNewVersion"
>
<el-link class="ml-2" underline="never" type="primary" @click="onLoadUpgradeInfo">
{{ $t('commons.button.update') }}
</el-link>
<el-divider direction="vertical" />
<el-link type="primary" underline="never" @click="toForum" v-if="!isFxplay">
<span class="font-normal">{{ $t('setting.forum') }}</span>
</el-link>
<el-divider direction="vertical" v-if="!isFxplay" />
<el-link type="primary" underline="never" @click="toDoc">
<span class="font-normal">{{ $t('setting.doc2') }}</span>
</el-link>
<el-divider direction="vertical" v-if="!isFxplay" />
<el-link type="primary" underline="never" @click="toGithub" v-if="!isFxplay">
<span class="font-normal">{{ $t('setting.project') }}</span>
</el-link>
<el-divider direction="vertical" />
</span>
<div class="flex flex-wrap items-center">
<el-link v-if="isEE" underline="never" type="primary" @click="toEdition">
{{ $t('license.ee') }}
</el-link>
<el-link v-else-if="isMasterPro" underline="never" type="primary" @click="toLxware">
{{ $t('license.pro') }}
</el-link>
<el-link v-else-if="isOffline" underline="never" type="primary" @click="to1Panel">
{{ $t('license.offLine') }}
</el-link>
<el-link v-else underline="never" type="primary" @click="toEdition">
{{ $t('license.community') }}
</el-link>
<el-link underline="never" class="version" type="primary" @click="getVersionLog()">
{{ version }}
</el-link>
<el-badge
is-dot
v-if="isAdmin && !isOffline && !isEE"
class="-mt-0.5"
:hidden="version === 'Waiting' || !hasNewVersion"
>
<el-link class="ml-2" underline="never" type="primary" @click="onLoadUpgradeInfo">
{{ $t('commons.button.update') }}
</el-link>
</el-badge>
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">{{ $t('setting.upgrading') }}</el-tag>
</div>
</el-badge>
<el-tag v-if="version === 'Waiting'" round class="ml-2.5">{{ $t('setting.upgrading') }}</el-tag>
</div>
</div>
@@ -65,7 +45,7 @@ import { MsgSuccess } from '@/utils/message';
import { onMounted, ref } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { docsUrl, isOffline, isFxplay, isMasterPro, isEE, isIntl, isAdmin, hasNewVersion } = useGlobalStore();
const { isOffline, isMasterPro, isEE, isIntl, isAdmin, hasNewVersion } = useGlobalStore();
const upgradeRef = ref();
const releasesRef = ref();
@@ -73,12 +53,6 @@ const version = ref<string>('');
const loading = ref(false);
const upgradeInfo = ref();
const upgradeVersion = ref();
const props = defineProps({
footer: {
type: Boolean,
default: false,
},
});
const search = async () => {
const res = await getSettingBaseInfo();
@@ -105,10 +79,6 @@ const to1Panel = () => {
window.open(url, '_blank', 'noopener,noreferrer');
};
const toDoc = () => {
window.open(docsUrl.value.endsWith('/') ? docsUrl.value : `${docsUrl.value}/`, '_blank', 'noopener,noreferrer');
};
const toEdition = () => {
if (!isIntl.value) {
window.open('https://1panel.cn/versions.html' + '', '_blank', 'noopener,noreferrer');
@@ -117,15 +87,6 @@ const toEdition = () => {
}
};
const toForum = () => {
let url = isIntl.value ? 'https://github.com/1Panel-dev/1Panel/discussions' : 'https://bbs.fit2cloud.com/c/1p/7';
window.open(url, '_blank', 'noopener,noreferrer');
};
const toGithub = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank', 'noopener,noreferrer');
};
const onLoadUpgradeInfo = async () => {
loading.value = true;
await loadUpgradeInfo()
+34
View File
@@ -0,0 +1,34 @@
import type { FooterNavigationSetting } from '@/components/footer-navigation/model';
type FooterSettingResult = {
data: FooterNavigationSetting;
};
type EnterpriseFooterSettingModule = {
getFooterSetting?: (silent?: boolean) => Promise<FooterSettingResult>;
};
type EnterpriseFooterSettingModuleLoader = () => Promise<EnterpriseFooterSettingModule>;
const enterpriseModules = import.meta.glob<EnterpriseFooterSettingModule>('@/enterprise/api/modules/footer-setting.ts');
function getEnterpriseFooterSettingModuleLoader(): EnterpriseFooterSettingModuleLoader | null {
for (const path in enterpriseModules) {
if (path.endsWith('/api/modules/footer-setting.ts')) {
return enterpriseModules[path];
}
}
return null;
}
export async function getEnterpriseFooterSetting(silent = false) {
const loader = getEnterpriseFooterSettingModuleLoader();
if (!loader) {
return null;
}
const module = await loader();
if (!module?.getFooterSetting) {
return null;
}
return module.getFooterSetting(silent);
}
+8
View File
@@ -6194,6 +6194,14 @@ const message = {
loginGroup: 'Login Page',
loginBtnLinkColor: 'Button/Link Color',
loginBtnLinkColorHelper: 'Will be displayed as the button/link color on the login page',
footerNavigation: 'Footer Navigation',
footerLoadFailed: 'Failed to load the footer navigation settings. Please try again.',
showLearnMore: 'Show Commercial Edition Info',
showForum: 'Show Forum Help',
showDocumentation: 'Show User Manual',
showProject: 'Show Project Repository',
footerSaveHelper: 'The footer navigation settings will be saved. Continue?',
footerResetHelper: 'The footer navigation settings will be restored to defaults. Continue?',
},
helper: {
wafTitle1: 'Interception Map',
+8
View File
@@ -6290,6 +6290,14 @@ const message = {
loginGroup: 'Página de login',
loginBtnLinkColor: 'Color de botones/enlaces',
loginBtnLinkColorHelper: 'Se aplica a botones y enlaces en la página de login',
footerNavigation: 'Navegación del pie de página',
footerLoadFailed: 'No se pudo cargar la configuración de navegación del pie de página. Inténtalo de nuevo.',
showLearnMore: 'Mostrar información de la edición comercial',
showForum: 'Mostrar ayuda del foro',
showDocumentation: 'Mostrar manual de usuario',
showProject: 'Mostrar repositorio del proyecto',
footerSaveHelper: 'Se guardará la configuración del pie de página. ¿Continuar?',
footerResetHelper: 'Se restaurará la configuración predeterminada del pie de página. ¿Continuar?',
},
helper: {
wafTitle1: 'Mapa de Intercepciones',
+8
View File
@@ -6140,6 +6140,14 @@ const message = {
loginGroup: 'صفحه ورود',
loginBtnLinkColor: 'رنگ دکمه/لینک',
loginBtnLinkColorHelper: 'به عنوان رنگ دکمه/لینک در صفحه ورود نمایش داده می‌شود',
footerNavigation: 'پیمایش پاورقی',
footerLoadFailed: 'بارگیری تنظیمات پیمایش پاورقی ناموفق بود. دوباره تلاش کنید.',
showLearnMore: 'نمایش اطلاعات نسخه تجاری',
showForum: 'نمایش راهنمای انجمن',
showDocumentation: 'نمایش راهنمای کاربر',
showProject: 'نمایش مخزن پروژه',
footerSaveHelper: 'تنظیمات پیمایش پاورقی ذخیره خواهد شد. ادامه می‌دهید؟',
footerResetHelper: 'تنظیمات پیمایش پاورقی به حالت پیش‌فرض بازگردانده خواهد شد. ادامه می‌دهید؟',
},
helper: {
wafTitle1: 'نقشه رهگیری',
+8
View File
@@ -6178,6 +6178,14 @@ const message = {
loginGroup: 'ログインページ',
loginBtnLinkColor: 'ボタンリンクの色',
loginBtnLinkColorHelper: 'ログインページに表示されるボタンリンクの色になります',
footerNavigation: 'フッターナビゲーション',
footerLoadFailed: 'フッターナビゲーション設定の読み込みに失敗しましたもう一度お試しください',
showLearnMore: '商用版の案内を表示',
showForum: 'フォーラムヘルプを表示',
showDocumentation: 'ユーザーマニュアルを表示',
showProject: 'プロジェクトのリンクを表示',
footerSaveHelper: 'フッターナビゲーション設定を保存します続行しますか',
footerResetHelper: 'フッターナビゲーション設定を既定値に戻します続行しますか',
},
helper: {
wafTitle1: 'インターセプションマップ',
+8
View File
@@ -6056,6 +6056,14 @@ const message = {
loginGroup: '로그인 페이지',
loginBtnLinkColor: '버튼/링크 색상',
loginBtnLinkColorHelper: '로그인 페이지의 버튼/링크 색상으로 표시됩니다',
footerNavigation: '바닥글 탐색',
footerLoadFailed: '바닥글 탐색 설정을 불러오지 못했습니다. 다시 시도해 주세요.',
showLearnMore: '상용 버전 정보 표시',
showForum: '포럼 도움말 표시',
showDocumentation: '사용 설명서 표시',
showProject: '프로젝트 주소 표시',
footerSaveHelper: '바닥글 탐색 설정을 저장합니다. 계속하시겠습니까?',
footerResetHelper: '바닥글 탐색 설정을 기본값으로 복원합니다. 계속하시겠습니까?',
},
helper: {
wafTitle1: '차단 지도',
+8
View File
@@ -6012,6 +6012,14 @@ const message = {
loginGroup: 'ໜ້າລັອກອິນ',
loginBtnLinkColor: 'ສີປຸ່ມ/ລິ້ງ',
loginBtnLinkColorHelper: 'ຈະສະແດງເປັນສີປຸ່ມ ຫຼື ລິ້ງໃນໜ້າລັອກອິນ',
footerNavigation: 'ການນຳທາງສ່ວນທ້າຍ',
footerLoadFailed: 'ໂຫຼດການຕັ້ງຄ່າການນຳທາງສ່ວນທ້າຍບໍ່ສຳເລັດ. ກະລຸນາລອງໃໝ່.',
showLearnMore: 'ສະແດງຂໍ້ມູນລຸ້ນການຄ້າ',
showForum: 'ສະແດງຄວາມຊ່ວຍເຫຼືອຈາກຟໍຣັມ',
showDocumentation: 'ສະແດງຄູ່ມືຜູ້ໃຊ້',
showProject: 'ສະແດງຄັງໂຄງການ',
footerSaveHelper: 'ຈະບັນທຶກການຕັ້ງຄ່າສ່ວນທ້າຍ. ສືບຕໍ່ບໍ?',
footerResetHelper: 'ຈະກູ້ຄືນການຕັ້ງຄ່າສ່ວນທ້າຍເປັນຄ່າເລີ່ມຕົ້ນ. ສືບຕໍ່ບໍ?',
},
helper: {
wafTitle1: 'ແຜນທີ່ການສະກັດກັ້ນ',
+8
View File
@@ -6284,6 +6284,14 @@ const message = {
loginGroup: 'Halaman Log Masuk',
loginBtnLinkColor: 'Warna Butang/Pautan',
loginBtnLinkColorHelper: 'Akan dipaparkan sebagai warna butang/pautan di halaman log masuk',
footerNavigation: 'Navigasi pengaki',
footerLoadFailed: 'Gagal memuatkan tetapan navigasi pengaki. Sila cuba lagi.',
showLearnMore: 'Tunjukkan maklumat edisi komersial',
showForum: 'Tunjukkan bantuan forum',
showDocumentation: 'Tunjukkan manual pengguna',
showProject: 'Tunjukkan repositori projek',
footerSaveHelper: 'Tetapan navigasi pengaki akan disimpan. Teruskan?',
footerResetHelper: 'Tetapan navigasi pengaki akan dipulihkan kepada lalai. Teruskan?',
},
helper: {
wafTitle1: 'Peta Pencegahan',
+8
View File
@@ -6320,6 +6320,14 @@ const message = {
loginGroup: 'Página de login',
loginBtnLinkColor: 'Cor do botão/link',
loginBtnLinkColorHelper: 'Será exibido como a cor do botão/link na página de login',
footerNavigation: 'Navegação do rodapé',
footerLoadFailed: 'Falha ao carregar as configurações de navegação do rodapé. Tente novamente.',
showLearnMore: 'Mostrar informações da edição comercial',
showForum: 'Mostrar ajuda do fórum',
showDocumentation: 'Mostrar manual do usuário',
showProject: 'Mostrar repositório do projeto',
footerSaveHelper: 'As configurações de navegação do rodapé serão salvas. Continuar?',
footerResetHelper: 'As configurações de navegação do rodapé serão restauradas. Continuar?',
},
helper: {
wafTitle1: 'Mapa de Interceptação',
+8
View File
@@ -6283,6 +6283,14 @@ const message = {
loginGroup: 'Страница входа',
loginBtnLinkColor: 'Цвет кнопки/ссылки',
loginBtnLinkColorHelper: 'Будет отображаться как цвет кнопки/ссылки на странице входа',
footerNavigation: 'Навигация в нижнем колонтитуле',
footerLoadFailed: 'Не удалось загрузить настройки навигации в нижнем колонтитуле. Повторите попытку.',
showLearnMore: 'Показывать информацию о коммерческой версии',
showForum: 'Показывать помощь форума',
showDocumentation: 'Показывать руководство пользователя',
showProject: 'Показывать репозиторий проекта',
footerSaveHelper: 'Настройки навигации в нижнем колонтитуле будут сохранены. Продолжить?',
footerResetHelper: 'Настройки навигации в нижнем колонтитуле будут восстановлены. Продолжить?',
},
helper: {
wafTitle1: 'Карта Перехватов',
+8
View File
@@ -6285,6 +6285,14 @@ const message = {
loginGroup: 'Giriş Sayfası',
loginBtnLinkColor: 'Buton/Bağlantı Rengi',
loginBtnLinkColorHelper: 'Giriş sayfasındaki buton/bağlantı rengi olarak gösterilecektir',
footerNavigation: 'Alt bilgi gezinmesi',
footerLoadFailed: 'Alt bilgi gezinme ayarları yüklenemedi. Lütfen tekrar deneyin.',
showLearnMore: 'Ticari sürüm bilgisini göster',
showForum: 'Forum yardımını göster',
showDocumentation: 'Kullanım kılavuzunu göster',
showProject: 'Proje deposunu göster',
footerSaveHelper: 'Alt bilgi gezinme ayarları kaydedilecek. Devam edilsin mi?',
footerResetHelper: 'Alt bilgi gezinme ayarları varsayılanlara döndürülecek. Devam edilsin mi?',
},
helper: {
wafTitle1: 'Engelleme Haritası',
+8
View File
@@ -5763,6 +5763,14 @@ const message = {
loginGroup: '登入頁面',
loginBtnLinkColor: '按鈕顏色',
loginBtnLinkColorHelper: '將顯示為登入頁面上的按鈕顏色',
footerNavigation: '底部導覽',
footerLoadFailed: '底部導覽設定載入失敗請重試',
showLearnMore: '顯示瞭解商業版',
showForum: '顯示論壇求助',
showDocumentation: '顯示使用手冊',
showProject: '顯示專案地址',
footerSaveHelper: '即將儲存底部導覽設定是否繼續',
footerResetHelper: '即將還原底部導覽預設設定是否繼續',
},
helper: {
wafTitle1: '攔截地圖',
+8
View File
@@ -4715,6 +4715,14 @@ const message = {
loginGroup: '登录页',
loginBtnLinkColor: '按钮/链接颜色',
loginBtnLinkColorHelper: '将在登录页面显示为按钮和链接颜色',
footerNavigation: '底部导航',
footerLoadFailed: '底部导航设置加载失败请重试',
showLearnMore: '显示了解商业版',
showForum: '显示论坛求助',
showDocumentation: '显示使用手册',
showProject: '显示项目地址',
footerSaveHelper: '即将保存底部导航设置是否继续',
footerResetHelper: '即将恢复底部导航默认设置是否继续',
},
helper: {
wafTitle1: '拦截地图',
+25 -4
View File
@@ -1,5 +1,5 @@
<template>
<div class="footer" :style="{ height: isMobile ? '108px' : '48px' }">
<div class="footer" :class="{ 'footer--mobile': isMobile }">
<div class="flex w-full flex-col gap-4 md:justify-between md:flex-row">
<div class="flex flex-wrap gap-4">
<a v-if="!isIntl && !isFxplay" href="https://fit2cloud.com/" target="_blank">
@@ -9,8 +9,9 @@
Copyright © {{ year }} {{ $t('commons.lingxia') }}
</a>
</div>
<div class="flex flex-row gap-2 md:flex-col lg:flex-row">
<SystemUpgrade :footer="true" />
<div class="footer-actions">
<FooterNavigation />
<SystemUpgrade />
</div>
</div>
</div>
@@ -18,6 +19,7 @@
<script setup lang="ts">
import SystemUpgrade from '@/components/system-upgrade/index.vue';
import FooterNavigation from '@/components/footer-navigation/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isFxplay, isIntl, isMobile } = useGlobalStore();
@@ -30,7 +32,8 @@ const year = new Date().getFullYear();
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
min-height: 48px;
height: auto;
background: var(--panel-footer-bg);
border-top: 1px solid var(--panel-footer-border);
box-sizing: border-box;
@@ -48,4 +51,22 @@ const year = new Date().getFullYear();
letter-spacing: 0.5px;
}
}
.footer--mobile {
min-height: 108px;
}
.footer-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
row-gap: 8px;
}
@media (max-width: 767px) {
.footer-actions {
column-gap: 8px;
justify-content: center;
}
}
</style>