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", "Version": "5.8.0",
"Title": "PureAdmin", "Title": "BunnyAdmin",
"Copyright": "Copyright © 2020-present",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
"MultiTagsCache": false, "MultiTagsCache": false,

View File

@ -2,11 +2,12 @@
import { getConfig } from '@/config'; import { getConfig } from '@/config';
const TITLE = getConfig('Title'); const TITLE = getConfig('Title');
const Copyright = getConfig('Copyright');
</script> </script>
<template> <template>
<footer class="layout-footer text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"> <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> <a class="hover:text-primary" href="https://github.com/pure-admin" target="_blank"> &nbsp;{{ TITLE }} </a>
</footer> </footer>
</template> </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 Setting from '@iconify-icons/ri/settings-3-line';
import Check from '@iconify-icons/ep/check'; import Check from '@iconify-icons/ep/check';
import { $t } from '@/plugins/i18n'; 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 { 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> </script>
<template> <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" /> <GlobalizationIcon class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none" />
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="translation"> <el-dropdown-menu class="translation">
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]" :style="getDropdownItemStyle(locale, 'zh')" @click="translationCh"> <el-dropdown-item
<IconifyIconOffline v-show="locale === 'zh'" :icon="Check" class="check-zh" /> v-for="item in i18nTypeStore.translationTypeList"
简体中文 :key="item.key"
</el-dropdown-item> :class="['dark:!text-white', getDropdownItemClass(locale, item.key)]"
<el-dropdown-item :class="['dark:!text-white', getDropdownItemClass(locale, 'en')]" :style="getDropdownItemStyle(locale, 'en')" @click="translationEn"> :style="getDropdownItemStyle(locale, item.key)"
<span v-show="locale === 'en'" class="check-en"> @click="translation(item.key)"
>
<span v-show="locale === item.key" class="check">
<IconifyIconOffline :icon="Check" /> <IconifyIconOffline :icon="Check" />
</span> </span>
English {{ item.value }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@ -127,12 +132,7 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
padding: 5px 40px; padding: 5px 40px;
} }
.check-zh { .check {
position: absolute;
left: 20px;
}
.check-en {
position: absolute; position: absolute;
left: 20px; left: 20px;
} }

View File

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

View File

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

View File

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

View File

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

View File

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