mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
feat: add footer navigation component and update translations (#13642)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -6140,6 +6140,14 @@ const message = {
|
||||
loginGroup: 'صفحه ورود',
|
||||
loginBtnLinkColor: 'رنگ دکمه/لینک',
|
||||
loginBtnLinkColorHelper: 'به عنوان رنگ دکمه/لینک در صفحه ورود نمایش داده میشود',
|
||||
footerNavigation: 'پیمایش پاورقی',
|
||||
footerLoadFailed: 'بارگیری تنظیمات پیمایش پاورقی ناموفق بود. دوباره تلاش کنید.',
|
||||
showLearnMore: 'نمایش اطلاعات نسخه تجاری',
|
||||
showForum: 'نمایش راهنمای انجمن',
|
||||
showDocumentation: 'نمایش راهنمای کاربر',
|
||||
showProject: 'نمایش مخزن پروژه',
|
||||
footerSaveHelper: 'تنظیمات پیمایش پاورقی ذخیره خواهد شد. ادامه میدهید؟',
|
||||
footerResetHelper: 'تنظیمات پیمایش پاورقی به حالت پیشفرض بازگردانده خواهد شد. ادامه میدهید؟',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'نقشه رهگیری',
|
||||
|
||||
@@ -6178,6 +6178,14 @@ const message = {
|
||||
loginGroup: 'ログインページ',
|
||||
loginBtnLinkColor: 'ボタン/リンクの色',
|
||||
loginBtnLinkColorHelper: 'ログインページに表示されるボタン/リンクの色になります',
|
||||
footerNavigation: 'フッターナビゲーション',
|
||||
footerLoadFailed: 'フッターナビゲーション設定の読み込みに失敗しました。もう一度お試しください。',
|
||||
showLearnMore: '商用版の案内を表示',
|
||||
showForum: 'フォーラムヘルプを表示',
|
||||
showDocumentation: 'ユーザーマニュアルを表示',
|
||||
showProject: 'プロジェクトのリンクを表示',
|
||||
footerSaveHelper: 'フッターナビゲーション設定を保存します。続行しますか?',
|
||||
footerResetHelper: 'フッターナビゲーション設定を既定値に戻します。続行しますか?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'インターセプションマップ',
|
||||
|
||||
@@ -6056,6 +6056,14 @@ const message = {
|
||||
loginGroup: '로그인 페이지',
|
||||
loginBtnLinkColor: '버튼/링크 색상',
|
||||
loginBtnLinkColorHelper: '로그인 페이지의 버튼/링크 색상으로 표시됩니다',
|
||||
footerNavigation: '바닥글 탐색',
|
||||
footerLoadFailed: '바닥글 탐색 설정을 불러오지 못했습니다. 다시 시도해 주세요.',
|
||||
showLearnMore: '상용 버전 정보 표시',
|
||||
showForum: '포럼 도움말 표시',
|
||||
showDocumentation: '사용 설명서 표시',
|
||||
showProject: '프로젝트 주소 표시',
|
||||
footerSaveHelper: '바닥글 탐색 설정을 저장합니다. 계속하시겠습니까?',
|
||||
footerResetHelper: '바닥글 탐색 설정을 기본값으로 복원합니다. 계속하시겠습니까?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '차단 지도',
|
||||
|
||||
@@ -6012,6 +6012,14 @@ const message = {
|
||||
loginGroup: 'ໜ້າລັອກອິນ',
|
||||
loginBtnLinkColor: 'ສີປຸ່ມ/ລິ້ງ',
|
||||
loginBtnLinkColorHelper: 'ຈະສະແດງເປັນສີປຸ່ມ ຫຼື ລິ້ງໃນໜ້າລັອກອິນ',
|
||||
footerNavigation: 'ການນຳທາງສ່ວນທ້າຍ',
|
||||
footerLoadFailed: 'ໂຫຼດການຕັ້ງຄ່າການນຳທາງສ່ວນທ້າຍບໍ່ສຳເລັດ. ກະລຸນາລອງໃໝ່.',
|
||||
showLearnMore: 'ສະແດງຂໍ້ມູນລຸ້ນການຄ້າ',
|
||||
showForum: 'ສະແດງຄວາມຊ່ວຍເຫຼືອຈາກຟໍຣັມ',
|
||||
showDocumentation: 'ສະແດງຄູ່ມືຜູ້ໃຊ້',
|
||||
showProject: 'ສະແດງຄັງໂຄງການ',
|
||||
footerSaveHelper: 'ຈະບັນທຶກການຕັ້ງຄ່າສ່ວນທ້າຍ. ສືບຕໍ່ບໍ?',
|
||||
footerResetHelper: 'ຈະກູ້ຄືນການຕັ້ງຄ່າສ່ວນທ້າຍເປັນຄ່າເລີ່ມຕົ້ນ. ສືບຕໍ່ບໍ?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'ແຜນທີ່ການສະກັດກັ້ນ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -6283,6 +6283,14 @@ const message = {
|
||||
loginGroup: 'Страница входа',
|
||||
loginBtnLinkColor: 'Цвет кнопки/ссылки',
|
||||
loginBtnLinkColorHelper: 'Будет отображаться как цвет кнопки/ссылки на странице входа',
|
||||
footerNavigation: 'Навигация в нижнем колонтитуле',
|
||||
footerLoadFailed: 'Не удалось загрузить настройки навигации в нижнем колонтитуле. Повторите попытку.',
|
||||
showLearnMore: 'Показывать информацию о коммерческой версии',
|
||||
showForum: 'Показывать помощь форума',
|
||||
showDocumentation: 'Показывать руководство пользователя',
|
||||
showProject: 'Показывать репозиторий проекта',
|
||||
footerSaveHelper: 'Настройки навигации в нижнем колонтитуле будут сохранены. Продолжить?',
|
||||
footerResetHelper: 'Настройки навигации в нижнем колонтитуле будут восстановлены. Продолжить?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: 'Карта Перехватов',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -5763,6 +5763,14 @@ const message = {
|
||||
loginGroup: '登入頁面',
|
||||
loginBtnLinkColor: '按鈕顏色',
|
||||
loginBtnLinkColorHelper: '將顯示為登入頁面上的按鈕顏色',
|
||||
footerNavigation: '底部導覽',
|
||||
footerLoadFailed: '底部導覽設定載入失敗,請重試。',
|
||||
showLearnMore: '顯示瞭解商業版',
|
||||
showForum: '顯示論壇求助',
|
||||
showDocumentation: '顯示使用手冊',
|
||||
showProject: '顯示專案地址',
|
||||
footerSaveHelper: '即將儲存底部導覽設定,是否繼續?',
|
||||
footerResetHelper: '即將還原底部導覽預設設定,是否繼續?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '攔截地圖',
|
||||
|
||||
@@ -4715,6 +4715,14 @@ const message = {
|
||||
loginGroup: '登录页',
|
||||
loginBtnLinkColor: '按钮/链接颜色',
|
||||
loginBtnLinkColorHelper: '将在登录页面显示为按钮和链接颜色',
|
||||
footerNavigation: '底部导航',
|
||||
footerLoadFailed: '底部导航设置加载失败,请重试。',
|
||||
showLearnMore: '显示了解商业版',
|
||||
showForum: '显示论坛求助',
|
||||
showDocumentation: '显示使用手册',
|
||||
showProject: '显示项目地址',
|
||||
footerSaveHelper: '即将保存底部导航设置,是否继续?',
|
||||
footerResetHelper: '即将恢复底部导航默认设置,是否继续?',
|
||||
},
|
||||
helper: {
|
||||
wafTitle1: '拦截地图',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user