【VUE3】练习项目——大事件后台管理

目录

[0 前言](#0 前言)

[1 准备工作](#1 准备工作)

[1.1 安装pnpm](#1.1 安装pnpm)

[1.2 创建vue项目](#1.2 创建vue项目)

[1.3 Eslint & Prettier的配置](#1.3 Eslint & Prettier的配置)

[1.4 husky 提交代码检查](#1.4 husky 提交代码检查)

[1.5 目录调整](#1.5 目录调整)

[1.6 VueRouter4](#1.6 VueRouter4)

[1.6.1 基础配置](#1.6.1 基础配置)

[1.6.2 路由跳转](#1.6.2 路由跳转)

[1.7 引入 Element Plus 组件库](#1.7 引入 Element Plus 组件库)

[1.8 Pinia](#1.8 Pinia)

[1.8.1 优化](#1.8.1 优化)

[1.9 封装请求工具](#1.9 封装请求工具)

[1.9.1 安装 axios 与配置框架](#1.9.1 安装 axios 与配置框架)

[1.9.2 示例代码](#1.9.2 示例代码)

[2 开发](#2 开发)


0 前言

黑马程序员视频地址:Vue3大事件项目-项目介绍和pnpm创建项目

接口文档:登录 - 黑马程序员-大事件


1 准备工作

1.1 安装pnpm

官网:pnpm - 速度快、节省磁盘空间的软件包管理器 | pnpm中文文档 | pnpm中文网

安装pnpm命令:

复制代码
npm i pnpm -g

pnpm创建vue项目命令:

复制代码
pnpm create vue

命令对比:

npm yarn pnpm
npm install yarn pnpm install
npm install axios yarn add axios pnpm add axios
npm install axios -D yarn add axios -D pnpm add axios -D
npm uninstall axios yarn remove axios pnpm remove axios
npm run dev yarn dev pnpm dev

1.2 创建vue项目

使用pnpm创建vue项目时,选择以下配置

复制代码
请选择要包含的功能: (↑/↓ 切换,空格选择,a 全选,回车确认)
|  [ ] TypeScript
|  [ ] JSX 支持
|  [+] Router(单页面应用开发)
|  [+] Pinia(状态管理)
|  [ ] Vitest(单元测试)
|  [ ] 端到端测试
|  [+] ESLint(错误预防)
|  [+] Prettier(代码格式化)

标记:警告提示(待解决)

创建完项目需要进入相应文件夹中,安装所有依赖

复制代码
pnpm install

1.3 Eslint & Prettier的配置

【VUE3】Eslint 与 Prettier 的配置-CSDN博客

推荐使用里面的方案二,即将 prettier 的规则让 eslint 来执行

因为 1.4 中的检查代码需要 eslint 来检查


1.4 husky 提交代码检查

husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )

第一步:初始化仓库

复制代码
git init

第二步:初始化 husky 工具配置

复制代码
pnpm dlx husky-init; pnpm install

第三步:修改 .husky/pre-commit 文件

复制代码
pnpm lint

但是这样会有一个问题!

我们可以打开 package.json 文件,看到里面的 lint 命令对应为:

javascript 复制代码
"lint": "eslint . --fix"

即默认进行的是全量检查,耗时问题,历史问题,因此需要再导入一个包 lint-staged :

第一步:安装

javascript 复制代码
pnpm i lint-staged -D

第二步:配置 package.json 文件

javascript 复制代码
{
  // ... 省略 ...
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

{
  "scripts": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
  }
}

第三步:修改 .husky/pre-commit 文件

javascript 复制代码
pnpm lint-staged

这样就会只检查修改过的文件,哪怕以前提交的文件有问题,也不会检查报错

可以通过控制 1.3 中的那篇文章的第七节中的 'no-undef': 'off'来模拟提交以前有问题的文件


1.5 目录调整

删除默认文件后,增加 api 与 utils 文件夹

如果使用 sass,则需安装对应依赖

javascript 复制代码
pnpm add sass -D

1.6 VueRouter4

更多内容见官网:Vue Router | Vue.js 的官方路由

1.6.1 基础配置

javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'

// createRouter 创建路由实例,===> new VueRouter()
// 1. history模式: createWebHistory()   http://xxx/user
// 2. hash模式: createWebHashHistory()  http://xxx/#/user

// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 '/'
// https://vitejs.dev/guide/build.html#public-base-path

// 如果将来你部署的域名路径是:http://xxx/my-path/user
// vite.config.ts  添加配置  base: my-path,路由这就会加上 my-path 前缀了

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router

1.6.2 路由跳转

由于 setup 下,this 指向 undefined ,因此需要引入包创建 router 与 router 对象

javascript 复制代码
<script setup>
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const gotoCart = () => {
  console.log(route)
  router.push('/individual')
}
</script>

<template>
  <div>我是App</div>
  <button @click="gotoCart()">跳转购物车页面</button>
</template>

也可以导入 1.6.1 中的 router 对象,调用其身上的 push 方法

ai说前者只能在setup语法糖中使用,只适用于组合式api


1.7 引入 Element Plus 组件库

官方手册:一个 Vue 3 UI 框架 | Element Plus

安装组件库

复制代码
pnpm install element-plus

按需引入:

第一步:安装插件

复制代码
pnpm add -D unplugin-vue-components unplugin-auto-import

第二步:把下列代码插入到你的 Vite 的配置文件中

javascript 复制代码
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

全部引入此处不赘述

注意:

1.在引入之后,无需任何配置,即可使用组件库内的组件

2.并且components下的vue组件也可以直接使用,无需导入


1.8 Pinia

见文档:【VUE3】Pinia-CSDN博客

注意:由于创建项目时勾选了Pinia,所以此处不需要再手动安装配置Pinia,直接可以使用

但是持久化还需手动配置,此处不再赘述

1.8.1 优化

将 main.js 中关于 pinia 的代码抽离到 store/index.js 中,并且将 store/modules 中的所有仓库文件统一从 index.js 中导出,方便管理且简化代码

javascript 复制代码
// main.js
// ...
import pinia from './stores'

// ...
app.use(pinia)

// ...
javascript 复制代码
// store/index.js

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persist)

export default pinia

export * from '@/stores/modules/user'
export * from '@/stores/modules/count'

// 上面的代码等同于
// import { useUserStore, ... } from '@/stores/modules/user'
// export { useUserStore, ... }
javascript 复制代码
// 组件.vue

import { useUserStore, useCountStore } from '@/stores'

const userStore = useUserStore()
const userCount = useCountStore()

1.9 封装请求工具

手册:axios中文文档|axios中文网 | axios

1.9.1 安装 axios 与配置框架

1.安装 axios

复制代码
pnpm add axios

2.框架代码

javascript 复制代码
// utils/request.js

import axios from 'axios'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
})

instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    return config
  },
  (err) => Promise.reject(err)
)

instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    return res
  },
  (err) => {
    // TODO 5. 处理401错误
    return Promise.reject(err)
  }
)

export default instance

1.9.2 示例代码

javascript 复制代码
// utils/request.js

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'
const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 100000
})

instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)

instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    if (res.data.code === 0) {
      return res
    }
    ElMessage({ message: res.data.message || '服务异常', type: 'error' })
    // TODO 4. 摘取核心响应数据
    return Promise.reject(res.data)
  },
  (err) => {
    ElMessage({ message: err.response.data.message || '服务异常', type: 'error' })
    // TODO 5. 处理401错误
    if (err.response?.status === 401) {
      router.push('/login')
    }
    return Promise.reject(err)
  }
)

export default instance

2 开发

2.1 路由配置

示例:采用 路由懒加载

javascript 复制代码
// router/index.js

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

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/login', component: () => import('@/views/login/LoginPage.vue') },
    {
      path: '/',
      component: () => import('@/views/layout/LayoutContainer.vue'),
      redirect: '/article/manage',
      children: [
        { path: '/article/manage', component: () => import('@/views/article/ArticleManage.vue') },
        { path: '/article/channel', component: () => import('@/views/article/ArticleChannel.vue') },
        { path: '/user/profile', component: () => import('@/views/user/UserProfile.vue') },
        { path: '/user/avatar', component: () => import('@/views/user/UserAvatar.vue') },
        { path: 'user/password', component: () => import('@/views/user/UserPassword.vue') }
      ]
    }
  ]
})

export default router

记得准备路由出口,如:

javascript 复制代码
// App.vue

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

2.2 登录注册

2.2.1 静态页面

1.安装 element-plus 图标库

javascript 复制代码
pnpm i @element-plus/icons-vue

2.静态结构准备

注意:登录页面与注册页面使用 v-if 与 v-else 控制切换

javascript 复制代码
// views/login/LoginPage.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
const isRegister = ref(true)
</script>

<template>
  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <el-form ref="form" size="large" autocomplete="off" v-if="isRegister">
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space> 注册 </el-button>
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = false"> ← 返回 </el-link>
        </el-form-item>
      </el-form>
      <el-form ref="form" size="large" autocomplete="off" v-else>
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            name="password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item class="flex">
          <div class="flex">
            <el-checkbox>记住我</el-checkbox>
            <el-link type="primary" :underline="false">忘记密码?</el-link>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button class="button" type="primary" auto-insert-space>登录</el-button>
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = true"> 注册 → </el-link>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>

<style lang="scss" scoped>
.login-page {
  height: 100vh;
  background-color: #fff;
  .bg {
    background:
      url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
      url('@/assets/login_bg.jpg') no-repeat center / cover;
    border-radius: 0 20px 20px 0;
  }
  .form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    user-select: none;
    .title {
      margin: 0 auto;
    }
    .button {
      width: 100%;
    }
    .flex {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
  }
}
</style>

2.2.2 规则校验

官方文档:Form 表单 | Element Plus

四大校验方式:

1.非空校验:required

2.长度校验:min、max

3.正则校验:pattern

4.自定义校验 :validator

第一步:声明表单数据对象与规则对象,其中表单数据对象必须是响应式的

javascript 复制代码
const formData = ref({
  username: '',
  password: '',
  repassword: ''
})
const formRules = {
  username: [
    { required: true, message: '用户名不能为空!', trigger: 'blur' },
 // blur是失去焦点事件
    { min: 5, max: 10, message: '用户名必须为5-10位字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空!', trigger: 'blur' },
    { pattern: /^\S{8,15}$/, message: '密码必须为8-15位非空字符', trigger: 'change' }
 // change为改变时校验,发现当检验提示错误后,若触发失焦事件,会导致提示消失,因此不推荐
  ]
}

第二步:给整个表单的大标签绑定数据对象与规则对象

html 复制代码
<el-form
...
:model="formData"
:rules="formRules"
>
<!-- ... -->
</el-form>

第三步:给输入框绑定数据,并且给表单元素标签绑定要使用的规则

html 复制代码
<el-form-item prop="username">    ❗
  <el-input
    :prefix-icon="User"
    placeholder="请输入用户名"
    v-model="formData.username"    ❗
  ></el-input>
</el-form-item>

自定义校验方式:

使用方式相同,只是配置规则时,validator 指向一个函数

参数:

rule:当前校验规则相关的信息

value:所校验的表单元素目前的表单值

callback:无论成功还是失败,都需要 callback 回调

javascript 复制代码
const formRules = {
  // ...
  repassword: [
    {
      validator: (rule, value, callback) => {
        if (value !== formData.value.password) {
          callback(new Error('两次密码不一致,请重新输入!'))
        } else {
          callback()
        }
      },
      trigger: 'blur'
    }
  ]
}

2.2.3 表单预校验

由于vue3的特性,即导入的组件内的方法默认不会暴露,因此需要获取组件对象,然后再获取,如:

第一步:获取组件对象

详细见 【VUE3】组合式API-CSDN博客 中的 9.2

javascript 复制代码
const form = ref()
html 复制代码
<el-form
ref="form"    ❗
size="large"
autocomplete="off"
v-if="isRegister"
:model="formData"
:rules="formRules"
>

第二步:调用 validate 方法

validate 方法即对整个表单的内容进行验证,接收一个回调函数,或返回 Promise

javascript 复制代码
const register = async () => {
  await form.value.validate()    ❗
  await userRegisterService(formData.value)    // 这个是提交数据api
  ElMessage.success('注册成功!')
  isRegister.value = false    // 切换登录
}

注意:因为之前我们设置了插件来帮我们自动导入Element组件,所以上述代码中的ElMessage方法我们没有导入,但是eslint会报错,可以在 eslint.config.js 中来配置,让其不报错,位置及代码如下:

javascript 复制代码
// ...

export default defineConfig([
  // ...
  {
    rules: {
      // ...
      'vue/no-setup-props-destructure': ['off'], // 放在这个下面
      globals: {
        ElMessage: 'readonly',
        ElMessageBox: 'readonly',
        ElLoading: 'readonly'
      }
    }
  },
])

2.2.4 封装注册api

javascript 复制代码
// api/user.js

import request from '@/utils/request'

export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })

