【Vue3+TS项目】硅谷甄选day03--layout组件搭建+路由配置+左侧菜单搭建

一、路由配置

需要至少四个一级路由:主页、登入页、404页、任意路由(指向404)

安装路由插件:4版本

pnpm i vue-router

src下新建文件夹views

分别创建404、login、home路由组件

src下新建router文件夹---包含index.ts和routes.ts

src/router/routes.ts

// 对外暴露配置路由(常量路由)
export const constantRoute = [
    {
        name: 'login',// 命名路由--做权限会用到
        path: '/login',
        component: () => import('@/views/login/index.vue')
    },
    {
        // 登入成功后展示数据的路由
        name: 'home',
        path: '/',
        component: () => import('@/views/home/index.vue')
    },
    {
        // 404路由
        name: '404',
        path: '/404',
        component: () => import('@/views/404/index.vue')
    },
    {
        // 任意路由
        name: 'any',
        path: '/:pathMatch(.*)*',
        redirect: '/404'
    }
]

src/router/index.ts

// 通过 vue-router 插件实现模板路由配置
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routes";
​
// 创建路由器
let router = createRouter({
    // 路由模式hash
    history: createWebHashHistory(),
    routes: constantRoute,
    // 滚动行为
    scrollBehavior() {
        return {
            left: 0,
            top: 0
        }
    }
​
})
​
export default router

入口文件中引入路由并注册 src/main.ts

// 引入路由
import router from '@/router'
// 注册模板路由
app.use(router)

一级路由在App组件中展示即可。

<router-view></router-view>

二、登入界面搭建

使用element布局搭建

栅格布局:span是占的分数,xs是屏幕小于760时占的分数,栅格一共是24份

使用icon图表组件:User、Lock(前缀图标:prefix-icon、后缀图标、切换密码图标show-password)

登入的静态模板组件 src/views/login/index.vue

<template>
    <div class="login_container">
        <el-row>
            <el-col :span="12" :xs="0">左侧占位的</el-col>
            <el-col :span="12" :xs="24">
                <el-form class="login_form">
                    <h1>Hello!</h1>
                    <h2>Welcome to 鱼仔甄选</h2>
                    <el-form-item>
                        <el-input :prefix-icon="User" v-model="loginForm.username"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-input type="password" :prefix-icon="Lock" v-model="loginForm.password" show-password></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button :loading="loading" type="primary" class="login_btn">登入</el-button>
                    </el-form-item>
                </el-form>
            </el-col>
        </el-row>
    </div>
</template>
  
<script setup lang="ts">
// 输入框前置的图标
import { User, Lock } from '@element-plus/icons-vue'
import { reactive } from 'vue';
​
//收集账号密码
let loginForm = reactive({ username: 'admin', password: '111111' })
​
</script>
  
<style scoped lang="scss">
.login_container {
    width: 100%;
    height: 100vh;
    background: url('@/assets/images/background.jpg') no-repeat;
    background-size: cover;
​
    .login_form {
        position: relative;
        width: 80%;
        top: 30vh;
        background: url('@/assets/images/login_form.png') no-repeat;
        background-size: cover;
        padding: 40px;
​
        h1 {
            color: white;
            font-size: 40px;
        }
​
        h2 {
            color: white;
            font-size: 20px;
            margin: 20px 0px;
        }
​
        .login_btn {
            width: 100%;
        }
    }
}
</style>
 

根据账户和密码,点击登入后会返回验证身份的信息,如token,那么我们使用pinia存储

点击登入之后需要在回调函数里做的事

  • 通知仓库发送登入请求

  • 请求成功:首页展示数据

  • 请求失败:弹出错误信息

首先安装pinia仓库

pnpm i pinia@2.0.34

新建大仓库文件 src/store/index.ts

import { createPinia } from 'pinia'
// 创建大仓库
let pinia = createPinia()
// 对外暴露:入口文件需要安装大仓库
export default pinia

入口文件中安装大仓库 src/main.ts

import pinia from '@/store'
app.use(pinia)

一个错误

