挑战一周完成Vue3项目Day2:路由配置+登录模块+layout组件+路由鉴权

news2024/9/20 20:18:26

一、路由配置

经过分析,项目一共需要4个一级路由:登录(login)、主页(home)、404、任意路由(重定向到404)。

1、安装路由插件

pnpm install vue-router

2、创建路由组件 

在src目录下新建views文件夹,在views中创建login、home、404路由组件。这几个路由组件里面也要新建index.vue,以便于测试。

3、配置路由

在src目录下新建router文件夹,书写路由配置(包含index.ts和routes.ts,注意一定是ts文件!

(1)src/router/routes.ts

// 对外暴露配置路由(常量路由)
export const constantRoute = [
    {
        // 登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login'
    },
    {
        // 登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/views/home/index.vue'),
        name: 'layout'
    },
    {
        // 404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404'
    },
    {
        // 任意路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any'
    }
]

(2)src/router/index.ts

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

4 、引入路由

在main.ts引入

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

5、测试

在App.vue中

<router-view></router-view>

运行项目:

在这里输入不同的路径,home/login/404 ,能够切换即为成功 

二、登录模块

1、登录路由静态的搭建 

采用element-plus中的Layout布局(栅格布局)、From表单组件、input组件、button组件。

Layout布局:一共是24 分栏,:span代表栅格占据的列数,:xs代表屏幕宽度<768px时栅格占据的列数。

input组件::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_from">
                    <h1>Hello</h1>
                    <h2>欢迎来到唧唧bong甄选</h2>
                    <el-form-item>
                        <el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button class="login_btn" type="primary" size="default">登录</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 loginFrom = 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_from{
        width: 80%;
        position: relative;
        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 0;
        }
 
        .login_btn{
            width: 100%;
        }
    }
}
</style>

 2、模板封装登录业务

我们需要做的:点击登录时,会携带用户名和密码向服务器发请求获取token,此时我们需要把token存储起来,用于后续向服务端发请求获取信息的身份验证,这里我们用pinia和loacalStroage进行存储。并且实现弹窗提示信息,以及加载清除效果。

(1)安装pinia

pnpm i pinia

(2)创建大仓库:src/store/index.ts

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

(3)在入口文件(main.ts)中引入并安装pinia:src/main.ts

// 引入大仓库
import pinia from './store'
// 安装仓库
app.use(pinia)

(4)创建小仓库:src/store/modules/user.ts

// 创建用户相关的小仓库
import { defineStore } from 'pinia'
// 接口
import {reqLogin} from '@/api/user'
// 引入数据类型
import type {loginForm} from '@/api/user/type'
//创建用户小仓库
let useUserStore = defineStore('User', {
  //小仓库存储数据地
  state: () => {
    return {
        token:localStorage.getItem("TOKEN"),//存储用户唯一标识,本地存储持久化token
    }
  },
  //异步|逻辑的地方
  actions: {
    // 用户登录的方法
    async userLogin(data:loginForm){
        // 登录请求
        let result:any = await reqLogin(data);
        console.log(result);
        //登录请求:成功200->token
        //登录请求:失败201->登录失败错误的信息
        if(result.code == 200){
            //由于pinia|vuex存储数据其实利用js对象
            //pinia仓库存储一下token
            this.token = result.data.token;
            //本地存储持久化存储一份
            localStorage.setItem("TOKEN",result.data.token);
            // 能保证当前asnyc函数返回一个成功的promise
            return 'ok';
        }else{
            return Promise.reject(new Error (result.data.message))
        }
    }
  },
  getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore

(5)在登录页面中引入小仓库,点击登录时通知user小仓库发请求,存储token: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_from">
                    <h1>Hello</h1>
                    <h2>欢迎来到椰果甄选</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" class="login_btn" type="primary" size="default" @click="login">登录</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,ref } from 'vue';
// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user'
import {useRouter} from 'vue-router';
import {ElNotification} from 'element-plus'
let useStore = useUserStore()
let $router = useRouter()
let loading = ref(false)
// 收集账号与密码的数据
let loginForm = reactive({
    username: 'admin',
    password: '111111'
})
// 登录按钮回调
const login = async()=>{
    // 加载效果:开始加载
    loading.value=true
    // 成功的话到首页,失败的话弹出失败信息
    try {
        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
        })
    }
}
</script>

(6)注意点

  • userLogin会返回一个Promise,此处可以使用try...catch...或.then来进行下一步结果处理。
  • 不管成功或失败,都需要使登录加载效果消失,因此也可以统一写在finally里面:
  • try {
            // 保证登录成功
            await useStore.userLogin(loginFrom)
            // 编程式导航跳转到展示数据首页
            $router.push('/')
            // 登录成功信息提示
            ElNotification({
                type: 'success',
                message: '登录成功',
            }) 
        } catch (error) {
            // 登录失败的提示信息
            ElNotification({
                type: 'error',
                message: (error as Error).message
            })
        } finally{
            // 登录成功/失败加载效果消失
            loading.value = false
        }

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

我们要做的,封装用户仓库数据ts类型的定义 ,因为之前在user.ts里面使用了很多未封装未声明类型的ts数据,写的也很长,所以需要封装一下。

