zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Geeker-Admin项目跟做笔记(vue3+vite+pinia)

2023-03-31 10:42:06 时间

一、路由配置

(一) 静态路由

1.配置路由

const routes: RouteRecordRaw[] = [
    {
        path: '/login',
        name: 'login',
        component: () => import('../pages/login/index.vue'),
        meta: {
            requiresAuth: false,
            title: '登录',
            key: 'login'
        }
    }
]

RouteRecordRaw:为了规范ts的开发,增加对路由对象类型的限制

2.创建一个路由对象

const router = createRouter({
    history: createWebHashHistory(),
    routes,
    strict: false,
    // 切换页面,滚动到最顶部
    scrollBehavior: () => ({ left: 0, top: 0 })
})

history:hash模式(链接地址中有一个#)

3. 暴露路由对象

export default router

4. main.js中引入注册路由

import router from './routers/index'
app.use(router).mount('#app')

5. App.vue中将路由显示出来

<template>
	<router-view></router-view>
</template>

6. 子路由的设置与引入
6.1 将侧边菜单Layout组件设置为一级路由

将路由组件展示到页面

export const Layout=()=>import("../layout/index.vue")

6.2 设置modules文件夹,存放各路由组件路由
6.3 子路由的书写

path:路由路径
name:路由名称
redirect:路由重定向
meta:路由元信息
meta.requireAuth:是否需要权限验证
param meta.keepAlive:是否需要缓存该路由
param meta.title:路由标题
param meta.key:路由key,用来匹配权限按钮
children:二级路由

import { RouteRecordRaw } from "vue-router";
import { Layout } from "../constant";

// 常用组件模块
const dashboardRouter: Array<RouteRecordRaw> = [
    {
        path: '/dashboard',
        component: Layout,
        redirect: '/dashboard/dataVisualize',
        meta: {
            title: 'Dashboard'
        },
        children: [
            {
                path: '/dashboard/dataVisualize',
                name: 'dataVisualize',
                component: () => import('@/pages/dashboard/dataVisualize/index.vue'),
                meta: {
                    keepAlive: true,
                    requiresAuth: true,
                    title: '数据可视化',
                    key: 'dataVisualize'
                }
            },
            {
                path: '/dashboard/embedded',
                name: 'embedded',
                component: () => import('@/pages/dashboard/embedded/index.vue'),
                meta: {
                    keepAlive: true,
                    requiresAuth: true,
                    title: '内嵌页面',
                    key: 'embedded'
                }
            },
        ]
    }]

export default dashboardRouter

6.4 在router.ts中导入所有路由

const metaRouters = import.meta.glob("./modules/*.ts", { eager: true });

如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入 { eager: true } 作为第二个参数

import.meta.glob全局导入参考文档vite官网

6.5 处理路由表

export const routerArray: RouteRecordRaw[] = []
// Object.keys 返回一个所有元素为字符串的数组
Object.keys(metaRouters).forEach(item => {
    Object.keys(<Object>metaRouters[item]).forEach((key:any)=>{
        routerArray.push(...metaRouters[item][key])
    })
})

Object.keys方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致。

6.6 注册路由

const routes: RouteRecordRaw[] = [
    ...routerArray
]

(二)动态路由

1. 动态路由配置
1.1 目录
在这里插入图片描述
1.2 配置参数

 * @description 动态路由参数配置简介
 * @param path ==> 菜单路径
 * @param name ==> 菜单别名
 * @param redirect ==> 重定向地址
 * @param component ==> 视图文件路径
 * @param meta ==> 菜单信息
 * @param meta.icon ==> 菜单图标
 * @param meta.title ==> 菜单标题
 * @param meta.activeMenu ==> 当前路由为详情页时,需要高亮的菜单
 * @param meta.isLink ==> 是否外链
 * @param meta.isHide ==> 是否隐藏
 * @param meta.isFull ==> 是否全屏(示例:数据大屏页面)
 * @param meta.isAffix ==> 是否固定在 tabs nav
 * @param meta.isKeepAlive ==> 是否缓存

2. 创建路由对象(index.ts)

const router = createRouter({
    history: createWebHashHistory(),
    routes: [...staticRouter, ...errorRouter],
    strict: false,
    scrollBehavior: () => ({ left: 0, top: 0 })
})

二、axios的配置

1. 创建axiosCancel.ts文件,用于有pending后直接取消

1.1 声明一个Map用于存储每个请求的标识和取消函数

Map对象的好处是可以快速判断是否有重复的请求

let pendingMap = new Map<string, Canceler>()

1.2 序列化参数

根据当前请求的信息生成请求的 Key

export const getPendingUrl = (config: AxiosRequestConfig) =>
    [
        config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)
    ].join('&')