Failed to load resource: the server responded with a status of 504 (Outdated Optimize Dep)

pinia版本不要太高(建议安装2.0.34,目前是2.1.3【2023.05.20】)

创建小仓库 src/store/modules/user.ts

// 用户相关的小仓库
import { defineStore } from 'pinia'
// 引入登入请求接口
import { reqLogin } from '@/api/user/index'
// 引入数据类型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from '@/store/modules/types/type'
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/token'
// 创建用户小仓库
let useUserStore = defineStore('User', {
    // 小仓库存储数据的地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户的唯一标识token
        }
    },
    // 异步、逻辑
    actions: {
        // 用户登入的方法
        async userLogin(data: loginForm) {
            let res: loginResponseData = await reqLogin(data)
            //登入请求:成功200----token
            //登入请求:失败201--登入失败错误的信息
            if (res.code === 200) {
                // pinia仓库存储token
                // pinia|vuex存储数据都是利用js对象,并非持久化
                this.token = (res.data.token as string)
                // 本地持久化存储一份
                SET_TOKEN((res.data.token as string))
                // 能保证当前async函数返回一个成功的promise
                return 'ok'
            } else {
                return Promise.reject(new Error(res.data.message))
            }
        }
    },
    getters: {
​
    }
})
​
export default useUserStore

登入页面中引入小仓库,点击登入时,通知user小仓库发请求,存数据

project\src\views\login\index.vue

userLogin这个函数会返回一个promise结果,根据结果来进行下一步(try或者点than写法都可)

// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user'
let useStore = useUserStore()
import { useRouter } from 'vue-router';
// element提示框
import { ElNotification } from 'element-plus';
// 获取路由器
let $router = useRouter()
// 登入按钮加载效果,element的小动画
let loading = ref(false)
​
// 点击登入的回调
const login = async () => {
​
    loading.value = true
    // 通知仓库发送登入请求---收集的账户密码带给服务器
    // 请求成功:首页展示数据
    // 请求失败:弹出错误信息
    try {
        // 保证登入成功,userLogin这个函数会返回一个promise结果,根据结果来进行下一步
        await useStore.userLogin(loginForm)
        // 编程式路由导航跳转到展示数据的首页
        $router.push('/')
        // 登入成功的提示消息
        ElNotification({
            type: 'success',
            message: '登入成功'
        })
        loading.value = false
    } catch (error) {
        loading.value = false
        // 登入失败的提示消息
        ElNotification({
            type: 'error',
            message: (error as Error).message
        })
    }
}
​
loading写在finally中也可以
finally {
    loading.value = false
}

用户仓库数据的ts类型定义

project\src\store\modules\types\type.ts

// 定义小仓库数据state类型
export interface UserState {
    token: null | string
}

登入接口返回的数据类型

project\src\api\user\type.ts

interface dataType {
    token?: string,
    message?: string
}
//定义登录接口返回数据类型
export interface loginResponseData {
    code: number,
    data: dataType
}

封装本地存储方法

project\src\utils\token.ts

// 封装本地存储 存储数据与读取数据的方法
export const SET_TOKEN = (token: string) => {
    localStorage.setItem('TOKEN', token)
}
​
export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN')
}

登入时间的判断与封装

project\src\utils\time.ts

// 获取时间的函数:早上|上午|下午|晚上
export const getTime = () => {
    let time = '';
    let hour = new Date().getHours()
    if (hour <= 9) {
        time = '早上好'
    } else if (hour <= 12) {
        time = '上午好'
​
    } else if (hour <= 14) {
        time = '中午好'
​
    } else if (hour <= 18) {
        time = '下午好'
​
    } else {
        time = '晚上好'
​
    }
    return time
}

Login组件中使用

import { getTime } from '@/utils/time'
​
ElNotification({
    title: `Hi!${getTime()}!`,
    type: 'success',
    message: '登入成功'
})

登入表单校验

规则:登入名大于等于5位、密码大于等于6位

利用element表单组件的校验功能

步骤:

使用model收集表单数据到代理对象身上::model="ruleForm"

