vue3大事件项目

创建项目(pnpm)

先安装pnpm,然后使用pnpm创建项目
注意安装pnpm的时候在一个空文件夹下安装,不要按照到电脑磁盘根目录,可能权限不够
pnpmalt text
pnpm创建项目alt text
为项目安装依赖alt text
项目接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835

本项目的技术栈 本项目技术栈基于 ES6vue3piniavue-router 、vite 、axios 和 element-plus

配置

Eslint+prettier配置代码风格

理解:让代码更严谨规范
配置文件.eslintrc.cjs (eslint.config.js)

  1. 二者结合起来配置
    Eslint规范纠错(校验),prettier风格美观配置(格式化)
  2. vue组件名称多单词组成(忽略index.vue)
  3. props解构(关闭)
    props解构默认报错,因为会丢失响应式,但是后续会有响应式,关闭这个报错
    视频提供的代码如下,但是由于视频项目修改的配置文件和我项目拥有的配置文件不一样,所以我问了chat帮我把以下代码添加到我的文件中
    配置的规则:
  4. 不适用双引号,全部使用单引号
  5. 每行代码的长度不超过80(做项目通常分屏,一边写代码一边看效果演示,80长度适合)
  6. 无分号
  7. 对象最后一个属性后面没有逗号
  8. 换行符号不做显示(因为win和mac这个不一致)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    rules: {
    'prettier/prettier': [
    'warn',
    {
    singleQuote: true, // 单引号
    semi: false, // 无分号
    printWidth: 80, // 每行宽度至多80字符
    trailingComma: 'none', // 不加对象|数组最后逗号
    endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
    }
    ],
    'vue/multi-word-component-names': [
    'warn',
    {
    ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
    }
    ],
    'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
    }

注意

如果有在vscode安装prettier插件要禁用 format on save 关闭
需要安装Eslint插件,并配置保存时自动修复(这个配置给vscode的设置配置,我在vue2大项目中跟着配置过了,这里就不写了)

.eslintrc.cjs文件和eslint.config.mjs文件

视频中出现的是前者而我项目创建的文件是后者,以下是chat的回复:
.eslintrc.cjs:是一个 CommonJS 模块格式的配置文件,通常用于较旧的 ESLint 版本或需要 CommonJS 的环境。
eslint.config.mjs:是一个 ECMAScript 模块格式 (ESM) 配置文件,通常用于支持 ES 模块的更现代的设置中。
.mjs代表“模块”,它使用export导出配置的语法

配置代码检查工作流

理解:在提交代码(上传仓库)前再做一遍检查,类似生命周期里的一个钩子,在特定的时期(提交时)执行特定的命令。
简要理解:不让你把编译器看出来的报错代码上传到仓库,只能上传vscode不报错的正确代码到仓库

  1. 初始化 git 仓库,执行 git init 即可

  2. 初始化 husky 工具配置,执行 pnpm dlx husky-init && pnpm install 即可https://typicode.github.io/husky/

  3. 修改 .husky/pre-commit 文件

操作:

  1. 在vscode打开项目终端,注意这里打开bash终端,因为这是和git相关的操作终端git bash
    2.初始化git
    git init
  2. 安装
    pnpm dlx husky-init && pnpm install
    成功后我的显示以下内容
    1
    2
    3
    4
    5
    6
    husky - Git hooks installed

    devDependencies:
    + husky 8.0.3 (9.1.6 is available)

    Done in 2.4s
    4.安装后在项目中自动新建了一个.husky文件夹,里面有pre-commit文件,文件里就是代码提交前要做的事情(根据写入的从package.json文件夹找规则)

把这个文件里的npm test进行修改
如果改为pnpm lint是全量检查,会有耗时问题和历史问题
实际上更推荐暂存区eslint校验,保证每次新写的代码是规范的

暂存区Eslint校验

理解:不让把暂存区中的报错代码上传到仓库

  1. 安装lint-staged包(还是在bash里面)
    pnpm i lint-staged -D

  2. package.json配置lint-staged命令
    在文件后面最后增加如下代码

    1
    2
    3
    4
    5
    "lint-staged": {
    "*.{js,ts,vue}": [
    "eslint --fix"
    ]
    }

    在scripts中的和后面增加”lint-staged”: “lint-staged”

    1
    2
    3
    4
    "scripts": {
    // ... 省略 ...
    "lint-staged": "lint-staged"
    }
  3. 修改 .husky/pre-commit 文件

    1
    pnpm lint-staged

    我的这里中间出了问题,chat帮我解决了,写在报错部分了

  4. 测试
    我在main.js中写了一行错误代码然后尝试添加提交到仓库,在提交时被发现拦截
    vscode git暂存区校验成功
    小乌龟 git暂存区校验成功

调整目录

默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动:

  • 删除一些初始化的默认文件
  • 修改剩余代码内容
  • 新增调整我们需要的目录结构
  • 拷贝初始化资源文件,安装预处理器插件
  1. 删除文件
    src/assets文件夹文件
    src/components文件夹文件
    src/stores文件夹文件
  2. 修改内容
  • src/router/index.js修改为以下内容(把routers内容清空了)

    1
    2
    3
    4
    5
    6
    7
    8
    import { createRouter, createWebHistory } from 'vue-router'

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

    export default router
  • src/App.vue内容清空

1
2
3
4
5
6
7
8
9
<script setup></script>

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

<style scoped></style>
  • src/main.js内容清空
1
2
3
4
5
6
7
8
9
10
11
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.mount('#app')
  1. 新增需要目录:api(请求模块)文件夹和utils(工具函数)文件夹

  2. 将项目需要的全局样式和图片文件,cv到assets文件夹, 并将assets/main.scss全局样式文件在main.js中引入
    安装 sass 依赖
    pnpm add sass -D
    main.js中添加下面代码,注意自己的样式尽量往下放让它权重高一点,保证它的优先级

    1
    import '@/assets/main.scss'

路由调整

vue3路由:vue-router4

相对于vue-router3,4相当于给VueRouter封装了一下,内部还是newVueRouter;导包是按需导入而不是导入VueRouter;history配置模式
vue3向前兼容vue2的语法

  • 路由模式
    1.history 模式使用 createWebHistory()
    2.hash 模式使用 createWebHashHistory()
    3.参数是基础路径,默认/
  • history: createWebHistory(import.meta.env.BASE_URL)
    在官网Vite官方文档-环境变量和模式中也有说明
    import.meta.env.BASE_URL 是vite中的环境变量,可以在vite.config.js中配置,base配置项,默认是/这样以后修改会很方便
    import.meta.env.BASE_URL 是Vite 环境变量:https://cn.vitejs.dev/guide/env-and-mode.html

在 Vue3 CompositionAPI 中
import {useRoute,useRouter } from ‘vue-router’
1.获取路由对象(写入) router useRouter
const router =useRouter
router是大的路由对象
2.获取路由参数(读取) route useRouteconst
route =useRoute()
route是路由参数

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createRouter, createWebHistory } from 'vue-router'

// createRouter 创建路由实例
// 配置 history 模式
// 1. history模式:createWebHistory 地址栏不带 #
// 2. hash模式: createWebHashHistory 地址栏带 #
// vue3向前兼容vue2的语法
const router = createRouter({
//在官网Vite官方文档-环境变量和模式中也有说明
//import.meta.env.BASE_URL 是vite中的环境变量
//可以在vite.config.js中配置,base配置项,这样以后修改会很方便
//默认是/
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})

export default router

按需引入-自动导入Element Plus