1.3 创建AxiosCanceler类

  • 添加请求—用于把当前请求信息添加到 pendingRequest对象中
addPending(config: AxiosRequestConfig) {
    // 在请求开始之前,对之前的请求做检查取消操作
    this.removePending(config)
    const url = getPendingUrl(config);
    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
        if (!pendingMap.has(url)) {
            // 如果pending中不存在当前请求,则添加进去
            pendingMap.set(url, cancel)
        }
    });
}
  • 移除请求–检查是否存在重复请求,若存在则需要取消已发出的请求
removePending(config: AxiosRequestConfig) {
    const url = getPendingUrl(config);
    if (pendingMap.has(url)) {
        // 如果pending中存在当前请求标识,需要取消当前请求,并且移除
        const cancel = pendingMap.get(url)
        cancel && cancel()
        pendingMap.delete(url)
    }
};
  • 清空所有pending
removeAllPending() {
    pendingMap.forEach(cancel => {
        cancel && isFunction(cancel) && cancel();
    })
    pendingMap.clear();
};

其中,isFunction()是自定义的一个类型

  • 重置
reset(): void {
   pendingMap = new Map<string, Canceler>()
}

2. axios封装

2.1 创建一个AxiosCanceler对象

import { AxiosCanceler } from "./helper/axiosCancel";
const axiosCanceler = new AxiosCanceler()

2.2 配置config对象

const config = {
    // 默认请求地址
    baseURL: import.meta.env.VITE_API_URL as string,
    // 设置超时时间:500
    timeout: ResultEnum.TIMEOUT as number
}

默认请求地址在.evn开头的文件中

2.3 创建RequestHttp类

  • 2.3.1 创建axios实例
service: AxiosInstance;//AXIOS实例

以下步骤在构造函数public-constructor中

  • 2.3.2 实例化axios
this.service = axios.create(config)
  • 2.3.3 配置请求拦截器

客户端发送请求 - 请求拦截器 - 服务器

this.service.interceptors.request.use(
   (config: AxiosRequestConfig) => {
       const globalStore = GlobalStore();
       // 将当前请求添加到pending中
       axiosCanceler.addPending(config);
       // 如果当前请求不需要显示Loading,在api服务站通过指定的第三个参数:{headers:{noLoading:true}}来控制不显示loading
       config.headers!.noLoading || showFullScreenLoading();
       //从GlobalStore仓库中获取token
       const token: string = globalStore.token;
       return { ...config, headers: { ...config.headers, "x-access-token": token } };
   },
   (error: AxiosError) => {
       return Promise.reject(error)
   }
);
  • 配置响应拦截器

服务器返回信息- 拦截统一处理 -客户端Js获取信息

this.service.interceptors.response.use(
   (response: AxiosResponse) => {
       const { data, config } = response;
       const globalStore = GlobalStore()
       // console.log('reaponse', data);
       // 在请求结束后,移除本次请求
       axiosCanceler.removePending(config)
       // 关闭Loading
       tryHideFullScreenLoading()
       //1. 登录失效(code==599)
       if (data.code == ResultEnum.OVERDUE) {
           ElMessage.error(data.msg)
           // setToken为仓库的action
           globalStore.setToken('')
           // 跳转至登录页面
           router.replace({
               path: "/login"
           })
           return Promise.reject(data);
       }
       // 2.全局错误信息拦截(防止下载文件的时候返回数据流,没有code,直接报错)
       //后面页面请求就不用判断data.code==200
       if (data.code && data.code !== ResultEnum.SUCCESS) {
           ElMessage.error(data.msg);
           return Promise.reject(data);
       }
       // 3.请求成功
       return data;
   },
   async (error: AxiosError) => {
       const { response } = error;
       tryHideFullScreenLoading();
       // 请求超时单独判断,因为请求超时没有reaponse
       if (error.message.indexOf("timeout") !== -1) ElMessage.error('请求超时,请稍后重试')
       // 根据响应的错误状态码,做不同的处理
       if (response) checkStatus(response.status);
       // 服务器结果都没有返回(可能服务器错误也可能服务端断网),断网处理:可以跳转到段网页面
       if (!window.navigator.onLine) router.replace({ path: "/errorPage/500" });
       return Promise.reject(error)
   }
)

