Vue3Project

152 阅读4分钟

参考项目架构:yiming_chang.gitee.io/vue-pure-ad…

项目搭建

  • 技术栈

    Vue3+vue-router+Vuex/pinia+elementPlus+axios+ts+sass
    
  • 使用vue脚手架(webpack)创建项目

    vue create 项目名称
    
  • 选择项目预设

    ? Please pick a preset: (Use arrow keys)
      Default ([Vue 3] babel, eslint)
      Default ([Vue 2] babel, eslint)
    > Manually select features (手动选择功能)
    
  • 项目所需的功能

    ? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
    >(*) Babel
     (*) TypeScript
     ( ) Progressive Web App (PWA) Support
     ( ) Router
     ( ) Vuex
     ( ) CSS Pre-processors
     ( ) Linter / Formatter
     ( ) Unit Testing
     ( ) E2E Testing
    
  • 选择Vue版本

    ? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
    > 3.x
      2.x
    
  • 是否使用类样式组件语法

    ? Use class-style component syntax? (y/N) `No`
    
  • 是否使用Babel和Typescript

    ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) `No`
    
  • 希望将Babel、ESLint等的配置放在哪

    # Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
    > In dedicated config files(专用配置文件中)
      In package.json (package.json中)
    
  • 保存为未来项目的预设

    Save this as a preset for future projects? (y/N) `No`
    
  • 目录结构

    node_modules: 项目依赖包
    public: 静态资源
    src: 项目核心代码
    	assets: 静态方法
        components: 公共组件
        app.vue: 根组件
        main.ts: 入口文件
        shims-vue.d.ts: 对vue组件的支持
    

安装element plus UI组件库

  • 安装

    # npm install element-plus --save
    
  • 按需导入

    • 安装两个插件

      # npm install -D unplugin-vue-components unplugin-auto-import
      
    • 新建 vue.config.ts 配置文件

      const AutoImport = require('unplugin-auto-import/webpack')
      const Components = require('unplugin-vue-components/webpack')
      const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
      module.exports = {
          // ...
          plugins: [
              AutoImport({
                  resolvers: [ElementPlusResolver()],
              }),
              Components({
                  resolvers: [ElementPlusResolver()],
              }),
          ],
      }
      
    # 发现按需导入无效
    解决方法:
    	1.使用手动导入的方式
        2.自己写一个插件,实现按需导入
    
  • 手写插件,实现按需导入

    // src > useHooks >useHooks.ts
    // 自己封装的按需导入插件
    // 本质:手动按需导入
    import { ElButton, ElInput } from "element-plus";
    let arrs = [
        { com: ElButton },
        { com: ElInput }
    ]
    // 注册
    export let installPlugs = {
        install(app: any) { // app:当前vue实例
            console.log(app);
            arrs.forEach((item) => {
                app.use(item.com) // 注册
            })
    
        }
    }
    
    // main.ts 全局挂载
    ...
    import { installPlugs } from "./useHooks/useHooks"; // 按需导入插件
    ...
    app.use(installPlugs) // 挂载按需导入插件
    ...
    

安装sass

  • 安装

    # npm install sass sass-loader
    

处理全局样式

1.高度的继承
2.盒子模型
3.项目的公共样式
4.处理icon
5.字体样式
  • src 目录下 新建一个 style 文件夹
  • 处理项目的样式,并进行模块化
  • main.ts 入口文件中进行全局引入

封装axios请求

  • 安装

    # npm install axios
    
  • 添加公共属性以及拦截器

    // src > https > index.ts
    // 封装axios请求
    import axios from "axios";
    // 创建axios实例
    let server = axios.create({
        baseURL: 'https://www.fastmock.site/mock/925c830ca442f3af92806aa5128523eb/zero', // 请求基本地址
        timeout: 6000 // 超时时间(ms)
    })
    // 添加请求拦截器
    server.interceptors.request.use(
        (config) => {
            // config:请求的配置对象
            return config;
        },
        (error) => {
            return Promise.reject(error);
        }
    );
    // 添加响应拦截器
    server.interceptors.response.use(
        (response) => {
            // response:响应成功的对象
            return response.data;
        },
        (error) => {
            return Promise.reject(error);
        }
    );
    export default server
    

添加路由

  • 安装

    # npm install vue-router
    
  • 创建路由实例,创建路由表

    // src > Route > index.ts
    // 引入路由方法
    import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
    // 创建路由表
    let routes: any = [
        {path: '/',redirect: '/login'},
        {path: '/login',component: () => import('../view/Login/index.vue')},
        {path: '/home',component: () => import('../view/Home/index.vue')}
    ]
    // 创建路由实例
    let router = createRouter({
        history: createWebHashHistory(),
        routes: routes
    })
    export default router
    
    • main 入口文件中全局挂载