(1)定义小仓库数据state类型:src\store\modules\types\type.ts

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

(2)登录接口返回的数据类型:src\api\user\type.ts 

登录请求可能返回成功/失败的数据,因此类型需要dataType需要包括成功的数据token和失败的数据message,且是可选的,要加上"?"。

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

(3)封装本地存储数据和读取方法:src/utils/token.ts

// 存储数据
export const SET_TOKEN = (token: string) => {
    localStorage.setItem('TOKEN', token)
}
 
// 本地存储获取数据
export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN')
}

 (4)store/modules/user.ts

// 创建用户相关的小仓库
import { defineStore } from 'pinia'
// 接口
import { reqLogin } from '@/api/user'
// 引入数据类型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './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 result: loginResponseData = await reqLogin(data)
      console.log(result)
      //登录请求:成功200->token
      //登录请求:失败201->登录失败错误的信息
      if (result.code == 200) {
        //由于pinia|vuex存储数据其实利用js对象
        //pinia仓库存储一下token
        this.token = (result.data.token as string)
        //本地存储持久化存储一份
        // localStorage.setItem('TOKEN', result.data.token as string)
        SET_TOKEN((result.data.token as string))
        // 能保证当前asnyc函数返回一个成功的promise
        return 'ok'
      } else {
        return Promise.reject(new Error(result.data.message))
      }
    },
  },
  getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore

 4、登录时间的判断与封装

实现这样一个效果,登录成功的话,显示早上中午晚上下午好。 

(1)在utils中封装一个函数:src/utils/time.js

// 封装一个函数:获取一个结果:当前早上|上午|中午|下午|晚上
export const getTime = () => {
    let time = ''
    // 通过内置的构造函数Date
    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
}

 (2)在login组件中引入并使用

// 引入当前时间的函数
import { getTime } from '@/utils/time'
......
 
// 登录成功信息提示
ElNotification({
     type: 'success',
     message: '欢迎回来',
     title: `HI,${getTime()}好`
})
 

 5、登录模块表单校验

使用element-plus的表单验证功能 ,步骤效果如下:

  1. 给el-form添加 :model="loginFrom"和:rules="rules"
  2. 给需要验证的每个el-form-item添加prop属性,如 prop="username"、prop="password"
  3. 定义表单校验需要配置对象rules
  4. 请求前使用 loginFroms.value.validate()触发表单中所有表单项的校验,保证全部的表单项校验通过再发请求
  • :model:要验证的表单数据对象
  • :rules="rules":表单验证规则
  • prop:要校验字段的属性名

views/login/index.vue 

// 第一步:给el-form添加 :model="loginFrom"和:rules="rules"
<el-form class="login_form" :model="loginForm" :rules="rules" ref="loginFroms">
 
// 第二步:给需要验证的每个el-form-item添加prop属性,如 prop="username"、prop="password"
<el-form-item prop="username">
    <el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
</el-form-item>
<el-form-item prop="password">
     <el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
</el-form-item>
 
// 第三步:定义表单校验需要配置对象rules
// 规则对象属性:
// required:代表这个字段必须校验
// min:文本长度至少多少位
// max:文本长度最多多少位
// message:错误的提示信息
// trigger:触发校验表单的时机,change:文本发生变化时触发校验,blur:失去焦点时触发校验
const rules = {
    username: [
        { required: true, min: 5, max: 10, message: '用户名长度应为5-10位', trigger: 'change' },
    ],
    password: [
        { required: true, min: 6, max: 10, message: '密码长度应为6-10位', trigger: 'change' },
​
    ]
}
 
第四步:请求前使用 loginFroms.value.validate()触发表单中所有表单项的校验,保证全部的表单项校验通过再发请求
// 通过ref属性获取el-form组件
let loginFroms = ref()
const login = async () => {
    // 保证全部的表单项校验通过再发请求
    await loginForms.value.validate()
    ......
}

 在 el-form 组件中,可以使用 ref 属性来获取表单的引用,然后调用该引用上的 validate 方法。这个方法会触发表单中所有表单项的校验,并返回一个 Promise 对象,该对象的 resolve 回调函数会在校验通过时被调用,而 reject 回调函数会在校验失败时被调用。

6、自定义校验表单

上面的验证比较简单,公司的开发项目中表单验证会更复杂,这个时候就要用到element-plus的自定义校验规则了 。

自定义校验表单的配置项中需要一个validator属性,值是一个方法,用于书写自定义规则。

实际开发还会用到正则表达式。

// 自定义校验规则函数
const validateUsername = (rule: any, value: any, callback: any) => {
    //rule:即为校验规则对象
    //value:即为表单元素文本内容
    //函数:如果符合条件callback放行通过即为
    //如果不符合条件callback方法,注入错误提示信息
    if(value.length >= 5){
        callback()
    }
    else{
        callback(new Error('用户名不少于5位'))
    }
}
 
const validatePassword = (rule: any, value: any, callback: any) => {
    if(value.length >= 6){
        callback()
    }
    else{
        callback(new Error('用户名不少于6位'))
    }
}
 