给表单添加rules属性,并制定验证规则(也是对象)::rules="rules"

给需要校验的表单项添加prop属性:props="username"

1
<el-form class="login_form" :model="loginForm" :rules="rules">
2
<el-form-item prop="username">
<el-form-item prop="password">
3
// 定义表单验证规则rules
// required是否需要验证、trigger是触发时机:blur/change
const rules = {
    username: [
        { required: true, min: 5, max: 16, message: '用户名长度应为5-16位', trigger: 'change' },
    ],
    password: [
        { required: true, min: 6, max: 16, message: '密码长度应为6-16位', trigger: 'change' },
​
    ]
}

通过表单验证的账号密码才允许发请求,所以我们通过ref获取表单元素,表单组件身上有个属性:validate(对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise),我们利用返回的promise结果进行下一步操作

//获取表单元素
let loginForms = ref()
const login = async () => {
    //保证所有表单项通过才发请求
    await loginForms.value.validate()
    ......
}

自定义验证表单

上面验证太简单了,想要更加复杂的规则就需要我们自定义验证表单(也是element提供)

自定义规则中需要一个 validator属性,值是一个方法

判断规则其实可以写正则,这边只是学习一下element的自定义校验

project\src\views\login\index.vue

// 自定义校验规则函数
const validateUsername = (rule: any, value: any, callback: any) => {
    //rule:即为校验规则对象
    //value:即为表单元素文本内容
    //函数:如果符合条件callBack放行通过即为
    //如果不符合条件callBack方法,注入错误提示信息
    if (value.length >= 5) {
        callback();
    } else {
        callback(new Error('账号长度至少五位'));
    }
}
const validatePassword = (rule: any, value: any, callback: any) => {
    // 判断条件可以写正则
    if (value.length >= 6) {
        callback();
    } else {
        callback(new Error('密码长度至少六位'));
    }
}
​
const rules = {
    username: [
        { trigger: 'change', validator: validateUsername },
    ],
    password: [
        { trigger: 'change', validator: validatePassword },
    ]
}

三、layout组件搭建

静态搭建(使用element更简单)

主页分三块内容:左侧展示菜单、右侧上方是顶部导航,下方是内容展示(二级路由展示)

project\src\layout\index.vue

<template>
    <div class="layout_container">
        <!-- 左侧菜单展示 -->
        <div class="layout_slider">
            111
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar">
            222
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main">
            <p style="height: 1000px">111</p>
        </div>
    </div>
</template>
  
<script setup lang="ts">
​
</script>
  
<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;
    background: blue;
​
    .layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
    }
​
    .layout_tabbar {
        position: fixed;
        top: 0;
        right: 0;
        width: calc(100% - $base-menu-width);
        height: $base-tabbar-height;
        background: $base-tabbar-background;
    }
​
    .layout_main {
        position: absolute;
        right: 0;
        top: $base-tabbar-height;
        width: calc(100% - $base-menu-width);
        height: calc(100vh - $base-tabbar-height);
        background: $base-main-background;
        padding: 20px;
        overflow: auto;
    }
}
</style>
  

样式变量

project\src\style\variable.scss

// layout组件
// 左侧菜单的宽度
$base-menu-width: 260px;
// 左侧菜单的颜色
$base-menu-background: #846e89;
// 顶部导航的高度
$base-tabbar-height: 100px;
// 顶部导航的颜色
$base-tabbar-background: #e0c7e3;
​
// 内容展示区域颜色
$base-main-background: #c6d182;

滚动条样式

project\src\style\index.scss

// 滚动条外观
::-webkit-scrollbar {
    width: 10px;
}
​
::-webkit-scrollbar-track {
    background: $base-menu-background;
}
​
::-webkit-scrollbar-thumb {
    width: 10px;
    background-color: yellowgreen;
    border-radius: 10px;
}

Logo组件封装

设置图片与标题的配置文件(方便别入修改)

project\src\settings.ts