添加暗黑模式

  • 新建一个模块

    <!-- 暗黑模式 主题切换 -->
    <template>
    	<div class="styleChange">
        <!-- 引入element的switch开关 -->
        <el-switch v-model="value" @change="changeStyle" inline-prompt :active-icon="Sunny" :inactive-icon="Moon" />
        </div>
    </template>
    
    <script lang='ts' setup>
        import { ref } from "vue"; // 引入vue的ref
        import { Sunny, Moon } from "@element-plus/icons-vue"; // icon图标
        import { useDark, useToggle } from "@vueuse/core"; // 定义主题的方法
        let value = ref(false) // 默认正常主题
        // 切换主题
        const changeStyle = () => {
            if (value.value) {
                // 定义主题样式
                const isDark = useDark({
                    valueDark: "dark",
                });
                const toggleDark = useToggle(isDark);
                toggleDark(); // 执行该主题样式
            } else {
                // 定义主题样式
                const isDark = useDark({
                    valueLight: "light",
                });
                const toggleDark = useToggle(isDark);
                toggleDark(); // 执行该主题样式
            }
        }
    </script>
    
  • 在单页面文件中添加一个类名

    <!-- index.html -->
    <html lang="" class="light">
    
  • 在入口文件中引入样式

    // main.ts
    import 'element-plus/theme-chalk/src/dark/css-vars.scss' // 暗黑模式样式
    

登录模块

<template>
    <div class="content">
        <el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px"
            class="demo-ruleForm">
            <!-- 账号 -->
            <el-form-item prop="username">
                <el-input v-model="ruleForm.username" autocomplete="off" :prefix-icon="User" />
            </el-form-item>
            <!-- 密码 -->
            <el-form-item prop="password">
                <el-input v-model="ruleForm.password" type="password" autocomplete="off" :prefix-icon="Lock" />
            </el-form-item>
            <!-- 验证码 -->
            <!-- 子传父 -->
            <ZinputVer @getVerify="getVerify" />
            <!-- 登录 -->
            <el-form-item>
                <el-button type="primary" @click="submitForm()">登录</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>

<script lang='ts' setup>
    import { reactive, ref } from 'vue'
    import type { FormInstance } from 'element-plus'
    import { User, Lock } from '@element-plus/icons-vue' // icon图标
    import ZinputVer from "../../components/ZinputVer/index.vue"; // 封装的验证码组件
    import { ElMessage } from 'element-plus' // 消息提示
    import { useRouter, useRoute } from "vue-router"; // 引入路由表和路由对象
    import { Login } from "../../https/api/login"; // 请求
    const ruleFormRef = ref<FormInstance>()
    // 使用路由表盒路由对象
    let router = useRouter()
    let route = useRoute()
    // 账号校验逻辑
    const validateName = (rule: any, value: any, callback: any) => {
        if (value === '') {
            callback(new Error('账号不能为空!'))
        } else {
            callback()
        }
    }
    // 密码检验逻辑
    const validatePass = (rule: any, value: any, callback: any) => {
        if (value === '') {
            callback(new Error('密码不能为空!'))
        } else {
            callback()
        }
    }
    // 表单收集的数据
    const ruleForm = reactive({
        username: '', // 账号
        password: '', // 密码
        verify: false // 验证码是否正确
    })
    // 校验
    const rules = reactive({
        username: [{ validator: validateName, trigger: 'blur' }],
        password: [{ validator: validatePass, trigger: 'blur' }]
    })
    // 父组件接收子组件的数据
    const getVerify = (val: boolean) => {
        ruleForm.verify = val

    }
    // 点击登录
    const submitForm = () => {
        if (!ruleForm.username || !ruleForm.password) {
            return ElMessage.warning('账号或密码不能为空哦~')
        }
        if (!ruleForm.verify) {
            return ElMessage.warning('验证码不正确哦~')
        }
        // 发起登录请求
        loginData(ruleForm.username, ruleForm.password)
    }
    // 登录请求
    const loginData = (name: string, password: string) => {
        // 发起请求
        Login({ name, password }).then((res: any) => {
            if (res.code == 200) {
                ElMessage.success('登录成功.')
                // 保存token
                sessionStorage.setItem('token', res.token)
                // 无记录路由跳转到内部页面
                router.replace('/layout')
            }
        })
    }
</script>
  • 封装的验证码组件

    <!-- 封装验证码组件 -->
    <template>
        <div class="zinputver">
            <div class="input">
                <el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="rules" label-width="120px"
                    class="demo-ruleForm">
                    <el-form-item prop="verify">
                        <el-input v-model="ruleForm.verify" autocomplete="off" :prefix-icon="Warning" />
                    </el-form-item>
                </el-form>
            </div>
            <div class="img">
                <img :src="image">
            </div>
        </div>
    </template>
    
    <script lang='ts' setup>
        import { reactive, ref } from 'vue'
        import type { FormInstance } from 'element-plus'
        import { Warning } from '@element-plus/icons-vue' // icon图标
        import { ElMessage } from 'element-plus' // 消息提示
        import { Verify } from "../../https/api/login"; // 请求
        let content = ref('') // 验证码
        let image = ref('') // 验证码图片
        // 发起请求获取验证码
        Verify().then((res: any) => {
            content.value = res.content
            image.value = res.img
        })
        const ruleFormRef = ref<FormInstance>()
        // 收集表单数据
        const ruleForm = reactive({
            verify: "" // 验证码
        })
        // 接收父传子的方法
        let emits = defineEmits(['getVerify'])
        // 验证码校验逻辑
        const validateVerify = (rule: any, value: any, callback: any) => {
            if (value === '') {
                callback(new Error('验证码不能为空!'))
            } else if (value) {
                if (value != content.value) {
                    ElMessage.warning('验证码输入错误.')
                    callback(new Error())
                    // 触发父组件中的方法,并传递参数
                    emits('getVerify', false)
                } else {
                    // 触发父组件中的方法,并传递参数
                    emits('getVerify', true)
                    callback()
                }
            }
        }
        // 校验
        const rules = reactive({
            verify: [{ validator: validateVerify, trigger: 'blur' }]
        })
    </script>
    