先安装,再按需导入,然后直接使用组件
成功后你要用哪个插件不需要像vue2项目里一样为它单独导入注册再使用,而是直接使用即可,它发现你使用会自动帮你导入!!!

  1. 安装
    官方文档:https://element-plus.org/zh-CN/
    打开文档-导航栏右侧指南-左侧基础-安装
    pnpm install element-plus
  2. 使用
    文档的基础-快速开始-按需导入-自动导入(根据文档操作)
    首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件
    pnpm add -D unplugin-vue-components unplugin-auto-import
  3. 配置
    根据文档cv代码到vite.config.js文件最后,然后修改:
    导包部分:已经有了的删掉,没有的移动到前面
    defineConfig- plugins部分:加入文档内容

使用一下!

配置结束后重新启动项目
然后就可以在项目的任意位置使用组件啦
还是官方文档-右上角组件-找到想要的样式-样式右下角查看源代码-找到想要的直接CV到项目!
成功用他两个按钮测试一下

小问题

我好像把东西安装到了项目文件上层的一个文件夹里了,而不是项目本身这个文件夹,后来删除项目父文件夹的其他文件,在项目里重新安装导入了
注意打开项目的时候一定要只打开项目根文件夹,不要再打错啦!!

Pinia构建持久化仓库

理解:解决多组件共享一个数据的情况

Pinia构建用户仓库

Pinia:在main.js中默认已经配好了(创建项目时配置的),所以只需要在stores文件夹中新建index.js文件和modules文件夹-建立仓库文件即可

新建用户仓库模块user.js,然后在App.vue里面测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineStore } from 'pinia'
import { ref } from 'vue'

// 用户模块
//token(自己本身的字段) setToken(登录) removeToken(退出)
//useUserStore 养成命名规范
export const useUserStore = defineStore(
'big-user',
() => {
//定义数据
const token = ref('')
///定义操作数据的方法(下面这两个可以二合一,传空串进来也相当于remove,但这样写更清楚)
const setToken = newToken => {
token.value = newToken
}
const removeToken = () => {
token.value = ''
}
//不要忘记return暴露出去!
token,
setToken,
removeToken
}
)

测试一下!

App.vue测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { useUserStore } from '@/stores/modules/user'
//拿到仓库对象,然后从仓库对象里面拿我们想要的数据和token
const userStore = useUserStore()
</script>

<template>
<div>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
<p>{{ userStore.token }}</p>
<el-button @click="userStore.setToken('测试:我是token字符串')"
>登录测试</el-button>
<el-button @click="userStore.removeToken">退出测试</el-button>
<router-view></router-view>
</div>
</template>

构建用户仓库成功

仓库持久化

需要用pinia的持久化插件(官方文档步骤)

  1. 先安装
    pnpm add pinia-plugin-persistedstate -D
  2. main.js中使用
1
2
3
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
  1. 配置 stores/user.js,传递第三个参数persist
    1
    2
    3
    4
    5
    6
    7

    export const useUserStore = defineStore(
    ....,
    {
    persist: true // 持久化
    }
    )
    配置后重启服务,token会持久化,刷新后token还在不会丢

Pinia仓库统一管理

操作目标:Pinia独立维护;仓库统一导出;
操作:在main.js中,将和pinia相关的内容提取到stores文件夹下的index.js文件中,让index.js作为统一出口
main.js中导入index.js导出的pinia即可
并且app.vue文件要导入仓库的对象只需要从哪个@/stores导入即可,就不用再/modules/user.js了,能这么写说明是在该文件夹下的index.js里面导出了

stores/index.js文件夹代码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//main.js中剪切过来的
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
//拆成两行
const pinia = createPinia()
pinia.use(persist)

export default pinia
//import { useUserStore } from './modules/user'
//export { useUserStore }
//import { useCountStore } from './modules/counter'
//export { useCountStore }
//上面这四行太繁琐,学习下面两行代码实现统一导出!!
export * from './modules/user' //接收user模块的所有按需导出
export * from './modules/counter' //接收/counter模块的所有按需导出

axios请求工具设置

和vue2大项目类似,看官方文档有步骤
项目接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835

  1. 安装axios
    pnpm add axios

  2. 新建 utils/request.js 封装 axios 模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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
  3. axios 基本配置
    utils/request.js文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    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: 10000 //10s
    })

    // 请求拦截器
    instance.interceptors.request.use(
    config => {
    // TODO 2. 携带token
    const useStore = useUserStore()
    if (useStore.token) {
    //看接口文档
    config.headers.Authorization = useStore.token
    }
    return config
    },
    err => Promise.reject(err)
    )

    // 响应拦截器
    instance.interceptors.response.use(
    res => {
    // TODO 4. 摘取核心响应数据
    if (res.data.code === 0) {
    //看接口文档,如果后端返回的数据是0
    return res
    }
    // TODO 3. 处理业务失败
    // 处理业务失败, 给错误提示,抛出错误
    //ElMessage类似vent,是从Element Plus里面导入的组件
    //记得上面先导入(视频弹幕说这上面不导入反而可以成功)
    ElMessage.error(res.data.message || '服务异常')
    return Promise.reject(res.data)
    },
    err => {
    // TODO 5. 处理401错误
    // 错误的特殊情况 => 401 权限不足 或 token 过期 => 拦截到登录
    if (err.response?.status === 401) {
    router.push('/login')
    }

    // 错误的默认情况 => 只要给提示
    ElMessage.error(err.response.data.message || '服务异常')
    return Promise.reject(err)
    }
    )

    export default instance
    export { baseURL }

中英国际化处理

官方文档-左侧导航栏-配置组件-Config Provider全局配置
默认是英文的,由于这里不涉及切换, 所以在 在App.vue 中直接导入配置,将整个项目全部包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

<template>
<div>
<!-- App.vue只需要留一个路由出口 router-view即可
locale="zhCn"主要是这个值的变化决定语言的变化,如果这里做个按钮修改值,那么即可修改语言-->
<el-config-provider :locale="zhCn">
<router-view></router-view>
</el-config-provider>
</div>
</template>

<style scoped></style>

路由配置

根据页面分析路由

根据提供的完整项目地址看项目观察:https://fe-bigevent-web.itheima.net/login
首先运行项目先看到登录页面,用户登录旁边有“注册”,点击后页面地址不变还是/login,这里是通过切换组件来实现的,公用一个路由
登录后进入首页,左侧导航栏有三个分类,点击每部分发现地址在变,对应的右侧在切换,说明这里要配置路由
架构:一级路由:登录页面,首页架子;二级路由:文章分类,文章管理,基本资料,更换头像,重置密码

约定路由规则

path 文件 功能 组件名 路由级别
/login views/login/LoginPage.vue 登录&注册 LoginPage 一级路由
/ views/layout/LayoutContainer.vue 布局架子 LayoutContainer 一级路由
├─ /article/manage views/article/ArticleManage.vue 文章管理 ArticleManage 二级路由
├─ /article/channel views/article/ArticleChannel.vue 频道管理 ArticleChannel 二级路由
├─ /user/profile views/user/UserProfile.vue 个人详情 UserProfile 二级路由
├─ /user/avatar views/user/UserAvatar.vue 更换头像 UserAvatar 二级路由
├─ /user/password views/user/UserPassword.vue 重置密码 UserPassword 二级路由

创建路由架子

类似vue2路由创建,在views文件夹下创建各个路由名命名的文件夹
然后在router/index.js配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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')
}
]
}
]

配置成功后在地址栏输入不同的地址有对应的显示
路由配置成功
路由配置成功

登录页

