【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' }


持续更新

相关推荐
恋猫de小郭40 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端