// 定义表单校验需要配置对象
const rules = {
    username: [
        { validator: validateUsername, trigger: 'change' },
    ],
    password: [
        { validator: validatePassword, trigger: 'change' },
    ]
}

三、layout组件 

1、 layout组件的静态搭建 

layout组件主页分为三部分:左侧菜单、顶部导航、内容展示区域。

(1)在src目录下创建layout组件:src/layout/index.vue

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

记得把router/routes.ts改为

// 登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout'

 (2)配置layout相关的样式的全局变量:src/styles/variable.scss

// 左侧菜单的宽度
$base-menu-width: 260px;
// 左侧菜单的背景颜色
$base-menu-background: #001529;
// 顶部导航的高度
$base-tabbar-height: 50px;
// 顶部导航的背景颜色
$base-tabbar-background: #ffffff;
// 内容展示区域的背景颜色
$base-main-background: #ccc8cc;

(3)设置滚动条样式:src/styles/index.scss

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

2、Logo组件的封装

(1)创建logo组件: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">
//引入设置标题与logo这配置文件
import setting from '@/setting'
</script>
 
<style scoped lang="scss">
.logo {
    width: 100%;
    height: $base-menu-logo-height;
    color: white;
    display: flex;
    align-items: center;
    padding: 10px;
 
    img {
        width: 40px;
        height: 40px;
    }
 
    p {
        font-size: $base-logo-title-fontSize;
        margin-left: 10px;
    }
}
</style>

(2)配置logo相关的样式的全局变量:src/styles/variable.scss

//左侧菜单logo高度设置
$base-menu-logo-height:50px;
 
//左侧菜单logo右侧文字大小
$base-logo-title-fontSize:16px;

(3)项目logo/标题配置文件:src/setting.ts

// 用于项目logo|标题配置
export default {
    title:'椰果uu甄选运营平台', // 项目标题
    logo:'/logo.png', // 项目logo设置
    logoHidden: true // logo组件是否隐藏设置
}

 (4)\在layout/index.vue引入Logo子组件(一定记得引入,不要放错位置)

3、左侧菜单组件 

3.1 递归组件生成动态菜单

如果菜单直接写死就不能修改,后续很麻烦,这里用到了element-ui递归组件

(1)创建menu组件:src/layout/menu/index.vue,并在layout中引入并使用menu组件 

(2)添加二级路由:src/router/routes.ts

 {
        // 登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout',
        meta: {
            title: 'layout',
            hidden: true 
        },
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue'),
                name: 'home',
                meta: {
                    title: '首页',
                    hidden: false 
                }
            }
        ]
    },

(3)将路由数组存储到store中(方便组件访问路由数据):src/store/modules/user.ts

// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 创建用户小仓库
const useUserStore = defineStore('User', {
    // 小仓库存储数据的地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(), //用户唯一的标识token
            //路由配置数据
            menuRoutes: constantRoute
        }
    },
    ......
})

(4)UserState中添加路由的类型定义:src/store/modules/types/type.ts

// 引入描述路由配置信息的类型(这个类型包含了路由的路径、组件、子路由等信息)
import type { RouteRecordRaw } from 'vue-router'
// 定义小仓库数据state类型
export interface UserState {
    token: string | null,
    menuRoutes: RouteRecordRaw[]
}

(5)layout组件中引入小仓库获取路由数据,通过props传递给menu组件:src/layout/index.vue

<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider">
            <Logo></Logo>
            <!-- 展示菜单 -->
            <el-scrollbar class="scrollbar">
                <el-menu background-color="#001529" text-color="white">
                    <!-- 传递路由数据给menu组件 -->
                    <Menu :menuList="userStore.menuRoutes"></Menu>
                </el-menu>
            </el-scrollbar>
        </div>
        ......
    </div>
</template>
 
<script setup lang="ts">
// 引入菜单组件
import Menu from './menu/index.vue'
 
// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user'
let userStore = useUserStore()
</script>

<style scoped lang="scss">
.layout_slider {
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-background;
        .scrollbar{
            width:100%;
            height: calc(100vh - $base-menu-logo-height - 20px);
            color:white;
            .el-menu{
                border-right: none;
            }
        }
    }
</style>

(6)给每个路由添加meta元信息:src/router/routes.ts

meta: {
        title: '登录', // 菜单标题
        hidden: true // 代表路由标题在菜单中是否隐藏  true:隐藏  false:显示
      }

(7)书写menu组件:src/layout/menu/index.vue

1. menu组件分三种情况:

  • 没有子路由
  • 有且只有一个子路由
  • 有一个以上的子路由