需求

静态解构,登录和注册的基本切换,注册功能的校验+调用后端成功注册,登录功能的校验+登录+存token
静态结构使用element-plus表单

静态布局

  1. 清空布局
    把之前测试组件路由的代码都清空掉,只留一个路由出口(我这里把之前的作为副本保存然后新建了一个空的app.vue)
  2. 安装图标库
    页面用到的一些小图标来自element-plus图标库
    pnpm i @element-plus/icons-vue
    然后重启项目
  3. 登录页静态布局
    在src文件夹/views/login.LoginPage.vue CV提供的静态结构
    结构说明
    官方文档有说明,不用背 https://element-plus.org/zh-CN/
    理解代码:

    el-row表示一行,一行分成24份;el-col表示列
    (1) :span=”12” 代表在一行中,占12份 (50%)
    (2) :span=”6” 表示在一行中,占6份 (25%)
    (3) :offset=”3” 代表在一行中,左侧margin占3份


    el-form 整个表单组件 el-form-item 表单的一行 (一个表单域) el-input 表单元素(输入框)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
<el-form>
<el-form-item>
<h1>注册</h1>
</el-form-item>
<el-form-item prop="username">
<el-input
v-model="formModel.username"
:prefix-icon="User" //element-plus 图标配置
placeholder="请输入用户名"
></el-input>
</el-form-item>
</el-form>

登录页结构

逻辑处理

  1. 表达切换
    进入login页面默认是登录表单,点击表单左下角注册按钮切换到注册,再点击返回按钮切换到登录
    使用const isRegister = ref(false)进行切换,true和false对应注册和登录
  2. 表单校验
    还是使用组件库,Form表单-表单校验
    https://element-plus.org/zh-CN/component/form.html#%E8%A1%A8%E5%8D%95%E6%A0%A1%E9%AA%8C
    (1) el-form 属性 :model=”ruleForm” 绑定的整个form的数据对象 { xxx, xxx, xxx }
    用来收集全部数据,这样后面提交或者重置都方便
    (2) el-form 属性 :rules=”rules” 绑定的整个rules规则对象 { xxx, xxx, xxx }
    可以理解为(2)里的每一个对应(1)里面每一个属性的校验规则
    (3) 表单元素 属性 v-model=”ruleForm.xxx” 给表单元素绑定form的某个子属性
    (4) el-form-item 属性 prop 配置生效的是哪个校验规则 (和rules中的字段要对应)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <el-form
    ref="ruleFormRef"
    style="max-width: 600px"
    :model="ruleForm" //收集表单全部数据
    :rules="rules"
    label-width="auto"
    class="demo-ruleForm"
    :size="formSize"
    status-icon
    >
    <el-form-item label="Activity name" prop="name">
    <el-input v-model="ruleForm.name" />
    </el-form-item>
  3. 在表单中给表单元素配置prop=”属性名”和v-module
    如密码
    1
    2
    3
    4
    5
    6
    7
    8
    <el-form-item prop="password">
    <el-input
    v-model="formModel.password"
    :prefix-icon="Lock"
    type="password"
    placeholder="请输入密码"
    ></el-input>
    </el-form-item>
  4. 提供数据对象和校验规则
    提供一个整个的用于提交的form数据对象formModel,看后端接口文档来定
    提供整个表单的校验规则rules(官方文档告诉了怎么配)
    注意rules校验属性名和上面对象的属性名一样
  • 非空校验 required: true message消息提示, trigger触发校验的时机 blur change
  • 长度校验 min:xx, max: xx
  • 正则校验 pattern: 正则规则 \S 非空字符
  • 自定义校验 => 自己写逻辑校验 (校验函数)
  • validator: (rule, value, callback)
    (1) rule 当前校验规则相关的信息
    (2) value 所校验的表单元素目前的表单值
    (3) callback 无论成功还是失败,都需要 callback 回调
  • callback() 校验成功
  • callback(new Error(错误信息)) 校验失败

‘blur’触发器: 当设置为’blur’时,验证将在表单元素失去焦点时触发。这适用于需要在用户完成输入后立即进行验证的场景,如文本输入框。例如,当用户填写完一个输入框并切换到另一个输入框时,可以立即检查该输入框的内容是否符合要求。

‘change’触发器: 相比之下,当设置为’change’时,验证将在表单元素的值发生变化时触发。这适用于如下拉选择框、日期选择器、复选框和单选框等元素,这些元素在用户选择选项后需要立即进行验证。

不设置触发器: 如果不设置trigger属性,Element UI将使用默认的触发器,通常是在表单提交时进行验证。这意味着,除非用户尝试提交表单,否则不会执行任何验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const formModel = ref({
username: '',
password: '',
repassword: ''
})
···
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '用户名必须是 5-10位 的字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是 6-15位 的非空字符',
trigger: 'blur'
}
],
repassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是 6-15位 的非空字符',
trigger: 'blur'
},
{
validator: (rule, value, callback) => {
// 判断 value 和 当前 form 中收集的 password 是否一致
if (value !== formModel.value.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback() // 就算校验成功,也需要callback
}
},
trigger: 'blur'
}
]
}

登录页结构

注册预校验

在注册提交之前要有一次预校验,比如空着点注册肯定不行,通过校验才能完成提交请求
文档Form表单组件-Form API最下面Form Exposes,form组件实例暴露出来的一些方法

validate方法 对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise
要想拿到form组件实例,要添加const form = ref()

1
2
3
4
5
const register = async () => {
// 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示
await form.value.validate()
alert('注册成功')
}

注册登录请求

  1. 创建api文件夹-user.js文件,根据后端接口写
1
2
3
4
5
6
7
8
9
10
11
12
13
import request from '@/utils/request'

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

// 登录接口
export const userLoginService = ({ username, password }) =>
request.post('/api/login', { username, password })

// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')

  1. 同时在app.vue导入api,修改注册请求(登录同理)
    1
    2
    3
    4
    5
    6
    7
    const register = async () => {
    // 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示
    await form.value.validate()
    await userRegisterService(formModel.value)
    alert('注册成功')
    isRegister.value = false //注册成功后切换到登录表单
    }
  2. 优化提示弹窗
    上面的alert明显不好看,我们要更好看的
    官方文档-左侧导航栏往下找到Feedback反馈组件-Message消息提示-不同状态-查看代码-poen四种表示方式
    这样的提示真好看
    这里用第二中提示注册成功
    1
    ElMessage.success('注册成功')
    修改后运行程序注册后能成功弹出,但是vscode上还是显示红色报错,这是因为前面我们按需自动导入了,但是eslink不知道,所以给eslink中配置全局变量规则可以不报错
    给eslint.config.js文件最后添加
    (视频成功我开始没成功,我的这个文件是eslint.config.js,视频是.eslintrc.cjs文件,前面做配置的时候就出现了问题我让AI帮我变成我的文件适用的代码,这里没有变)
    (后来关掉vscode重新打开项目发现不报错了,重启是个好方法!)
    1
    2
    3
    4
    5
    6
    7
    8
    module.exports = {
    ...
    globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
    }
    }
    注册成功

首页

需求

  1. 基本架子拆解(菜单组件的使用):采用分成左右两部分的架子,左侧是菜单(el-menu包含el-menu-item;二级菜单嵌套el-sub-menu,el-menu-item),官方文档学习
  2. 登录访问拦截:首页的内容必须是登录过的用户才能访问,没有登录的用户不能访问
  3. 用户基本信息获取和渲染:首页-个人中心-基本信息,展示用户的基本资料,右上角用户头像小菜单也能到达
  4. 右上角小菜单和退出功能
  5. 优化-退出询问

