feat: 🚀 动态语言类型

This commit is contained in:
bunny 2024-10-09 15:48:26 +08:00
parent d303cbce44
commit 013eb326c3
9 changed files with 240 additions and 297 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,6 +1,7 @@
{
"Version": "5.8.0",
"Title": "PureAdmin",
"Title": "BunnyAdmin",
"Copyright": "Copyright © 2020-present",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,

View File

@ -2,11 +2,12 @@
import { getConfig } from '@/config';
const TITLE = getConfig('Title');
const Copyright = getConfig('Copyright');
</script>
<template>
<footer class="layout-footer text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]">
Copyright © 2020-present
{{ Copyright }}
<a class="hover:text-primary" href="https://github.com/pure-admin" target="_blank"> &nbsp;{{ TITLE }} </a>
</footer>
</template>

View File

@ -13,10 +13,13 @@ import LogoutCircleRLine from '@iconify-icons/ri/logout-circle-r-line';
import Setting from '@iconify-icons/ri/settings-3-line';
import Check from '@iconify-icons/ep/check';
import { $t } from '@/plugins/i18n';
import { userI18nTypeStore } from '@/store/i18n/i18nType';
const { layout, device, logout, onPanel, pureApp, username, userAvatar, avatarsStyle, toggleSideBar, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { t, locale, translationCh, translationEn } = useTranslationLang();
const { locale, translation } = useTranslationLang();
const i18nTypeStore = userI18nTypeStore();
</script>
<template>
@ -35,15 +38,17 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
<GlobalizationIcon class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none" />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" :style="getDropdownItemStyle(locale, 'zh')" @click="translationCh">
<IconifyIconOffline v-show="locale === 'zh'" :icon="Check" class="check-zh" />
简体中文
</el-dropdown-item>
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" :style="getDropdownItemStyle(locale, 'en')" @click="translationEn">
<span v-show="locale === 'en'" class="check-en">
<el-dropdown-item
v-for="item in i18nTypeStore.translationTypeList"
:key="item.key"
:class="['dark:!text-white', getDropdownItemClass(locale, item.key)]"
:style="getDropdownItemStyle(locale, item.key)"
@click="translation(item.key)"
>
<span v-show="locale === item.key" class="check">
<IconifyIconOffline :icon="Check" />
</span>
English
{{ item.value }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -127,12 +132,7 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
padding: 5px 40px;
}
.check-zh {
position: absolute;
left: 20px;
}
.check-en {
.check {
position: absolute;
left: 20px;
}

View File

@ -1,97 +1,90 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref, unref, watch, onMounted, nextTick } from "vue";
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { nextTick, onMounted, ref, unref, watch } from 'vue';
defineOptions({
name: "LayFrame"
name: 'LayFrame',
});
const props = defineProps<{
frameInfo?: {
frameSrc?: string;
fullPath?: string;
};
frameInfo?: {
frameSrc?: string;
fullPath?: string;
};
}>();
const { t } = useI18n();
const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameSrc = ref<string>('');
const frameRef = ref<HTMLElement | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function hideLoading() {
loading.value = false;
loading.value = false;
}
function init() {
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent("onload", () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent('onload', () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
}
watch(
() => currentRoute.fullPath,
path => {
if (
currentRoute.name === "Redirect" &&
path.includes(props.frameInfo?.fullPath)
) {
frameSrc.value = path; // redirect
loading.value = true;
}
//
if (props.frameInfo?.fullPath === path) {
frameSrc.value = props.frameInfo?.frameSrc;
}
}
() => currentRoute.fullPath,
path => {
if (currentRoute.name === 'Redirect' && path.includes(props.frameInfo?.fullPath)) {
frameSrc.value = path; // redirect
loading.value = true;
}
//
if (props.frameInfo?.fullPath === path) {
frameSrc.value = props.frameInfo?.frameSrc;
}
},
);
onMounted(() => {
init();
init();
});
</script>
<template>
<div
v-loading="loading"
class="frame"
:element-loading-text="t('status.pureLoad')"
>
<iframe ref="frameRef" :src="frameSrc" class="frame-iframe" />
</div>
<div v-loading="loading" :element-loading-text="t('status.pureLoad')" class="frame">
<iframe ref="frameRef" :src="frameSrc" class="frame-iframe" />
</div>
</template>
<style lang="scss" scoped>
.frame {
position: absolute;
inset: 0;
position: absolute;
inset: 0;
.frame-iframe {
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
}
.frame-iframe {
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
}
}
.main-content {
margin: 2px 0 0 !important;
margin: 2px 0 0 !important;
}
</style>

View File

@ -1,40 +1,29 @@
import { useNav } from "./useNav";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { onBeforeMount, type Ref, watch } from "vue";
import { useNav } from './useNav';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { onBeforeMount, type Ref, watch } from 'vue';
export function useTranslationLang(ref?: Ref) {
const { $storage, changeTitle, handleResize } = useNav();
const { locale } = useI18n();
const route = useRoute();
const { $storage, changeTitle, handleResize } = useNav();
const { locale } = useI18n();
const route = useRoute();
function translationCh() {
$storage.locale = { locale: "zh" };
locale.value = "zh";
ref && handleResize(ref.value);
}
function translation(value: string) {
$storage.locale = { locale: value };
locale.value = value;
ref && handleResize(ref.value);
}
function translationEn() {
$storage.locale = { locale: "en" };
locale.value = "en";
ref && handleResize(ref.value);
}
watch(
() => locale.value,
() => {
changeTitle(route.meta);
},
);
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
onBeforeMount(() => {
locale.value = $storage.locale?.locale ?? 'zh';
});
onBeforeMount(() => {
locale.value = $storage.locale?.locale ?? "zh";
});
return {
route,
locale,
translationCh,
translationEn
};
return { route, locale, translation };
}

View File

@ -1,36 +1,23 @@
<script lang="ts" setup>
import "animate.css";
import 'animate.css';
// src/components/CommonIcon/src/offlineIcon.ts 使addIcon
import "@/components/CommonIcon/src/offlineIcon";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { useAppStoreHook } from "@/store/modules/app";
import { useSettingStoreHook } from "@/store/modules/settings";
import {
deviceDetection,
useDark,
useGlobal,
useResizeObserver
} from "@pureadmin/utils";
import {
computed,
defineComponent,
h,
onBeforeMount,
onMounted,
reactive,
ref
} from "vue";
import { useI18n } from "vue-i18n";
import { useLayout } from "./hooks/useLayout";
import { setType } from "./types";
import '@/components/CommonIcon/src/offlineIcon';
import { useDataThemeChange } from '@/layout/hooks/useDataThemeChange';
import { useAppStoreHook } from '@/store/modules/app';
import { useSettingStoreHook } from '@/store/modules/settings';
import { deviceDetection, useDark, useGlobal, useResizeObserver } from '@pureadmin/utils';
import { computed, defineComponent, h, onBeforeMount, onMounted, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLayout } from './hooks/useLayout';
import { setType } from './types';
import BackTopIcon from "@/assets/svg/back_top.svg?component";
import LayContent from "./components/lay-content/index.vue";
import LayNavbar from "./components/lay-navbar/index.vue";
import LaySetting from "./components/lay-setting/index.vue";
import NavHorizontal from "./components/lay-sidebar/NavHorizontal.vue";
import NavVertical from "./components/lay-sidebar/NavVertical.vue";
import LayTag from "./components/lay-tag/index.vue";
import BackTopIcon from '@/assets/svg/back_top.svg?component';
import LayContent from './components/lay-content/index.vue';
import LayNavbar from './components/lay-navbar/index.vue';
import LaySetting from './components/lay-setting/index.vue';
import NavHorizontal from './components/lay-sidebar/NavHorizontal.vue';
import NavVertical from './components/lay-sidebar/NavVertical.vue';
import LayTag from './components/lay-tag/index.vue';
const { t } = useI18n();
const appWrapperRef = ref();
@ -41,197 +28,165 @@ const pureSetting = useSettingStoreHook();
const { $storage } = useGlobal<GlobalPropertiesApi>();
const set: setType = reactive({
sidebar: computed(() => {
return useAppStoreHook().sidebar;
}),
sidebar: computed(() => {
return useAppStoreHook().sidebar;
}),
device: computed(() => {
return useAppStoreHook().device;
}),
device: computed(() => {
return useAppStoreHook().device;
}),
fixedHeader: computed(() => {
return pureSetting.fixedHeader;
}),
fixedHeader: computed(() => {
return pureSetting.fixedHeader;
}),
classes: computed(() => {
return {
hideSidebar: !set.sidebar.opened,
openSidebar: set.sidebar.opened,
withoutAnimation: set.sidebar.withoutAnimation,
mobile: set.device === "mobile"
};
}),
classes: computed(() => {
return {
hideSidebar: !set.sidebar.opened,
openSidebar: set.sidebar.opened,
withoutAnimation: set.sidebar.withoutAnimation,
mobile: set.device === 'mobile',
};
}),
hideTabs: computed(() => {
return $storage?.configure.hideTabs;
})
hideTabs: computed(() => {
return $storage?.configure.hideTabs;
}),
});
function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel);
$storage.layout = {
layout: `${layoutModel}`,
theme: $storage.layout?.theme,
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor,
themeColor: $storage.layout?.themeColor,
overallStyle: $storage.layout?.overallStyle
};
window.document.body.setAttribute('layout', layoutModel);
$storage.layout = {
layout: `${layoutModel}`,
theme: $storage.layout?.theme,
darkMode: $storage.layout?.darkMode,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor,
themeColor: $storage.layout?.themeColor,
overallStyle: $storage.layout?.overallStyle,
};
}
function toggle(device: string, bool: boolean) {
useAppStoreHook().toggleDevice(device);
useAppStoreHook().toggleSideBar(bool, "resize");
useAppStoreHook().toggleDevice(device);
useAppStoreHook().toggleSideBar(bool, 'resize');
}
//
let isAutoCloseSidebar = true;
useResizeObserver(appWrapperRef, entries => {
if (isMobile) return;
const entry = entries[0];
const [{ inlineSize: width, blockSize: height }] = entry.borderBoxSize;
useAppStoreHook().setViewportSize({ width, height });
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
* width > 990 展开侧边栏
*/
if (width > 0 && width <= 760) {
toggle("mobile", false);
isAutoCloseSidebar = true;
} else if (width > 760 && width <= 990) {
if (isAutoCloseSidebar) {
toggle("desktop", false);
isAutoCloseSidebar = false;
}
} else if (width > 990 && !set.sidebar.isClickCollapse) {
toggle("desktop", true);
isAutoCloseSidebar = true;
} else {
toggle("desktop", false);
isAutoCloseSidebar = false;
}
if (isMobile) return;
const entry = entries[0];
const [{ inlineSize: width, blockSize: height }] = entry.borderBoxSize;
useAppStoreHook().setViewportSize({ width, height });
width <= 760 ? setTheme('vertical') : setTheme(useAppStoreHook().layout);
/** width app-wrapper
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
* width > 990 展开侧边栏
*/
if (width > 0 && width <= 760) {
toggle('mobile', false);
isAutoCloseSidebar = true;
} else if (width > 760 && width <= 990) {
if (isAutoCloseSidebar) {
toggle('desktop', false);
isAutoCloseSidebar = false;
}
} else if (width > 990 && !set.sidebar.isClickCollapse) {
toggle('desktop', true);
isAutoCloseSidebar = true;
} else {
toggle('desktop', false);
isAutoCloseSidebar = false;
}
});
onMounted(() => {
if (isMobile) {
toggle("mobile", false);
}
if (isMobile) {
toggle('mobile', false);
}
});
onBeforeMount(() => {
useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
});
const LayHeader = defineComponent({
name: "LayHeader",
render() {
return h(
"div",
{
class: { "fixed-header": set.fixedHeader },
style: [
set.hideTabs && layout.value.includes("horizontal")
? isDark.value
? "box-shadow: 0 1px 4px #0d0d0d"
: "box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08)"
: ""
]
},
{
default: () => [
!pureSetting.hiddenSideBar &&
(layout.value.includes("vertical") || layout.value.includes("mix"))
? h(LayNavbar)
: null,
!pureSetting.hiddenSideBar && layout.value.includes("horizontal")
? h(NavHorizontal)
: null,
h(LayTag)
]
}
);
}
name: 'LayHeader',
render() {
return h(
'div',
{
class: { 'fixed-header': set.fixedHeader },
style: [set.hideTabs && layout.value.includes('horizontal') ? (isDark.value ? 'box-shadow: 0 1px 4px #0d0d0d' : 'box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08)') : ''],
},
{
default: () => [
!pureSetting.hiddenSideBar && (layout.value.includes('vertical') || layout.value.includes('mix')) ? h(LayNavbar) : null,
!pureSetting.hiddenSideBar && layout.value.includes('horizontal') ? h(NavHorizontal) : null,
h(LayTag),
],
},
);
},
});
</script>
<template>
<div ref="appWrapperRef" :class="['app-wrapper', set.classes]">
<div
v-show="
set.device === 'mobile' &&
set.sidebar.opened &&
layout.includes('vertical')
"
class="app-mask"
@click="useAppStoreHook().toggleSideBar()"
/>
<NavVertical
v-show="
!pureSetting.hiddenSideBar &&
(layout.includes('vertical') || layout.includes('mix'))
"
/>
<div
:class="[
'main-container',
pureSetting.hiddenSideBar ? 'main-hidden' : ''
]"
>
<div v-if="set.fixedHeader">
<LayHeader />
<!-- 主体内容 -->
<LayContent :fixed-header="set.fixedHeader" />
</div>
<el-scrollbar v-else>
<el-backtop
:title="t('buttons.pureBackTop')"
target=".main-container .el-scrollbar__wrap"
>
<BackTopIcon />
</el-backtop>
<LayHeader />
<!-- 主体内容 -->
<LayContent :fixed-header="set.fixedHeader" />
</el-scrollbar>
</div>
<!-- 系统设置 -->
<LaySetting />
</div>
<div ref="appWrapperRef" :class="['app-wrapper', set.classes]">
<div v-show="set.device === 'mobile' && set.sidebar.opened && layout.includes('vertical')" class="app-mask" @click="useAppStoreHook().toggleSideBar()" />
<NavVertical v-show="!pureSetting.hiddenSideBar && (layout.includes('vertical') || layout.includes('mix'))" />
<div :class="['main-container', pureSetting.hiddenSideBar ? 'main-hidden' : '']">
<div v-if="set.fixedHeader">
<LayHeader />
<!-- 主体内容 -->
<LayContent :fixed-header="set.fixedHeader" />
</div>
<el-scrollbar v-else>
<el-backtop :title="t('buttons.pureBackTop')" target=".main-container .el-scrollbar__wrap">
<BackTopIcon />
</el-backtop>
<LayHeader />
<!-- 主体内容 -->
<LayContent :fixed-header="set.fixedHeader" />
</el-scrollbar>
</div>
<!-- 系统设置 -->
<LaySetting />
</div>
</template>
<style lang="scss" scoped>
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
position: relative;
width: 100%;
height: 100%;
&::after {
display: table;
clear: both;
content: "";
}
&::after {
display: table;
clear: both;
content: '';
}
&.mobile.openSidebar {
position: fixed;
top: 0;
}
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.app-mask {
position: absolute;
top: 0;
z-index: 2001;
width: 100%;
height: 100%;
background: #000;
opacity: 0.3;
position: absolute;
top: 0;
z-index: 2001;
width: 100%;
height: 100%;
background: #000;
opacity: 0.3;
}
.re-screen {
margin-top: 12px;
margin-top: 12px;
}
</style>

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { unref } from "vue";
import { useRouter } from "vue-router";
<script lang="ts" setup>
import { unref } from 'vue';
import { useRouter } from 'vue-router';
defineOptions({
name: "Redirect"
name: 'Redirect',
});
const { currentRoute, replace } = useRouter();
@ -11,14 +11,14 @@ const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute);
const { path } = params;
const _path = Array.isArray(path) ? path.join("/") : path;
const _path = Array.isArray(path) ? path.join('/') : path;
replace({
path: "/" + _path,
query
path: '/' + _path,
query,
});
</script>
<template>
<div />
<div />
</template>

View File

@ -22,7 +22,11 @@ export const userI18nTypeStore = defineStore('i18nTypeStore', {
loading: false,
};
},
getters: {},
getters: {
translationTypeList(state) {
return state.datalist.map(item => ({ key: item.typeName, value: item.summary }));
},
},
actions: {
/**
* *