2.4 常用请求方法封装

get<T>(url: string, params?: object, _object = {}):
    Promise<ResultData<T>> {
    return this.service.get(url, { params, ..._object });
}

post<T>(url: string, params?: object, _object = {}):
    Promise<ResultData<T>> {
    return this.service.post(url, params, _object);
}

2.5 暴露RequestHttp类

export default new RequestHttp(config)

3. 使用

以登录接口为例
3.1. 为组件的api标注类型(api–interface–index.ts)

  • 3.1.1 请求响应参数(不包含data)
export interface Result {
    code: string,
    msg: string
}
  • 3.1.2 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
    data?: T;
}
  • 3.1.3 登录模块
export namespace Login {
    export interface ReqLoginForm {        username: string;
        password: string;
    }
    export interface ResLogin {
        access_token: string
    }
}

3.2 后端为服务器端口名(api–config–servicePort.ts)

export const PORT1 = "/geeker";

3.3 登录模块(api–modules–login.ts)

import { Login } from "../interface";
import { PORT1 } from "@/api/config/servicePort";
import http from "../../api"

// 用户登录接口
export const loginApi=(params:Login.ReqLoginForm)=>{
    return http.post<Login.ResLogin>(PORT1+`/login`,params);
}

3.4 在登录组件中使用

formEl.validate(async valid => {
    if (!valid) return
    loading.value = true
    try {
      const requestLoginForm:Login.ReqLoginForm={
        username:loginFrom.username,
        password:md5(loginFrom.password)
      }
      const res=await loginApi(requestLoginForm)
      ElMessage.success('登录成功')
    } finally {
      loading.value = false
    }
})

3.5 请求结果
在这里插入图片描述

三、pinia仓库的使用

四、Header 设计笔记

1. 国际化(中英文切换)

参考文档:Mpx框架—国际化i18n
4.1.1安装vue-i18n

npm install vue-i18n --save

4.1.2 在vite.config.ts中对vue-i18n进行配置

alias:{
  'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
}

4.1.3 配置i18n

  • index.ts引入createI18n
import { createI18n } from "vue-i18n";
  • 设置语言
    (1) 中文zh.ts
export default{
    home:{
        welcome:"欢迎使用"
    },
    tabs:{
        more:"更多",
        closeCurrent:"关闭当前",
        closeOther:"关闭其他",
        closeAll:"关闭所有"
    }
}

(2)英文en.ts

export default{
    home:{
        welcome:"Welcome"
    },
    tabs:{
        more:"More",
        closeCurrent:"Close current",
        closeOther:"Close other",
        closeAll:"Close All"
    }
}
  • 将上面两个文件引入index.ts
import zh from "./modules/zh";
import en from "./modules/en";
  • 配置createI18n
const i18n = createI18n({
    legacy: false,  // 如果要支持 compositionAPI,此项必须设置为 false
    locale: "zh", //设置语言类型
    globalInjection: true,//全局注册$t方法
    messages:{
        zh,
        en
    }
});
export default i18n;

4.1.4 main.js中引入并配置i18n

import i18n from '@/language/index'
const app = createApp(App)
app.use(i18n).mount('#app')

4.1.5 使用

<span>{{$t('tabs.more')}}</span>

五、Menu 设计笔记

1. Menu 组件结构

<template>
    <div class="menu">
    	//子组件
        <Logo :isCollapse="isCollapse" />
        <el-scrollbar>
            <el-menu>
                <el-menu>
                	//子组件
                    <SubItem :menuList="menuList" />
                </el-menu>
            </el-menu>
        </el-scrollbar>
    </div>
</template>

2. 父级菜单与子菜单