首页基本架子

views/layout/LayoutContauer.vue
主要用到el-menu组件,Container布局容器(官方文档)

el-container

  • el-aside 左侧

    • el-menu 左侧边栏菜单
  • el-container 右侧

    • el-header 右侧头部
      • el-dropdown
    • el-main 右侧主体
      • router-view

架子代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<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-menu 整个菜单组件
:default-active="$route.path" 配置默认高亮的菜单项
这个值一旦和下面的Index相等就会高亮
router router选项开启,el-menu-item 的 index 就是点击跳转的路径

el-menu-item 菜单项
index="/article/channel" 配置的是访问的跳转路径,配合default-active的值,实现高亮
active-text-color激活时的颜色


-->
<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 -->
<el-sub-menu index="/user">
<!-- 多级菜单的大标题 - 具名插槽 title -->
<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>{{
userStore.user.nickname || userStore.user.username
}}</strong>
</div>
<el-dropdown placement="bottom-end" @command="handleCommand">
<!-- 展示给用户,默认看到的 -->
<span class="el-dropdown__box">
<el-avatar :src="userStore.user.user_pic || 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>

登录访问拦截

和vue2项目的登录拦截一样,在router文件夹/index.js文件添加
一定记得要从仓库导入用户仓库!!不然永远拿不到用户的token
vue router官方文档-导航守卫https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
登录访问拦截 => 默认是直接放行的
根据返回值决定,是放行还是拦截!!!!

返回值:

  1. undefined / true 直接放行
  2. false 拦回from的地址页面
  3. 具体路径 或 路径对象 拦截到对应的地址
    ‘/login’ { name: ‘login’ }
    1
    2
    3
    4
    5
    6
    7
    8
    //一定记得要从仓库导入用户仓库!!不然永远拿不到用户的token
    import { useUserStore } from '@/stores'

    router.beforeEach((to) => {
    如果没有token, 且访问的是非登录页,拦截到登录,其他情况正常放行
    const useStore = useUserStore()
    if (!useStore.token && to.path !== '/login') return '/login'
    })

用户基本信息的获取和渲染

  1. 封装接口:
    去后端拿获取用户的的基本信息的接口,api/user.js后面新建方法:获取用户基本信息(注意方法命名规范)
    1
    2
    export const userGetInfoService = () => request.get('/my/userinfo')

  2. 个人信息放到仓库中:
    和token的维护类似,个人信息也会在多个地方使用,放到用户仓库中统一维护。在store/modules/user.js文件中定义数据user和获取用户信息的getUser方法,最后在return里暴露出去
    1
    2
    3
    4
    5
    6
    const user = ref({})
    const getUser = async () => {
    const res = await userGetInfoService() // 请求获取数据
    user.value = res.data.data
    //这里到底几个.data怎么拿到数据存从后端接口文档返回的数据信息看
    }
  3. 页面调用
    回到首页页面,从仓库里导入用户仓库
    1
    import { useUserStore } from '@/stores'
    然后使用生命周期钩子,让页面加载完后发送请求,获取到用户信息
    1
    2
    3
    4
    5
    const userStore = useUserStore()
    //onMounted生命周期钩子,页面加载完发送请求
    onMounted(() => {
    userStore.getUser()
    })
    成功后pinia里面有显示
    这里面user对象是empty,因为还没有设置个人信息
  4. 动态渲染信息
    1
    2
    3
    4
    5
    <div>
    黑马程序员:<strong>{{ userStore.user.nickname || userStore.user.username }}</strong>
    </div>

    <el-avatar :src="userStore.user.user_pic || avatar" />

右上角小菜单和退出功能

右上角头像下拉菜单还是官方文档里面有
注意退出登录不光要跳转到登录页,还要清空本地的数据,token和用户信息
清除用户信息:先在用户仓库新增一个方法setUser,让用于信息=
首页相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const handleCommand = async (key) => {
if (key === 'logout') {
// 退出操作
// 清除本地的数据 (token + user信息)
userStore.removeToken()
userStore.setUser({})
router.push('/login')
} else {
// 跳转操作
router.push(`/user/${key}`)
}
}
···

<el-header>
<div>
黑马程序员:<strong>{{
userStore.user.nickname || userStore.user.username
}}</strong>
</div>
<el-dropdown placement="bottom-end" @command="handleCommand">
<!-- 展示给用户,默认看到的 -->
<span class="el-dropdown__box">
<el-avatar :src="userStore.user.user_pic || 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>

优化-退出询问

