新手入门:从零搭建vue3+webpack实战项目模板

news2024/12/22 20:04:55

搭建一个 vue3 + webpack5 + element-plus 基本模板 (vue3 + webpack5 从零配置项目)。
本项目结构可以作为实战项目的基本结构搭建学习,作为刚学习完vue还没有实战项目经验的小伙伴练习比较合适。

项目地址:

GitHub:https://github.com/with-the-winds/vue3-vuecli-template

gitee: https://gitee.com/with_the_winds/vue3-vuecli-template

前言

本项目是通过 vue-cli 创建的 vue3 项目。基本的创建思路都是基于 ruoyi框架 的项目。其实vue-admin-template 和 ruoyi 框架的都是差不多的,因为这两个项目是只有使用的文档,没有搭建的文档思路,所以我自己这写了下搭建的思路。

那为什么不直接使用该项目呢?是因为我觉得该项目的模板过于庞大(虽然不需要的可以直接删除,但是觉得有点麻烦)所以自己仿照着写一个模板(自己写的也更有利于自己理解和使用)。

环境

node: v16.16.0

npm: v8.11.0

vue3 + webpack5 + vue-router4 + vuex4

直接使用

通过 git clone 或者直接下载 zip 包的方式进行下载代码。

// gitee 地址
git clone https://gitee.com/with_the_winds/vue3-vuecli-template.git
// github 地址
git clone https://github.com/with-the-winds/vue3-vuecli-template.git

Project setup

cd vue3-vuecli-template
npm install

Compiles and hot-reloads for development

npm run serve

Compiles and minifies for production

npm run build

Customize configuration

See Configuration Reference.

搭建过程

搭建过程主要是让初学者知道模板是如何搭建的,并方便查阅信息。

一、通过 vue-cli 创建项目

vue create own-vue3-vuecli-template

安装选项:
选择安装vuex、vue-router、sass,后面默认这些是已经安装,不会再重复安装了。

二、创建环境变量

在根目录(own-vue3-vuecli-template文件下)创建 .env.development 和 .env.production 两个文件,用于存放开发环境和生产环境的环境变量值。

.env.development:

# 开发环境配置
NODE_ENV = 'development'

# 页面标题
VUE_APP_TITLE = '页面标题'

# 开发环境/重写路径(公共路径)
VUE_APP_BASE_URL = '/dev-api'

# 百度地图KEY
VUE_APP_BAIDU_MAP_KEY = ''

.env.production:

# 生产环境配置
NODE_ENV = 'production'

# 页面标题
VUE_APP_TITLE = '页面标题'

# 生产环境/重写路径(公共路径)
VUE_APP_BASE_URL = '/prod-api'

# 百度地图KEY
VUE_APP_BAIDU_MAP_KEY = ''

后面自己要调整的地方自己修改。

三、安装 element-plus

也可以使用别的组件库,这里使用 element-plus。

npm install element-plus --save

安装完成后这边不进行全局引用,使用按需引入的自动导入方式,这时候需要安装 unplugin-vue-componentsunplugin-auto-import这两款插件。

npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到Webpack 的配置文件中:
vue.config.js:

const { defineConfig } = require('@vue/cli-service')

const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = defineConfig({
  transpileDependencies: true,
  // 和webpapck属性完全一致,最后会进行合并
  configureWebpack:{
    //配置webpack自动按需引入element-plus
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()],
      }),
      Components({
        resolvers: [ElementPlusResolver()],
      }),
    ],
  }
})

示例:
HomeView.vue:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
    <el-button type="success">按钮</el-button>
  </div>
</template>

image.png
可以看到在没有引入 element-plus ,已经可以正常使用了。

四、安装 axios

1.安装 axios 实现网络请求

npm install axios --save

2.封装 axios

在 src 目录下创建一个 utils文件夹,用于存放常用的 js 工具文件。并在 utils 文件夹下新建一个 request.js 文件。

2.1 先创建 axios 的基本结构

request.js:

