vite+vue3+ts项目搭建之集成Layout组件、全局自动注册基础组件、缓存页面

阅读: 评论:0

vite+vue3+ts项目搭建之集成Layout组件、全局自动注册基础组件、缓存页面

vite+vue3+ts项目搭建之集成Layout组件、全局自动注册基础组件、缓存页面

最终效果

在线预览

  • Link:/

一、layout组件

目录结构

目前开源的后台管理系统layout组件一般分为:头部组件(navbar)、页签组件(tagsview)、左侧菜单(sidebar)、内容渲染区域(AppMain)

1、NavBar组件(Header组件)—拆分:左侧组件、右侧组件

1、左侧组件—收缩菜单栏组件,代码如下:

<template><el-icon class="collapse-icon" @click="changeCollapse"><component :is="globalStore.isCollapse ? 'expand' : 'fold'"></component></el-icon>
</template><script setup lang="ts">
import { useGlobalStore } from "@/store/modules/global";const globalStore = useGlobalStore();
const changeCollapse = () => globalStore.setGlobalState("isCollapse", !globalStore.isCollapse);
</script><style scoped lang="scss">
.collapse-icon {margin-right: 20px;font-size: 22px;cursor: pointer;color: var(--el-text-color-primary);
}
</style>

2、左侧组件—面包屑组件,代码如下:

<template><div :class="['breadcrumb-box', !globalStore.breadcrumbIcon && 'no-icon']"><el-breadcrumb :separator-icon="ArrowRight"><transition-group name="breadcrumb"><el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path"><div class="el-breadcrumb__inner is-link" @click="onBreadcrumbClick(item, index)"><el-icon class="breadcrumb-icon" v-show=&#a.icon && globalStore.breadcrumbIcon"><component :is=&#a.icon"></component></el-icon><span class="breadcrumb-title">{{ a.title }}</span></div></el-breadcrumb-item></transition-group></el-breadcrumb></div>
</template><script setup lang="ts">
import { computed } from "vue";
import { HOME_URL } from "@/config";
import { useRoute, useRouter } from "vue-router";
import { ArrowRight } from "@element-plus/icons-vue";
import { useAuthStore } from "@/store/modules/auth";
import { useGlobalStore } from "@/store/modules/global";const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const globalStore = useGlobalStore();const breadcrumbList = computed(() => {let breadcrumbData = authStore.breadcrumbListGet[route.matched[route.matched.length - 1].path] ?? [];// 不需要首页面包屑可删除以下判断if (breadcrumbData[0]?.path !== HOME_URL) {breadcrumbData = [{ path: HOME_URL, meta: { icon: "HomeFilled", title: "首页" } }, ...breadcrumbData];}return breadcrumbData;
});// Click Breadcrumb
const onBreadcrumbClick = (item: Menu.MenuOptions, index: number) => {if (index !== breadcrumbList.value.length - 1) router.push(item.path);
};
</script><style scoped lang="scss">
.breadcrumb-box {display: flex;align-items: center;padding-right: 50px;overflow: hidden;mask-image: linear-gradient(90deg, #000000 0%, #000000 calc(100% - 50px), transparent);.el-breadcrumb {white-space: nowrap;.el-breadcrumb__item {position: relative;display: inline-block;float: none;.el-breadcrumb__inner {display: inline-flex;.breadcrumb-icon {margin-top: 2px;margin-right: 6px;font-size: 16px;}.breadcrumb-title {margin-top: 3px;}}:deep(.el-breadcrumb__separator) {position: relative;top: -1px;}}}
}
.no-icon {.el-breadcrumb {.el-breadcrumb__item {top: -2px;:deep(.el-breadcrumb__separator) {top: 2px;}}}
}
</style>

3、最终左侧组件,代码如下:

<template><div class="tool-bar-lf"><CollapseIcon id="collapseIcon" /><Breadcrumb id="breadcrumb" v-if="globalStore.breadcrumb" /></div>
</template><script setup lang="ts">
import { useGlobalStore } from "@/store/modules/global";
import CollapseIcon from "./components/CollapseIcon.vue";
import Breadcrumb from "./components/Breadcrumb.vue";
const globalStore = useGlobalStore();
</script><style scoped lang="scss">
.tool-bar-lf {display: flex;align-items: center;justify-content: center;overflow: hidden;white-space: nowrap;
}
</style>

4、右侧组件–国际化,代码如下:

<template><el-dropdown trigger="click" @command="changeLanguage"><i :class="'iconfont icon-zhongyingwen'" class="toolBar-icon"></i><template #dropdown><el-dropdown-menu><el-dropdown-itemv-for="item in languageList":key="item.value":command="item.value":disabled="language === item.value">{{ item.label }}</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { computed } from "vue";
import { useGlobalStore } from "@/store/modules/global";
const i18n = useI18n();
const globalStore = useGlobalStore();
const language = computed(() => globalStore.language);
const languageList = [{ label: "简体中文", value: "zh" },{ label: "English", value: "en" }
];
const changeLanguage = (lang: string) => {i18n.locale.value = lang;globalStore.setGlobalState("language", lang);
};
</script>

5、右侧组件–菜单搜索,代码如下:

<template><div class="menu-search-dialog"><i @click="handleOpen" :class="'iconfont icon-search'" class="toolBar-icon"></i><el-dialog v-model="isShowSearch" destroy-on-close :modal="false" :show-close="false" fullscreen @click="closeSearch"><el-autocompletev-model="searchMenu"ref="menuInputRef"value-key="path"placeholder="菜单搜索 :支持菜单名称、路径":fetch-suggestions="searchMenuList"@select="handleClickMenu"@click.stop><template #prefix><el-icon><Search /></el-icon></template><template #default="{ item }"><el-icon><component :is=&#a.icon"></component></el-icon><span> {{ a.title }} </span></template></el-autocomplete></el-dialog></div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/store/modules/auth";
const router = useRouter();
const authStore = useAuthStore();
const menuList = computed(() => authStore.flatMenuListGet.filter(item => !a.isHide));
const searchMenuList = (queryString: string, cb: Function) => {const results = queryString ? menuList.value.filter(filterNodeMethod(queryString)) : menuList.value;cb(results);
};
// 打开搜索框
const isShowSearch = ref(false);
const menuInputRef = ref();
const searchMenu = ref("");
const handleOpen = () => {isShowSearch.value = true;nextTick(() => {setTimeout(() => {menuInputRef.value.focus();});});
};
// 搜索窗关闭
const closeSearch = () => {isShowSearch.value = false;
};
// 筛选菜单
const filterNodeMethod = (queryString: string) => {return (restaurant: Menu.MenuOptions) => {return (LowerCase().LowerCase()) > -1 ||LowerCase().LowerCase()) > -1);};
};
// 点击菜单跳转
const handleClickMenu = (menuItem: Menu.MenuOptions | Record<string, any>) => {searchMenu.value = "";if (a.isLink) window.a.isLink, "_blank");else router.push(menuItem.path);closeSearch();
};
</script><style scoped lang="scss">
.menu-search-dialog {:deep(.el-dialog) {background-color: rgb(0 0 0 / 50%);border-radius: 0 !important;box-shadow: unset !important;.el-dialog__header {border-bottom: none !important;}}:deep(.el-autocomplete) {position: absolute;top: 100px;left: 50%;width: 550px;transform: translateX(-50%);.el-input__wrapper {background-color: var(--el-bg-color);}}
}
.el-autocomplete__popper {.el-icon {position: relative;top: 2px;font-size: 16px;}span {margin: 0 0 0 10px;font-size: 14px;}
}
</style>