用户可能不小心点到退出,所以在退出前进行弹窗询问
官方文档-左侧导航栏Message Box 消息弹出框-确认消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const handleCommand = async (key) => {
if (key === 'logout') {
// 退出操作(新增-弹窗询问)
await ElMessageBox.confirm('你确认要进行退出么', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
// 清除本地的数据 (token + user信息)
userStore.removeToken()
userStore.setUser({})
router.push('/login')
} else {
// 跳转操作
router.push(`/user/${key}`)
}
}

文章分类页面

需求

  1. 基本架子-PageContainer
  2. 封装文章分类渲染 &loading 处理
  3. 文章分类添加编辑 [element-plus 弹层]
  4. 文章分类删除

文章分类架子

article/ArticleChannel.vue
官方文档-左侧导航栏Data数据展示-Card卡片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>文章分类</span>
<div class="extra">
<el-button type="primary">添加分类</el-button>
</div>
</div>
</template>
...
</el-card>
</template>

<style lang="scss" scoped>
.page-container {
min-height: 100%;
box-sizing: border-box;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

封装组件

由于内容部分的样式不止是在文章分类页面会使用,其他页面展示内容部分也需要用,所以把他们封装成组件更好地复用
封装到:components文件夹/PageContainer.vue文件
写死的地方使用插槽:文章标题(props 父传子),文章内容(default默认插槽),右侧按钮(extra具名插槽)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<script setup>
defineProps({
title: {
required: true,
type: String
}
})
</script>

<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>{{ title }}</span>
<div class="extra">
<slot name="extra"></slot>
</div>
</div>
</template>
<slot></slot>
</el-card>
</template>

<style lang="scss" scoped>
.page-container {
min-height: 100%;
box-sizing: border-box;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>

使用组件改进页面

页面中直接使用测试 (unplugin-vue-components 会自动注册)

  • 文章分类测试:
1
2
3
4
5
6
7
8
9
<template>
<page-container title="文章分类">
<template #extra>
<el-button type="primary"> 添加分类 </el-button>
</template>

主体部分
</page-container>
</template>
  • 文章管理测试:
1
2
3
4
5
6
7
8
9
<template>
<page-container title="文章管理">
<template #extra>
<el-button type="primary">发布文章</el-button>
</template>

主体部分
</page-container>
</template>

文章分类渲染和加载处理

api接口

无论是渲染什么都要发请求获取数据,所以要在api文件夹创建artical.js文件,从接口文档找-获取-文章分类
注意接口说明:获取分类每个用户之间是独立的
我这里把文章相关的所有接口都放进来了

1
2
3
4
5
6
7
8
9
10
11
12
13
import request from '@/utils/request'
// 获取文章分类
export const artGetChannelsService = () => request.get('/my/cate/list')
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
request.put('/my/cate/info', data)
// 删除文章分类
export const artDelChannelService = (id) =>
request.delete('/my/cate/del', {
params: { id }
})

页面调用接口获取数据

在页面中发送获取接口请求

1
2
3
4
5
6
7
8
import {artGetChannelsService} from '../../api/article'
//把数据存到channelList中
const channelList = ref([])

const getChannelList = async () => {
const res = await artGetChannelsService()
channelList.value = res.data.data //看接口文档返回的格式
}

页面渲染数据(表格组件)(用的多)

官方文档-Table表格
下面是官方文档的代码,下面的tableData对象包裹表格的数据

:data–表格名称
prop对象中找对应属性名渲染
label 列名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="Date" width="180" />
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</template>

<script lang="ts" setup>
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
}
]
</script>
  • 在文档下面-Table表格-单选有有说:如果需要显示索引,可以增加
  • el-table-column,设置type属性为Index.即可显示从哪个1开始的序号,跟着它给表格加索引号
  • 图标还是从组件库拿

加载处理

还是官方文档-反馈组件-Loading加载

  1. 定义变量,v-loading绑定
1
2
3
const loading = ref(false)

<el-table v-loading="loading">
  1. 发送请求前开启,请求结束关闭
1
2
3
4
5
6
const getChannelList = async () => {
loading.value = true
const res = await artGetChannelsService()
channelList.value = res.data.data
loading.value = false
}

文章分类的添加编辑和删除

点击添加和编辑都会显示element plues弹层,放在一起实现

Dialog对话框组件

在官方文档-左侧导航栏反馈组件-Dialog对话框中
看他给的代码

点击按钮,把他dialogVisible的布尔值改成true就会展示对话框
记得去上面提供一个变量dialogVisible
:before-closes 是关闭对话框之前要做的事情(比如说关闭提醒),这里不要就把这局删了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<el-button plain @click="dialogVisible = true">
Click to open the Dialog
</el-button>

<el-dialog
v-model="dialogVisible"
title="Tips"
width="500"
:before-close="handleClose"
>
<span>This is a message</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">
Confirm
</el-button>
</div>
</template>
</el-dialog>
</template>

把它封装成组件(放到article文件夹/components文件夹/ChannelEdit.vue中),组件对外暴露一个方法open,基于open传递的参数来区分是添加还是编辑

1
2
3
4
5
6
7
8
9
10
11
12
//open({})  => 表单无需渲染,说明是添加
// open({ id, cate_name, ... }) => 表单需要渲染,说明是编辑
// open调用后,可以打开弹窗
const open = (row) => {
dialogVisible.value = true
formModel.value = { ...row } // 添加 → 重置了表单内容,编辑 → 存储了需要回显的数据
}

// 向外暴露方法,意味着在外部可以调用组件的方法,可以传参可以显示
defineExpose({
open
})

页面导入对话框组件

先导入,然后页面直接用(记得加个ref给这个组件做绑定)
如果是添加那么对话框里没东西,如果是编辑里面是row,就是 channelList 的一项

1
2
3
4
5
6
7
8
9
10
import ChannelEdit from './components/ChannelEdit.vue'
const dialog = ref()
const onEditChannel = (row) => {
dialog.value.open(row)
}
const onAddChannel = () => {
dialog.value.open({})
}
···
<channel-edit ref="dialog" @success="onSuccess"></channel-edit>

完善组件-添加和编辑

  1. 提供数据
    回到封装对话框组件的地方,在里面提供要处理的数据formModel,这里是分类名称和分类别名(看接口文档找属性名,cate_name和cate_alias),然后在对话框表单里面和formModel做绑定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const formModel = ref({
    cate_name: '',
    cate_alias: ''
    })
    ···
    <el-form :model="formModel">
    <el-form-item label="分类名称" prop="cate_name">
    <el-input
    v-model="formModel.cate_name"
    placeholder="请输入分类名称"
    ></el-input>
    </el-form-item>
    <el-form-item label="分类别名" prop="cate_alias">
    <el-input
    v-model="formModel.cate_alias"
    placeholder="请输入分类别名"
    ></el-input>
    </el-form-item>
    </el-form>

  2. 添加规则
    给el-form添加rules,同时在上面提供规则
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     <el-form :model="formModel" :rules="rules">
    ···
    const rules={
    cate_name: [
    { required: true, message: '请输入分类名称', trigger: 'blur' },
    {
    pattern: /^\S{1,10}$/,
    message: '分类名必须是 1-10 位的非空字符',
    trigger: 'blur'
    }
    ],
    cate_alias: [
    { required: true, message: '请输入分类别名', trigger: 'blur' },
    {
    pattern: /^[a-zA-Z0-9]{1,15}$/,
    message: '分类名必须是 1-15 位的字母或数字',
    trigger: 'blur'
    }
    ]
    }

  3. open方法优化
    新增 ormModel.value = { …row },但这里没有听懂,大概意思是要编辑的时候把原来有的内容显示到对话框里面
    1
    2
    3
    4
    const open = (row) => {
    dialogVisible.value = true
    formModel.value = { ...row } // 添加 → 重置了表单内容,编辑 → 存储了需要回显的数据
    }
  4. 修改对话框题目
    柑橘能否获取到id判断是编辑还是添加

提交前的校验

类似用户注册前最后的校验
先给form一个ref,然后对它进行判断
formModel.value是表单对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  <el-form ref="formRef">
···
const emit = defineEmits(['success'])
const onSubmit = async () => {
await formRef.value.validate()
const isEdit = formModel.value.id
if (isEdit) {
await artEditChannelService(formModel.value)
ElMessage.success('编辑成功')
} else {
await artAddChannelService(formModel.value)
ElMessage.success('添加成功')
}
dialogVisible.value = false
emit('success')
}

成功后让页面再请求一下数据,立刻更新页面

1
2
3
4
//在调用组件的页面添加
const onSuccess = () => {
getChannelList()
}

成功后效果是这样的添加文章分类
添加分类后展示
编辑文章分类

文章分类的删除

调用ElMessageBox提示框确认是否删除
删除这里的后端接口参数是query,传Id过去,这里和前面的有区别

1
2
3
4
5
6
7
8
9
10
11
const onDelChannel = async (row) => {
await ElMessageBox.confirm('你确认要删除该分类么', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelChannelService(row.id)
ElMessage.success('删除成功')
getChannelList()
}

删除文章分类提示
成功删除文章分类

文章管理页面

需求

  1. 文章列表渲染,上面有搜索功能,下面有分页跳转功能
  2. 添加文章功能(抽屉样式展示,包括文件上传)
  3. 编辑文章功能(公用抽屉)
  4. 删除文章功能

静态布局

常用的布局和按钮组件虽然官方文档有,但是不加以总是去翻,建议自己记住

article/components/ArticleManage.vue

  1. 一个表单区域一个表格区域,表单上方有搜索框,三个input在一行展示,去官方文档找属性
    只需要在el-form 后面跟一个inline即可在一行现实

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        <!--表单区域 这里添加inline让他们能在同一行显示,默认值true-->
    <el-form inline>
    <el-form-item label="文章分类:">
    <el-select>
    <el-option label="新闻" value="111"></el-option>
    <el-option label="体育" value="222"></el-option>
    </el-select>
    </el-form-item>
    <el-form-item label="发布状态:">
    <el-select>
    <el-option label="已发布" value="已发布"></el-option>
    <el-option label="草稿" value="草稿"></el-option>
    </el-select>
    </el-form-item>
    <el-form-item>
    <el-button type="primary">搜索</el-button>
    <el-button>重置</el-button>
    </el-form-item>
    </el-form>

    一行显示

  2. 表格:还是先去接口看后台会返回什么样的数据,然后根据数据写表格
    给表格做渲染记得跟:data

    1
    <el-table :data="articleList">

    先给一些假数据做渲染,从接口拿出后来给的返回的数据样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from 'vue'
// 假数据
const articleList = ref([
{
id: 5961,
title: '新的文章啊',
pub_date: '2022-07-10 14:53:52.604',
state: '已发布',
cate_name: '体育'
},
{
id: 5962,
title: '新的文章啊',
pub_date: '2022-07-10 14:54:30.904',
state: null,
cate_name: '体育'
}
])
  1. 表格中要链接一样的效果用还可以加属性删除下划线修改颜色
  2. 学用作用域插槽row,可以获取当前行的数据v-for便利item
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <el-table :data="articleList" style="width: 100%">
    <el-table-column label="文章标题" width="400">
    <template #default="{ row }">
    <el-link type="primary" :underline="false">{{ row.title }}</el-link>
    </template>
    </el-table-column>
    <el-table-column label="分类" prop="cate_name"></el-table-column>
    <el-table-column label="发表时间" prop="pub_date"> </el-table-column>
    <el-table-column label="状态" prop="state"></el-table-column>
    <el-table-column label="操作" width="100">
    <template #default="{ row }">
    <el-button
    :icon="Edit"
    circle
    plain
    type="primary"
    @click="onEditArticle(row)"
    ></el-button>
    <el-button
    :icon="Delete"
    circle
    plain
    type="danger"
    @click="onDeleteArticle(row)"
    ></el-button>
    </template>
    </el-table-column>
    <template #empty>
    <el-empty description="没有数据" />
    </template>
    </el-table>


    const onEditArticle = (row) => {
    console.log(row)
    }
    const onDeleteArticle = (row) => {
    console.log(row)
    }

文章分类菜单

文章分类菜单封装

分析:
在文章搜索这行,文章分类的选择内容要发请求获取数据进行渲染,发布状态是固定的
在后续添加文章部分,还有文章分类的下拉菜单,多个地方需要获取这个数据
所以把获取文章分类这个菜单进行封装:article文件夹/components文件夹/ChannelSelect.vue
操作:

  1. 先把静态结构搬过来
    1
    2
    3
    4
    5
    6
    <template>
    <el-select>
    <el-option></el-option>
    <el-option></el-option>
    </el-select>
    </template>
  2. 导入api接口方法
    方法返回的数组存到res里面,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { artGetChannelsService } from '@/api/article.js'
    import { ref } from 'vue'

    const channelList = ref([])
    const getChannelList = async () => {
    const res = await artGetChannelsService()
    channelList.value = res.data.data
    console.log(channelList.value)
    //刷新页面看看有没有获取到这个数据
    //获取到数据就可以基于数据进行动态渲染了
    }
    getChannelList()
  3. 基于数据让菜单动态渲染
    注意用v-for一定要加:key
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <template>
    <el-select>
    <el-option
    v-for="channel in channelList"
    :key="channel.id"
    :label="channel.cate_name"
    :value="channel.id"
    ></el-option>
    </el-select>
    </template>
  4. 数据传递(难点)
    (1) 父组件中:
    菜单里的数据一定是父亲传递过来的,由父组件来维护,通常希望是v-model来维护
    在父组件提供一个变量来绑定:
    const cateId= ref(这里是分类文章添加时候的id号)
    然后在调用组件的组件上绑定v-model=”cateId”

    Vue2中,v-model是 :value 和 @input 的简写
    Vue3中,v-model是modelValue 和 @update:modelValue 的简写

(2) 菜单组件中:
在组件中定义props和emit事件

1
2
3
4
5
6
defineProps({
modeValue:{
type:[Number,String]//看后端数据的类型
}
})
const emit = defineEmits(['update:modeValue'])

父传子传递下来的的给el-select菜单组件,所以给el-select绑定传递过来的值,要拆开v-model,同时值更新了要触发事件从组件往上传递给父亲

1
2
3
4
5
6
7
8
9
10
11
12
<el-select
:modelValue="modelValue"
@update:modelValue="emit('update:modelValue', $event)"
:style="{ width }"
>
<el-option
v-for="channel in channelList"
:key="channel.id"
:label="channel.cate_name"
:value="channel.id"
></el-option>
</el-select>

新的检查按钮

获取文章分类完善和文章状态菜单

父组件完善
获取文章列表后来传递的参数和cateId进一步优化:不再使用cateId,变成获取请求参数对象params,在父组件定义参数绑定

1
2
3
4
5
6
const params = ref({
pagenum: 1, //当前页面
pagesize: 5,//当前页面要的数据条数
cate_id: '',//分钟分类ID
state: '' //文章状态(可以选已发布或草稿)
})

使用子组件时,把原来v-model绑定的cateId修改成对象.参数

1
<channel-select v-model="params.cate_id"></channel-select>

同时发布状态的下拉菜单也和后端的请求绑定

1
2
3
4
<el-select v-model="params.state">
<el-option label="已发布" value="已发布"></el-option>
<el-option label="草稿" value="草稿"></el-option>
</el-select>

文章列表渲染

  1. 看接口文档,获取接口
    api/article.js
    1
    2
    3
    4
    5
    // 文章:获取文章列表
    export const artGetListService = (params) =>
    request.get('/my/article/list', {
    params
    })
    然后再ArticleManage.vue文件导入接口
    1
    2
    import { artGetListService } from '@/api/article.js'

    看接口文档返回的数据,然后声明total,下面进行渲染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const total = ref(0) // 总条数

    // 基于params参数,获取文章列表
    const getArticleList = async () => {
    const res = await artGetListService(params.value)
    articleList.value = res.data.data
    //res.data是后台返回的结果,再.data是数据
    total.value = res.data.total
    loading.value = false
    }
  2. 刷新页面,发现页面已经能够渲染了,但是获取到的发表时间和我们平时的时间格式不一样,这里需要微调
    封装一个工具函数,utils文件夹下新建format.js文件
    1
    2
    3
    4
    import { dayjs } from 'element-plus'

    export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')

    然后在.vue文件导入并调用
    1
    2
    3
    4
    5
    6
    7
    import { formatTime } from '@/utils/format.js'
    //在发布时间的地方调用
    <el-table-column label="发表时间" prop="pub_date">
    <template #default="{ row }">
    {{ formatTime(row.pub_date) }}
    </template>
    </el-table-column>

分页功能

  1. 官方文档-左侧导航栏Pagination分页-右侧导航栏附加功能-看源代码找到最完整的,把它放在表格区域el-table下面,各个参数的具体说明官方文档下面也有
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <el-pagination
    v-model:current-page="currentPage4"
    v-model:page-size="pageSize4"
    :page-sizes="[100, 200, 300, 400]"//供用户选择的每页条数,注意要包含上面写的目前生效的每页条数
    :size="size" //分页大小
    :disabled="disabled" //是否要禁用
    :background="background" //要不要加背景颜色
    layout="total, sizes, prev, pager, next, jumper" //控制工具栏有什么内容的,写什么就按什么顺序展示什么
    :total="400"
    @size-change="handleSizeChange"//page-sizes每页条数变化的时候会触发这个按钮
    @current-change="handleCurrentChange"
    //current-page和page-size任意一个变化都会触发这个函数
    //想要做的对应的处理都可以写在上面这两个change函数里面
    />
    修改数和文档代码,和原有的数据进行绑定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <el-pagination
    v-model:current-page="params.pagenum"
    v-model:page-size="params.pagesize"
    :page-sizes="[2, 3, 5, 10]" //供用户选择的每页条数,注意要包含上面写的目前生效的每页条数
    :background="true"
    layout="jumper, total, sizes, prev, pager, next"
    :total="total"
    @size-change="onSizeChange"
    @current-change="onCurrentChange"
    style="margin-top: 20px; justify-content: flex-end"
    />
  2. 写处理分页逻辑的函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 处理分页逻辑
    const onSizeChange = (size) => {
    // console.log('当前每页条数', size)
    // 只要是每页条数变化了,那么原本正在访问的当前页意义不大了,数据大概率已经不在原来那一页了
    // 只要是页面条数变化了,那么重新渲染回到第一页
    params.value.pagenum = 1
    params.value.pagesize = size
    // 基于最新的当前页 和 每页条数,渲染数据
    getArticleList()
    }
    const onCurrentChange = (page) => {
    // 更新当前页
    params.value.pagenum = page
    // 基于最新的当前页,渲染数据
    getArticleList()
    }

给表格添加loading效果

  1. 先在上面定义loading
    1
    const loading = ref(false) // loading状态
  2. 当要发送请求获取数据时,刚发送时让loading=true,发送完获取到请求后改为false,同时给el-table添加v-loading指令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 基于params参数,获取文章列表
    const getArticleList = async () => {
    //发请求前开loading
    loading.value = true

    const res = await artGetListService(params.value)
    articleList.value = res.data.data
    total.value = res.data.total
    //发请求后关loading
    loading.value = false
    }
    ···
    <el-table :data="articleList" v-loading="loading">

搜索和重置实现

  1. 添加对应的点击事件函数
    1
    2
    3
    4
    <el-form-item>
    <el-button @click="onSearch" type="primary">搜索</el-button>
    <el-button @click="onReset">重置</el-button>
    </el-form-item>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 搜索逻辑:按照最新的条件,重新检索,从第一页开始展示
    const onSearch = () => {
    params.value.pagenum = 1 // 重置页面
    getArticleList()
    }

    // 重置逻辑:将筛选条件清空,重新检索,从第一页开始展示
    const onReset = () => {
    params.value.pagenum = 1 // 重置页面
    params.value.cate_id = ''
    params.value.state = ''
    getArticleList()
    }

添加和编辑文章(共用一个抽屉)

抽屉组件

  1. 抽屉和dialog对话框差不多,但是对话框很小,抽屉组件能够放更多的东西,整体的思路是一样的
    官方文档-左侧导航栏Feedback反馈组件-Drawer抽屉-查看源代码/el-drawer
    1
    2
    3
    4
    5
    6
    7
    8
    <el-drawer
    v-model="drawer" 绑定布尔值控制现实隐藏
    title="I am the title"
    :direction="direction" 控制方向
    :before-close="handleClose" 控制关闭前要不要询问
    >
    <span>Hi, there!</span>
    </el-drawer>
  2. 在页面使用(后面要封装成组件)
    组件放在分页区域下面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //先准备数据
    import { ref } from 'vue'
    const visibleDrawer = ref(false)
    //然后再分页区域下面写容器
    <el-drawer
    v-model="visibleDrawer"
    title="大标题"
    direction="rtl"
    size="50%"
    >
    <span>Hi there!</span>
    </el-drawer>
    //点击修改布尔值显示抽屉
    //默认隐藏点击添加或编辑显示
    <el-button type="primary" @click="onAddArticle">发布文章</el-button>

    const visibleDrawer = ref(false)
    const onAddArticle = () => {
    visibleDrawer.value = true
    }

封装抽屉组件(添加功能)

封装抽屉组件

article/componse/新建ArticleEdit.vue

  1. 添加和编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件。组件对外暴露一个方法open, 基于open的参数,初始化表单数据,并判断区分是添加还是编辑

    open({ })=> 添加操作,添加表单初始化无数据
    open({ id: xx, … })=> 编辑操作,编辑表单初始化需回显

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <script setup>
    import { ref } from 'vue'
    const visibleDrawer = ref(false)

    const open = (row) => {
    visibleDrawer.value = true
    console.log(row)
    }

    defineExpose({
    open
    })
    </script>

    <template>
    <!-- 抽屉 -->
    <el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%">
    <span>Hi there!</span>
    </el-drawer>
    </template>
  1. 通过 ref 绑定
    1
    2
    3
    const articleEditRef = ref()
    <!-- 弹窗 -->
    <article-edit ref="articleEditRef"></article-edit>
  2. 点击调用方法显示弹窗
    1
    2
    3
    4
    5
    6
    7
    // 编辑新增逻辑
    const onAddArticle = () => {
    articleEditRef.value.open({})
    }
    const onEditArticle = (row) => {
    articleEditRef.value.open(row)
    }
  1. 完善组件内容
  1. 准备数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const formModel = ref({
    title: '',
    cate_id: '',
    cover_img: '',
    content: '',
    state: ''
    })

    const open = async (row) => {
    visibleDrawer.value = true
    if (row.id) {
    console.log('编辑回显')
    } else {
    console.log('添加功能')
    }
    }
  2. 准备 form 表单结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import ChannelSelect from './ChannelSelect.vue'

<template>
<el-drawer
v-model="visibleDrawer"
:title="formModel.id ? '编辑文章' : '添加文章'"
direction="rtl"
size="50%"
>
<!-- 发表文章表单 -->
<el-form :model="formModel" ref="formRef" label-width="100px">
<el-form-item label="文章标题" prop="title">
<el-input v-model="formModel.title" placeholder="请输入标题"></el-input>
</el-form-item>
<el-form-item label="文章分类" prop="cate_id">
<channel-select
v-model="formModel.cate_id"
width="100%"
></channel-select>
</el-form-item>
<el-form-item label="文章封面" prop="cover_img"> 文件上传 </el-form-item>
<el-form-item label="文章内容" prop="content">
<div class="editor">富文本编辑器</div>
</el-form-item>
<el-form-item>
<el-button type="primary">发布</el-button>
<el-button type="info">草稿</el-button>
</el-form-item>
</el-form>
</el-drawer>
</template>
  1. 一打开默认重置添加的 form 表单数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const defaultForm = {
title: '',
cate_id: '',
cover_img: '',
content: '',
state: ''
}
const formModel = ref({ ...defaultForm })

const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
console.log('添加功能')
formModel.value = { ...defaultForm }
}
}
  1. 扩展 下拉菜单 width props