5.2.1 利用pinia设计菜单仓库MenuStore

  • 设计MenuStore的ts类型MenuState
export interface MenuState {
    menuList: Menu.MenuOptions[];
}
  • MenuStore仓库设计
import { defineStore } from "pinia";
import { MenuState } from "../interface";
import piniaPersistConfig from "@/config/piniaPersist";

export const MenuStore = defineStore({
    id: "MenuStore",
    state: (): MenuState => ({
        // menu list
        menuList: []
    }),
    getters: {},
    actions: {
        async setMenuList(menuList: Menu.MenuOptions[]) {
            this.menuList = menuList
        }
    },
    //开启该插件,开启数据存储
    persist: piniaPersistConfig("MenuState")
})

id : 作为store的第一个参数,是store唯一的名称(必须!!!)
state:相当于data
geters:相当于computed
actions:相当于methods
开启持久化:persist: piniaPersistConfig(“MenuState”)

5.2.2 从MenuStore中获取menuList,并将它传递给子组件SubItem

  • 在Menu组件中引入MenuStore
import { MenuStore } from '@/store/modules/menu';
  • 使用计算属性从MenuStore中获取menulist
const menuStore = MenuStore();
const menuList = computed((): Menu.MenuOptions[] => menuStore.menuList);
  • 将menuList传递给子组件
<SubItem :menuList="menuList" />
  • SubItem子组件接收列表
defineProps<{ menuList: Menu.MenuOptions[] }>();

tips :当前菜单列表为空

5.2.2 获取菜单列表

login.ts文件

  • 引入本地Json文件(mock.js
import menu from '@/assets/json/menu.json'
  • 获取菜单列表
export const getMenuList = () => {
    return menu;
}
  • 使用递归处理路由菜单,生成一维数组(util.ts)
/**
 * @description: 使用递归处理路由菜单,生成一维数组
 * @param {Array} menuList 所有菜单列表
 * @param {Array} newArr 菜单的一维数组
 * @return array
 */
export function handleRouter(routerList: Menu.MenuOptions[], newArr: string[] = []) {
    // console.log(routerList);
    routerList.forEach((item: Menu.MenuOptions) => {
        typeof item === "object" && item.path && newArr.push(item.path);
        item.children && item.children.length && handleRouter(item.children, newArr)
    })
    // console.log(newArr);
    return newArr;
}

Menu.vue:获取菜单列表

onMounted(async () => {
    // 获取菜单列表
    try {
        const res = await getMenuList();
        console.log(res);
        if (!res.data) return;
        // 把路由菜单处理成一维数组(存储到pinia中)
        const dynamicRouter = handleRouter(res.data);
        menuStore.setMenuList(res.data);
    } finally {

    }
})

5.2.3 router-view将路由组件渲染至页面

<el-main>
  <router-view v-slot="{Component,route}">
    <transition appear name="fade-transform" mode="out-in">
      <keep-alive :include="cacheRouter">
        <component :is="Component" :key="route.path"></component>
      </keep-alive>
    </transition>
  </router-view>
</el-main>
  • v-slot = “{Component,route}”— 接收Props的默认插槽,并解构

  • 当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。

  • cacheRouter — 使用递归,过滤需要缓存的路由

_route 所有路由表
_cache 缓存的路由表

import { RouteRecordRaw, RouteRecordName } from "vue-router";
import { routerArray } from "./router";

let cacheRouter: any[] = [];
const filterKeepAlive = (_route: RouteRecordRaw[], _cache: RouteRecordName[]): void => {
    _route.forEach(item => {
        item.meta?.keepAlive && item.name && _cache.push(item.name);
        item.children && item.children.length !== 0 && filterKeepAlive(item.children,_cache)
    })
};

filterKeepAlive(routerArray,cacheRouter);

export default cacheRouter;

cacheRouter:
在这里插入图片描述

六、tabs标签页

1. 利用pinia设计tabs仓库

6.1.1 点击左侧菜单栏添加tabs标签页

  • 不添加黑名单中的路径:如果路径包含在黑名单中,则退出方法
if (TABS_BLACK_LIST.includes(tabsItem.path)) return;
  • 6.1.2 定义标签信息

title:标签名(默认值:首页)
path:标签路径(默认值:HOME_URL)
close:标签状态(标签是否关闭,默认关闭)

 const tabInfo: TabsOptions = {
    title: tabsItem.title,
    path: tabsItem.path,
    close: tabsItem.close
};
  • 如果tabsMenuList中每一个元素的路径都等于 tabsItem.path ,则把该元素添加至tabsMenuList

Array.every:一个数组内的所有元素是否都能通过某个指定函数的测试,都通过则返回true,否则返回false

if (this.tabsMenuList.every(item => item.path !== tabsItem.path)) {
    this.tabsMenuList.push(tabInfo);
};
  • 将路径赋值给tabsMenuValue
this.setTabsMenuValue(tabsItem.path);

完整代码:

async addTabs(tabsItem: TabsOptions) {
    // 不添加黑名单中的路径
    if (TABS_BLACK_LIST.includes(tabsItem.path)) return;
    const tabInfo: TabsOptions = {
        title: tabsItem.title,
        path: tabsItem.path,
        close: tabsItem.close
    };
    if (this.tabsMenuList.every(item => item.path !== tabsItem.path)) {
        this.tabsMenuList.push(tabInfo);
    };
    this.setTabsMenuValue(tabsItem.path);
},

6.1.2 关闭标签页

  • 循环遍历tabsMenuList
if (tabsMenuValue === tabsPath) {
    tabsMenuList.forEach((item, index) => {
        if (item.path !== tabsPath) return;
        const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
        if (!nextTab) return;
        tabsMenuValue = nextTab.path;
        router.push(nextTab.path);
    });
}

完整代码

async removeTabs(tabsPath: string) {
    let tabsMenuValue = this.tabsMenuValue;
    let tabsMenuList = this.tabsMenuList;
    if (tabsMenuValue === tabsPath) {
        tabsMenuList.forEach((item, index) => {
            if (item.path !== tabsPath) return;
            const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
            if (!nextTab) return;
            tabsMenuValue = nextTab.path;
            router.push(nextTab.path);
        });
    }
    this.tabsMenuValue = tabsMenuValue;
    this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabsPath);
},