2. 点击菜单item跳转路由(@click="goRoute"):

  • <el-menu-item>标签有click事件(菜单点击时的回调函数,回调参数是el-menu-item实例。
<template>
    <template v-for="(item, index) in menuList" :key="item.path">
        <!-- 没有子路由 -->
        <template v-if="!item.children">
            <el-menu-item :index="item.path" v-if="!item.meta.hidden" @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 :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @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 :menuList="item.children"></Menu>
        </el-sub-menu>
    </template>
</template>
 
<script setup lang="ts">
import { useRouter } from "vue-router";
// 获取父组件传递过来的全部路由数组
defineProps(['menuList'])
// 获取路由对象
let $router = useRouter()
// 点击菜单的回调
const goRoute = (vc: any) => {
    // 路由跳转
    $router.push(vc.index)
}
</script>
<script lang="ts">
export default {
    name: 'Menu'
}
</script>
 
<style scoped></style>

 3.2菜单图标完成

(1)将element-plus图标 注册成全局组件:src/components/index.ts

具体可参考官网:Icon 图标 | Element Plus (gitee.io)

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

 (2)菜单图标由路由配置决定,meta中添加 icon 字段:src/router/routes.ts

meta: {
            ......
            icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
        }

(3)在menu中使用element-plus图标:src/layout/menu/index.vue

<el-icon>
    <component :is="item.meta.icon"></component>
</el-icon>
// 点击菜单的回调
const goRoute = (vc: any) => {
    // 路由跳转
    $router.push(vc.index)
}

3.3项目全部路由配置

  • 首页重定向到home
  • 权限管理和商品管理的一级路由用的还是组件 layout
  • 增加权限管理商品管理路由以及子路由
  • 实现一些切换效果

(1) src/router/routes.ts 

// 对外暴露配置路由(常量路由)
export const constantRoute = [
    {
        // 登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',
        meta: {
            title: '登录', // 菜单标题
            hidden: true, // 代表路由标题在菜单中是否隐藏  true:隐藏  false:显示
            icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
        }
    },
    {
        // 登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout',
        meta: {
            title: 'layout',
            hidden: true,
            icon: 'Avatar',
        },
        redirect: '/home',
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue'),
                name: 'home',
                meta: {
                    title: '首页',
                    hidden: false,
                    icon: 'HomeFilled',
                }
            }
        ]
    },
    {
        // 404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',
        meta: {
            title: '404',
            hidden: true,
            icon: 'BrushFilled',
        }
    },
    {
        path: '/screen',
        component: () => import('@/views/screen/index.vue'),
        name: 'Screen',
        meta: {
            title: '数据大屏',
            hidden: false,
            icon: 'Platform',
        }
    },
    {
        path: '/acl',
        component: () => import('@/layout/index.vue'),
        name: 'Acl',
        meta: {
            title: '权限管理',
            icon: 'Lock',
        },
        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',
        },
        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',
                }
            },
        ]
    },
 
    {
        // 任意路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any',
        meta: {
            title: '任意路由',
            hidden: true,
            icon: 'Wallet',
        }
    }
]

(2)layout右侧展示区域封装成一个组件 main(为了实现一些动画效果):src/layout/main/main.vue

关于路由过度可参考官网:过渡动效 | Vue Router (vuejs.org)

<template>
    <!-- 路由组件出口的位置 -->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由组件的子路由 -->
            <component :is="Component" />
        </transition>
    </router-view>
</template>
 
<script setup lang="ts">
 
</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>

 (3)在layout组件中引入main:src/layout/index.vue

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

4、顶部tabbar组件

4.1 顶部tabbar组件静态搭建与拆分 

(1)左侧菜单刷新折叠问题解决:src/layout/index.vue

// el-menu中新增default-active属性
<el-menu background-color="#001529" text-color="white" :default-active="$route.path">
     <!-- 根据路由动态生成菜单 -->
     <Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
 
// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()

将layout/menu/index.vue的el-icon转移到插槽的上方

<el-icon>
                    <component :is="item.children[0].meta.icon"></component>
                </el-icon>
                <template #title>
                    <span>{{ item.children[0].meta.title }}</span>
                </template>

(2)tabbar组件封装:拆分成左侧面包屑组件(breadcrumb)和右侧设置组件(setting)

(3)面包屑组件:scr/layout/tabbar/breadcrumb/index.vue

<template>
    <!-- 顶部左侧静态 -->
    <el-icon style="margin-right: 10px;">
        <Expand />
    </el-icon>
    <!-- 左侧面包屑 -->
    <el-breadcrumb separator-icon="ArrowRight">
        <el-breadcrumb-item>权限管理</el-breadcrumb-item>
        <el-breadcrumb-item>用户管理</el-breadcrumb-item>
    </el-breadcrumb>
</template>
 
<script setup lang="ts"></script>
 
<style scoped></style>

(3)设置组件:scr/layout/tabbar/setting/index.vue

<template>
    <el-button size="small" icon="Refresh" circle></el-button>
    <el-button size="small" icon="FullScreen" circle></el-button>
    <el-button size="small" icon="Setting" circle></el-button>
    <img src=".../public/logo.png" style="width: 20px;height: 20px;margin: 0 10px;">
    <!-- 下拉菜单 -->
    <el-dropdown>
        <span class="el-dropdown-link">
            admin
            <el-icon class="el-icon--right">
                <arrow-down />
            </el-icon>
        </span>
        <template #dropdown>
            <el-dropdown-menu>
                <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
        </template>
    </el-dropdown>
</template>
 
<script setup lang="ts"></script>
 
<style scoped></style>

(4) tabbar组件:scr/layout/tabbar/index.vue

