一、路由配置
需要至少四个一级路由:主页、登入页、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>