2. Tabs.vue

6.2.1 监听路由变化

// getter函数形式
watch(
    () => route.path,
    () => {
        let params = {
            title: route.meta.title as string,
            path: route.path,
            close: true
        };
        tabStore.addTabs(params)
    },
    {
        immediate: true
    }
)

七、数据大屏

1. 数据大屏自适应屏幕大小

7.1.1 为外层盒子添加一个ref属性

<div class="dataScreen_container">
    <div class="dataScreen" ref="dataScreenRef">
    </div>
</div>

7.1.2 初始化ref

const dataScreenRef = ref<HTMLElement | null>(null);

7.1.3 初始化时为外层盒子加上缩放属性,防止界面刷新时就已经缩放

onMounted(() => {
  // 初始化时为外层盒子加上缩放属性,防止界面刷新时就已经缩放
  if (dataScreenRef.value) {
    dataScreenRef.value.style.transform = `scale(${getScale()}) translate(-50%,-50%)`
    dataScreenRef.value.style.width=`1920px`;
    dataScreenRef.value.style.height=`1080px`;

    // 为浏览器绑定事件
    window.addEventListener("resize",resize)
  }
})

7.1.4 根据浏览器大小推断缩放比例

const getScale = (width = 1920, height = 1080) => {
  let ww = window.innerWidth / width;
  let wh = window.innerWidth / height;
  return ww < wh ? ww : wh;
}

7.1.5 浏览器监听resize事件

const resize=()=>{
  if(dataScreenRef.value){
   dataScreenRef.value.style.transform=`scale(${getScale()})translate(-50%,-50%)`
  }
}

7.1.6 css设计

.dataScreen_container {
    width: 100%;
    height: 100%;
    background: url('./images/bg.png') no-repeat;
    background-repeat: no-repeat;
    background-attachment: fixed;
    background-position: center;
    background-size: 100% 100%;
    background-size: cover;
 .dataScreen{
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 999;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transition: all 0.3s;
    transform-origin: left top;
    }
}

2. 常用EChart资源

EChart官网MCChartPPChartisqqw