<template>
    <div class="tabbar">
        <div class="tabbar_left">
            <Breadcrumb />
        </div>
        <div class="tabbar_right">
            <Setting />
        </div>
    </div>
</template>
 
<script setup lang="ts">
import Breadcrumb from './breadcrumb/index.vue'
import Setting from './setting/index.vue'
</script>
 
<style scoped lang="scss">
.tabbar {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: space-between;
 
    .tabbar_left {
        display: flex;
        margin-left: 20px;
        align-items: center;
    }
 
    .tabbar_right {
        display: flex;
        align-items: center;
    }
}
</style>

效果:

4.2 菜单折叠效果实现

(1) 

定义控制折叠/展开响应式数据fold:src/store/modules/setting.ts

因为layout组件和breadcrumb组件都需要用到fold,说定义在仓库比较合适

// 小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
 
const useLayoutSettingStore = defineStore('SettingStore', {
    state: () => {
        return {
            fold: false, // 用户控制菜单折叠还是收起
        }
    }
})
 
export default useLayoutSettingStore

 (2)面包屑组件折叠图标切换实现:src/layout/tabbar/breadcrumb/index.vue

<template>
    <!-- 顶部左侧静态 -->
    <el-icon style="margin-right: 10px;" @click="changeIcon">
        <component :is="layoutSettingStore.fold ? 'Expand' : 'Fold'"></component>
    </el-icon>
    ......
</template>
 
<script setup lang="ts">
import useLayoutSettingStore from "@/store/modules/setting";
// 获取layout配置相关的仓库
let layoutSettingStore = useLayoutSettingStore()
// 点击图标的方法
const changeIcon = () => {
    // 图标进行切换
    layoutSettingStore.fold = !layoutSettingStore.fold
}
</script>

(3)layout组件菜单折叠效果实现:src/layout/index.vue

步骤:

  • 通过el-menu标签的collapse属性配合fold实现菜单折叠/展开效果
  • 给左侧菜单、顶部导航、右侧内容展示区域添加动态类fold实现折叠/展开的布局改变
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider" :class="{ fold: layoutSettingStore.fold ? true : false }">
            <Logo></Logo>
            <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scrollbar">
                <!-- 菜单组件 -->
                <el-menu background-color="#001529" text-color="white" :default-active="$route.path"
                    :collapse="layoutSettingStore.fold">
                    <!-- 根据路由动态生成菜单 -->
                    <Menu :menuList="userStore.menuRoutes"></Menu>
                </el-menu>
            </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar" :class="{ fold: layoutSettingStore.fold ? true : false }">
            <Tabbar></Tabbar>
        </div>
        <!-- 内容展示区域 -->
        <div class="layout_main" :class="{ fold: layoutSettingStore.fold ? true : false }">
            <Main></Main>
        </div>
    </div>
</template>
 
<script setup lang="ts">
......
import useLayoutSettingStore from "@/store/modules/setting";
let userStore = useUserStore()
// 获取layout配置仓库
let layoutSettingStore = useLayoutSettingStore()
</script>
 
<style scoped lang="scss">
.layout_container {
    ......
        &.fold {
            width: $base-menu-min-width;
        }
    }
 
    .layout_tabbar {
        ......
        &.fold {
            width: calc(100vw - $base-menu-min-width);
            left: $base-menu-min-width;
        }
    }
 
    .layout_main {
        ......
        &.fold {
            width: calc(100vw - $base-menu-min-width);
            left: $base-menu-min-width;
        }
    }
}
</style>

 (4)记得定义variable.scss

4.3 顶部面包屑动态展示 

  • 通过$route.matched获取匹配的路由信息实现动态展示。
  • 点击首页不需要展示layout路由,所以删除router.ts文件中layout路由中的元信息title和icon的值,并通过v-show判断是否展示。
  • 通过 :to 可使点击面包屑跳转匹配路由。

(1)src/layout/tabbar/breadcrumb/index.vue 

 <!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
     <!-- 面包屑动态展示路由图标与标题 -->
      <el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
         <!-- 图标 -->
         <el-icon>
             <component :is="item.meta.icon"></component>
         </el-icon>
         <!-- 标题 -->
         <span>{{ item.meta.title }}</span>
     </el-breadcrumb-item>
</el-breadcrumb>
 
 
// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()

(2)点击商品管理、权限管理等一级路由的面包屑时,默认跳转到它的首个二级路由,因此需要在router.ts文件中给商品管理、权限管理的路由添加重定向。也要把layout路由的title和icon隐藏(删除)

redirect: '/acl/user',
redirect: '/product/trademark',

4.4刷新业务的实现

刷新业务就是路由组件销毁和重建的过程。涉及顶部导航组件和内容区域组件通信,因此可以使用store存储刷新业务相关标识。

可以利用main来监听有没有发生变化,如果监听到发生了变化说明用户点击过刷新按钮,路由组件销毁

(1) 小仓库中添加刷新标识数据:src/store/modules/setting.ts

refresh: false,// 用于控制刷新效果

(2)顶部导航setting组件实现控制下仓库refresh变化 :src/layout/tabbar/setting/index.vue

