超详细!!!electron-vite-vue开发桌面应用之layout布局(九)

云风网
云风笔记
云风知识库

设置完基本的路由之后,接下来就可以进行常规的layout页面布局改造了

一、router/index.ts路由加入layout组件

在这里插入图片描述

import { createRouter, createWebHashHistory  } from 'vue-router'
import Layout from '@/layout'
//公共路由
export const constantRoutes = [
  {
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '',
    component: Layout,
    redirect: '/index',
    children: [
      {
        path: '/index',
        component: () => import('@/views/index.vue'),
        name: 'Index',
        meta: { title: '首页', icon: 'home', affix: true }
      }
    ]
  },
  {
    path: '/',
    component: Layout,
    hidden: false,
    redirect: 'noredirect',
    children: [
      {
        path: 'userManage',
        component: () => import('@/views/userManage/index.vue'),
        name: 'UserManage',
        meta: { title: '用户管理', icon: 'user' }
      }
    ]
  },
  {
    name: "Func",
    path: "/func",
    hidden: false,
    redirect: "noRedirect",
    component: Layout,
    alwaysShow: true,
    meta: {
        title: "业务管理",
        icon: "func",
        noCache: false,
        link: null
    },
    children: [
        {
            name: "NoteManage",
            path: "noteManage",
            hidden: false,
            component: () => import('@/views/func/noteManage/index.vue'),
            meta: {
                title: "语录管理",
                icon: "#",
                noCache: false,
                link: null
            }
        }
    ]
  }
]
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
  // {
  //   path: '/system/user-auth',
  //   component: Layout,
  //   hidden: true,
  //   permissions: ['system:user:edit'],
  //   children: [
  //     {
  //       path: 'role/:userId(\\d+)',
  //       component: () => import('@/views/func/noteManage/index.vue'),
  //       name: 'AuthRole',
  //       meta: { title: '分配角色', activeMenu: '/system/user' }
  //     }
  //   ]
  // }
]
const router = createRouter({
  //  hash 模式。
  history: createWebHashHistory(),
  routes: constantRoutes
})

export default router

注意事项

electron + vue无法使用history模式

在Electron + Vue项目中,如果遇到无法使用history模式的问题,通常是因为Vue Router默认使用的是HTML5的history模式,它依赖于浏览器的history API来管理路由。然而,在Electron中直接使用history模式时会遇到问题,因为Electron的本地页面文件被认为是在本地文件系统上,而不是在服务器上,导致路由变化时尝试从文件系统获取资源而不是通过Vue的路由处理。

二、在src目录下新建layout组件文件结构

在这里插入图片描述

1、layout/index.vue
<script setup lang="ts">
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar } from './components'

</script>

<template>
  <div class="app-wrapper">
    <sidebar class="sidebar-container" />
    <div class="main-container">
      <div class="fixed-header">
        <navbar />
      </div>
      <app-main class="main"/>
    </div> 
  </div>
</template>

<style scoped lang="scss">
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";