//用于项目logo|标题配置
export default {
    title: '鱼仔甄选', //项目的标题
    logo: '/logo.png', //项目logo设置
    logoHidden: true, //logo组件是否隐藏设置
}

Logo组件

project\src\layout\logo\index.vue

<template>
    <div class="logo"  v-if="setting.logoHidden">
        <img :src="setting.logo" alt="">
        <p>{{ setting.title }}</p>
    </div>
</template>
  
<script setup lang="ts">
// 引入设置标题与图片的配置文件
import setting from '@/settings'
</script>
  
<style scoped lang="scss">
.logo {
    display: flex;
    width: 100%;
    height: 50px;
    align-items: center;
    color: wheat;
    padding: 20px;
​
    img {
        width: 60px;
        height: 40px;
    }
​
    p {
        font-size: 20px;
        margin-left: 30px;
    }
}
</style>
  

左侧菜单的搭建

新建 menu 组件,并在layout中引入和使用<Menu />

project\src\layout\menu\index.vue

添加二级路由配置

project\src\router\routes.ts

    {
        // 登入成功后展示数据的路由
        name: 'layout',
        path: '/',
        component: () => import('@/layout/index.vue'),
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue')
            }
        ]
    },

将路由数组放进仓库中,这样后面其他组件就可以使用(遍历啊什么的)

project\src\store\modules\user.ts

// 引入路由常量
import { constantRoute } from '@/router/routes'
​
menuRoutes: constantRoute,// 仓库存储生成菜单需要的数组(路由配置数组)

menuRoutes类型定义

project\src\store\modules\types\type.ts

import type { RouteRecordRaw } from "vue-router"
​
// 定义小仓库数据state类型
export interface UserState {
    token: null | string,
    menuRoutes: RouteRecordRaw[]
}

其他组件就可以使用路由配置数组了

layout组件就可以使用(给Menu组件传过去)

project\src\layout\index.vue

// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
let userStore = useUserStore()
​
// 给Menu组件传过去,Menu组件就可以根据路由动态生成左侧菜单(其实也可以Menu组件自己从小仓库中取)
 <Menu :menuList="userStore.menuRoutes" />

路由中都加上路由元信息

        meta: {
            title: '登入',// 左侧菜单展示的名字
            hidden: true,// 是否在左侧菜单中显示
        }

menu组件:使用menuList生成动态菜单,只有一个二级就不用折叠

注意递归组件,注意判断条件,注意index要写,注意script可以写两次但ls要一样(递归组件需要名字)

点击菜单进行路由跳转element提供了两种方法,menu-item的属性或者事件,注意路由重定向----@click="goRoute"

<template>
    <div>
        <template v-for="item in menuList" :key="item.path">
            <!-- 没有子路由 -->
            <!-- 有些路由不需要展示,比如login,再套一层判断 hidden-->
            <template v-if="!item.children">
                <el-menu-item v-if="!item.meta.hidden" :index="item.path" @click="goRoute">
                    <template #title>
                        <el-icon>
                            <component :is="item.meta.icon"></component>
                        </el-icon>
                        <span>{{ item.meta.title }}</span>
                    </template>
                </el-menu-item>
            </template>
            <!-- 只有一个子路由 -->
            <template v-if="item.children && item.children.length == 1">
                <el-menu-item v-if="!item.children[0].meta.hidden" :index="item.children[0].path" @click="goRoute">
                    <template #title>
                        <el-icon>
                            <component :is="item.children[0].meta.icon"></component>
                        </el-icon>
                        <span>{{ item.children[0].meta.title }}</span>
                    </template>
                </el-menu-item>
            </template>
            <!-- 两个以上的子路由 -->
​
            <el-sub-menu v-if="item.children && item.children.length > 1" :index="item.path">
                <template #title>
                    <el-icon>
                        <component :is="item.meta.icon"></component>
                    </el-icon>
                    <span>{{ item.meta.title }}</span>
                </template>
                <Menu :menu-list="item.children" />
            </el-sub-menu>
​
        </template>
    </div>
</template>
  