// 给刷新按钮绑定点击事件
<el-button size="small" icon="Refresh" circle @click="updateRefresh"></el-button>
 
 
// 获取仓库中刷新标识
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 刷新按钮点击回调
const updateRefresh = () => {
    // 更新刷新标识
    layoutSettingStore.refresh = !layoutSettingStore.refresh
}

(3)main组件中监听小仓库refresh是否变化,控制路由销毁与重建:src/layout/main/index.vue

 <component :is="Component" v-if="flag" />
 
 
import { watch, ref, nextTick } from 'vue'
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 控制当前组件是否销毁重建
let flag = ref(true)
// 监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(() => layoutSettingStore.refresh, () => {
    // 点击刷新按钮:路由组件销毁
    flag.value = false
    nextTick(() => {
        flag.value = true
    })
})

4.5全屏模式的切换

这里利用DOM实现全屏切换(不同浏览器可能会有兼容问题),也可以使用插件实现。 

(1)src/layout/tabbar/setting/index.vue 

// 给全屏按钮绑定点击事件
<el-button size="small" icon="FullScreen" circle @click="fullScreen"></el-button>
 
 
// 全屏按钮点击回调
const fullScreen = () => {
    // DOM对象的一个属性:可以用来判断当前是不是全屏模式(全屏:true,不是全屏:false)
    let full = document.fullscreenElement
    // 切换为全屏模式
    if (!full) {
        // 文档根节点的方法requestFullscreen,实现全屏模式
        document.documentElement.requestFullscreen()
    } else {
        // 变为不是全屏模式 -> 退出全屏模式
        document.exitFullscreen()
    }
}

4.6 获取用户信息与token理解

发生登录请求时由后端返回的唯一标识,后续向后端发送各种请求都需要携带token,因此token作为每次请求都需带的公共参数,放在请求拦截器里,通过config配置项hearders携带最合适。

(1) src/utils/request.ts

// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user';
 
request.interceptors.request.use((config) => {
    // config配置对象,包括hearders属性请求头,经常给服务端携带公共参数
    let useStore = useUserStore()
    if(useStore.token){
        config.headers.token = useStore.token
    }
    // 返回配置对象
    return config;
});

(2)home首页挂载完毕发请求获取用户信息:src/views/home/index.vue

import {onMounted} from 'vue'
// 获取仓库
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
// 目前首页挂载完毕发请求获取用户信息
onMounted(() => {
    useStore.userInfo()
})

(3)用户小仓库:src/store/modules/user.ts

在type.ts文件中定义username、avatar类型:

username: string,
avatar: string
 // 小仓库存储数据的地方
    state: (): UserState => {
        return {
            ......
            username:'',
            avatar:''
        }
    },
    // 异步|逻辑的地方
    actions: {
        ......
        // 获取用户信息
        async userInfo(){
            // 获取用户信息进行存储仓库当中(用户头像、名字)
            let result = await reqUserInfo()
            // 如果获取信息成功,存储下用户信息
            if(result.code === 200){
                this.username = result.data.checkUser.username
                this.avatar = result.data.checkUser.avatar
            }
        }
    },

(4)在setting组件中,通过user小仓库获取用户信息进行展示:src/layout/tabbar/setting/index.vue 

 ......
<img :src="userStore.avatar" style="width: 20px;height: 20px;margin: 0 10px;border-radius: 50%;">
    <!-- 下拉菜单 -->
<el-dropdown>
    <span class="el-dropdown-link">
        {{ userStore.username }}
    </span>
        ......
</el-dropdown>
 
 
// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
let userStore = useUserStore()

(我这里的图像获取不了)

4.7 退出登录业务

退出登录时,需要做的事情 :

  • 需要向服务器发请求(退出登录接口)
  • 仓库中关于用户相关的数据清空(token|username|avatar)
  • 跳转到登录页面

(1)src/layout/tabbar/setting/index.vue 

<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
 
import {useRouter,useRoute} from 'vue-router'
// 获取路由器对象
let $router = useRouter();
let $route = useRoute();
 
// 退出登录点击回调
const logout = () => {
    // 第一件事情:需要向服务器发请求(退出登录接口)----目前还没有
    // 第二件事情:仓库中关于用户相关的数据清空(token|username|avatar)
    useStore.userLogout()
    // 第三件事情:跳转到登录页面,通过query参数传递退出登录前的路径
    $router.push({ path: '/login', query: { redirect: $route.path } })
}

 (2)封装删除token本地存储的方法:src/utils/token.ts

// 本地存储删除数据方法
export const REMOVE_TOKEN = () => {
    localStorage.removeItem('TOKEN')
}

(3)封装删除token本地存储的方法:src/utils/token.ts 

// 本地存储删除数据方法
export const REMOVE_TOKEN = () => {
    localStorage.removeItem('TOKEN')
}

(4)用户小仓库:src/store/modules/user.ts

// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
 
// 退出登录
userLogout() {
   // 目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失败)
   this.token = ''
   this.username = ''
   this.avatar = ''
   REMOVE_TOKEN()
   }

(5)login组件添加登录前判断跳转路由的逻辑:src/views/login/index.vue