调用见 2.2.3


2.2.5 登录校验

与注册校验相同,此处不再赘述

需要注意的是,记得在请求完成后,将token存到store中,并且跳转页面


2.3 首页

2.3.1 静态资源

html 复制代码
<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>

<template>
  <el-container class="layout-container">
    <el-aside width="200px">
      <div class="el-aside__logo"></div>
      <el-menu
        active-text-color="#ffd04b"
        background-color="#232323"
        :default-active="$route.path"
        text-color="#fff"
        router
      >
        <el-menu-item index="/article/channel">
          <el-icon><Management /></el-icon>
          <span>文章分类</span>
        </el-menu-item>
        <el-menu-item index="/article/manage">
          <el-icon><Promotion /></el-icon>
          <span>文章管理</span>
        </el-menu-item>
        <el-sub-menu index="/user">
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>个人中心</span>
          </template>
          <el-menu-item index="/user/profile">
            <el-icon><User /></el-icon>
            <span>基本资料</span>
          </el-menu-item>
          <el-menu-item index="/user/avatar">
            <el-icon><Crop /></el-icon>
            <span>更换头像</span>
          </el-menu-item>
          <el-menu-item index="/user/password">
            <el-icon><EditPen /></el-icon>
            <span>重置密码</span>
          </el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>
    <el-container>
      <el-header>
        <div>黑马程序员:<strong>小帅鹏</strong></div>
        <el-dropdown placement="bottom-end">
          <span class="el-dropdown__box">
            <el-avatar :src="avatar" />
            <el-icon><CaretBottom /></el-icon>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile" :icon="User"
                >基本资料</el-dropdown-item
              >
              <el-dropdown-item command="avatar" :icon="Crop"
                >更换头像</el-dropdown-item
              >
              <el-dropdown-item command="password" :icon="EditPen"
                >重置密码</el-dropdown-item
              >
              <el-dropdown-item command="logout" :icon="SwitchButton"
                >退出登录</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
      <el-footer>大事件 ©2023 Created by 黑马程序员</el-footer>
    </el-container>
  </el-container>