import axios from 'axios'

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
  // 请求之前要处理的地方
  
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
  // 响应成功返回要处理的地方

  return res
  },
  error => {
    // 响应失败要处理的地方
    console.log('err' + error)
    let { message } = error;

    return Promise.reject(error)
  }
)

export default service

2.2 安装和封装 js-cookie

登录的时候一般都是需要 token 验证的,所以需要使用 cookie 存放 token,当然也可以存放在 localStorage 或 sessionStorage 中(一般还是在cookie中)。
安装:

npm install js-cookie --save

封装:
在 utils 文件夹下创建一个 auth.js 文件,并复制如下代码:
auth.js:

import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

2.3 完善 axios 的结构

对于 request.js 进行完善,使封装的 axios 功能更加丰富。实现的功能:

1.请求时设置 token 的可选项,因为有的接口不一定要用 token;

2.请求报错;

3.响应成功返回和响应报错(响应返回的code根据实际项目和后端对接)。

在 utils 文件夹下创建 errorCode.js 文件,用于存放响应返回错误 code 的消息值(根据实际项目)。
errorCode.js:

export default {
  '401': '认证失败,无法访问系统资源',
  '403': '当前操作没有权限',
  '404': '访问资源不存在',
  'default': '系统未知错误,请反馈给管理员'
}

完善 request.js 文件。
request.js:

import axios from 'axios'
import { getToken } from './auth'
import errorCode from './errorCode'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';

// 是否显示重新登录
export let isRelogin = { show: false };

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 3000
})

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token (默认是存在的,只有传值 false 的时候是不需要token)
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer' + getToken() // 让每个请求携带 token 根据实际情况修改
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(
  res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if (res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer') {
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          isRelogin.show = false;
          // 调用退出登录接口...
        }).catch(() => {
          isRelogin.show = false
        })
      }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({ message: msg, type: 'error' })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification({ type: 'error', title: msg})
      return Promise.reject('error')
    } else {
      return res.data
    }
  },
  error => {
    // 响应失败要处理的地方
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({ message: message, type: 'error', duration: 3 * 1000 })
    return Promise.reject(error)
  }
)

export default service

可以从上面看到,在网络请求的时候如果返回401时,做了重新登录的提示,这边的退出登陆接口会通过状态管理store退出登录,因为store这边还没有实际写,所以暂时空留位置,后面会补充进去。

五、使用封装的 request.js 创建 api

上述的 axios 到现在已经封装完成,然后现在应该创建一个接口,看如何使用封装的 axios。

在 src 文件夹下创建一个 api 文件夹,然后在 api 文件夹下创建一个 login.js 文件,用于存放登录等相关接口。

login.js:

import request from '../utils/request.js'