import { useRouter, useRoute } from 'vue-router'
// 获取路由对象
let $route = useRoute()
 
......
 try:{
// 判断登录的时候,路由的路径当中是否有query参数,如果有就往query参数跳转,没有就跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })
.......
}

4.8暗黑模式与主题切换 

四、路由鉴权和进度条业务 

路由鉴权: 项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)。

(1)安装nprogress插件:pnpm i nprogress

(2)src/permission.ts  

// 路由鉴权:项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)
import router from '@/router'
import setting from '@/setting'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
// @ts-ignore
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
nprogress.configure({ showSpinner: false })
// 获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user'
import pinia from './store'
let useStore = useUserStore(pinia)
// 全局守卫:项目中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
    // to:你将要访问哪个路由
    // from:你从哪个路由而来
    // next:路由的放行函数
    // 进度条开始
    nprogress.start()
    // 获取token,去判断用户登录,还是未登录
    let token = useStore.token
    // 获取用户名字
    let username = useStore.username
    // 用户登录判断
    if (token) {
        // 登录成功,不能访问login,指向home
        if (to.path == '/login') {
            next({ path: '/' })
        } else {
            // 登录成功访问其余六个路由(登录排除)
            // 有用户信息
            if (username) {
                // 放行
                next()
            } else {
                // 如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
                try {
                    // 获取用户信息
                    await useStore.userInfo()
                    // 放行
                    next()
                } catch (error) {
                    // token过期:获取不到用户信息了
                    // 用户手动修改本地存储token
                    // 退出登录->用户相关的数据清空
                    useStore.userLogout()
                    next({ path: '/login' })
                }
            }
        }
    } else {
        // 用户未登录判断
        if (to.path == '/login') {
            next()
        } else {
            next({ path: '/login', query: { redirect: to.path } })
        }
    }
})
// 全局后置守卫
router.afterEach((to: any, from: any) => {
    document.title = `${setting.title} - ${to.meta.title}`
    // 进度条结束
    nprogress.done()
})
 
// 第一个问题:任意路由切换实现进度条业务 ---nprogress
// 第二个问题:路由鉴权(路由组件访问权限的设置)
// 全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
 
// 用户未登录:可以访问login,其余六个路由不能访问(指向login)
// 用户登录成功:不可以访问login(指向首页)

PS:在组件的外部通过同步的语句获取仓库的数据是拿不到的。如果想获取小仓库的数据,必须先得有大仓库(pinia) 。

(3)在入口文件(main.ts)引入鉴权文件

// 引入路由鉴权文件

import './permission

今天就到此为止吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1631500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

237基于matlab的偏振态仿真

基于matlab的偏振态仿真&#xff0c;不同偏振态下光强计算。本仿真软件可以仿真波片对偏振光的相位调制过程。用户可以通过改变波片的类型&#xff0c;波片长轴与 X 轴的夹角&#xff0c;起偏器透光与 X 轴的夹角&#xff0c;检偏器透光轴与 X 轴的夹角等参数&#xff0c;来观察…

服务器部署教程下(线下、线上部署)

1、线下部署 1.1 前端 首先将拉代码下来&#xff0c;cd到想启动项目的目录下(控制台 cd 文件夹名称) 比如 blog-v3(cd blog-v3)要在存在package.json文件的目录才能进行依赖下载、项目启动操作 检查一下自己的node版本是否为18级以上(node -v) 博客前台blog-v3使用vite4开发…

【介绍下IDM的实用功能】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

五•一颂|广州流辰信息致敬每一个辛勤的劳动者,祝大家五一快乐!

时光飞逝&#xff0c;一年一度的五一国际劳动节如期而至。在这个竞争激烈的社会中&#xff0c;拥有勤劳品质的人儿总会在适当的时机迎来人生的高光时刻。或许你的人生经历非常丰富&#xff0c;或顺利&#xff0c;或坎坷&#xff0c;不管是哪种状态&#xff0c;勤劳的人应该是这…

大核注意力 LKA | Visual Attention Network

论文名称&#xff1a;《Visual Attention Network》 论文地址&#xff1a;2202.09741 (arxiv.org) 尽管最初是为自然语言处理任务而设计的&#xff0c;但自注意力机制最近在各个计算机视觉领域迅速崭露头角。然而&#xff0c;图像的二维特性给计算机视觉中的自注意力应用带来了…

Polyscope,一款简洁的三维可视化工具!

Polyscope是用于三维数据&#xff08;如meshes、point clouds&#xff09;的可视化工具&#xff0c;通过编程或动态GUI完成&#xff1b;支持C和Python编程&#xff1b;追求“一行代码”为数据提供有用的可视界面展示。 下面来简单介绍Polyscope使用。 Polyscope效果 Point Cl…

【数据结构】最小生成树(Prim算法、Kruskal算法)解析+完整代码

5.1 最小生成树 定义 对一个带权连通无向图 G ( V , E ) G(V,E) G(V,E)&#xff0c;生成树不同&#xff0c;每棵树的权&#xff08;即树中所有边上的权值之和&#xff09;也可能不同。 设R为G的所有生成树的集合&#xff0c;若T为R中边的权值之和最小的生成树&#xff0c;则T称…