6、右侧组件–主题切换(布局设置),代码如下:

<template><div class="theme-setting" style="cursor: pointer"><i :class="'iconfont icon-theme'" class="toolBar-icon" @click="openDrawer"></i></div>
</template><script setup lang="ts">
import mittBus from "@/utils/mittBus";
const openDrawer = () => {it("openThemeDrawer");
};
</script>

7、右侧组件–主题切换—布局设置(样式看源码),代码如下:

<template><el-drawer v-model="drawerVisible" title="布局设置" size="300px"><!-- 布局切换 --><el-divider class="divider" content-position="center"><el-icon><Notification /></el-icon>布局切换</el-divider><div class="layout-box mb30"><el-tooltip effect="dark" content="纵向" placement="top" :show-after="200"><div :class="['layout-item layout-vertical', { 'is-active': layout == 'vertical' }]" @click="setLayout('vertical')"><div class="layout-dark"></div><div class="layout-container"><div class="layout-light"></div><div class="layout-content"></div></div><el-icon v-if="layout == 'vertical'"><CircleCheckFilled /></el-icon></div></el-tooltip><el-tooltip effect="dark" content="经典" placement="top" :show-after="200"><div :class="['layout-item layout-classic', { 'is-active': layout == 'classic' }]" @click="setLayout('classic')"><div class="layout-dark"></div><div class="layout-container"><div class="layout-light"></div><div class="layout-content"></div></div><el-icon v-if="layout == 'classic'"><CircleCheckFilled /></el-icon></div></el-tooltip><el-tooltip effect="dark" content="横向" placement="top" :show-after="200"><div :class="['layout-item layout-transverse', { 'is-active': layout == 'transverse' }]" @click="setLayout('transverse')"><div class="layout-dark"></div><div class="layout-content"></div><el-icon v-if="layout == 'transverse'"><CircleCheckFilled /></el-icon></div></el-tooltip><el-tooltip effect="dark" content="分栏" placement="top" :show-after="200"><div :class="['layout-item layout-columns', { 'is-active': layout == 'columns' }]" @click="setLayout('columns')"><div class="layout-dark"></div><div class="layout-light"></div><div class="layout-content"></div><el-icon v-if="layout == 'columns'"><CircleCheckFilled /></el-icon></div></el-tooltip></div><!-- 全局主题 --><el-divider class="divider" content-position="center"><el-icon><ColdDrink /></el-icon>全局主题</el-divider><div class="theme-item"><span>主题颜色</span><el-color-picker v-model="primary" :predefine="colorList" @change="changePrimary" /></div><div class="theme-item"><span>暗黑模式</span><SwitchDark /></div><div class="theme-item"><span>灰色模式</span><el-switch v-model="isGrey" @change="changeGreyOrWeak('grey', !!$event)" /></div><div class="theme-item"><span>色弱模式</span><el-switch v-model="isWeak" @change="changeGreyOrWeak('weak', !!$event)" /></div><div class="theme-item mb40"><span>侧边栏反转色<el-tooltip effect="dark" content="该属性目前只在纵向布局模式下生效" placement="top"><el-icon><QuestionFilled /></el-icon></el-tooltip></span><el-switch v-model="asideInverted" :disabled="layout !== 'vertical'" @change="setAsideTheme" /></div><!-- 界面设置 --><el-divider class="divider" content-position="center"><el-icon><Setting /></el-icon>界面设置</el-divider><div class="theme-item"><span>折叠菜单</span><el-switch v-model="isCollapse" /></div><div class="theme-item"><span>面包屑</span><el-switch v-model="breadcrumb" /></div><div class="theme-item"><span>面包屑图标</span><el-switch v-model="breadcrumbIcon" /></div><div class="theme-item"><span>标签栏</span><el-switch v-model="tabs" /></div><div class="theme-item"><span>标签栏图标</span><el-switch v-model="tabsIcon" /></div></el-drawer>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { storeToRefs } from "pinia";
import { useTheme } from "@/hooks/useTheme";
import { useGlobalStore } from "@/store/modules/global";
import { LayoutType } from "@/store/interface";
import { DEFAULT_PRIMARY } from "@/config";
import mittBus from "@/utils/mittBus";
import SwitchDark from "@/components/SwitchDark/index.vue";
const { changePrimary, changeGreyOrWeak, setAsideTheme } = useTheme();
const globalStore = useGlobalStore();
const { layout, primary, isGrey, isWeak, asideInverted, isCollapse, breadcrumb, breadcrumbIcon, tabs, tabsIcon } =storeToRefs(globalStore);
// 预定义主题颜色
const colorList = [DEFAULT_PRIMARY,"#daa96e","#0c819f","#409eff","#27ae60","#ff5c93","#e74c3c","#fd726d","#f39c12","#9b59b6"
];
// 设置布局方式
const setLayout = (val: LayoutType) => {globalStore.setGlobalState("layout", val);setAsideTheme();
};
// 打开主题设置
const drawerVisible = ref(false);
("openThemeDrawer", () => (drawerVisible.value = true));
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>