// 登录
export function login(data) {
  return request({
    url: '/login',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}

// 获取用户详细信息
export function getInfo() {
  return request({
    url: '/getInfo',
    method: 'get'
  })
}

// 退出登录
export function logout() {
  return request({
    url: '/logout',
    method: 'post'
  })
}

这样就可以在别的页面上进行使用接口了。

六、配置vue.config.js

对于创建的接口如果运行在开发环境中,那么需要设置下代理服务器 devServer,生产环境一般用nginx配置就可以了,所以在 vue.config.js 文件下配置下代理服务器,顺便再配置下别的选项。

对于vue中webpack的默认配置和相关配置信息可以阅读下这篇文章 https://juejin.cn/post/6886698055685373965#heading-3 和 官方文档一起配合食用更佳!!!

安装下 compression-webpack-plugin 插件用于 gzip 压缩打包。

npm install compression-webpack-plugin --save-dev

完整配置如下(包含了gzip插件的配置和代理服务器的配置):

vue.config.js:

'use strict'
const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

const { defineConfig } = require('@vue/cli-service')

const name = process.env.VUE_APP_TITLE || '网页标题' // 网页标题

const port = process.env.port || process.env.npm_config_port || 8080 // 端口

// element-plus 按需导入自动导入的插件
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
// 实现 gzip 压缩打包
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = defineConfig({
  // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
  // 例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径
  // 例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置 publicPath 为 /my-app/
  publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
  // 当运行 vue-cli-service build 时生成的生产环境构建文件的目录。注意目标目录的内容在构建之前会被清除 (构建时传入 --no-clean 可关闭该行为)。
  outputDir: 'dist',
  // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录
  assetsDir: 'static',
  // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
  productionSourceMap: false,
  // 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。你可以启用本选项,以避免构建后的代码中出现未转译的第三方依赖(对所有的依赖都进行转译可能会降低构建速度)
  transpileDependencies: false,
  // webpack-dev-server 相关配置
  devServer: {
    host: '0.0.0.0',
    port: port,
    open: true,
    proxy: {
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:8088`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    }
  },
  // 和webpapck属性完全一致,最后会进行合并
  configureWebpack:{
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    },
    //配置webpack自动按需引入element-plus
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()],
      }),
      Components({
        resolvers: [ElementPlusResolver()],
      }),
      // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
      new CompressionPlugin({
        test: /\.(js|css|html)?$/i,     // 压缩文件格式
        filename: '[path][base].gz',   // 压缩后的文件名
        algorithm: 'gzip',              // 使用gzip压缩
        threshold: 10240,               // 最小文件开启压缩
        minRatio: 0.8                   // 压缩率小于1才会压缩
      })
    ],
  }
})

七、搭建 store

对状态管理进行模块化,一般像 vue-admin-template 、ruoyi-vue 这些模板都会添加移动端设备的判断但是我不太会所以这边就不添加了。

创建 modules 文件夹

在 store 文件夹下创建 modules 文件夹用于存放模块。然后在 modules 文件夹下再创建一个 user.js 文件,用于存放用户的相关信息。

实现 user.js 模块

该文件实现的思路:

1.存放用户登录后的相关信息(一般来说存放token、角色信息、按钮的权限信息,具体还是根据实际项目来);

2.退出登录时清空用户的相关信息。

user.js:

import { login, logout, getInfo } from "@/api/login";
import { getToken, setToken, removeToken } from "@/utils/auth";

const user = {
  state: {
    token: getToken(),
    name: '',
    roles: [],
    permissions: []
  },
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_NAME: (state, name) => {
      state.name = name
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles
    },
    SET_PERMISSIONS: (state, permissions) => {
      state.permissions = permissions
    }
  },
  actions: {
    // 登录
    Login({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        login(userInfo).then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          const user = res.user
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_NAME', user.userName)
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 退出系统
    LogOut({ commit, state }) {
      return new Promise((resolve, reject) => {
        logout().then(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSIONS', [])
          removeToken()
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    }
  }
}

export default user

引入 user.js 模块

在 store 文件夹下的 index.js 文件中引入 user.js 文件。

index.js:

import { createStore } from 'vuex'
import user from './modules/user'

export default createStore({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    user
  }
})

处理 request.js 遗留的退出登录的功能

在 request.js 文件中,之前有个 401 状态码调用退出登录接口的功能没有实现,现在补充完整。

引入 store 文件:

import store from '@/store';

添加退出登录接口:

if (!isRelogin.show) {
  isRelogin.show = true;
  ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录','系统提示', {
    confirmButtonText: '重新登录',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    isRelogin.show = false;
    store.dispatch('LogOut').then(() => {
      location.href = '/index';
    })
  }).catch(() => {
    isRelogin.show = false
  })
}

八、添加 permission.js

在 src 文件夹下创建 permission.js 文件:

该文件的作用主要是通过路由导航守卫,实现路由的控制:

1.当 token 消失后不能跳转别的页面,要返回登录页面;

2.点击登录之后不能通过回退按钮返回到登录页面(这个不是必须的看具体实现,另外三条是必须必须的);

3.点击退出登录后会自动跳转到登录页面,同样不能越过登录通过回退按钮进入到其他页面;

4.由于页面刷新会重置store,所以要实现vuex的持久化(当然对于vuex的持久化是有插件的,我这边是没有使用)。

安装插件nprogress用于控制进度条的显示:

npm i nprogress -S

permission.js:

import NProgress from "nprogress";
import 'nprogress/nprogress.css'
import router from "./router";
import store from "./store";
import { ElMessage } from "element-plus";
import { getToken } from "./utils/auth";
import { isRelogin } from "./utils/request";

NProgress.configure({ showSpinner: false }) // 关闭加载微调器

const whiteList = ['/login'] // 设置白名单,用于任何人可访问

// 钩子函数
router.beforeEach((to, from, next) => {
  NProgress.start()
  if (getToken()) {
    // to.meta.title && store.dispatch('settings/setTitle', to.meta.title) // 用于设置title
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        isRelogin.show = true
        // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(() => {
          isRelogin.show = false
          // 用于生成路由
          // store.dispatch('GenerateRoutes').then(accessRoutes => {
          //   // 根据roles权限生成可访问的路由表
          //   router.addRoutes(accessRoutes) // 动态添加可访问路由表
          //   next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
          // })
        }).catch(err => {
            store.dispatch('LogOut').then(() => {
              ElMessage({ type: 'error', message: err })
              next({ path: '/' })
            })
          })
      } else {
        next()
      }
    }
  } else {
    // 没有token
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单,直接进入
      next()
    } else {
      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

目前页面重定向的页面都是登录页面,所以现在修改路由配置。

注意:这里我注释了两个地方,一个是生成路由的部分,还有一个是设置页面 title 的部分。对于生成路由的部分就是用来动态生成路由,而不是把页面的路由都静态放在 router.js 页面中,由后端接口传递过来(这部分请根据实际项目接口写)。

创建完成之后引入到 main.js 文件,用于实现控制页面的权限。

main.js:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import './permission' // permission control

createApp(App).use(store).use(router).mount('#app')

九、搭建路由页面

页面分为公共页面和动态加载的页面,公共页面一般包含登录页面和404页面。

创建登录页面

把原本的 AboutView.vue 文件 和 HomeView.vue 文件删除。注意把 APP.vue 文件中的链接去掉,只留下。然后在 views 文件夹下面创建一个 login 文件夹,在 login 文件夹下面创建一个 index.vue 文件。

login/index.vue:

<template>
  <div class="login">
    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" size="small" class="login-form">
      <title>登录表单</title>
      <el-form-item prop="username" label="账号:">
        <el-input 
          v-model="loginForm.username" 
          type="text" 
          placeholder="账号">
        </el-input>
      </el-form-item>
      <el-form-item prop="password" label="密码:">
        <el-input 
          v-model="loginForm.password" 
          type="password" 
          placeholder="密码" 
          @keyup.enter="handleLogin">
        </el-input>
      </el-form-item>
      <el-form-item style="width: 100%;">
        <el-button 
          :loading="loading" 
          type="primary" 
          @click.prevent="handleLogin"
        >
          <span v-if="!loading">登录</span>
          <span v-else>登 录 中 ...</span>
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import store from '@/store'

export default {
  data() {
    return {
      loginForm: {
        username: '',
        password: ''
      },
      loginRules: {
        username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
        password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }]
      },
      loading: false
    }
  },
  methods: {
    handleLogin() {
      this.$refs.loginRef.validate(valid => {
        if (valid) {
          this.loading = true
          store.dispatch('user/Login', this.loginForm).then(() => {
            this.$router.push({ path: '/home' })
          })
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

这里没有写样式,只是简单搭建下页面,让功能能运行就行。

创建 404 页面

在 view 文件夹下创建 error 文件夹,在error 文件夹下创建 404.vue 文件。
这里有用到两个404的图片,分别在 assets/images/base 文件夹下面。

error/404.vue:

<template>
  <div class="wscn-http404-container">
    <div class="wscn-http404">
      <div class="pic-404">
        <img class="pic-404__parent" src="@/assets/images/base/404.png" alt="404">
        <img class="pic-404__child left" src="@/assets/images/base/404_cloud.png" alt="404">
        <img class="pic-404__child mid" src="@/assets/images/base/404_cloud.png" alt="404">
        <img class="pic-404__child right" src="@/assets/images/base/404_cloud.png" alt="404">
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">
          404错误!
        </div>
        <div class="bullshit__headline">
          {{ message }}
        </div>
        <div class="bullshit__info">
          对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
        </div>
        <router-link to="/" class="bullshit__return-home">
          返回首页
        </router-link>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Page404',
  computed: {
    message() {
      return '找不到网页!'
    }
  }
}
</script>

<style lang="scss" scoped>
.wscn-http404-container{
  transform: translate(-50%,-50%);
  position: absolute;
  top: 40%;
  left: 50%;
}
.wscn-http404 {
  position: relative;
  width: 1200px;
  padding: 0 50px;
  overflow: hidden;
  .pic-404 {
    position: relative;
    float: left;
    width: 600px;
    overflow: hidden;
    &__parent {
      width: 100%;
    }
    &__child {
      position: absolute;
      &.left {
        width: 80px;
        top: 17px;
        left: 220px;
        opacity: 0;
        animation-name: cloudLeft;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      &.mid {
        width: 46px;
        top: 10px;
        left: 420px;
        opacity: 0;
        animation-name: cloudMid;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1.2s;
      }
      &.right {
        width: 62px;
        top: 100px;
        left: 500px;
        opacity: 0;
        animation-name: cloudRight;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      @keyframes cloudLeft {
        0% {
          top: 17px;
          left: 220px;
          opacity: 0;
        }
        20% {
          top: 33px;
          left: 188px;
          opacity: 1;
        }
        80% {
          top: 81px;
          left: 92px;
          opacity: 1;
        }
        100% {
          top: 97px;
          left: 60px;
          opacity: 0;
        }
      }
      @keyframes cloudMid {
        0% {
          top: 10px;
          left: 420px;
          opacity: 0;
        }
        20% {
          top: 40px;
          left: 360px;
          opacity: 1;
        }
        70% {
          top: 130px;
          left: 180px;
          opacity: 1;
        }
        100% {
          top: 160px;
          left: 120px;
          opacity: 0;
        }
      }
      @keyframes cloudRight {
        0% {
          top: 100px;
          left: 500px;
          opacity: 0;
        }
        20% {
          top: 120px;
          left: 460px;
          opacity: 1;
        }
        80% {
          top: 180px;
          left: 340px;
          opacity: 1;
        }
        100% {
          top: 200px;
          left: 300px;
          opacity: 0;
        }
      }
    }
  }
  .bullshit {
    position: relative;
    float: left;
    width: 300px;
    padding: 30px 0;
    overflow: hidden;
    &__oops {
      font-size: 32px;
      font-weight: bold;
      line-height: 40px;
      color: #1482f0;
      opacity: 0;
      margin-bottom: 20px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-fill-mode: forwards;
    }
    &__headline {
      font-size: 20px;
      line-height: 24px;
      color: #222;
      font-weight: bold;
      opacity: 0;
      margin-bottom: 10px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.1s;
      animation-fill-mode: forwards;
    }
    &__info {
      font-size: 13px;
      line-height: 21px;
      color: grey;
      opacity: 0;
      margin-bottom: 30px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.2s;
      animation-fill-mode: forwards;
    }
    &__return-home {
      display: block;
      float: left;
      width: 110px;
      height: 36px;
      background: #1482f0;
      border-radius: 100px;
      text-align: center;
      color: #ffffff;
      opacity: 0;
      font-size: 14px;
      line-height: 36px;
      cursor: pointer;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.3s;
      animation-fill-mode: forwards;
    }
    @keyframes slideUp {
      0% {
        transform: translateY(60px);
        opacity: 0;
      }
      100% {
        transform: translateY(0);
        opacity: 1;
      }
    }
  }
}
</style>

重写 router 文件

router/index.vue:

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/login/index.vue')
  },
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/404',
    component: () => import('../views/error/404.vue')
  },
  {
    path: '/:catchAll(.*)',
    redirect: '/404'
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

注意:对于 404 页面一定要放在最后面,这里由于是静态路由所以我直接放在这边,正确的方式应该是通过动态路由添加路由完成之后,最后再添加 path: ‘/:catchAll(.*)’ 这个路由页面。这是因为 vue 匹配路由的规则是一个个遍历过去,如果把这个 path 放在中间,那么后面的路由页面就不会被匹配到了,会直接去到404页面。

注意:这个时候记得在 permission.js 文件的白名单中添加 ‘/404’ 路径,不然会被重定向(要明白peimission.js的作用)。

permission.js:

const whiteList = ['/login','/404'] // 设置白名单,用于任何人可访问

十、添加 Layout 页面

这个步骤请根据实际项目来写,因为我这里后台项目写的多,所以基本登录页面进入后就是左边导航栏或者上面导航栏,然后内容显示在导航栏的右边或下面。
在 src 文件夹下创建 layout 文件夹,然后在 layout 文件夹下创建一个index.vue文件。这个文件就作为登录成功后的入口页面。

layout/index.vue:

<template>
  <div class="app-wrapper">
    <div class="sidebar"></div>
    <div class="app-main">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {

    }
  }
}
</script>

<style lang="scss" scoped>
.app-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
}
.sidebar {
  width: 200px;
  height: 100%;
  background-color: red;
  position: absolute;
  top: 0;
  left: 0;
}
.app-main {
  width: calc(100% - 200px);
  height: 100%;
  background-color: rgb(68, 150, 222);
  position: absolute;
  top: 0;
  left: 200px;
}
</style>

这里分为了左右布局,简单的用红色表示侧边栏,蓝色表示内容区域。现在基本结构就这样搭建好了,如果需要添加别的功能就可以不断添加,比如侧边栏的组件封装。

现在在 views 文件夹下创建一个 home文件夹,然后在 home 文件夹下创建一个 index.vue 文件。该文件用来测试刚刚创建的侧边栏和内容区域是否可行。

home/index.vue:

<template>
  <div>
    <h1>这是 Home 页面</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      
    }
  }
}
</script>

注意:修改下APP.vue 的样式,不然当前html没有高度:

APP.vue:

<style lang="scss">
html {
  width: 100%;
  height: 100%;
}
body {
  margin: 0;
  width: 100%;
  height: 100%;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  width: 100%;
  height: 100%;
}
</style>

添加路由:
在 router 文件夹下的index.js文件配置刚刚新增的home页面:

router/index.js:

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/login/index.vue')
  },
  {
    path: '/',
    redirect: '/login'
  },
  {
    path: '/layout',
    name: 'Layout',
    component: () => import('../layout/index.vue'),
    redirect: '/home',
    children: [{
      path: '/home',
      name: 'Home',
      component: () => import('../views/home/index.vue')
    }]
  },
  {
    path: '/404',
    component: () => import('../views/error/404.vue')
  },
  {
    path: '/:catchAll(.*)',
    redirect: '/404'
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

注意:不要忘记在 permission.js 文件中添加白名单,不然权限无法访问新增的页面,因为现在没有实际的登录接口,所以一直会被重定向。

permission.js:

const whiteList = ['/login','/404','/layout', '/home'] // 设置白名单,用于任何人可访问

效果:
image.png
这样基本的项目结构就全部完成了,实际项目开发的时候记得把白名单的页面设置成自己想要的页面。

上面搭建的步骤中动态路由获取的内容没有添加,一般就是放在获取用户信息接口后面,然后通过 addRoute() 方法添加到路由上面。

结尾

项目到这里基本就已经结束了,后面应该会根据自己的实际项目,添加一些功能,比如权限获取、侧边栏的封装、防重复请求等等,但是应该是不会更新在文档上了(有机会的话会写在后面的补充上面),所以搭建文档和实际拉取下来的项目可能有点出入。

补充

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

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

相关文章

如何将手写笔记转换成电子版格式?

记笔记是一种非常有效的学习方法。它不仅可以帮助我们加深对所学内容的理解&#xff0c;还能让我们收集更多有用的信息&#xff0c;以方便后续的查看和复习。不过&#xff0c;用传统的纸质笔记本记录笔记存在一定的弊端&#xff0c;比如说不易保存、不易携带等等。所以&#xf…

Mac下的java.io.FileNotFoundException: ~/Desktop/a.sql (No such file or directory)

【问题】&#xff1a; 今天在运行一个文件读取的Demo时&#xff0c;报如下错误: java.io.FileNotFoundException: ~/Desktop/a.sql (No such file or directory)如下图所示 &#xff1a; 可是这个文件命名可以通过终端窗口访问到啊&#xff1f; 【解决方案】&#xff…

STM32外设系列—HC-SR04(超声波)

文章目录 一、超声波测距基本原理二、超声波传感器简介三、HC-SR04测距实现思路四、超声波测距程序实现4.1 HC-SR04初始化程序4.3 TIM开关程序4.4 获取定时时间4.5 计算测量距离4.6 宏定义 五、应用实例六、拓展应用 一、超声波测距基本原理 超声波测距的原理非常简单&#xf…

高压放大器在压电陶瓷驱动器中的应用

高压放大器是一种将低电压信号放大成高电压信号的电子设备。它广泛运用于各种领域&#xff0c;如医疗、工业、军事以及科学研究。压电陶瓷驱动器是一种利用压电效应来驱动机械运动的装置。这两种设备经常被用于控制和操作许多不同类型的系统。 压电陶瓷是一种能够将电能转化为机…

监控摄像头的像素200万,400万,800万都是什么意思,200万像素、400万像素、800万像素是如何换算出来的?

一、像素 像素&#xff08;Pixel&#xff09;是用来表示图像分辨率的单位&#xff0c;数字越大&#xff0c;表示图像中的细节可以更精细地展现。当我们谈论监控摄像头的像素时&#xff0c;通常指的是摄像头图像传感器上的像素数量。像素的数量可以通过传感器上的横向像素数乘以…

win如何使用OpenSSL生成自签名证书,使 http 升级为 https

win如何使用OpenSSL生成自签名证书&#xff0c;使 http 升级为 https 前言 HTTPS其实就是HTTP over SSL&#xff0c;也就是让HTTP连接建立在SSL安全连接之上。 创建自签名证书需要安装openssl。参考本文安装OpenSSL部分。 使用OpenSSL生成自签名证书的步骤&#xff1a;参考…

python spider 爬虫 之 Selenium 系列 (-) Selenium

京东的 seckill 秒杀 专区 用 urllib 是获取不到的 回顾一下urllib 爬虫 # urllib 爬虫 from urllib import request headers {} url # 请求定制 req request(urlurl, headers headers) # 模拟请求 response request(req) content response.read().decode(utf-…

windows下安装Visual Studio + CMake+OpenCV + OpenCV contrib

目录 1 安装visual studio 2 安装CMake 3 OpenCV源码安装 3.1 OpenCV源码下载 3.2 OpenCV contrib源码下载 3.3 安装OpenCV 3.4 安装OpenCV-crontrib 3.5 VS生成代码 4 环境配置 最近在研究windows系统上部署安装目标检测算法&#xff0c;需要用到OpenCV软件&#xff…

智能指针+拷贝构造+vector容器+多态引起的bug

今天在调试一段代码的时候&#xff0c;VC编译提示&#xff1a; error C2280: “T485CommCtrlPara::T485CommCtrlPara(const T485CommCtrlPara &)”: 尝试引用已删除的函数 函数执行部分如下&#xff1a; 看意思是这个pComm485Pro已经消亡了&#xff0c;后续push_back到ve…

高速电路设计系列分享-信号链精度分析(中)

目录 概要 整体架构流程 技术名词解释 技术细节 1.直流无源误差 小结 概要 提示&#xff1a;这里可以添加技术概要 在任何设计中&#xff0c;信号链精度分析都可能是一项非常重要的任务&#xff0c;必须充分了解。之前&#xff0c; 我们讨论了在整个信号链累积起来并且最终会…

统一日志处理----AOP/面向切面编程

AOP Aspect Oriented Programing&#xff1a;面向切面编程 AOP是对OOP的补充&#xff0c;进一步提高编程的效率 AOP的常见使用场景有&#xff1a;权限检查、记录日志、事务管理等 如下图所示结构&#xff0c;每个模块都含有相同的系统需求&#xff0c;而这些需求和模块本身的功…

Flutter进阶-动画详解

目录 动画类别 一、隐式&#xff08;全自动&#xff09;动画 二、显式动画&#xff08;手动控制&#xff09; 三、其他动画(CustomPainter) 动画类别 Flutter 中有多种类型的动画&#xff1a; 隐式动画&#xff1a;通过更改部件属性自动触发的预定义动画&#xff0c;例如 …

什么是cookie

1、cookie是什么 Cookie&#xff0c;有时也用其复数形式Cookies。类型为“小型文本文件”&#xff0c;是某些网站为了辨别用户身份&#xff0c;进行Session跟踪而储存在用户本地终端上的数据&#xff08;通常经过加密&#xff09;&#xff0c;由用户客户端计算机暂时或永久保存…

Python强类型编程

Python是一门强类型的动态类型语言&#xff0c;具体如下特性&#xff1a; 可以动态构造脚本执行、修改函数、对象类型结构、变量类型但不允许类型不匹配的操作 第一个例子体现动态性&#xff1a;用字符串直接执行代码&#xff0c;动态构建了一个函数并执行&#xff0c;甚至给…

力扣744.寻找比目标字母大的最小字符(java暴力查找法,二分查找法)

题目描述&#xff1a; 给你一个字符数组 letters&#xff0c;该数组按非递减顺序排序&#xff0c;以及一个字符 target。letters 里至少有两个不同的字符。 返回 letters 中大于 target 的最小的字符。如果不存在这样的字符&#xff0c;则返回 letters 的第一个字符。 [外链…

岭回归(Ridge)不同alpha值对归回结果的影响

对于有些矩阵&#xff0c;矩阵中某个元素的一个很小的变动&#xff0c;会引起最后计算结果误差很大&#xff0c;这种矩阵称为“病态矩阵”。有些时候不正确的计算方法也会使一个正常的矩阵在运算中表现出病态。对于高斯消去法来说&#xff0c;如果主元&#xff08;即对角线上的…

亚马逊测评:如何有效使用IP和养号设备环境

随着网络科技的崛起&#xff0c;越来越多的本土企业入驻亚马逊电子商务平台上&#xff0c;这导致了对产品评价需求的激增。然而&#xff0c;评价并非随意进行&#xff0c;它需要多方面的资源&#xff0c;并需要密切注意一些重要环节。以下是我分享给大家一些宝贵的知识&#xf…

如何实现敏捷交付中的自动化测试优化

在提及自动化测试的时候&#xff0c;很多人会把工具的使用等同于自动化测试。自动化测试应该是一个策略性的系统化工程&#xff0c;不只有自动化工具。自动化测试要发挥其频繁快速的质量反馈作用&#xff0c;还需要团队从文化和技术上去建设和学习。 提到敏捷交付&#xff0c;…

数据库监控与调优【十二】—— JOIN语句优化

JOIN语句优化-JOIN种类、算法与原理 JOIN的种类 笛卡尔连接&#xff08;cross join&#xff09; -- 举例&#xff1a;通过笛卡尔连接查询两张表的结果集和单查两张表的结果集对比 SELECT count( * ) FROM users a CROSS JOIN orders b; SELECT ( SELECT count( * ) FROM user…

SpringBoot + Vue前后端分离项目实战 || 四:用户管理功能实现

系列文章&#xff1a; SpringBoot Vue前后端分离项目实战 || 一&#xff1a;Vue前端设计 SpringBoot Vue前后端分离项目实战 || 二&#xff1a;Spring Boot后端与数据库连接 SpringBoot Vue前后端分离项目实战 || 三&#xff1a;Spring Boot后端与Vue前端连接 文章目录 前端…