1
2
3
4
5
6
7
8
9
10
11
12
13
defineProps({
modelValue: {
type: [Number, String]
},
width: {
type: String
}
})

<el-select
...
:style="{ width }"
>
文件上传

文件上传通常有两种方式
1.点击文件上传的加号后,就一定把图片上传到后台,后台返回一个url地址,存储这个文件的url地址,点击发布的时候提交。
弊端:可能产生垃圾图片
2.用户不管怎么选择上传文件都是本地的预览,只有点击发布或者草稿的时候才会真正地提交上传到后台。
弊端:一次上传的东西多可能回导致卡顿(添加loading效果)
到底选择哪个看看接口文档。官方文档有其他的信息-左侧导航栏上传部分
在这个项目中选择第二种上传方式

  1. 关闭自动上传,准备结构

    此处需要关闭 element-plus 的自动上传,不需要配置 action 等参数,只需要做前端的本地预览图片即可,无需在提交前上传图标
    语法:URL.createObjectURL(…) 创建本地预览的地址,来预览

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { Plus } from '@element-plus/icons-vue'

    <el-upload
    class="avatar-uploader"
    :auto-upload="false"
    :show-file-list="false"
    :on-change="onUploadFile"
    >
    <img v-if="imgUrl" :src="imgUrl" class="avatar" />
    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
    </el-upload>
  2. 准备数据 和 选择图片的处理逻辑