8、右侧组件–全屏,代码如下:

<template><div class="fullscreen icon_full"><i :class="['iconfont', isFullscreen ? 'icon-exitfullscreen' : 'icon-fullscreen']" class="toolBar-icon" @click="toggle"></i></div>
</template><script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
const { toggle, isFullscreen } = useFullscreen();
</script><style scoped lang="scss">
.icon_full {cursor: pointer;
}
</style>

9、右侧组件–头像下拉组件(个人信息及登出),代码如下:

<template><el-dropdown trigger="click"><div class="avatar"><img src="@/assets/logo/logo.png" alt="avatar" /></div><template #dropdown><el-dropdown-menu class="user_info"><el-dropdown-item @click="openDialog('infoRef')"><el-icon><User /></el-icon>{{ $t("header.personalData") }}</el-dropdown-item><el-dropdown-item @click="openDialog('passwordRef')"><el-icon><Edit /></el-icon>{{ $t("header.changePassword") }}</el-dropdown-item><el-dropdown-item><el-icon><Memo /></el-icon><a href="/" target="_blank">vue2基础组件文档</a></el-dropdown-item><el-dropdown-item><el-icon><Memo /></el-icon><a href="/" target="_blank">vue3基础组件文档</a></el-dropdown-item><el-dropdown-item @click="logout" divided><el-icon><SwitchButton /></el-icon>{{ $t("header.logout") }}</el-dropdown-item></el-dropdown-menu></template></el-dropdown><!-- infoDialog --><InfoDialog ref="infoRef"></InfoDialog><!-- passwordDialog --><PasswordDialog ref="passwordRef"></PasswordDialog>
</template>
<script setup lang="ts">
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
import { useUserStore } from "@/store/modules/user";
import { ElMessageBox, ElMessage } from "element-plus";
import InfoDialog from "./InfoDialog.vue";
import PasswordDialog from "./PasswordDialog.vue";
const userStore = useUserStore();
// 退出登录
const logout = () => {firm("您是否确认退出登录?", "温馨提示", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning"}).then(async () => {// 1.执行退出登录接口userStore.FedLogOut();window.location.href = qiankunWindow.__POWERED_BY_QIANKUN__ ? "/wocwin-qiankun/" : "/wocwin-admin/";ElMessage.success("退出登录成功!");});
};
// 打开修改密码和个人信息弹窗
const infoRef = ref<InstanceType<typeof InfoDialog> | null>(null);
const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null);
const openDialog = (ref: string) => {if (ref == "infoRef") infoRef.value?.openDialog();if (ref == "passwordRef") passwordRef.value?.openDialog();
};
</script><style scoped lang="scss">
.avatar {width: 40px;height: 40px;overflow: hidden;cursor: pointer;border-radius: 50%;img {width: 100%;height: 100%;}
}
.user_info {cursor: pointer;:deep(.el-dropdown-menu__item) {display: flex;align-items: center;flex-direction: inherit;a {user-select: none;}}
}
</style>

10、完整右侧组件

<template><div class="tool-bar-ri"><div class="header-icon"><Language id="language" /><SearchMenu id="searchMenu" /><el-tooltip content="主题切换" effect="dark" placement="bottom"><ThemeSetting id="themeSetting" /></el-tooltip><el-tooltip content="全屏预览" effect="dark" placement="bottom"><Fullscreen id="fullscreen" /></el-tooltip></div><span class="username">{{ username }}</span><Avatar /></div>
</template>
<script setup lang="ts">
import { useUserStore } from "@/store/modules/user";
import Language from "./components/Language.vue";
import SearchMenu from "./components/SearchMenu.vue";
import ThemeSetting from "./components/ThemeSetting.vue";
import Fullscreen from "./components/Fullscreen.vue";
import Avatar from "./components/Avatar.vue";
const userStore = useUserStore();
const username = computed(() => userStore.name || "wocwin");
</script>
<style scoped lang="scss">
.tool-bar-ri {display: flex;align-items: center;justify-content: center;padding-right: 25px;.header-icon {display: flex;align-items: center;* {margin-left: 21px;}}.username {margin: 0 20px;font-size: 15px;}
}
</style>

2、TagsView页签组件(Tabs组件)—拆分:内容全屏组件、页面刷新组件、页签操作、右击页签显示操作

1、内容全屏组件

<template><el-divider direction="vertical" /><div @click="maximize" class="tabs_icon"><el-tooltip effect="dark" :content="$t('tabs.maximize')" placement="bottom"><el-icon><FullScreen /></el-icon></el-tooltip></div>
</template><script setup lang="ts">
import { useGlobalStore } from "@/store/modules/global";
const globalStore = useGlobalStore();
// maximize current page
const maximize = () => {globalStore.setGlobalState("maximize", true);
};
</script>

2、页面刷新组件

<template><el-divider direction="vertical" /><div @click="refresh" class="tabs_icon"><el-tooltip effect="dark" :content="$t(&#fresh')" placement="bottom"><el-icon><Refresh /></el-icon></el-tooltip></div><el-divider direction="vertical" />
</template><script setup lang="ts">
import { useRoute } from "vue-router";
import { useKeepAliveStore } from "@/store/modules/keepAlive";
const route = useRoute();
const keepAliveStore = useKeepAliveStore();
// refresh current page
const refreshCurrentPage: Function = inject("refresh") as Function;
const refresh = () => {setTimeout(() => {veKeepAliveName(route.name as string);refreshCurrentPage(false);nextTick(() => {keepAliveStore.addKeepAliveName(route.name as string);refreshCurrentPage(true);});}, 0);
};
</script>

3、页签操作