动态生成侧边栏

  • 父组件

    <!-- 侧边栏 -->
    <template>
        <div class="navList">
            <!-- 侧边栏内容 -->
            <div class="navListContent">
                <el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo"
                    :default-active="defaultActive" :collapse="isCollapse" text-color="#fff">
                    <!-- 展示侧边栏 -->
                    <LeftNav :navList="navList" />
                </el-menu>
            </div>
            <!-- 展开和收缩 -->
            <div class="navBottom">
                <i v-if="show" class="iconfont icon-shousuocaidan" @click="changShow"></i>
                <i v-else class="iconfont icon-shousuocaidan-copy" @click="changShow"></i>
            </div>
        </div>
    </template>
    
    <script lang='ts' setup>
    import { ref } from "vue";
    import LeftNav from "./LeftNav.vue"; // 侧边栏组件
    import { navlistData } from "../../https/api/layout"; // 请求
    let navList = ref([]) // 侧边栏数据
    let show = ref(true) // 侧边栏是否展开
    let isCollapse = ref(false) // 是否水平折叠
    let defaultActive = ref('/index') // 默认激活项
    const getNavList = () => {
        // 发起请求
        navlistData({ tokan: sessionStorage.getItem('token') }).then((res: any) => {
            navList.value = res.navList
        })
    }
    getNavList()
    // 点击展开与收缩
    const changShow = () => {
        show.value = !show.value
        if (show.value) {
            isCollapse.value = false
        } else {
            isCollapse.value = true
        }
    }
    </script>
    
  • 组件抽离,递归组件

    <template>
        <div v-for="(item, index) in navList" :key="index">
            <!-- 有嵌套 -->
            <el-sub-menu :index="(item as any).path" v-if="(item as any).children">
                <!-- 标题 -->
                <template #title>
                    <el-icon>
                        <location />
                    </el-icon>
                    <span>{{ (item as any).name }}</span>
                </template>
                <!-- 递归组件 -->
                <LeftNav :navList="(item as any).children" />
            </el-sub-menu>
            <!-- 无嵌套 -->
            <el-menu-item :index="(item as any).path" v-else>
                <el-icon>
                    <icon-menu />
                </el-icon>
                <template #title>{{ (item as any).name }}</template>
            </el-menu-item>
        </div>
    </template>
    
    <script lang='ts' setup>
    import {
        Menu as IconMenu,
        Location,
    } from '@element-plus/icons-vue' // icon图标
    // 接收父传子的数据
    let props = defineProps({
        navList: {
            type: Array
        }
    })
    </script>
    

权限:判断是否登录(内外部页面)

  • 前端:处理内部外部页面

  • 后端:处理内部外部接口

  • 方法:通过路由守卫实现

    // Router > beforeRouter.ts
    // 路由守卫
    import router from "./index"; // 引入路由
    // 创建路由前置守卫
    router.beforeEach((to, from, next) => {
        // 判断内部、外部页面
        if (to.fullPath != '/login') { // 内部
            //判断是否登录
            if (sessionStorage.getItem('token')) { // 已登录
                next() // 放行
            } else { // 未登录
                next('/login') // 跳转登录页
            }
        } else { // 外部
            next() // 放行
        }
    })
    

添加进度条

  • 第三方库:juejin.cn/post/713754…

  • 安装第三方库:

    # npm i nprogress -S
    
  • 在路由守卫中添加进度条

    import router from "./index"; // 引入路由
    // 引入进度条和样式
    import NProgress from 'nprogress'
    import 'nprogress/nprogress.css'
    // 创建路由前置守卫
    router.beforeEach((to, from, next) => {
        NProgress.start(); // 开启进度条
        ...
    })
    // 创建路由后置守卫
    router.afterEach(() => {
        NProgress.done(); // 关闭进度条
    })
    

    导出表格

  • export-from-json

  • 下包:npm i --save export-from-json

  • 引包:import exportFromJSON from 'export-from-json'

  • 使用:

    • 导出的数据:const data = [{ foo: 'foo'}, { bar: 'bar' }]
    • 导出的文件名:const fileName = 'download'
    • 导出的文件后缀:const exportType = exportFromJSON.types.csv
    • 导出:exportFromJSON({ data, fileName, exportType })
🌰:
const data = this.bannerList
const fileName = '轮播图列表'
const exportType = 'xls'
exportFromJSON({ data, fileName, exportType })

分页

  • 每页显示的数据
arr.slice((页码-1)*每页条数,页码*每页条数)