1
2
3
4
5
const imgUrl = ref('')
const onUploadFile = (uploadFile) => {
imgUrl.value = URL.createObjectURL(uploadFile.raw)
formModel.value.cover_img = uploadFile.raw
}
  1. 样式美化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.avatar-uploader {
:deep() {
.avatar {
width: 178px;
height: 178px;
display: block;
}
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
}
}
文章内容(富文本编辑器)

使用VueQuill富文本编辑器,官方文档查看
https://vueup.github.io/vue-quill/

  1. 安装包,注册成局部组件
1
pnpm add @vueup/vue-quill@latest

官方文档有在但文件中注册和全局注册,由于这里只是这个地方需要这个功能,所以用局部注册

1
2
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
  1. 页面中使用绑定
1
2
3
4
5
6
7
8
<div class="editor">
<quill-editor
theme="snow"
v-model:content="formModel.content"//数据双向绑定
contentType="html" //注意要设置数据格式类型
>
</quill-editor>
</div>
  1. 样式美化
1
2
3
4
5
6
.editor {
width: 100%;
:deep(.ql-editor) {
min-height: 200px;
}
}
发布
  1. 封装添加接口
1
2
export const artPublishService = (data) =>
request.post('/my/article/add', data)
  1. 注册点击事件调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<el-form-item>