<script setup lang="ts">
import { useRouter } from 'vue-router';
defineProps(['menuList'])
// 获取路由对象
let $router = useRouter()
​
// 点击菜单进行路由跳转
const goRoute = (vc) => {
    $router.push(vc.index)
}
​
</script>
<script lang="ts">
export default {
    name: 'Menu'
}
</script>
  
<style scoped lang="scss"></style>
  

菜单图标(使用elememnt):动态展示,将图标注册成全局组件,然后放在路由元信息中

project\src\components\index.ts

// 引入element全部图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
​
// 全局组件的对象
const allGlobalComponents = { SvgIcon: SvgIcon };
// 对外暴露一个插件对象
export default {
    install(app) {
        // 将图表组件全部注册为全局组件
        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
            app.component(key, component)
        }
    }
}

添加路由组件,完整的路由

一级路由:数据大屏、权限管理(二级组件:用户、角色、菜单管理)、商品管理(二级路由:sku、spu、品牌、属性)

权限和商品的一级路由用的还是组件 layout

首页直接重定向到home

project\src\router\routes.ts

{
        name: 'screen',
        path: '/screen',
        component: () => import('@/views/screen/index.vue'),
        meta: {
            hidden: false,
            title: '数据大屏',
            icon: 'Platform',
        },
    },
    {
        path: '/acl',
        component: () => import('@/layout/index.vue'),
        name: 'acl',
        meta: {
            title: '权限管理',
            icon: 'Lock',
        },
        redirect: '/acl/user',
        children: [
            {
                path: '/acl/user',
                component: () => import('@/views/acl/user/index.vue'),
                name: 'user',
                meta: {
                    title: '用户管理',
                    icon: 'User',
                },
            },
            {
                path: '/acl/role',
                component: () => import('@/views/acl/role/index.vue'),
                name: 'role',
                meta: {
                    title: '角色管理',
                    icon: 'UserFilled',
                },
            },
            {
                path: '/acl/permission',
                component: () => import('@/views/acl/permission/index.vue'),
                name: 'permission',
                meta: {
                    title: '菜单管理',
                    icon: 'Monitor',
                },
            },
        ],
    },
    {
        path: '/product',
        component: () => import('@/layout/index.vue'),
        name: 'product',
        meta: {
            title: '商品管理',
            icon: 'Goods',
        },
        redirect: '/product/trademark',
        children: [
            {
                path: '/product/trademark',
                component: () => import('@/views/product/trademark/index.vue'),
                name: 'trademark',
                meta: {
                    title: '品牌管理',
                    icon: 'ShoppingCartFull',
                },
            },
            {
                path: '/product/attr',
                component: () => import('@/views/product/attr/index.vue'),
                name: 'attr',
                meta: {
                    title: '属性管理',
                    icon: 'ChromeFilled',
                },
            },
            {
                path: '/product/spu',
                component: () => import('@/views/product/spu/index.vue'),
                name: 'spu',
                meta: {
                    title: 'SPU管理',
                    icon: 'Calendar',
                },
            },
            {
                path: '/product/sku',
                component: () => import('@/views/product/sku/index.vue'),
                name: 'sku',
                meta: {
                    title: 'SKU管理',
                    icon: 'Orange',
                },
            },
        ],
    },

layout右侧展示区域封装成一个组件 main,想做一点过度动画

project\src\layout\main\index.vue

<template>
    <!-- 路由组件出口的位置 -->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由组件的子路由 -->
            <component :is="Component" />
        </transition>
    </router-view>
</template>
​
<script setup lang="ts">
​
</script>
<script lang="ts">
export default {
    name: "Main"
}
</script>
​
<style scoped>
.fade-enter-from {
    opacity: 0;
    transform: scale(0);
}
​
.fade-enter-active {
    transition: all .3s;
}
​
.fade-enter-to {
    opacity: 1;
    transform: scale(1);
}
</style>

layout组件引入main并展示

project\src\layout\index.vue

// 右侧内容展示组件
import Main from '@/layout/main/index.vue'
​
<!-- 内容展示区域 -->
<div class="layout_main">
    <Main />
</div>

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值