<template><el-dropdown trigger="click" :teleported="false"><el-icon><ArrowDown /></el-icon><template #dropdown><el-dropdown-menu><el-dropdown-item @click="closeCurrentTab"><el-icon><Remove /></el-icon>{{ $t("tabs.closeCurrent") }}</el-dropdown-item><el-dropdown-item @click="closeOtherTab"><el-icon><CircleClose /></el-icon>{{ $t("tabs.closeOther") }}</el-dropdown-item><el-dropdown-item @click="closeAllTab"><el-icon><FolderDelete /></el-icon>{{ $t("tabs.closeAll") }}</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template>
<script setup lang="ts">
import { HOME_URL } from "@/config";
import { useTabsStore } from "@/store/modules/tabs";
import { useKeepAliveStore } from "@/store/modules/keepAlive";
import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const tabStore = useTabsStore();
const keepAliveStore = useKeepAliveStore();
// Close Current
const closeCurrentTab = () => {if (a.isAffix) veTabs(route.fullPath);veKeepAliveName(route.name as string);
};
// Close Other
const closeOtherTab = () => {tabStore.closeMultipleTab(route.fullPath);keepAliveStore.setKeepAliveName([route.name] as string[]);
};
// Close All
const closeAllTab = () => {tabStore.closeMultipleTab();keepAliveStore.setKeepAliveName();router.push(HOME_URL);
};
</script>

4、右击页签显示操作

<template><ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"><li @click="closeCurrentTab"><el-icon><Remove /></el-icon>{{ $t("tabs.closeCurrent") }}</li><li @click="closeOtherTab"><el-icon><CircleClose /></el-icon>{{ $t("tabs.closeOther") }}</li><li @click="closeAllTab"><el-icon><FolderDelete /></el-icon>{{ $t("tabs.closeAll") }}</li></ul>
</template><script setup lang="ts">
import { HOME_URL } from "@/config";
import { useRoute, useRouter } from "vue-router";
import { useTabsStore } from "@/store/modules/tabs";
import { useKeepAliveStore } from "@/store/modules/keepAlive";
defineProps({visible: {type: Boolean,required: false},top: {type: Number,default: 0},left: {type: Number,default: 0}
});
const route = useRoute();
const router = useRouter();
const tabStore = useTabsStore();
const keepAliveStore = useKeepAliveStore();
// Close Current
const closeCurrentTab = () => {if (a.isAffix) veTabs(route.fullPath);veKeepAliveName(route.name as string);
};
// Close Other
const closeOtherTab = () => {tabStore.closeMultipleTab(route.fullPath);keepAliveStore.setKeepAliveName([route.name] as string[]);
};
// Close All
const closeAllTab = () => {tabStore.closeMultipleTab();keepAliveStore.setKeepAliveName();router.push(HOME_URL);
};
</script>

5、完整代码