</template>

<style lang="scss" scoped>
.layout-container {
  height: 100vh;
  .el-aside {
    background-color: #232323;
    &__logo {
      height: 120px;
      background: url('@/assets/logo.png') no-repeat center / 120px auto;
    }
    .el-menu {
      border-right: none;
    }
  }
  .el-header {
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    .el-dropdown__box {
      display: flex;
      align-items: center;
      .el-icon {
        color: #999;
        margin-left: 10px;
      }

      &:active,
      &:focus {
        outline: none;
      }
    }
  }
  .el-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #666;
  }
}
</style>

2.3.2 访问拦截

官方手册:导航守卫 | Vue Router

javascript 复制代码
// router/index.js
import { useUserStore } from '@/stores'

// ...
router.beforeEach((to) => {
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') return '/login'
})

默认是直接放行

根据返回值决定,是放行还是拦截

返回值:

  1. undefined / true :直接放行

  2. false :拦截到 from 的地址页面

  3. 具体路径 或 路径对象 :拦截到对于的地址

'/login' { name: 'login' }


持续更新

相关推荐
周星星日记几秒前
10.vue3中组件实现原理(上)
前端·vue.js·面试
小华同学ai1 分钟前
6.4K star!轻松搞定专业领域大模型推理,这个知识增强框架绝了!
前端·github
专业抄代码选手3 分钟前
【VUE】在vue中,Watcher与Dep的关系
前端·面试
Lazy_zheng7 分钟前
从 DOM 监听到 Canvas 绘制:一套完整的水印实现方案
前端·javascript·面试
尘寰ya9 分钟前
前端面试-微前端
前端·面试·职场和发展
蘑菇头爱平底锅10 分钟前
数字孪生-DTS-孪创城市-前端实现动态地铁分布线路图
前端·javascript·数据可视化
祯民14 分钟前
AI 时代前端进阶:10分钟入门基于 HuggingFace Transformers 库开源模型私有化部署
前端·aigc
驱动小百科30 分钟前
chrome无法访问此网站怎么回事 分享5种解决方法
前端·chrome·谷歌浏览器·谷歌浏览器无法访问此网站·无法访问此网站
尘寰ya31 分钟前
前端面试-垃圾回收机制
java·前端·面试