.app-wrapper {
  position: relative;
  height: 100%;
  width: 100%;
  .fixed-header {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 9;
    width: calc(100% - #{$base-sidebar-width});
    transition: width 0.28s;
    background-color: #606787;
    color: #ffffff;
  }
  .main{
    width: 100%;
  }
}
</style>

2、layout/components/Navbar.vue
<script setup lang="ts">
const route = useRoute()
const title = computed(() => route.meta.title);
</script>

<template>
    <div class="navbar">
        <div class="navbar-container">
            <h3>{{ title }}</h3>
        </div>
    </div>
</template>

<style scoped lang="scss">
@import "@/assets/styles/variables.module.scss";
.navbar {
  height: $base-navbar-height;
  overflow: hidden;
  position: relative;
}
</style>
3、layout/components/AppMain.vue
<template>
  <section class="app-main">
    <div class="app-main-container">
      <router-view v-slot="{ Component, route }" :key="keyVal">
        <transition name="fade-transform" mode="out-in">
          <keep-alive >
            <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
          </keep-alive>
        </transition>
      </router-view>
    </div>
  </section>
</template>

<script setup>
const route = useRoute();
const keyVal = computed(() => {
  return route.path + Math.random();
});
</script>

<style lang="scss" scoped>
@import "@/assets/styles/variables.module.scss";
.app-main {
  min-height: calc(100vh - #{$base-navbar-height});
  width: 100%;
  position: relative;
  overflow: hidden; 
  .app-main-container {
    height: calc(100vh - #{$base-navbar-height});
    border-radius:24px;
    padding: 24px;
  }
}

.fixed-header + .app-main {
  padding-top: $base-navbar-height;
}

.hasTagsView {
  .app-main {
    /* 84 = navbar + tags-view = 50 + 34 */
    min-height: calc(100vh - 132px);
  }

  .fixed-header + .app-main {
    padding-top: 132px;
  }
}
</style>

<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
  .fixed-header {
    padding-right: 6px;
  }
}

::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

::-webkit-scrollbar-track {
  background-color: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background-color: #c0c0c0;
  border-radius: 3px;
}
</style>


4、layout/components/Sidebar/index.vue
<template>
    <div>
        <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper" style="padding: 20px;">
            <el-menu
                :default-active="activeMenu"
                :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
                :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
                :unique-opened="true"
                :active-text-color="theme"
                :collapse-transition="false"
                mode="vertical"
            >
                <sidebar-item
                    v-for="(route, index) in sidebarRouters"
                    :key="route.path + index"
                    :item="route"
                    :base-path="route.path"
                />
            </el-menu>
        </el-scrollbar>
    </div>
</template>
<script setup lang="ts">
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.scss'
import usePermissionStore from '@/store/modules/permission'
import useSettingsStore from '@/store/modules/settings'
const permissionStore = usePermissionStore()
const settingsStore = useSettingsStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const route = useRoute()
const activeMenu = computed(() => {
  const { meta, path } = route;
  if (meta.activeMenu) {
    return meta.activeMenu;
  }
  return path;
})
onBeforeMount(() => {
    permissionStore.generateRoutes()
})


</script>
<style scoped lang="scss">

</style>

5、layout/components/Sidebar/sidebarItem.vue
<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }" @click="handleClick(item.path)">
          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
        </el-menu-item>
      </app-link>
    </template>

    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
      <template v-if="item.meta" #title>
        <svg-icon :icon-class="item.meta && item.meta.icon" />
        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
      </template>

      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-sub-menu>
  </div>
</template>

<script setup>
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import { getNormalPath } from '@/utils/mis'

const props = defineProps({
  // route object
  item: {
    type: Object,
    required: true
  },
  isNest: {
    type: Boolean,
    default: false
  },
  basePath: {
    type: String,
    default: ''
  }
})

const onlyOneChild = ref({});

function hasOneShowingChild(children = [], parent) {
  if (!children) {
    children = [];
  }
  const showingChildren = children.filter(item => {
    if (item.hidden) {
      return false
    } else {
      // Temp set(will be used if only has one showing child)
      onlyOneChild.value = item
      return true
    }
  })

  // When there is only one child router, the child router is displayed by default
  if (showingChildren.length === 1) {
    return true
  }

  // Show parent if there are no child router to display
  if (showingChildren.length === 0) {
    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
    return true
  }

  return false
};
function handleClick(routePath){
  localStorage.setItem('routePath', routePath)
}
function resolvePath(routePath, routeQuery) {
  if (isExternal(routePath)) {
    return routePath
  }
  if (isExternal(props.basePath)) {
    return props.basePath
  }
  if (routeQuery) {
    let query = JSON.parse(routeQuery);
    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
  }
  return getNormalPath(props.basePath + '/' + routePath)
}

function hasTitle(title){
  if (title.length > 5) {
    return title;
  } else {
    return "";
  }
}
</script>

6、layout/components/Sidebar/Link.vue
<template>
  <component :is="type" v-bind="linkProps()">
    <slot />
  </component>
</template>

<script setup>
import { isExternal } from '@/utils/validate'

const props = defineProps({
  to: {
    type: [String, Object],
    required: true
  }
})

const isExt = computed(() => {
  return isExternal(props.to)
})

const type = computed(() => {
  if (isExt.value) {
    return 'a'
  }
  return 'router-link'
})

function linkProps() {
  if (isExt.value) {
    return {
      href: props.to,
      target: '_blank',
      rel: 'noopener'
    }
  }
  return {
    to: props.to
  }
}
</script>

7、layout/components/innerLink/index.vue
<template>
  <div :style="'height:' + height">
    <iframe
      :id="iframeId"
      style="width: 100%; height: 100%"
      :src="src"
      frameborder="no"
    ></iframe>
  </div>
</template>

<script setup>
const props = defineProps({
  src: {
    type: String,
    default: "/"
  },
  iframeId: {
    type: String
  }
});

const height = ref(document.documentElement.clientHeight - 94.5 + "px");
</script>

app.vue改造内容如下

<script setup lang="ts">
const route = useRoute();
const keyVal = computed(() => {
  return route.path + Math.random();
});
</script>

<template>
  <router-view v-slot="{ Component }" :key="keyVal">
    <keep-alive>
      <component :is="Component" :key="route.path"/>
    </keep-alive>
  </router-view>
</template>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

niech_cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值