OpenMM——教程学习(1)

如何从零开始做一个蛋白小分子动力学模拟 AmberTools将被用来生成输入文件&#xff0c;OpenMM 将被用来运行模拟&#xff0c;模拟平台为在线百度AI Stuio, 并使用GPU加速。 First thing’s first, 到PDB 蛋白数据库下载一需要模拟的靶点晶体&#xff0c;备用。 1. H web server…

告别人工校对烦恼,Kompas AI智能纠错一键搞定

在快节奏的工作环境中&#xff0c;撰写和校对公文是必不可少的环节。然而&#xff0c;传统的人工校对方式既耗时又容易出错&#xff0c;严重影响了工作效率和公文质量。在这里&#xff0c;我想向大家分享一款专业的校对助手——Kompas AI。它是一款采用先进的自然语言处理技术的…

分享:怎么做老阳分享的选品师项目比较赚钱

在当今的商业环境中&#xff0c;选品师项目逐渐成为了一个炙手可热的创业选择。老阳作为业内知名的选品师&#xff0c;其分享的经验和方法对于想要入行或提升业绩的选品师来说&#xff0c;无疑是宝贵的财富。那么&#xff0c;如何才能做好老阳分享的选品师项目&#xff0c;实现…

ssm088基于JAVA的汽车售票网站abo+vue

汽车售票网站的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对汽车售票信息管理混乱&#xff0c;出错率…

【Camera KMD ISP SubSystem笔记】Request 流转与Bubble机制

ISP中断类型 SOF: 一帧图像数据开始传输 EOF: 一帧图像数据传输完成 REG_UPDATE: ISP寄存器更新完成(每个reg group都有独立的这个中断) EPOCH: ISP某一行结尾(默认20)就会产生此中断 BUFFER DONE: 一帧图像数据ISP完全写到DDR了 管理Isp request的几个List st…

系统思考——与其一样,不如不同

感谢开放大学的持续邀请&#xff0c;为创新创业的学生提供赋能。昨天的主题是《创业核心&#xff1a;从内在优势到价值创造》。在近期参与的创业活动中&#xff0c;我注意到许多创业者在介绍自己的公司时常说&#xff1a;“我们是做***的&#xff0c;最大的优势是比其家便宜。”…

mapbox实现3D模型飞行

贴个群号 WebGIS学习交流群461555818&#xff0c;欢迎大家 效果图 其实这种移动的可视化效果&#xff0c;在二维和三维中实现的思路都是一样的&#xff0c;无非是规划好路径&#xff0c;用几个关键节点&#xff0c;然后插值计算中间的点用以平滑移动效果&#xff0c;实时的根…

ETL中双流合并和多流合并的区别

一、ETL工具 ETLCloud数据集成平台集实时数据集成和离线数据集成以及API发布为一体的数据集成平台。与其他开源数据集成工具相比&#xff0c;采用轻量化架构、具有更快的部署速度、更快的数据传输速度、更低的运维成本&#xff0c;同时支持多租户的团队协作能力&#xff0c;能…

IDM下载器安装cmd注册

一、下载注册 安装包去IDM官网下载最新的试用版即可 或者直达百度网盘下载&#xff08;担心被河蟹&#xff0c;放在txt中了&#xff09;包含IDM下载器安装包和注册软件 IDM下载器安装包和注册软件下载地址链接 https://download.csdn.net/download/qq_31237581/89215452 如果…

一个自卑的人怎么变得自信

一个自卑的人怎么变得自信 自卑感是一种常见的心理状态&#xff0c;它可能源于个人对自己能力、外貌、价值等方面的负面评价。自卑感不仅会影响一个人的情绪状态&#xff0c;还可能阻碍其在生活、学习和工作中的表现。然而&#xff0c;自信并非一蹴而就的品质&#xff0c;它需要…

Elsevier——投稿系统遇到bug时的解决方法

重要&#xff1a;找期刊客服&#xff01;&#xff01;&#xff01; 一、方法&#xff1a; 1. 点击进入与官方客服的对话 2. 按要求输入个人信息 3. 输入遇到的问题 比如&#xff1a; 主题&#xff1a;The Current Status is jammed. 详细描述&#xff1a;The Current State o…

Flask框架进阶-Flask流式输出和受访配置--纯净详解版

Flask流式输出&#x1f680; 在工作的项目当中遇到了一种情况&#xff0c;当前端页面需要对某个展示信息进行批量更新&#xff0c;如果直接将全部的数据算完之后&#xff0c;再返回更新&#xff0c;则会导致&#xff0c;前端点击刷新之后等待时间过长&#xff0c;开始考虑到用进…

ESP32-S3如何用socket通信

实验目的&#xff1a; 通过 Socket 编程实现 pyWiFi-ESP32-S3 与电脑服务器助手建立连接&#xff0c;相互收 发数据。 首先先来简单了解一下Socket 我们先来看看网络层级模型图&#xff0c;这是构成网络通信的基础&#xff1a; 我们看看 TCP/IP 模型的传输层和应用层&…