<template><div class="tabs-box"><div class="tabs-menu"><el-tabs v-model="tabsMenuValue" type="card" @tab-click="tabClick" @contextmenu="openMenu($event)" @tab-remove="tabRemove"><el-tab-pane v-for="item in tabsMenuList" :key="item.path" :label="item.title" :name="item.path" :closable="item.close"><template #label><el-icon class="tabs-icon" v-show="item.icon && tabsIcon"><component :is="item.icon"></component></el-icon>{{ item.title }}</template></el-tab-pane></el-tabs><div class="right-tag"><Refresh /><MoreButton class="tabs_icon" /><Maximize /></div></div><Contextmenu :visible="visible" :left="left" :top="top" /></div>
</template><script setup lang="ts">
import Sortable from "sortablejs";
import { useRoute, useRouter } from "vue-router";
import { useGlobalStore } from "@/store/modules/global";
import { useTabsStore } from "@/store/modules/tabs";
import { useAuthStore } from "@/store/modules/auth";
import { useKeepAliveStore } from "@/store/modules/keepAlive";
import { TabsPaneContext, TabPaneName } from "element-plus";
import MoreButton from "./components/MoreButton.vue"; // TAB operation
import Refresh from "./components/Refresh.vue";
import Maximize from "./components/Maximize.vue";
import Contextmenu from "./components/Contextmenu.vue";
const route = useRoute();
const router = useRouter();
const tabStore = useTabsStore();
const authStore = useAuthStore();
const globalStore = useGlobalStore();
const keepAliveStore = useKeepAliveStore();
const tabsMenuValue = ref(route.fullPath);
const tabsMenuList = computed(() => tabStore.tabsMenuList);
const tabsIcon = computed(() => globalStore.tabsIcon);
const visible = ref(false);
const top = ref(0);
const left = ref(0);
// 右击打开页签操作
const openMenu = (e: MouseEvent) => {e.preventDefault();const { clientX, clientY } = e;left.value = clientX;top.value = clientY + 5;if (tabsMenuList.value.length < 2) {visible.value = false;} else {visible.value = true;}
};
const closeMenu = () => {visible.value = false;
};
watch(visible, value => {if (value) {document.body.addEventListener("click", closeMenu);} else {veEventListener("click", closeMenu);}
});
onMounted(() => {tabsDrop();initTabs();
});
// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue)
watch(() => route.fullPath,() => {if (a.isFull) return;tabsMenuValue.value = route.fullPath;const tabsParams = {icon: a.icon as string,title: a.title as string,path: route.fullPath,name: route.name as string,close: !a.isAffix};tabStore.addTabs(tabsParams);a.isKeepAlive && keepAliveStore.addKeepAliveName(route.name as string);},{ immediate: true }
);
// tabs 拖拽排序
const tabsDrop = () => {ate(document.querySelector(".el-tabs__nav") as HTMLElement, {draggable: ".el-tabs__item",animation: 300,onEnd({ newIndex, oldIndex }) {const tabsList = [...tabStore.tabsMenuList];const currRow = tabsList.splice(oldIndex as number, 1)[0];tabsList.splice(newIndex as number, 0, currRow);tabStore.setTabs(tabsList);}});
};
// 初始化需要固定的 tabs
const initTabs = () => {authStore.flatMenuListGet.forEach(item => {if (a.isAffix && !a.isHide && !a.isFull) {const tabsParams = {icon: a.icon,title: a.title,path: item.path,name: item.name,close: !a.isAffix};tabStore.addTabs(tabsParams);}});
};
// Tab Click
const tabClick = (tabItem: TabsPaneContext) => {const fullPath = tabItem.props.name as string;router.push(fullPath);
};
// Remove Tab
const tabRemove = (fullPath: TabPaneName) => {const name = tabStore.tabsMenuList.filter(item => item.path == fullPath)[0].name || "";veKeepAliveName(name);veTabs(fullPath as string, fullPath == route.fullPath);
};
</script>
<style scoped lang="scss">
.tabs-box {background-color: var(--el-bg-color);.tabs-menu {position: relative;width: 100%;.el-dropdown {:deep(.el-dropdown-menu__item) {display: flex;align-items: center;flex-direction: inherit;}}.right-tag {position: fixed;top: 55px;right: 15px;z-index: 100;display: flex;align-items: center;:deep(.el-divider) {margin: 0;}:deep(.tabs_icon) {width: 36px;height: 40px;display: flex;align-items: center;justify-content: center;color: var(--el-text-color-primary);cursor: pointer;}:deep(.el-button) {height: 39px;line-height: 39px;border-radius: 0;padding: 15px;}}:deep(.el-tabs) {.el-tabs__header {box-sizing: border-box;height: 40px;padding: 0 10px;margin: 0;.el-tabs__nav-wrap {position: absolute;width: calc(100% - 140px);.el-tabs__nav {display: flex;border: none;.el-tabs__item {display: flex;align-items: center;justify-content: center;color: #afafaf;border: none;.tabs-icon {margin: 1.5px 4px 0 0;font-size: 15px;}.is-icon-close {margin-top: 1px;}&.is-active {color: var(--el-color-primary);&::before {position: absolute;bottom: 0;width: 100%;height: 0;content: "";border-bottom: 2px solid var(--el-color-primary) !important;}}}}}}}}:deep(.contextmenu) {margin: 0;background-color: var(--el-bg-color-overlay);z-index: 3000;position: absolute;list-style-type: none;padding: 5px 0;border-radius: 4px;font-size: 14px;font-weight: 400;color: var(--el-text-color-regular);box-shadow: 2px 2px 3px 0 #00000030;li {margin: 0;padding: 7px 16px;cursor: pointer;display: flex;align-items: center;&:hover {background-color: #eee;border-radius: 4px;color: var(--el-color-primary);}.el-icon {margin-right: 5px;}}}
}
</style>

3、Sidebar左侧菜单组件(Menu组件–SubMenu),代码如下:

<template><template v-for="subItem in menuList" :key="subItem.path"><el-sub-menu v-if="subItem.children?.length" :index="subItem.path"><template #title><el-icon><component :is=&#a.icon"></component></el-icon><span class="sle">{{ a.title }}</span></template><SubMenu :menuList="subItem.children" /></el-sub-menu><el-menu-item v-else :index="subItem.path" @click="handleClickMenu(subItem)"><el-icon><component :is=&#a.icon"></component></el-icon><template #title><span class="sle">{{ a.title }}</span></template></el-menu-item></template>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
defineProps<{ menuList: Menu.MenuOptions[] }>();
const router = useRouter();
const handleClickMenu = (subItem: Menu.MenuOptions) => {if (a.isLink) return window.a.isLink, "_blank");router.push(subItem.path);
};
</script><style lang="scss">
.el-sub-menu .el-sub-menu__title:hover {color: var(--el-menu-hover-text-color) !important;background-color: transparent !important;
}
.el-menu--collapse {.is-active {.el-sub-menu__title {color: #ffffff !important;background-color: var(--el-color-primary) !important;}}
}
.el-menu-item {&:hover {color: var(--el-menu-hover-text-color);}&.is-active {color: var(--el-menu-active-color) !important;background-color: var(--el-menu-active-bg-color) !important;&::before {position: absolute;top: 0;bottom: 0;width: 4px;content: "";background-color: var(--el-color-primary);}}
}
.vertical,
.classic,
.transverse {.el-menu-item {&.is-active {&::before {left: 0;}}}
}
.columns {.el-menu-item {&.is-active {&::before {right: 0;}}}
}
.classic,
.transverse {#driver-highlighted-element-stage {background-color: #606266 !important;}
}
</style>

4、AppMain内容渲染区域组件(Main组件)

1、关闭内容全屏组件,代码如下:

<template><div class="maximize" @click="exitMaximize"><el-icon><Close /></el-icon></div>
</template>
<script setup lang="ts">
import { useGlobalStore } from "@/store/modules/global";
const globalStore = useGlobalStore();
const exitMaximize = () => {globalStore.setGlobalState("maximize", false);
};
</script>
<style scoped lang="scss">
.maximize {position: fixed;top: -25px;right: -25px;z-index: 999;width: 52px;height: 52px;cursor: pointer;background-color: var(--el-color-info);border-radius: 50%;opacity: 0.7;&:hover {background-color: var(--el-color-info-dark-2);}.el-icon {position: relative;top: 68%;left: 32%;font-size: 16px;color: #ffffff;transform: translate(-50%, -50%);}
}
</style>

2、Main组件,代码如下:

<template><Maximize v-if="maximize" /><Tabs v-if="tabs" /><el-main><router-view v-slot="{ Component, route }"><transition appear name="fade-transform" mode="out-in"><keep-alive :include="keepAliveName"><component :is="Component" :key="route.fullPath" v-if="isRouterShow" /></keep-alive></transition></router-view></el-main>
</template><script setup lang="ts">
import { ref, onBeforeUnmount, provide, watch } from "vue";
import { storeToRefs } from "pinia";
import { useDebounceFn } from "@vueuse/core";
import { useGlobalStore } from "@/store/modules/global";
import { useKeepAliveStore } from "@/store/modules/keepAlive";
import Maximize from "./components/Maximize.vue";
import Tabs from "@/layout/components/Tabs/index.vue";
const globalStore = useGlobalStore();
const { maximize, isCollapse, layout, tabs } = storeToRefs(globalStore);
const keepAliveStore = useKeepAliveStore();
const { keepAliveName } = storeToRefs(keepAliveStore);
// 注入刷新页面方法
const isRouterShow = ref(true);
const refreshCurrentPage = (val: boolean) => (isRouterShow.value = val);
provide("refresh", refreshCurrentPage);
// 监听当前页面是否最大化,动态添加 class
watch(() => maximize.value,() => {const app = ElementById("app") as HTMLElement;if (maximize.value) app.classList.add("main-maximize");else ve("main-maximize");},{ immediate: true }
);
// 监听布局变化,在 body 上添加相对应的 layout class
watch(() => layout.value,() => {const body = document.body as HTMLElement;body.setAttribute("class", layout.value);},{ immediate: true }
);
// 监听窗口大小变化,折叠侧边栏
const screenWidth = ref(0);
const listeningWindow = useDebounceFn(() => {screenWidth.value = document.body.clientWidth;if (!isCollapse.value && screenWidth.value < 1200) globalStore.setGlobalState("isCollapse", true);if (isCollapse.value && screenWidth.value > 1200) globalStore.setGlobalState("isCollapse", false);
}, 100);
window.addEventListener("resize", listeningWindow, false);
onBeforeUnmount(() => {veEventListener("resize", listeningWindow);
});
</script>
<style scoped lang="scss">
.el-main {box-sizing: border-box;padding: 0;overflow-x: hidden;background-color: var(--el-bg-color-page);:deep(.t_layout_page_item) {margin: 0;margin-bottom: 8px;}
}
.el-footer {height: auto;padding: 0;
}
</style>

二、LayoutClassic经典布局,代码如下:

<!-- 经典布局 -->
<template><el-container class="layout"><el-header><div class="header-lf"><div class="logo flx-center" @click="goIndex"><img class="logo-img" src="@/assets/logo/logo.png" alt="logo" /><span class="logo-text">wocwin Admin</span></div><ToolBarLeft /></div><div class="header-ri"><ToolBarRight /></div></el-header><el-container class="classic-content"><el-aside><div class="aside-box" :style="{ width: isCollapse ? '65px' : '210px' }"><el-scrollbar><el-menu:default-active="activeMenu":router="false":collapse="isCollapse":collapse-transition="false":unique-opened="true"><SubMenu :menuList="menuList" /></el-menu></el-scrollbar></div></el-aside><el-container class="classic-main"><Main /></el-container></el-container></el-container>
</template>
<script setup lang="ts" name="layoutClassic">
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/store/modules/auth";
import { useGlobalStore } from "@/store/modules/global";
import Main from "@/layout/components/Main/index.vue";
import SubMenu from "@/layout/components/Menu/SubMenu.vue";
import ToolBarLeft from "@/layout/components/Header/ToolBarLeft.vue";
import ToolBarRight from "@/layout/components/Header/ToolBarRight.vue";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
const route = useRoute();
const authStore = useAuthStore();
const globalStore = useGlobalStore();
const isCollapse = computed(() => globalStore.isCollapse);
const menuList = computed(() => authStore.showMenuListGet);
const activeMenu = computed(() => (a.activeMenu ? a.activeMenu : route.path) as string);
const goIndex = () => {window.location.href = qiankunWindow.__POWERED_BY_QIANKUN__ ? "/wocwin-qiankun/" : "/wocwin-admin/";
};
</script>
<style scoped lang="scss">
.el-container {width: 100%;height: 100%;:deep(.el-header) {box-sizing: border-box;display: flex;align-items: center;justify-content: space-between;height: 55px;padding: 0 15px 0 0;background-color: #191a20;border-bottom: 1px solid #191a20;.header-lf {display: flex;align-items: center;overflow: hidden;white-space: nowrap;.logo {flex-shrink: 0;width: 210px;margin-right: 16px;cursor: pointer;.logo-img {width: 28px;object-fit: contain;margin-right: 6px;border-radius: 50%;}.logo-text {font-size: 21.5px;font-weight: bold;color: #dadada;white-space: nowrap;}}.tool-bar-lf {.collapse-icon {color: #e5eaf3;}.el-breadcrumb__inner.is-link {color: #e5eaf3;&:hover {color: var(--el-color-primary);}}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover {color: #cfd3dc;}}}.header-ri {.tool-bar-ri {.toolBar-icon,.username {color: #e5eaf3;}}}}.classic-content {display: flex;height: calc(100% - 50px);:deep(.el-aside) {z-index: 5;width: auto;background-color: var(--el-menu-bg-color);border-right: 1px solid var(--el-border-color-light);.aside-box {display: flex;flex-direction: column;height: 100%;transition: width 0.3s ease;.el-menu {width: 100%;overflow-x: hidden;border-right: none;}}}.classic-main {display: flex;flex-direction: column;}}
}
html.dark {.el-container {:deep(.el-header) {border-bottom: 1px solid var(--el-border-color-light);}}
}
</style>

三、LayoutColumns分栏布局,代码如下:

<!-- 分栏布局 -->
<template><el-container class="layout"><div class="aside-split"><div class="logo flx-center" @click="goIndex"><img class="logo-img" src="@/assets/logo/logo.png" alt="logo" /></div><el-scrollbar><div class="split-list"><divclass="split-item":class="{ 'split-active': splitActive === item.path || `/${splitActive.split('/')[1]}` === item.path }"v-for="item in menuList":key="item.path"@click="changeSubMenu(item)"><el-icon><component :is=&#a.icon"></component></el-icon><span class="title">{{ a.title }}</span></div></div></el-scrollbar></div><el-aside :class="{ 'not-aside': !subMenuList.length }" :style="{ width: isCollapse ? '65px' : '210px' }"><div class="logo flx-center"><span class="logo-text" v-show="subMenuList.length">{{ isCollapse ? "W" : "wocwin Admin" }}</span></div><el-scrollbar><el-menu:default-active="activeMenu":router="false":collapse="isCollapse":collapse-transition="false":unique-opened="true"><SubMenu :menuList="subMenuList" /></el-menu></el-scrollbar></el-aside><el-container><el-header><ToolBarLeft /><ToolBarRight /></el-header><Main /></el-container></el-container>
</template>
<script setup lang="ts" name="layoutColumns">
import { ref, computed, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/store/modules/auth";
import { useGlobalStore } from "@/store/modules/global";
import Main from "@/layout/components/Main/index.vue";
import ToolBarLeft from "@/layout/components/Header/ToolBarLeft.vue";
import ToolBarRight from "@/layout/components/Header/ToolBarRight.vue";
import SubMenu from "@/layout/components/Menu/SubMenu.vue";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const globalStore = useGlobalStore();
const isCollapse = computed(() => globalStore.isCollapse);
const menuList = computed(() => authStore.showMenuListGet);
const activeMenu = computed(() => (a.activeMenu ? a.activeMenu : route.path) as string);
const subMenuList = ref<Menu.MenuOptions[]>([]);
const splitActive = ref("");
watch(() => [menuList, route],() => {// 当前菜单没有数据直接 returnif (!menuList.value.length) return;splitActive.value = route.path;const menuItem = menuList.value.filter((item: Menu.MenuOptions) => {return route.path === item.path || `/${route.path.split("/")[1]}` === item.path;});if (menuItem[0].children?.length) return (subMenuList.value = menuItem[0].children);subMenuList.value = [];},{deep: true,immediate: true}
);
// change SubMenu
const changeSubMenu = (item: Menu.MenuOptions) => {splitActive.value = item.path;if (item.children?.length) return (subMenuList.value = item.children);subMenuList.value = [];router.push(item.path);
};
const goIndex = () => {window.location.href = qiankunWindow.__POWERED_BY_QIANKUN__ ? "/wocwin-qiankun/" : "/wocwin-admin/";
};
</script><style scoped lang="scss">
.el-container {width: 100%;height: 100%;.aside-split {display: flex;flex-direction: column;flex-shrink: 0;width: 70px;height: 100%;background-color: #191a20;border-right: 1px solid var(--el-border-color-light);.logo {box-sizing: border-box;height: 55px;cursor: pointer;border-bottom: 1px solid #282a35;.logo-img {width: 32px;border-radius: 50%;object-fit: contain;}}.el-scrollbar {height: calc(100% - 55px);.split-list {flex: 1;.split-item {display: flex;flex-direction: column;align-items: center;justify-content: center;height: 70px;cursor: pointer;transition: all 0.3s ease;&:hover {background-color: #292b35;}.el-icon {font-size: 21px;}.title {margin-top: 6px;font-size: 12px;}.el-icon,.title {color: #e5eaf3;}}.split-active {background-color: var(--el-color-primary) !important;.el-icon,.title {color: #ffffff !important;}}}}}.not-aside {width: 0 !important;border-right: none !important;}.el-aside {display: flex;flex-direction: column;height: 100%;overflow: hidden;z-index: 5;padding: 0;background-color: var(--el-menu-bg-color);border-right: 1px solid var(--el-border-color-light);transition: width 0.3s ease;.el-scrollbar {height: calc(100% - 55px);.el-menu {width: 100%;overflow-x: hidden;border-right: none;}}.logo {box-sizing: border-box;height: 55px;border-bottom: 1px solid var(--el-border-color-light);.logo-text {font-size: 24px;font-weight: bold;color: var(--el-menu-text-color);white-space: nowrap;}}}.el-header {box-sizing: border-box;display: flex;align-items: center;justify-content: space-between;height: 55px;padding: 0 15px;background-color: #ffffff;border-bottom: 1px solid var(--el-border-color-light);:deep(.tool-bar-ri) {.toolBar-icon,.username {color: var(--el-text-color-primary);}}}
}
</style>

四、LayoutTransverse横向布局,代码如下:

<!-- 横向布局 -->
<template><el-container class="layout"><el-header><div class="logo flx-center" @click="goIndex"><img class="logo-img" src="@/assets/logo/logo.png" alt="logo" /><span class="logo-text">wocwin Admin</span></div><el-menu mode="horizontal" :default-active="activeMenu" :router="false" :unique-opened="true"><!-- 不能直接使用 SubMenu 组件,无法触发 el-menu 隐藏省略功能 --><template v-for="subItem in menuList" :key="subItem.path"><el-sub-menu v-if="subItem.children?.length" :index="subItem.path + 'el-sub-menu'" :key="subItem.path"><template #title><el-icon><component :is=&#a.icon"></component></el-icon><span>{{ a.title }}</span></template><SubMenu :menuList="subItem.children" /></el-sub-menu><el-menu-item v-else :index="subItem.path" :key="subItem.path + 'el-menu-item'" @click="handleClickMenu(subItem)"><el-icon><component :is=&#a.icon"></component></el-icon><template #title><span>{{ a.title }}</span></template></el-menu-item></template></el-menu><ToolBarRight /></el-header><Main /></el-container>
</template><script setup lang="ts" name="layoutTransverse">
import { computed } from "vue";
import { useAuthStore } from "@/store/modules/auth";
import { useRoute, useRouter } from "vue-router";
import Main from "@/layout/components/Main/index.vue";
import ToolBarRight from "@/layout/components/Header/ToolBarRight.vue";
import SubMenu from "@/layout/components/Menu/SubMenu.vue";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const menuList = computed(() => authStore.showMenuListGet);
const activeMenu = computed(() => (a.activeMenu ? a.activeMenu : route.path) as string);const handleClickMenu = (subItem: Menu.MenuOptions) => {if (a.isLink) return window.a.isLink, "_blank");router.push(subItem.path);
};
const goIndex = () => {window.location.href = qiankunWindow.__POWERED_BY_QIANKUN__ ? "/wocwin-qiankun/" : "/wocwin-admin/";
};
</script><style scoped lang="scss">
.el-container {width: 100%;height: 100%;:deep(.el-header) {box-sizing: border-box;display: flex;align-items: center;justify-content: space-between;height: 55px;padding: 0 15px 0 0;background-color: #191a20;border-bottom: 1px solid var(--el-border-color-light);.logo {width: 210px;margin-right: 30px;cursor: pointer;.logo-img {width: 28px;border-radius: 50%;object-fit: contain;margin-right: 6px;}.logo-text {font-size: 21.5px;font-weight: bold;color: #dadada;white-space: nowrap;}}.el-menu {flex: 1;height: 100%;overflow: hidden;border-bottom: none;.el-sub-menu__hide-arrow {width: 65px;height: 55px;}.is-active {background-color: var(--el-color-primary) !important;border-bottom-color: var(--el-color-primary) !important;&::before {width: 0;}.el-sub-menu__title {background-color: var(--el-color-primary) !important;border-bottom-color: var(--el-color-primary) !important;}}}.tool-bar-ri {.toolBar-icon,.username {color: #e5eaf3;}}}
}
</style>

五、LayoutVertical纵向布局,代码如下:

<!-- 纵向布局 -->
<template><el-container class="layout"><el-aside><div class="aside-box" :style="{ width: isCollapse ? '65px' : '210px' }"><div class="logo flx-center" @click="goIndex"><img class="logo-img" src="@/assets/logo/logo.png" alt="logo" /><span class="logo-text" v-show="!isCollapse">wocwin Admin</span></div><el-scrollbar><el-menu:default-active="activeMenu":collapse="isCollapse":router="false":unique-opened="true":collapse-transition="false"><SubMenu :menuList="menuList" /></el-menu></el-scrollbar></div></el-aside><el-container><el-header><ToolBarLeft /><ToolBarRight /></el-header><Main /></el-container></el-container>
</template><script setup lang="ts" name="layoutVertical">
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/store/modules/auth";
import { useGlobalStore } from "@/store/modules/global";
import Main from "@/layout/components/Main/index.vue";
import ToolBarLeft from "@/layout/components/Header/ToolBarLeft.vue";
import ToolBarRight from "@/layout/components/Header/ToolBarRight.vue";
import SubMenu from "@/layout/components/Menu/SubMenu.vue";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";const route = useRoute();
const authStore = useAuthStore();
const globalStore = useGlobalStore();
const isCollapse = computed(() => globalStore.isCollapse);
const menuList = computed(() => authStore.showMenuListGet);
const activeMenu = computed(() => (a.activeMenu ? a.activeMenu : route.path) as string);
const goIndex = () => {window.location.href = qiankunWindow.__POWERED_BY_QIANKUN__ ? "/wocwin-qiankun/" : "/wocwin-admin/";
};
</script><style scoped lang="scss">
.el-container {width: 100%;height: 100%;:deep(.el-aside) {z-index: 5;width: auto;background-color: var(--el-menu-bg-color);border-right: 1px solid var(--el-border-color-light);.aside-box {display: flex;flex-direction: column;height: 100%;transition: width 0.3s ease;.el-scrollbar {height: calc(100% - 55px);.el-menu {width: 100%;overflow-x: hidden;border-right: none;}}.logo {box-sizing: border-box;cursor: pointer;height: 55px;.logo-img {width: 28px;border-radius: 50%;object-fit: contain;margin-right: 6px;}.logo-text {font-size: 21.5px;font-weight: bold;color: var(--el-logo-text-color);white-space: nowrap;}}}}.el-header {box-sizing: border-box;display: flex;align-items: center;justify-content: space-between;height: 55px;padding: 0 15px;background-color: var(--el-bg-color);border-bottom: 1px solid var(--el-border-color-light);:deep(.tool-bar-ri) {.toolBar-icon,.username {color: var(--el-text-color-primary);}}}
}
</style>

六、最终Layout组件,代码如下:

<template><suspense><template #default><component :is="LayoutComponents[layout]" /></template><template #fallback><Loading /></template></suspense><ThemeDrawer />
</template>
<script setup lang="ts" name="layoutAsync">
import { computed, defineAsyncComponent, type Component } from "vue";
import { LayoutType } from "@/store/interface";
import { useGlobalStore } from "@/store/modules/global";
import Loading from "@/components/Loading/index.vue";
import ThemeDrawer from "./components/ThemeDrawer/index.vue";
const LayoutComponents: Record<LayoutType, Component> = {vertical: defineAsyncComponent(() => import("./LayoutVertical/index.vue")),classic: defineAsyncComponent(() => import("./LayoutClassic/index.vue")),transverse: defineAsyncComponent(() => import("./LayoutTransverse/index.vue")),columns: defineAsyncComponent(() => import("./LayoutColumns/index.vue"))
};
const globalStore = useGlobalStore();
const layout = computed(() => globalStore.layout);
</script><style scoped lang="scss">
.layout {min-width: 730px;
}
</style>

七、如何更改element-plus主体颜色,代码如下:

@forward 'element-plus/theme-chalk/src/common/var.scss' with ($colors: (
'primary': (
'base': #355db4,),
"success": (
"base": #67C23A,),)
);
@import "element-plus/theme-chalk/src/index.scss"; // 如果想要引入所有的样式

八、全局自动注册基础组件

1、在各自组件中新增install.ts,代码如下:

import { App } from 'vue'
import Component from './index.vue'
export default {install(app: App) {appponent('TTable', Component)}
}

2、在components下的baseComponents文件夹,新建install.ts,代码如下

/*  统一注册 baseComponents 目录下的全部组件 */
import { App } from 'vue'
export default {install: (app: App) => {// 引入所有组件下的安装模块const modules:any = a.globEager('./**/install.ts')for (const path in modules) {app.use(modules[path].default)}}
}

3、在main.ts中如下操作:

// 统一注册 baseComponents
import baseComponentsInstall from '@/components/baseComponents/install'
// 自动注册全部本地组件
app.use(baseComponentsInstall)

4、页面使用

九、缓存页面——给页面添加name属性

组件地址

gitHub组件地址

gitee码云组件地址

相关文章

基于ElementUi再次封装基础组件文档


vue3+ts基于Element-plus再次封装基础组件文档

本文发布于:2024-01-31 19:00:41,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170669884330671.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:组件   缓存   全局   页面   基础
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23