<el-button @click="onPublish('已发布')" type="primary">发布</el-button>
<el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</el-form-item>

// 发布文章
const emit = defineEmits(['success'])
const onPublish = async (state) => {
// 将已发布还是草稿状态,存入 state
formModel.value.state = state

// 转换 formData 数据
const fd = new FormData()
for (let key in formModel.value) {
fd.append(key, formModel.value[key])
}

if (formModel.value.id) {
console.log('编辑操作')
} else {
// 添加请求
await artPublishService(fd)
ElMessage.success('添加成功')
visibleDrawer.value = false
emit('success', 'add')
}
}
  1. 父组件监听事件,重新渲染
1
2
3
4
5
6
7
8
9
10
11
<article-edit ref="articleEditRef" @success="onSuccess"></article-edit>

// 添加修改成功
const onSuccess = (type) => {
if (type === 'add') {
// 如果是添加,需要跳转渲染最后一页,编辑直接渲染当前页
const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
params.value.pagenum = lastPage
}
getArticleList()
}

添加完成后的内容重置

1
2
3
4
5
6
7
8
9
10
11
12
const formRef = ref()
const editorRef = ref()
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
formModel.value = { ...defaultForm }
imgUrl.value = ''
editorRef.value.setHTML('')
}
}

编辑功能

封装接口

如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显

  1. 封装接口,根据 id 获取详情数据
    1
    2
    export const artGetDetailService = (id) =>
    request.get('my/article/info', { params: { id } })
  2. 页面中调用渲染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const open = async (row) => {
    visibleDrawer.value = true
    if (row.id) {
    console.log('编辑回显')
    const res = await artGetDetailService(row.id)
    formModel.value = res.data.data
    imgUrl.value = baseURL + formModel.value.cover_img
    // 提交给后台,需要的是 file 格式的,将网络图片,转成 file 格式
    // 网络图片转成 file 对象, 需要转换一下
    formModel.value.cover_img = await imageUrlToFile(imgUrl.value, formModel.value.cover_img)
    } else {
    console.log('添加功能')
    ...
    }
    }

chatGPT prompt:封装一个函数,基于 axios, 网络图片地址,转 file 对象, 请注意:写中文注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
try {
// 第一步:使用axios获取网络图片数据
const response = await axios.get(url, { responseType: 'arraybuffer' });
const imageData = response.data;

// 第二步:将图片数据转换为Blob对象
const blob = new Blob([imageData], { type: response.headers['content-type'] });

// 第三步:创建一个新的File对象
const file = new File([blob], fileName, { type: blob.type });

return file;
} catch (error) {
console.error('将图片转换为File对象时发生错误:', error);
throw error;
}
}
编辑文章功能
  1. 封装编辑接口
    1
    export const artEditService = (data) => request.put('my/article/info', data)
  2. 提交时调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const onPublish = async (state) => {
    ...
    if (formModel.value.id) {
    await artEditService(fd)
    ElMessage.success('编辑成功')
    visibleDrawer.value = false
    emit('success', 'edit')
    } else {
    // 添加请求
    ...
    }
    }
删除文章
  1. 封装删除接口
1
export const artDelService = (id) => request.delete('my/article/info', { params: { id } })
  1. 页面中添加确认框调用
1
2
3
4
5
6
7
8
9
10
const onDeleteArticle = async (row) => {
await ElMessageBox.confirm('你确认删除该文章信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getArticleList()
}

个人中心板块(借助AI大模型开发)

报错及解决

配置-配置代码检查工作流-暂存区eslint校验

我开始完成这部分代码后发现添加内容到暂存区后无法提交到仓库,终端git add . 然后git commit -m ‘vue3项目学习’报错内容如下,在文件夹中使用小乌龟工具提交也是出现一样的报错内容,无法提交:

1
TypeError: Key "rules": Key "prettier/prettier": Could not find plugin "prettier".

git报错


解决: 没找到我的prettier,开始chat建议我在vscode里面安装prettier插件,但是课程视频里不让使用这个vscode的插件,而是用创建项目在package.json里面提供的prettier,我看了我的这个文件里面依赖也是有"prettier": "^3.3.3"的,继续问,AI说是我的eslint.config.js中没有导入这个依赖,在里面加入两行代码然后成功提交
1
2
3
4
5
import pluginPrettier from 'eslint-plugin-prettier' // 引入 prettier 插件
// 添加自定义规则部分
plugins: {
prettier: pluginPrettier, // 添加 prettier 插件
},

我的gitee仓库不显示vue3大事件项目文件

仓库里没办法看到项目的代码,我开始以为是文件夹的问题,新建文件夹还是一样的不行
但是无法对里面的内容进行添加,提交,推送的操作,还无法解决
最后删掉.git文件试了好几遍成功

.git文件

开始这个项目里有.git文件,chat建议我删除
. 理解 .git 目录的作用
.git 目录:这是 Git 用来跟踪版本控制信息的目录。它包含了该仓库的所有版本历史、配置、分支信息等。
如果你在一个文件夹中删除 .git 目录,意味着你将失去该文件夹的所有 Git 版本控制信息,这个文件夹将不再被视为一个 Git 仓库。

.husky文件夹

发现我的这个文件夹里面也有一个 .gitignore文件文件,里面是*
这个删不删没有影响

登录访问拦截后运行项目页面空白

原因是在router/index.js中没有从仓库中导入用户仓库,导致无法获取登陆后存到仓库的用户token,拿不到数据
丢失代码

1
import { useUserStore } from '@/stores'

文件eslint.config.js配置失败

优化提示弹窗部分导入了组件库组件

1
ElMessage.success('注册成功')

修改后运行程序注册后能成功弹出,但是vscode上还是显示红色报错,这是因为前面我们按需自动导入了,但是eslink不知道,所以给eslink中配置全局变量规则可以不报错
给eslint.config.js文件最后添加
(视频成功我开始没成功,我的这个文件是eslint.config.js,视频是.eslintrc.cjs文件,前面做配置的时候就出现了问题我让AI帮我变成我的文件适用的代码,这里没有变)
后来关掉vscode重新打开项目发现不报错了,重启是个好方法!